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} should be called with $scope.coupons + # @param id {number} ID of the coupon to delete + ## + $scope.deleteCoupon = (coupons, id) -> + if typeof id != 'number' + console.error('[EditPricingController::deleteCoupon] 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_coupon') + , -> + # the admin has confirmed, delete the coupon + Coupon.delete {id: id}, (res) -> + growl.success(_t('coupon_was_successfully_deleted')) + $scope.coupons.splice(findItemIdxById(coupons, id), 1) - findPlanIdxById = (plans, id)-> - (plans.map (plan)-> - plan.id - ).indexOf(id) + , (error) -> + console.error('[EditPricingController::deleteCoupon] Error: '+error.statusText) if error.statusText + if error.status == 422 + growl.error(_t('unable_to_delete_the_specified_coupon_already_in_use')) + else + growl.error(_t('unable_to_delete_the_specified_coupon_an_unexpected_error_occurred')) + + + + ## + # Open a modal allowing to select an user and send him the details of the provided coupon + # @param coupon {Object} The coupon to send + ## + $scope.sendCouponToUser = (coupon) -> + $uibModal.open + templateUrl: '<%= asset_path "admin/pricing/sendCoupon.html" %>' + resolve: + coupon: -> coupon + size: 'md' + controller: ['$scope', '$uibModalInstance', 'Coupon', 'coupon', '_t', ($scope, $uibModalInstance, Coupon, coupon, _t) -> + + ## Member, receiver of the coupon + $scope.ctrl = + member: null + + ## Details of the coupon to send + $scope.coupon = coupon + + ## Callback to validate sending of the coupon + $scope.ok = -> + Coupon.send {coupon_code: coupon.code, user_id: $scope.ctrl.member.id}, (res) -> + growl.success(_t('coupon_successfully_sent_to_USER', {USER: $scope.ctrl.member.name})) + $uibModalInstance.close({user_id: $scope.ctrl.member.id}) + , (err) -> + growl.error(_t('an_error_occurred_unable_to_send_the_coupon')) + + ## Callback to close the modal and cancel the sending process + $scope.cancel = -> + $uibModalInstance.dismiss('cancel') + ] + + + ### PRIVATE SCOPE ### ## # Kind of constructor: these actions will be realized first when the controller is loaded @@ -329,6 +390,19 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state", + ## + # Retrieve an item index by its ID from the given array of objects + # @param items {Array<{id:number}>} + # @param id {number} + # @returns {number} item index in the provided array + ## + findItemIdxById = (items, id)-> + (items.map (item)-> + item.id + ).indexOf(id) + + + ## # Group the given credits array into a map associating the plan ID with its associated trainings/machines # @return {Object} the association map diff --git a/app/assets/javascripts/controllers/admin/settings.coffee b/app/assets/javascripts/controllers/admin/settings.coffee index f46a279d6..ea83a1525 100644 --- a/app/assets/javascripts/controllers/admin/settings.coffee +++ b/app/assets/javascripts/controllers/admin/settings.coffee @@ -45,7 +45,6 @@ Application.Controllers.controller "SettingsController", ["$scope", 'Setting', ' $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 } @@ -74,6 +73,14 @@ Application.Controllers.controller "SettingsController", ["$scope", 'Setting', ' $scope.cancelDelay = name: 'booking_cancel_delay' value: parseInt(settingsPromise.booking_cancel_delay) + + $scope.enableReminder = + name: 'reminder_enable' + value: (settingsPromise.reminder_enable == 'true') + + $scope.reminderDelay = + name: 'reminder_delay' + value: parseInt(settingsPromise.reminder_delay) @@ -108,7 +115,7 @@ Application.Controllers.controller "SettingsController", ["$scope", 'Setting', ' value = setting.value Setting.update { name: setting.name }, { value: value }, (data)-> - growl.success(_t('customization_of_SETTING_successfully_saved', {SETTING:setting.name})) + growl.success(_t('customization_of_SETTING_successfully_saved', {SETTING:_t(setting.name)})) , (error)-> console.log(error) diff --git a/app/assets/javascripts/controllers/admin/statistics.coffee b/app/assets/javascripts/controllers/admin/statistics.coffee.erb similarity index 72% rename from app/assets/javascripts/controllers/admin/statistics.coffee rename to app/assets/javascripts/controllers/admin/statistics.coffee.erb index 454b9961f..53d289217 100644 --- a/app/assets/javascripts/controllers/admin/statistics.coffee +++ b/app/assets/javascripts/controllers/admin/statistics.coffee.erb @@ -1,7 +1,9 @@ 'use strict' -Application.Controllers.controller "StatisticsController", ["$scope", "$state", "$rootScope", "Statistics", "es", "Member", '_t', 'membersPromise', 'statisticsPromise' -, ($scope, $state, $rootScope, Statistics, es, Member, _t, membersPromise, statisticsPromise) -> + + +Application.Controllers.controller "StatisticsController", ["$scope", "$state", "$rootScope", '$uibModal', "es", "Member", '_t', 'membersPromise', 'statisticsPromise' +, ($scope, $state, $rootScope, $uibModal, es, Member, _t, membersPromise, statisticsPromise) -> @@ -53,9 +55,13 @@ Application.Controllers.controller "StatisticsController", ["$scope", "$state", ## total of the stat field for non simple types $scope.sumStat = 0 + ## Results of custom aggregations for the current type + $scope.customAggs = {} + ## default: results are not sorted $scope.sorting = ca: 'none' + date: 'desc' ## active tab will be set here $scope.selectedIndex = null @@ -148,6 +154,7 @@ Application.Controllers.controller "StatisticsController", ["$scope", "$state", $scope.customFilter.value = null $scope.customFilter.exclude = false $scope.sorting.ca = 'none' + $scope.sorting.date = 'desc' buildCustomFiltersList() refreshStats() @@ -271,6 +278,33 @@ Application.Controllers.controller "StatisticsController", ["$scope", "$state", + ## + # Open a modal dialog asking the user for details about exporting the statistics tables to an excel file + ## + $scope.exportToExcel = -> + options = + templateUrl: '<%= asset_path "admin/statistics/export.html" %>' + size: 'sm' + controller: 'ExportStatisticsController' + resolve: + dates: -> + start: $scope.datePickerStart.selected + end: $scope.datePickerEnd.selected + query: -> + custom = buildCustomFilterQuery() + buildElasticDataQuery($scope.type.active.key, custom, $scope.agePicker.start, $scope.agePicker.end, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected), $scope.sorting) + index: -> + key: $scope.selectedIndex.es_type_key + type: -> + key: $scope.type.active.key + + $uibModal.open options + .result['finally'](null).then (info)-> + console.log(info) + + + + ### PRIVATE SCOPE ### ## @@ -306,14 +340,10 @@ Application.Controllers.controller "StatisticsController", ["$scope", "$state", $scope.sumCA = 0 $scope.averageAge = 0 $scope.sumStat = 0 + $scope.customAggs = {} $scope.totalHits = null $scope.searchDate = new Date() - 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 + custom = buildCustomFilterQuery() 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) @@ -324,6 +354,8 @@ Application.Controllers.controller "StatisticsController", ["$scope", "$state", $scope.averageAge = Math.round(res.aggregations.average_age.value * 100) / 100 $scope.sumStat = res.aggregations.total_stat.value $scope.scrollId = res._scroll_id + for custom in $scope.type.active.custom_aggregations + $scope.customAggs[custom.field] = res.aggregations[custom.field].value @@ -347,6 +379,9 @@ Application.Controllers.controller "StatisticsController", ["$scope", "$state", "type": index "size": RESULTS_PER_PAGE "scroll": ES_SCROLL_TIME+'m' + "stat-type": type + "start-date": moment($scope.datePickerStart.selected).format() + "end-date": moment($scope.datePickerEnd.selected).format() "body": buildElasticDataQuery(type, custom, $scope.agePicker.start, $scope.agePicker.end, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected), $scope.sorting) , (error, response) -> if (error) @@ -429,7 +464,7 @@ Application.Controllers.controller "StatisticsController", ["$scope", "$state", "field": "age" "total_stat": "sum": - "field": "sta" + "field": "stat" } q @@ -485,8 +520,141 @@ Application.Controllers.controller "StatisticsController", ["$scope", "$state", + ## + # Build and return an object according to the custom filter set by the user, used to request elasticsearch + # @return {Object|null} + ## + buildCustomFilterQuery = -> + custom = null + if !angular.isUndefinedOrNull($scope.customFilter.criterion) and + !angular.isUndefinedOrNull($scope.customFilter.criterion.key) and + !angular.isUndefinedOrNull($scope.customFilter.value) + custom = {} + custom.key = $scope.customFilter.criterion.key + custom.value = $scope.customFilter.value + custom.exclude = $scope.customFilter.exclude + custom + + # init the controller (call at the end !) initialize() ] + + + +Application.Controllers.controller 'ExportStatisticsController', [ '$scope', '$uibModalInstance', 'Export','dates', 'query', 'index', 'type', 'CSRF', 'growl', '_t' +, ($scope, $uibModalInstance, Export, dates, query, index, type, CSRF, growl, _t) -> + + ## Retrieve Anti-CSRF tokens from cookies + CSRF.setMetaTags() + + ## Bindings for date range + $scope.dates = dates + + ## Body of the query to export + $scope.query = JSON.stringify(query) + + ## API URL where the form will be posted + $scope.actionUrl = '/stats/'+index.key+'/export' + + ## Key of the current search' statistic type + $scope.typeKey = type.key + + ## Form action on the above URL + $scope.method = "post" + + ## Anti-CSRF token to inject into the download form + $scope.csrfToken = angular.element('meta[name="csrf-token"]')[0].content + + ## Binding of the export type (global / current) + $scope.export = + type: 'current' + + ## datePicker parameters for interval beginning + $scope.exportStart = + format: Fablab.uibDateFormat + opened: false # default: datePicker is not shown + minDate: null + maxDate: moment().subtract(1, 'day').toDate() + options: + startingDay: Fablab.weekStartingDay + + ## datePicker parameters for interval ending + $scope.exportEnd = + format: Fablab.uibDateFormat + opened: false # default: datePicker is not shown + minDate: null + maxDate: moment().subtract(1, 'day').toDate() + options: + startingDay: Fablab.weekStartingDay + + + + ## + # Callback to open the datepicker (interval start) + # @param $event {Object} jQuery event object + ## + $scope.toggleStartDatePicker = ($event) -> + $scope.exportStart.opened = !$scope.exportStart.opened + + + + ## + # Callback to open the datepicker (interval end) + # @param $event {Object} jQuery event object + ## + $scope.toggleEndDatePicker = ($event) -> + $scope.exportEnd.opened = !$scope.exportEnd.opened + + + + ## + # Callback when exchanging the export type between 'global' and 'current view' + # Adjust the query and the requesting url according to this type. + ## + $scope.setRequest = -> + if $scope.export.type == 'global' + $scope.actionUrl = '/stats/global/export' + $scope.query = JSON.stringify( + "query": + "bool": + "must": [ + { + "range": + "date": + "gte": moment($scope.dates.start).format() + "lte": moment($scope.dates.end).format() + } + ] + ) + else + $scope.actionUrl = '/stats/'+index.key+'/export' + $scope.query = JSON.stringify(query) + + + + ## + # Callback to close the modal, telling the caller what is exported + ## + $scope.exportData = -> + statusQry = {category: 'statistics', type: $scope.export.type, query: $scope.query} + unless $scope.export.type == 'global' + statusQry['type'] = index.key + statusQry['key'] = type.key + + Export.status(statusQry).then (res) -> + unless (res.data.exists) + growl.success _t('export_is_running_you_ll_be_notified_when_its_ready') + + $uibModalInstance.close(statusQry) + + + + ## + # Callback to cancel the export and close the modal + ## + $scope.cancel = -> + $uibModalInstance.dismiss('cancel') +] diff --git a/app/assets/javascripts/controllers/admin/trainings.coffee.erb b/app/assets/javascripts/controllers/admin/trainings.coffee.erb index 9672cc9d6..6ae3e12ab 100644 --- a/app/assets/javascripts/controllers/admin/trainings.coffee.erb +++ b/app/assets/javascripts/controllers/admin/trainings.coffee.erb @@ -1,7 +1,148 @@ 'use strict' -Application.Controllers.controller "TrainingsController", ["$scope", "$state", "$uibModal", 'Training', 'trainingsPromise', 'machinesPromise', '_t', 'growl' -, ($scope, $state, $uibModal, Training, trainingsPromise, machinesPromise, _t, growl) -> +### COMMON CODE ### + +## +# Provides a set of common callback methods to the $scope parameter. These methods are used +# in the various trainings' admin controllers. +# +# Provides : +# - $scope.submited(content) +# - $scope.fileinputClass(v) +# +# Requires : +# - $state (Ui-Router) [ 'app.admin.trainings' ] +## +class TrainingsController + constructor: ($scope, $state) -> + + ## + # For use with ngUpload (https://github.com/twilson63/ngUpload). + # Intended to be the callback when the upload is done: any raised error will be stacked in the + # $scope.alerts array. If everything goes fine, the user is redirected to the trainings list. + # @param content {Object} JSON - The upload's result + ## + $scope.submited = (content) -> + if !content.id? + $scope.alerts = [] + angular.forEach content, (v, k)-> + angular.forEach v, (err)-> + $scope.alerts.push + msg: k+': '+err + type: 'danger' + else + $state.go('app.admin.trainings') + + + + ## + # Changes the current user's view, redirecting him to the machines list + ## + $scope.cancel = -> + $state.go('app.admin.trainings') + + + + ## + # For use with 'ng-class', returns the CSS class name for the uploads previews. + # The preview may show a placeholder or the content of the file depending on the upload state. + # @param v {*} any attribute, will be tested for truthiness (see JS evaluation rules) + ## + $scope.fileinputClass = (v)-> + if v + 'fileinput-exists' + else + 'fileinput-new' + + + +## +# Controller used in the training creation page (admin) +## +Application.Controllers.controller "NewTrainingController", [ '$scope', '$state', 'machinesPromise', 'CSRF' +, ($scope, $state, machinesPromise, CSRF) -> + + + + ### PUBLIC SCOPE ### + + ## Form action on the following URL + $scope.method = 'post' + + ## API URL where the form will be posted + $scope.actionUrl = '/api/trainings/' + + ## list of machines + $scope.machines = machinesPromise + + + + ### PRIVATE SCOPE ### + + ## + # Kind of constructor: these actions will be realized first when the controller is loaded + ## + initialize = -> + CSRF.setMetaTags() + + ## Using the TrainingsController + new TrainingsController($scope, $state) + + + ## !!! MUST BE CALLED AT THE END of the controller + initialize() +] + + + +## +# Controller used in the training edition page (admin) +## +Application.Controllers.controller "EditTrainingController", [ '$scope', '$state', '$stateParams', 'trainingPromise', 'machinesPromise', 'CSRF' +, ($scope, $state, $stateParams, trainingPromise, machinesPromise, CSRF) -> + + + + ### PUBLIC SCOPE ### + + ## Form action on the following URL + $scope.method = 'patch' + + ## API URL where the form will be posted + $scope.actionUrl = '/api/trainings/' + $stateParams.id + + ## Details of the training to edit (id in URL) + $scope.training = trainingPromise + + ## list of machines + $scope.machines = machinesPromise + + + + ### PRIVATE SCOPE ### + + ## + # Kind of constructor: these actions will be realized first when the controller is loaded + ## + initialize = -> + CSRF.setMetaTags() + + ## Using the TrainingsController + new TrainingsController($scope, $state) + + + ## !!! MUST BE CALLED AT THE END of the controller + initialize() +] + + + + +## +# Controller used in the trainings management page, allowing admins users to see and manage the list of trainings and reservations. +## +Application.Controllers.controller "TrainingsAdminController", ["$scope", "$state", "$uibModal", 'Training', 'trainingsPromise', 'machinesPromise', '_t', 'growl', 'dialogs' +, ($scope, $state, $uibModal, Training, trainingsPromise, machinesPromise, _t, growl, dialogs) -> @@ -40,35 +181,6 @@ Application.Controllers.controller "TrainingsController", ["$scope", "$state", " - ## - # Create a new empty training object and append it to the $scope.trainings list - ## - $scope.addTraining = -> - $scope.inserted = - name: '' - machine_ids: [] - $scope.trainings.push($scope.inserted) - - - - ## - # Saves a new training / Update an existing training to the server (form validation callback) - # @param data {Object} training name, associated machine(s) and default places number - # @param id {number} training id, in case of update - ## - $scope.saveTraining = (data, id) -> - if id? - Training.update {id: id}, - training: data - else - Training.save - training: data - , (resp) -> - $scope.trainings[$scope.trainings.length-1] = resp - console.log(resp) - - - ## # Removes the newly inserted but not saved training / Cancel the current training modification # @param rowform {Object} see http://vitalets.github.io/angular-xeditable/ @@ -138,30 +250,17 @@ Application.Controllers.controller "TrainingsController", ["$scope", "$state", " # @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 - ] - ) + dialogs.confirm + resolve: + object: -> + title: _t('confirmation_required') + msg: _t('do_you_really_want_to_delete_this_training') + , -> # deletion confirmed + 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')) @@ -197,8 +296,15 @@ Application.Controllers.controller "TrainingsController", ["$scope", "$state", " # The selected training details will be loaded from the API and rendered into the accordions. ## $scope.selectTrainingToMonitor = -> - Training.get {id: $scope.monitoring.training.id}, (training) -> + Training.availabilities {id: $scope.monitoring.training.id}, (training) -> $scope.groupedAvailabilities = groupAvailabilities([training]) + # we open current year/month by default + now = moment() + $scope.accordions[training.name] = {} + $scope.accordions[training.name][now.year()] = + isOpenFirst: true + $scope.accordions[training.name][now.year()][now.month()] = + isOpenFirst: true diff --git a/app/assets/javascripts/controllers/application.coffee.erb b/app/assets/javascripts/controllers/application.coffee.erb index d9b12e334..095c50892 100644 --- a/app/assets/javascripts/controllers/application.coffee.erb +++ b/app/assets/javascripts/controllers/application.coffee.erb @@ -1,7 +1,7 @@ 'use strict' -Application.Controllers.controller 'ApplicationController', ["$rootScope", "$scope", "$window", "Session", "AuthService", "Auth", "$uibModal", "$state", 'growl', 'Notification', '$interval', "Setting", '_t' -, ($rootScope, $scope, $window, Session, AuthService, Auth, $uibModal, $state, growl, Notification, $interval, Setting, _t) -> +Application.Controllers.controller 'ApplicationController', ["$rootScope", "$scope", "$window", "Session", "AuthService", "Auth", "$uibModal", "$state", 'growl', 'Notification', '$interval', "Setting", '_t', 'Version' +, ($rootScope, $scope, $window, Session, AuthService, Auth, $uibModal, $state, growl, Notification, $interval, Setting, _t, Version) -> @@ -14,14 +14,24 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco ### PUBLIC SCOPE ### + ## Fab-manager's version + $scope.version = + version: '' + ## # Set the current user to the provided value and initialize the session # @param user {Object} Rails/Devise user ## $scope.setCurrentUser = (user) -> - $rootScope.currentUser = user - Session.create(user); - getNotifications() + unless angular.isUndefinedOrNull(user) + $rootScope.currentUser = user + Session.create(user); + getNotifications() + # fab-manager's app-version + if user.role == 'admin' + $scope.version = Version.get() + else + $scope.version = {version: ''} ## @@ -88,6 +98,7 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco # default user's parameters $scope.user = is_allow_contact: true + is_allow_newsletter: false # Errors display $scope.alerts = [] @@ -98,11 +109,18 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco $scope.ok = -> # try to create the account $scope.alerts = [] + # remove 'organization' attribute + orga = $scope.user.organization + delete $scope.user.organization + # register on server Auth.register($scope.user).then (user) -> # creation successful $uibModalInstance.close(user) , (error) -> # creation failed... + # restore organization param + $scope.user.organization = orga + # display errors angular.forEach error.data.errors, (v, k) -> angular.forEach v, (err) -> $scope.alerts.push @@ -197,6 +215,7 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco # try to retrieve any currently logged user Auth.login().then (user) -> $scope.setCurrentUser(user) + # force users to complete their profile if they are not if user.need_completion $state.transitionTo('app.logged.profileCompletion') , (error) -> diff --git a/app/assets/javascripts/controllers/calendar.coffee b/app/assets/javascripts/controllers/calendar.coffee new file mode 100644 index 000000000..bb020c1ff --- /dev/null +++ b/app/assets/javascripts/controllers/calendar.coffee @@ -0,0 +1,168 @@ +'use strict' + +## +# Controller used in the public calendar global +## + +Application.Controllers.controller "CalendarController", ["$scope", "$state", "$aside", "moment", "Availability", 'Slot', 'Setting', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', '_t', 'uiCalendarConfig', 'CalendarConfig', 'trainingsPromise', 'machinesPromise', +($scope, $state, $aside, moment, Availability, Slot, Setting, growl, dialogs, bookingWindowStart, bookingWindowEnd, _t, uiCalendarConfig, CalendarConfig, trainingsPromise, machinesPromise) -> + + + ### PRIVATE STATIC CONSTANTS ### + currentMachineEvent = null + machinesPromise.forEach((m) -> m.checked = true) + trainingsPromise.forEach((t) -> t.checked = true) + + ## check all formation/machine is select in filter + isSelectAll = (type, scope) -> + scope[type].length == scope[type].filter((t) -> t.checked).length + + ### PUBLIC SCOPE ### + + ## List of trainings + $scope.trainings = trainingsPromise + + ## List of machines + $scope.machines = machinesPromise + + ## add availabilities source to event sources + $scope.eventSources = [] + + ## filter availabilities if have change + $scope.filterAvailabilities = (filter, scope) -> + scope ||= $scope + scope.filter = $scope.filter = + trainings: isSelectAll('trainings', scope) + machines: isSelectAll('machines', scope) + evt: filter.evt + dispo: filter.dispo + $scope.calendarConfig.events = availabilitySourceUrl() + + + ## a variable for formation/machine/event/dispo checkbox is or not checked + $scope.filter = + trainings: isSelectAll('trainings', $scope) + machines: isSelectAll('machines', $scope) + evt: true + dispo: true + + ## toggle to select all formation/machine + $scope.toggleFilter = (type, filter) -> + $scope[type].forEach((t) -> t.checked = filter[type]) + $scope.filterAvailabilities(filter, $scope) + + $scope.openFilterAside = -> + $aside.open + templateUrl: 'filterAside.html' + placement: 'right' + size: 'md' + backdrop: false + resolve: + trainings: -> + $scope.trainings + machines: -> + $scope.machines + filter: -> + $scope.filter + toggleFilter: -> + $scope.toggleFilter + filterAvailabilities: -> + $scope.filterAvailabilities + controller: ['$scope', '$uibModalInstance', 'trainings', 'machines', 'filter', 'toggleFilter', 'filterAvailabilities', ($scope, $uibModalInstance, trainings, machines, filter, toggleFilter, filterAvailabilities) -> + $scope.trainings = trainings + $scope.machines = machines + $scope.filter = filter + + $scope.toggleFilter = (type, filter) -> + toggleFilter(type, filter) + + $scope.filterAvailabilities = (filter) -> + filterAvailabilities(filter, $scope) + + $scope.close = (e) -> + $uibModalInstance.dismiss() + e.stopPropagation() + ] + + + ### PRIVATE SCOPE ### + + calendarEventClickCb = (event, jsEvent, view) -> + ## current calendar object + calendar = uiCalendarConfig.calendars.calendar + if event.available_type == 'machines' + currentMachineEvent = event + calendar.fullCalendar('changeView', 'agendaDay') + calendar.fullCalendar('gotoDate', event.start) + else + if event.available_type == 'event' + $state.go('app.public.events_show', {id: event.event_id}) + else if event.available_type == 'training' + $state.go('app.public.training_show', {id: event.training_id}) + else + $state.go('app.public.machines_show', {id: event.machine_id}) + + ## agendaDay view: disable slotEventOverlap + ## agendaWeek view: enable slotEventOverlap + toggleSlotEventOverlap = (view) -> + # set defaultView, because when we change slotEventOverlap + # ui-calendar will trigger rerender calendar + $scope.calendarConfig.defaultView = view.type + today = if currentMachineEvent then currentMachineEvent.start else moment().utc().startOf('day') + if today > view.start and today < view.end and today != view.start + $scope.calendarConfig.defaultDate = today + else + $scope.calendarConfig.defaultDate = view.start + if view.type == 'agendaDay' + $scope.calendarConfig.slotEventOverlap = false + else + $scope.calendarConfig.slotEventOverlap = true + + ## function is called when calendar view is rendered or changed + viewRenderCb = (view, element) -> + toggleSlotEventOverlap(view) + if view.type == 'agendaDay' + # get availabilties by 1 day for show machine slots + uiCalendarConfig.calendars.calendar.fullCalendar('refetchEvents') + + eventRenderCb = (event, element) -> + if event.tags.length > 0 + html = '' + for tag in event.tags + html += "#{tag.name} " + element.find('.fc-title').append("
"+html) + return + + getFilter = -> + t = $scope.trainings.filter((t) -> t.checked).map((t) -> t.id) + m = $scope.machines.filter((m) -> m.checked).map((m) -> m.id) + {t: t, m: m, evt: $scope.filter.evt, dispo: $scope.filter.dispo} + + availabilitySourceUrl = -> + "/api/availabilities/public?#{$.param(getFilter())}" + + initialize = -> + ## fullCalendar (v2) configuration + $scope.calendarConfig = CalendarConfig + events: availabilitySourceUrl() + slotEventOverlap: true + header: + left: 'month agendaWeek agendaDay' + center: 'title' + right: 'today prev,next' + minTime: moment.duration(moment(bookingWindowStart.setting.value).format('HH:mm:ss')) + maxTime: moment.duration(moment(bookingWindowEnd.setting.value).format('HH:mm:ss')) + defaultView: if window.innerWidth <= 480 then 'agendaDay' else 'agendaWeek' + eventClick: (event, jsEvent, view)-> + calendarEventClickCb(event, jsEvent, view) + viewRender: (view, element) -> + viewRenderCb(view, element) + eventRender: (event, element, view) -> + eventRenderCb(event, element) + + + + + ## !!! MUST BE CALLED AT THE END of the controller + initialize() +] diff --git a/app/assets/javascripts/controllers/events.coffee.erb b/app/assets/javascripts/controllers/events.coffee.erb index b74cd734d..39083272f 100644 --- a/app/assets/javascripts/controllers/events.coffee.erb +++ b/app/assets/javascripts/controllers/events.coffee.erb @@ -1,13 +1,7 @@ 'use strict' -Application.Controllers.controller "EventsController", ["$scope", "$state", 'Event', ($scope, $state, Event) -> - - - - ### PRIVATE STATIC CONSTANTS ### - - # Number of events added to the page when the user clicks on 'load next events' - EVENTS_PER_PAGE = 12 +Application.Controllers.controller "EventsController", ["$scope", "$state", 'Event', 'categoriesPromise', 'themesPromise', 'ageRangesPromise' +, ($scope, $state, Event, categoriesPromise, themesPromise, ageRangesPromise) -> @@ -16,33 +10,40 @@ Application.Controllers.controller "EventsController", ["$scope", "$state", 'Eve ## The events displayed on the page $scope.events = [] - ## By default, the pagination mode is activated to limit the page size - $scope.paginateActive = true - ## The currently displayed page number $scope.page = 1 + ## List of categories for the events + $scope.categories = categoriesPromise + + ## List of events themes + $scope.themes = themesPromise + + ## List of age ranges + $scope.ageRanges = ageRangesPromise + + ## Hide or show the 'load more' button + $scope.noMoreResults = false + + ## Active filters for the events list + $scope.filters = + category_id: null + theme_id: null + age_range_id: null + + + ## - # Adds EVENTS_PER_PAGE events to the bottom of the page, grouped by month + # Adds a resultset of events to the bottom of the page, grouped by month ## $scope.loadMoreEvents = -> - Event.query {page: $scope.page}, (data) -> + Event.query Object.assign({page: $scope.page}, $scope.filters), (data) -> $scope.events = $scope.events.concat data - if data.length > 0 - $scope.paginateActive = false if ($scope.page-2)*EVENTS_PER_PAGE+data.length >= data[0].nb_total_events + groupEvents($scope.events) + $scope.page += 1 - $scope.eventsGroupByMonth = _.groupBy($scope.events, (obj) -> - _.map ['month', 'year'], (key, value) -> obj[key] - ) - $scope.monthOrder = _.sortBy _.keys($scope.eventsGroupByMonth), (k)-> - monthYearArray = k.split(',') - date = new Date() - date.setMonth(monthYearArray[0]) - date.setYear(monthYearArray[1]) - return -date.getTime() - else - $scope.paginateActive = false - $scope.page += 1 + if (!data[0] || data[0].nb_total_events <= $scope.events.length) + $scope.noMoreResults = true @@ -55,13 +56,69 @@ Application.Controllers.controller "EventsController", ["$scope", "$state", 'Eve + ## + # Callback to refresh the events list according to the filters set + ## + $scope.filterEvents = -> + # reinitialize results datasets + $scope.page = 1 + $scope.eventsGroupByMonth = {} + $scope.events = [] + $scope.monthOrder = [] + $scope.noMoreResults = false + + # run a search query + Event.query Object.assign({page: $scope.page}, $scope.filters), (data) -> + $scope.events = data + groupEvents(data) + $scope.page += 1 + + if (!data[0] || data[0].nb_total_events <= $scope.events.length) + $scope.noMoreResults = true + + + + ## + # Test if the provided event occurs on a single day or on many days + # @param event {{start_date:Date, end_date:Date}} Event object as retreived from the API + # @return {boolean} false if the event occurs on many days + ## + $scope.onSingleDay = (event) -> + moment(event.start_date).isSame(event.end_date, 'day') + + + ### PRIVATE SCOPE ### ## # Kind of constructor: these actions will be realized first when the controller is loaded ## initialize = -> - $scope.loadMoreEvents() + $scope.filterEvents() + + + + ## + # Group the provided events by month/year and concat them with existing results + # Then compute the ordered list of months for the complete resultset. + # Affect the resulting events groups in $scope.eventsGroupByMonth and the ordered month keys in $scope.monthOrder. + # @param {Array} Events retrived from the API + ## + groupEvents = (events) -> + if events.length > 0 + eventsGroupedByMonth = _.groupBy(events, (obj) -> + _.map ['month', 'year'], (key, value) -> obj[key] + ) + $scope.eventsGroupByMonth = Object.assign($scope.eventsGroupByMonth, eventsGroupedByMonth) + + monthsOrder = _.sortBy _.keys($scope.eventsGroupByMonth), (k)-> + monthYearArray = k.split(',') + date = new Date() + date.setMonth(monthYearArray[0]) + date.setYear(monthYearArray[1]) + return -date.getTime() + + $scope.monthOrder = monthsOrder @@ -75,13 +132,12 @@ Application.Controllers.controller "EventsController", ["$scope", "$state", 'Eve -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) -> +Application.Controllers.controller "ShowEventController", ["$scope", "$state", "$stateParams", "Event", '$uibModal', 'Member', 'Reservation', 'Price', 'CustomAsset', 'eventPromise', 'growl', '_t', 'Wallet', 'helpers', 'priceCategoriesPromise', 'settingsPromise', +($scope, $state, $stateParams, Event, $uibModal, Member, Reservation, Price, CustomAsset, eventPromise, growl, _t, Wallet, helpers, priceCategoriesPromise, settingsPromise) -> ### PUBLIC SCOPE ### - $scope.reducedAmountAlert = reducedAmountAlert.setting.value ## reservations for the currently shown event $scope.reservations = [] @@ -92,18 +148,30 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", " ## parameters for a new reservation $scope.reserve = - nbPlaces: [] - nbReducedPlaces: [] + nbPlaces: + normal: [] nbReservePlaces: 0 - nbReserveReducedPlaces: 0 + tickets: {} toReserve: false amountTotal : 0 + totalSeats: 0 + ## Discount coupon to apply to the basket, if any + $scope.coupon = + applied: null - - # get the details for the current event (event's id is recovered from the current URL) + ## Get the details for the current event (event's id is recovered from the current URL) $scope.event = eventPromise + ## List of price categories for the events + $scope.priceCategories = priceCategoriesPromise + + ## 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) + ## @@ -117,21 +185,27 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", " ## - # Callback to call when the number of places change in the current booking + # Callback to call when the number of tickets to book changes in the current booking ## $scope.changeNbPlaces = -> - reste = $scope.event.nb_free_places - $scope.reserve.nbReservePlaces - $scope.reserve.nbReducedPlaces = [0..reste] - $scope.computeEventAmount() + # compute the total remaing places + remain = $scope.event.nb_free_places - $scope.reserve.nbReservePlaces + for ticket of $scope.reserve.tickets + remain -= $scope.reserve.tickets[ticket] + # we store the total number of seats booked, this is used to know if the 'pay' button must be shown + $scope.reserve.totalSeats = $scope.event.nb_free_places - remain + # update the availables seats for full price tickets + fullPriceRemains = $scope.reserve.nbReservePlaces + remain + $scope.reserve.nbPlaces.normal = [0..fullPriceRemains] + # update the available seats for other prices tickets + for key of $scope.reserve.nbPlaces + if key != 'normal' + priceRemain = $scope.reserve.tickets[key] + remain + $scope.reserve.nbPlaces[key] = [0..priceRemain] - ## - # 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] + # recompute the total price $scope.computeEventAmount() @@ -185,11 +259,13 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", " 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) + Wallet.getWalletByUser {user_id: $scope.ctrl.member.id}, (wallet) -> + amountToPay = helpers.getAmountToPay($scope.reserve.amountTotal, wallet.amount) + if $scope.currentUser.role isnt 'admin' and amountToPay > 0 + payByStripe(reservation) + else + if $scope.currentUser.role is 'admin' or amountToPay 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')) @@ -206,20 +282,31 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", " reservable_type: 'Event' slots_attributes: [] nb_reserve_places: $scope.reserve.nbReservePlaces - nb_reserve_reduced_places: $scope.reserve.nbReserveReducedPlaces + tickets_attributes: [] + # a single slot is used for events reservation.slots_attributes.push start_at: $scope.event.start_date end_at: $scope.event.end_date availability_id: $scope.event.availability.id + # iterate over reservations per prices + for price_id, seats of $scope.reserve.tickets + reservation.tickets_attributes.push + event_price_category_id: price_id + booked: seats + # set the attempting marker $scope.attempting = true + # save the reservation to the API Reservation.save reservation: reservation, (reservation) -> + # reservation successfull afterPayment(reservation) $scope.attempting = false , (response)-> + # reservation failed $scope.alerts = [] $scope.alerts.push msg: response.data.card[0] type: 'danger' + # unset the attempting marker $scope.attempting = false @@ -227,7 +314,7 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", " ## # 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 reservation {{id:number, reservable_id:number, nb_reserve_places:number}} # @param e {Object} see https://docs.angularjs.org/guide/expression#-event- ## $scope.modifyReservation = (reservation, e)-> @@ -247,7 +334,7 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", " # 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) + if e.nb_free_places > reservation.total_booked_seats $scope.reservation.reservable_id = e.id break @@ -277,23 +364,29 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", " $uibModalInstance.dismiss('cancel') ] .result['finally'](null).then (reservation)-> + # remove the reservation from the user's reservations list for this event (occurrence) $scope.reservations.splice(index, 1) - $scope.event.nb_free_places = $scope.event.nb_free_places + reservation.nb_reserve_places + reservation.nb_reserve_reduced_places + # add the number of places transfered (to the new date) to the total of free places for this event + $scope.event.nb_free_places = $scope.event.nb_free_places + reservation.total_booked_seats + # remove the number of places transfered from the total of free places of the receiving occurrance 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 + if e.id is parseInt(reservation.reservable.id, 10) + e.nb_free_places = e.nb_free_places - reservation.total_booked_seats ## - # Checks if the provided reservation is able to be modified - # @param reservation {{nb_reserve_places:number, nb_reserve_reduced_places:number}} + # Checks if the provided reservation is able to be moved (date change) + # @param reservation {{total_booked_seats:number}} ## $scope.reservationCanModify = (reservation)-> + slotStart = moment(reservation.slots[0].start_at) + now = moment() + 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 + isAble = true if e.nb_free_places >= reservation.total_booked_seats + return (isAble and $scope.enableBookingMove and slotStart.diff(now, "hours") >= $scope.moveBookingDelay) @@ -305,36 +398,62 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", " # 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) -> + Price.compute mkRequestParams(r, $scope.coupon.applied), (res) -> $scope.reserve.amountTotal = res.price else $scope.reserve.amountTotal = null + ## + # Return the URL allowing to share the current project on the Facebook social network + ## + $scope.shareOnFacebook = -> + 'https://www.facebook.com/share.php?u='+$state.href('app.public.events_show', {id: $scope.event.id}, {absolute: true}).replace('#', '%23') + + + + ## + # Return the URL allowing to share the current project on the Twitter social network + ## + $scope.shareOnTwitter = -> + 'https://twitter.com/intent/tweet?url='+encodeURIComponent($state.href('app.public.events_show', {id: $scope.event.id}, {absolute: true}))+'&text='+encodeURIComponent($scope.event.title) + + + + ## + # Return the textual description of the conditions applyable to the given price's category + # @param category_id {number} ID of the price's category + ## + $scope.getPriceCategoryConditions = (category_id) -> + for cat in $scope.priceCategories + if cat.id == category_id + return cat.conditions + + + ### 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 + # set the controlled user as the current user if the current user is not an admin if $scope.currentUser if $scope.currentUser.role isnt 'admin' $scope.ctrl.member = $scope.currentUser - # 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] + resetEventReserve() # if non-admin, get the current user's reservations into $scope.reservations if $scope.currentUser getReservations($scope.event.id, 'Event', $scope.currentUser.id) + # watch when a coupon is applied to re-compute the total price + $scope.$watch 'coupon.applied', (newValue, oldValue) -> + unless newValue == null and oldValue == null + $scope.computeEventAmount() ## @@ -353,8 +472,8 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", " # 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, nb_reserve_places:Number, nb_reserve_reduced_places:Number}} + # @param event {Object} Current event + # @return {{user_id:number, reservable_id:number, reservable_type:string, slots_attributes:Array, nb_reserve_places:number}} ## mkReservation = (member, reserve, event) -> reservation = @@ -363,7 +482,7 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", " reservable_type: 'Event' slots_attributes: [] nb_reserve_places: reserve.nbReservePlaces - nb_reserve_reduced_places: reserve.nbReserveReducedPlaces + tickets_attributes: [] reservation.slots_attributes.push start_at: event.start_date @@ -371,22 +490,49 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", " availability_id: event.availability.id offered: event.offered || false + for evt_px_cat in event.prices + booked = reserve.tickets[evt_px_cat.id] + if booked > 0 + reservation.tickets_attributes.push + event_price_category_id: evt_px_cat.id + booked: booked + reservation + ## + # Format the parameters expected by /api/prices/compute or /api/reservations and return the resulting object + # @param reservation {Object} as returned by mkReservation() + # @param coupon {Object} Coupon as returned from the API + # @return {{reservation:Object, coupon_code:string}} + ## + mkRequestParams = (reservation, coupon) -> + params = + reservation: reservation + coupon_code: (coupon.code if coupon) + + params + + ## # 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] + nbPlaces: + normal: [0..$scope.event.nb_free_places] nbReservePlaces: 0 - nbReserveReducedPlaces: 0 + tickets: {} toReserve: false amountTotal : 0 + totalSeats: 0 + + for evt_px_cat in $scope.event.prices + $scope.reserve.nbPlaces[evt_px_cat.id] = [0..$scope.event.nb_free_places] + $scope.reserve.tickets[evt_px_cat.id] = 0 + $scope.event.offered = false @@ -403,16 +549,24 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", " reservation: -> reservation price: -> - Price.compute({reservation: reservation}).$promise + Price.compute(mkRequestParams(reservation, $scope.coupon.applied)).$promise + wallet: -> + Wallet.getWalletByUser({user_id: reservation.user_id}).$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) -> + coupon: -> + $scope.coupon.applied + controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'cgv', 'Auth', 'Reservation', 'growl', 'wallet', 'helpers', '$locale', '$filter', 'coupon', + ($scope, $uibModalInstance, $state, reservation, price, cgv, Auth, Reservation, growl, wallet, helpers, $locale, $filter, coupon) -> + # user wallet amount + $scope.walletAmount = wallet.amount + # Price - $scope.amount = price.price + $scope.amount = helpers.getAmountToPay(price.price, wallet.amount) # CGV $scope.cgv = cgv.custom_asset @@ -420,6 +574,10 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", " # Reservation $scope.reservation = reservation + $scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM + + $scope.numberFilter = $filter('number') + # Callback for the stripe payment authorization $scope.payment = (status, response) -> if response.error @@ -427,7 +585,7 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", " else $scope.attempting = true $scope.reservation.card_token = response.id - Reservation.save reservation: $scope.reservation, (reservation) -> + Reservation.save mkRequestParams($scope.reservation, coupon), (reservation) -> $uibModalInstance.close(reservation) , (response)-> $scope.alerts = [] @@ -453,24 +611,42 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", " reservation: -> reservation price: -> - Price.compute({reservation: reservation}).$promise - controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', ($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation) -> + Price.compute(mkRequestParams(reservation, $scope.coupon.applied)).$promise + wallet: -> + Wallet.getWalletByUser({user_id: reservation.user_id}).$promise + coupon: -> + $scope.coupon.applied + controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', 'wallet', '$locale', 'helpers', '$filter', 'coupon', + ($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation, wallet, $locale, helpers, $filter, coupon) -> + # user wallet amount + $scope.walletAmount = wallet.amount + # Price - $scope.amount = price.price + $scope.price = price.price + + # price to pay + $scope.amount = helpers.getAmountToPay(price.price, wallet.amount) # Reservation $scope.reservation = reservation + $scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM + + $scope.numberFilter = $filter('number') + # Button label if $scope.amount > 0 - $scope.validButtonName = _t('confirm_(payment_on_site)') + $scope.validButtonName = _t('confirm_payment_of_html', {ROLE:$scope.currentUser.role, AMOUNT:$filter('currency')($scope.amount)}, "messageformat") else - $scope.validButtonName = _t('confirm') + if price.price > 0 and $scope.walletAmount == 0 + $scope.validButtonName = _t('confirm_payment_of_html', {ROLE:$scope.currentUser.role, AMOUNT:$filter('currency')(price.price)}, "messageformat") + else + $scope.validButtonName = _t('confirm') # Callback to validate the payment $scope.ok = -> $scope.attempting = true - Reservation.save reservation: $scope.reservation, (reservation) -> + Reservation.save mkRequestParams($scope.reservation, coupon), (reservation) -> $uibModalInstance.close(reservation) $scope.attempting = true , (response)-> @@ -496,7 +672,7 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", " # @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 + $scope.event.nb_free_places = $scope.event.nb_free_places - reservation.total_booked_seats resetEventReserve() $scope.reserveSuccess = true $scope.reservations.push reservation diff --git a/app/assets/javascripts/controllers/machines.coffee.erb b/app/assets/javascripts/controllers/machines.coffee.erb index a5ee09123..1144abe3f 100644 --- a/app/assets/javascripts/controllers/machines.coffee.erb +++ b/app/assets/javascripts/controllers/machines.coffee.erb @@ -126,7 +126,7 @@ _reserveMachine = (machine, e) -> # modal is close with validation $scope.ok = -> - $state.go('app.logged.trainings_reserve') + $state.go('app.logged.trainings_reserve', {id: $scope.machine.trainings[0].id}) $uibModalInstance.close(machine) # modal is closed with escaping @@ -231,8 +231,8 @@ Application.Controllers.controller "EditMachineController", ["$scope", '$state', ## # Controller used in the machine details page (public) ## -Application.Controllers.controller "ShowMachineController", ['$scope', '$state', '$uibModal', '$stateParams', '_t', 'Machine', 'growl', 'machinePromise' -, ($scope, $state, $uibModal, $stateParams, _t, Machine, growl, machinePromise) -> +Application.Controllers.controller "ShowMachineController", ['$scope', '$state', '$uibModal', '$stateParams', '_t', 'Machine', 'growl', 'machinePromise', 'dialogs' +, ($scope, $state, $uibModal, $stateParams, _t, Machine, growl, machinePromise, dialogs) -> ## Retrieve the details for the machine id in the URL, if an error occurs redirect the user to the machines list $scope.machine = machinePromise @@ -245,11 +245,17 @@ Application.Controllers.controller "ShowMachineController", ['$scope', '$state', if $scope.currentUser.role isnt 'admin' console.error _t('unauthorized_operation') else - # delete the machine then redirect to the machines listing - machine.$delete -> - $state.go('app.public.machines_list') - , (error)-> - growl.warning(_t('the_machine_cant_be_deleted_because_it_is_already_reserved_by_some_users')) + dialogs.confirm + resolve: + object: -> + title: _t('confirmation_required') + msg: _t('do_you_really_want_to_delete_this_machine') + , -> # deletion confirmed + # delete the machine then redirect to the machines listing + machine.$delete -> + $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 ## @@ -268,38 +274,26 @@ Application.Controllers.controller "ShowMachineController", ['$scope', '$state', # 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) -> +Application.Controllers.controller "ReserveMachineController", ["$scope", "$state", '$stateParams', "$uibModal", '_t', "moment", 'Machine', 'Auth', 'dialogs', '$timeout', 'Price', 'Member', 'Availability', 'Slot', 'Setting', 'CustomAsset', 'plansPromise', 'groupsPromise', 'growl', 'machinePromise', 'settingsPromise', 'Wallet', 'helpers', 'uiCalendarConfig', 'CalendarConfig', +($scope, $state, $stateParams, $uibModal, _t, moment, Machine, Auth, dialogs, $timeout, Price, Member, Availability, Slot, Setting, CustomAsset, plansPromise, groupsPromise, growl, machinePromise, settingsPromise, Wallet, helpers, uiCalendarConfig, CalendarConfig) -> ### 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' + FREE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::MACHINE_COLOR %>' # Slot already booked by another user - UNAVAILABLE_SLOT_BORDER_COLOR = '#1d98ec' + UNAVAILABLE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::MACHINE_IS_RESERVED_BY_USER %>' # Slot free to be booked - BOOKED_SLOT_BORDER_COLOR = '#b2e774' + BOOKED_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::IS_RESERVED_BY_CURRENT_USER %>' ### 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 = [] @@ -321,12 +315,13 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat ## total amount of the bill to pay $scope.amountTotal = 0 + ## Discount coupon to apply to the basket, if any + $scope.coupon = + applied: null + ## 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 @@ -340,34 +335,12 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat member: {} ## current machine to reserve - $scope.machine = {} + $scope.machine = machinePromise ## 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 + $scope.calendarConfig = CalendarConfig + minTime: moment.duration(moment(settingsPromise.booking_window_start).format('HH:mm:ss')) + maxTime: moment.duration(moment(settingsPromise.booking_window_end).format('HH:mm:ss')) eventClick: (event, jsEvent, view) -> calendarEventClickCb(event, jsEvent, view) eventRender: (event, element, view) -> @@ -391,12 +364,6 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat ## 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')) - ## @@ -412,7 +379,7 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat $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' + uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' @@ -425,7 +392,7 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat $scope.slotToPlace.backgroundColor = 'white' $scope.slotToPlace.title = '' $scope.slotToPlace = null - $scope.calendar.fullCalendar 'rerenderEvents' + uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' @@ -458,7 +425,7 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat $scope.slotToModify.can_modify = false $scope.slotToModify = null - $scope.calendar.fullCalendar 'rerenderEvents' + uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' , (err) -> # failure growl.error(_t('unable_to_change_the_reservation')) console.error(err) @@ -476,7 +443,7 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat $scope.slotToModify.backgroundColor = 'white' $scope.slotToModify = null - $scope.calendar.fullCalendar 'rerenderEvents' + uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' @@ -532,8 +499,8 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat $scope.plansAreShown = false updateCartPrice() $timeout -> - $scope.calendar.fullCalendar 'refetchEvents' - $scope.calendar.fullCalendar 'rerenderEvents' + uiCalendarConfig.calendars.calendar.fullCalendar 'refetchEvents' + uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' @@ -569,11 +536,13 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat 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) + Wallet.getWalletByUser {user_id: $scope.ctrl.member.id}, (wallet) -> + amountToPay = helpers.getAmountToPay($scope.amountTotal, wallet.amount) + if $scope.currentUser.role isnt 'admin' and amountToPay > 0 + payByStripe(reservation) + else + if $scope.currentUser.role is 'admin' or amountToPay 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')) @@ -637,11 +606,10 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat if $scope.currentUser.role isnt 'admin' $scope.ctrl.member = $scope.currentUser - $scope.machine = Machine.get {id: $stateParams.id} - , -> - return - , -> - $state.go('app.public.machines_list') + # watch when a coupon is applied to re-compute the total price + $scope.$watch 'coupon.applied', (newValue, oldValue) -> + unless newValue == null and oldValue == null + updateCartPrice() @@ -670,13 +638,28 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat + ## + # Format the parameters expected by /api/prices/compute or /api/reservations and return the resulting object + # @param reservation {Object} as returned by mkReservation() + # @param coupon {Object} Coupon as returned from the API + # @return {{reservation:Object, coupon_code:string}} + ## + mkRequestParams = (reservation, coupon) -> + params = + reservation: reservation + coupon_code: (coupon.code if coupon) + + params + + + ## # 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) -> + Price.compute mkRequestParams(r, $scope.coupon.applied), (res) -> $scope.amountTotal = res.price setSlotsDetails(res.details) else @@ -732,7 +715,7 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat $scope.slotToModify = event event.backgroundColor = '#eee' event.title = _t('i_change') - $scope.calendar.fullCalendar 'rerenderEvents' + uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' else if type == 'cancel' dialogs.confirm resolve: @@ -750,7 +733,7 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat $scope.canceledSlot.is_reserved = false $scope.canceledSlot.can_modify = false $scope.canceledSlot = null - $scope.calendar.fullCalendar 'rerenderEvents' + uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' , -> # error while canceling growl.error _t('cancellation_failed') , -> @@ -758,14 +741,14 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat $scope.selectedPlan = null $scope.modifiedSlots = null - $scope.calendar.fullCalendar 'rerenderEvents' + uiCalendarConfig.calendars.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/ @@ -776,6 +759,7 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat for tag in event.tags html += "#{tag.name}" element.find('.fc-time').append(html) + return @@ -791,12 +775,20 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat reservation: -> reservation price: -> - Price.compute({reservation: reservation}).$promise + Price.compute(mkRequestParams(reservation, $scope.coupon.applied)).$promise + wallet: -> + Wallet.getWalletByUser({user_id: reservation.user_id}).$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) -> + coupon: -> + $scope.coupon.applied + controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'cgv', 'Auth', 'Reservation', 'wallet', 'helpers', '$locale', '$filter', 'coupon', + ($scope, $uibModalInstance, $state, reservation, price, cgv, Auth, Reservation, wallet, helpers, $locale, $filter, coupon) -> + # user wallet amount + $scope.walletAmount = wallet.amount + # Price - $scope.amount = price.price + $scope.amount = helpers.getAmountToPay(price.price, wallet.amount) # CGV $scope.cgv = cgv.custom_asset @@ -804,6 +796,12 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat # Reservation $scope.reservation = reservation + # Currency symbol or abreviation for the current locale + $scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM + + # Used in wallet info template to interpolate some translations + $scope.numberFilter = $filter('number') + ## # Callback to process the payment with Stripe, triggered on button click ## @@ -813,7 +811,7 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat else $scope.attempting = true $scope.reservation.card_token = response.id - Reservation.save reservation: $scope.reservation, (reservation) -> + Reservation.save mkRequestParams($scope.reservation, coupon), (reservation) -> $uibModalInstance.close(reservation) , (response)-> $scope.alerts = [] @@ -839,27 +837,47 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat reservation: -> reservation price: -> - Price.compute({reservation: reservation}).$promise - controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', ($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation) -> + Price.compute(mkRequestParams(reservation, $scope.coupon.applied)).$promise + wallet: -> + Wallet.getWalletByUser({user_id: reservation.user_id}).$promise + coupon: -> + $scope.coupon.applied + controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', 'wallet', 'helpers', '$filter', '$locale', 'coupon', + ($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation, wallet, helpers, $filter, $locale, coupon) -> - # Price - $scope.amount = price.price + # user wallet amount + $scope.walletAmount = wallet.amount + + # Global price (total of all items) + $scope.price = price.price + + # Price to pay (wallet deducted) + $scope.amount = helpers.getAmountToPay(price.price, wallet.amount) # Reservation $scope.reservation = reservation + # Currency symbol or abreviation for the current locale + $scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM + + # Used in wallet info template to interpolate some translations + $scope.numberFilter = $filter('number') + # Button label if $scope.amount > 0 - $scope.validButtonName = _t('confirm_(payment_on_site)') + $scope.validButtonName = _t('confirm_payment_of_html', {ROLE:$scope.currentUser.role, AMOUNT:$filter('currency')($scope.amount)}, "messageformat") else - $scope.validButtonName = _t('confirm') + if price.price > 0 and $scope.walletAmount == 0 + $scope.validButtonName = _t('confirm_payment_of_html', {ROLE:$scope.currentUser.role, AMOUNT:$filter('currency')(price.price)}, "messageformat") + 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) -> + Reservation.save mkRequestParams($scope.reservation, coupon), (reservation) -> $uibModalInstance.close(reservation) $scope.attempting = true , (response)-> @@ -930,8 +948,8 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan) $scope.plansAreShown = false - $scope.calendar.fullCalendar 'refetchEvents' - $scope.calendar.fullCalendar 'rerenderEvents' + uiCalendarConfig.calendars.calendar.fullCalendar 'refetchEvents' + uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' diff --git a/app/assets/javascripts/controllers/main_nav.coffee.erb b/app/assets/javascripts/controllers/main_nav.coffee.erb index 17dbab535..b1938b850 100644 --- a/app/assets/javascripts/controllers/main_nav.coffee.erb +++ b/app/assets/javascripts/controllers/main_nav.coffee.erb @@ -16,18 +16,23 @@ Application.Controllers.controller "MainNavController", ["$scope", "$location", { state: 'app.public.machines_list' linkText: 'reserve_a_machine' - linkIcon: 'calendar' + linkIcon: 'cogs' } { - state: 'app.logged.trainings_reserve' + state: 'app.public.trainings_list' linkText: 'trainings_registrations' linkIcon: 'graduation-cap' } { state: 'app.public.events_list' - linkText: 'courses_and_workshops_registrations' + linkText: 'events_registrations' linkIcon: 'tags' } + { + state: 'app.public.calendar' + linkText: 'public_calendar' + linkIcon: 'calendar' + } { state: 'app.public.projects_list' linkText: 'projects_gallery' @@ -73,7 +78,7 @@ Application.Controllers.controller "MainNavController", ["$scope", "$location", } { state: 'app.admin.events' - linkText: 'courses_and_workshops_monitoring' + linkText: 'manage_the_events' linkIcon: 'tags' } { diff --git a/app/assets/javascripts/controllers/plans.coffee.erb b/app/assets/javascripts/controllers/plans.coffee.erb index 529f2ba25..14c74443e 100644 --- a/app/assets/javascripts/controllers/plans.coffee.erb +++ b/app/assets/javascripts/controllers/plans.coffee.erb @@ -1,7 +1,7 @@ 'use strict' -Application.Controllers.controller "PlansIndexController", ["$scope", "$rootScope", "$state", '$uibModal', 'Auth', 'dialogs', 'growl', 'plansPromise', 'groupsPromise', 'Subscription', 'Member', 'subscriptionExplicationsPromise', '_t' -, ($scope, $rootScope, $state, $uibModal, Auth, dialogs, growl, plansPromise, groupsPromise, Subscription, Member, subscriptionExplicationsPromise, _t) -> +Application.Controllers.controller "PlansIndexController", ["$scope", "$rootScope", "$state", '$uibModal', 'Auth', 'dialogs', 'growl', 'plansPromise', 'groupsPromise', 'Subscription', 'Member', 'subscriptionExplicationsPromise', '_t', 'Wallet', 'helpers' +, ($scope, $rootScope, $state, $uibModal, Auth, dialogs, growl, plansPromise, groupsPromise, Subscription, Member, subscriptionExplicationsPromise, _t, Wallet, helpers) -> @@ -30,11 +30,20 @@ Application.Controllers.controller "PlansIndexController", ["$scope", "$rootScop member_id: null ## already subscribed plan of the current user - $scope.paidPlan = null + $scope.paid = + plan: null ## plan to subscribe (shopping cart) $scope.selectedPlan = null + ## Discount coupon to apply to the basket, if any + $scope.coupon = + applied: null + + ## Storage for the total price (plan price + coupon, if any) + $scope.cart = + total: null + ## text that appears in the bottom-right box of the page (subscriptions rules details) $scope.subscriptionExplicationsAlert = subscriptionExplicationsPromise.setting.value @@ -44,7 +53,7 @@ Application.Controllers.controller "PlansIndexController", ["$scope", "$rootScop ## $scope.updateMember = -> $scope.selectedPlan = null - $scope.paidPlan = null + $scope.paid.plan = null $scope.group.change = false Member.get {id: $scope.ctrl.member.id}, (member) -> $scope.ctrl.member = member @@ -61,6 +70,7 @@ Application.Controllers.controller "PlansIndexController", ["$scope", "$rootScop if $scope.isAuthenticated() if $scope.selectedPlan != plan $scope.selectedPlan = plan + updateCartPrice() else $scope.selectedPlan = null else @@ -72,10 +82,13 @@ Application.Controllers.controller "PlansIndexController", ["$scope", "$rootScop # Callback to trigger the payment process of the subscription ## $scope.openSubscribePlanModal = -> - if $scope.currentUser.role isnt 'admin' - payByStripe() - else - payOnSite() + Wallet.getWalletByUser {user_id: $scope.ctrl.member.id}, (wallet) -> + amountToPay = helpers.getAmountToPay($scope.cart.total, wallet.amount) + if $scope.currentUser.role isnt 'admin' and amountToPay > 0 + payByStripe() + else + if $scope.currentUser.role is 'admin' or amountToPay is 0 + payOnSite() @@ -144,12 +157,33 @@ Application.Controllers.controller "PlansIndexController", ["$scope", "$rootScop if $scope.currentUser if $scope.currentUser.role isnt 'admin' $scope.ctrl.member = $scope.currentUser - $scope.paidPlan = $scope.currentUser.subscribed_plan + $scope.paid.plan = $scope.currentUser.subscribed_plan $scope.group.id = $scope.currentUser.group_id $scope.$on 'devise:new-session', (event, user)-> $scope.ctrl.member = user + # watch when a coupon is applied to re-compute the total price + $scope.$watch 'coupon.applied', (newValue, oldValue) -> + unless newValue == null and oldValue == null + updateCartPrice() + + + + ## + # Compute the total amount for the current reservation according to the previously set parameters + # and assign the result in $scope.reserve.amountTotal + ## + updateCartPrice = -> + # first we check that a user was selected + if Object.keys($scope.ctrl.member).length > 0 + $scope.cart.total = $scope.selectedPlan.amount + # apply the coupon if any + if $scope.coupon.applied + discount = $scope.cart.total * $scope.coupon.applied.percent_off / 100 + $scope.cart.total -= discount + else + $scope.reserve.amountTotal = null ## @@ -162,18 +196,43 @@ Application.Controllers.controller "PlansIndexController", ["$scope", "$rootScop 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 + price: -> $scope.cart.total + wallet: -> + Wallet.getWalletByUser({user_id: $scope.ctrl.member.id}).$promise + coupon: -> $scope.coupon.applied + controller: ['$scope', '$uibModalInstance', '$state', 'selectedPlan', 'member', 'price', 'Subscription', 'CustomAsset', 'wallet', 'helpers', '$locale', '$filter', 'coupon', + ($scope, $uibModalInstance, $state, selectedPlan, member, price, Subscription, CustomAsset, wallet, helpers, $locale, $filter, coupon) -> + # user wallet amount + $scope.walletAmount = wallet.amount + + # Final price to pay by the user + $scope.amount = helpers.getAmountToPay(price, wallet.amount) + + # The plan that the user is about to subscribe $scope.selectedPlan = selectedPlan + + # Currency symbol or abreviation for the current locale + $scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM + + # Used in wallet info template to interpolate some translations + $scope.numberFilter = $filter('number') + # retrieve the CGV CustomAsset.get {name: 'cgv-file'}, (cgv) -> $scope.cgv = cgv.custom_asset + + ## + # Callback for click on the 'proceed' button. + # Handle the stripe's card tokenization process response and save the subscription to the API with the + # card token just created. + ## $scope.payment = (status, response) -> if response.error growl.error(response.error.message) else $scope.attempting = true Subscription.save + coupon_code: (coupon.code if coupon) subscription: plan_id: selectedPlan.id user_id: member.id @@ -188,7 +247,7 @@ Application.Controllers.controller "PlansIndexController", ["$scope", "$rootScop .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.paid.plan = angular.copy($scope.selectedPlan) $scope.selectedPlan = null @@ -203,12 +262,50 @@ Application.Controllers.controller "PlansIndexController", ["$scope", "$rootScop resolve: selectedPlan: -> $scope.selectedPlan member: -> $scope.ctrl.member - controller: ['$scope', '$uibModalInstance', '$state', 'selectedPlan', 'member', 'Subscription', ($scope, $uibModalInstance, $state, selectedPlan, member, Subscription) -> + price: -> $scope.cart.total + wallet: -> + Wallet.getWalletByUser({user_id: $scope.ctrl.member.id}).$promise + coupon: -> $scope.coupon.applied + controller: ['$scope', '$uibModalInstance', '$state', 'selectedPlan', 'member', 'price', 'Subscription', 'wallet', 'helpers', '$locale', '$filter', 'coupon', + ($scope, $uibModalInstance, $state, selectedPlan, member, price, Subscription, wallet, helpers, $locale, $filter, coupon) -> + # user wallet amount + $scope.walletAmount = wallet.amount + + # subcription price, coupon subtracted if any + $scope.price = price + + # price to pay + $scope.amount = helpers.getAmountToPay($scope.price, wallet.amount) + + # Currency symbol or abreviation for the current locale + $scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM + + # Used in wallet info template to interpolate some translations + $scope.numberFilter = $filter('number') + + # The plan that the user is about to subscribe $scope.plan = selectedPlan + + # The member who is subscribing a plan $scope.member = member + + # Button label + if $scope.amount > 0 + $scope.validButtonName = _t('confirm_payment_of_html', {ROLE:$scope.currentUser.role, AMOUNT:$filter('currency')($scope.amount)}, "messageformat") + else + if price.price > 0 and $scope.walletAmount == 0 + $scope.validButtonName = _t('confirm_payment_of_html', {ROLE:$scope.currentUser.role, AMOUNT:$filter('currency')(price.price)}, "messageformat") + else + $scope.validButtonName = _t('confirm') + + ## + # Callback for the 'proceed' button. + # Save the subscription to the API + ## $scope.ok = -> $scope.attempting = true Subscription.save + coupon_code: (coupon.code if coupon) subscription: plan_id: selectedPlan.id user_id: member.id @@ -219,16 +316,18 @@ Application.Controllers.controller "PlansIndexController", ["$scope", "$rootScop $scope.alerts.push({msg: _t('an_error_occured_during_the_payment_process_please_try_again_later'), type: 'danger' }) $scope.attempting = false + ## + # Callback for the 'cancel' button. + # Close the modal box. + ## $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.paid.plan = angular.copy($scope.selectedPlan) $scope.selectedPlan = null diff --git a/app/assets/javascripts/controllers/projects.coffee.erb b/app/assets/javascripts/controllers/projects.coffee.erb index 07d386702..c34e97e4d 100644 --- a/app/assets/javascripts/controllers/projects.coffee.erb +++ b/app/assets/javascripts/controllers/projects.coffee.erb @@ -7,16 +7,19 @@ # in the various projects' admin controllers. # # Provides : +# - $scope.totalSteps # - $scope.machines = [{Machine}] # - $scope.components = [{Component}] # - $scope.themes = [{Theme}] # - $scope.licences = [{Licence}] +# - $scope.allowedExtensions = [{String}] # - $scope.submited(content) # - $scope.cancel() # - $scope.addFile() # - $scope.deleteFile(file) # - $scope.addStep() # - $scope.deleteStep(step) +# - $scope.changeStepIndex(step, newIdx) # # Requires : # - $scope.project.project_caos_attributes = [] @@ -24,7 +27,7 @@ # - $state (Ui-Router) [ 'app.public.projects_show', 'app.public.projects_list' ] ## class ProjectsController - constructor: ($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics)-> + constructor: ($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics, dialogs, allowedExtensions, _t)-> ## Retrieve the list of machines from the server Machine.query().$promise.then (data)-> @@ -50,6 +53,12 @@ class ProjectsController id: d.id name: d.name + ## Total number of documentation steps for the current project + $scope.totalSteps = $scope.project.project_steps_attributes.length + + ## List of extensions allowed for CAD attachements upload + $scope.allowedExtensions = allowedExtensions + ## @@ -74,14 +83,6 @@ class ProjectsController - ## - # Changes the user's view to the projects list page - ## - $scope.cancel = -> - $state.go('app.public.projects_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. @@ -122,22 +123,55 @@ class ProjectsController # This will create a single new empty entry into the project's steps list. ## $scope.addStep = -> - $scope.project.project_steps_attributes.push {} + $scope.totalSteps += 1 + $scope.project.project_steps_attributes.push { step_nb: $scope.totalSteps } + ## - # This will remove the given stip from the project's steps list. If the step was previously saved + # This will remove the given step from the project's steps list. If the step was previously saved # on the server, it will be marked for deletion for the next saving. Otherwise, it will be simply truncated from # the steps array. # @param file {Object} the file to delete ## $scope.deleteStep = (step) -> - index = $scope.project.project_steps_attributes.indexOf(step) - if step.id? - step._destroy = true - else - $scope.project.project_steps_attributes.splice(index, 1) + dialogs.confirm + resolve: + object: -> + title: _t('confirmation_required') + msg: _t('do_you_really_want_to_delete_this_step') + , -> # deletion confirmed + index = $scope.project.project_steps_attributes.indexOf(step) + if step.id? + step._destroy = true + else + $scope.project.project_steps_attributes.splice(index, 1) + + # update the new total number of steps + $scope.totalSteps -= 1 + # reindex the remaning steps + for s in $scope.project.project_steps_attributes + if s.step_nb > step.step_nb + s.step_nb -= 1 + + + + ## + # Change the step_nb property of the given step to the new value provided. The step that was previously at this + # index will be assigned to the old position of the provided step. + # @param event {Object} see https://docs.angularjs.org/guide/expression#-event- + # @param step {Object} the project's step to reindex + # @param newIdx {number} the new index to assign to the step + ## + $scope.changeStepIndex = (event, step, newIdx) -> + event.preventDefault() if event + for s in $scope.project.project_steps_attributes + if s.step_nb == newIdx + s.step_nb = step.step_nb + step.step_nb = newIdx + break + false $scope.autoCompleteName = (nameLookup) -> @@ -286,8 +320,8 @@ Application.Controllers.controller "ProjectsController", ["$scope", "$state", 'P ## # Controller used in the project creation page ## -Application.Controllers.controller "NewProjectController", ["$scope", "$state", 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', '$document', 'CSRF', 'Diacritics' -, ($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, CSRF, Diacritics) -> +Application.Controllers.controller "NewProjectController", ["$scope", "$state", 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', '$document', 'CSRF', 'Diacritics', 'dialogs', 'allowedExtensions', '_t' +, ($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, CSRF, Diacritics, dialogs, allowedExtensions, _t) -> CSRF.setMetaTags() ## API URL where the form will be posted @@ -304,7 +338,7 @@ Application.Controllers.controller "NewProjectController", ["$scope", "$state", $scope.matchingMembers = [] ## Using the ProjectsController - new ProjectsController($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics) + new ProjectsController($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics, dialogs, allowedExtensions, _t) ] @@ -312,8 +346,8 @@ Application.Controllers.controller "NewProjectController", ["$scope", "$state", ## # Controller used in the project edition page ## -Application.Controllers.controller "EditProjectController", ["$scope", "$state", '$stateParams', 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', '$document', 'CSRF', 'projectPromise', 'Diacritics' -, ($scope, $state, $stateParams, Project, Machine, Member, Component, Theme, Licence, $document, CSRF, projectPromise, Diacritics) -> +Application.Controllers.controller "EditProjectController", ["$scope", "$state", '$stateParams', 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', '$document', 'CSRF', 'projectPromise', 'Diacritics', 'dialogs', 'allowedExtensions', '_t' +, ($scope, $state, $stateParams, Project, Machine, Member, Component, Theme, Licence, $document, CSRF, projectPromise, Diacritics, dialogs, allowedExtensions, _t) -> CSRF.setMetaTags() ## API URL where the form will be posted @@ -330,7 +364,7 @@ Application.Controllers.controller "EditProjectController", ["$scope", "$state", name: u.full_name ## Using the ProjectsController - new ProjectsController($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics) + new ProjectsController($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics, dialogs, allowedExtensions, _t) ] @@ -394,6 +428,8 @@ Application.Controllers.controller "ShowProjectController", ["$scope", "$state", else console.error _t('unauthorized_operation') + + ## # Open a modal box containg a form that allow the end-user to signal an abusive content # @param e {Object} jQuery event @@ -429,4 +465,19 @@ Application.Controllers.controller "ShowProjectController", ["$scope", "$state", growl.error(_t('an_error_occured_while_sending_your_report')) ] + + + ## + # Return the URL allowing to share the current project on the Facebook social network + ## + $scope.shareOnFacebook = -> + 'https://www.facebook.com/share.php?u='+$state.href('app.public.projects_show', {id: $scope.project.slug}, {absolute: true}).replace('#', '%23') + + + + ## + # Return the URL allowing to share the current project on the Twitter social network + ## + $scope.shareOnTwitter = -> + 'https://twitter.com/intent/tweet?url='+encodeURIComponent($state.href('app.public.projects_show', {id: $scope.project.slug}, {absolute: true}))+'&text='+encodeURIComponent($scope.project.name) ] diff --git a/app/assets/javascripts/controllers/trainings.coffee.erb b/app/assets/javascripts/controllers/trainings.coffee.erb index 03be4e08c..d4377c9ab 100644 --- a/app/assets/javascripts/controllers/trainings.coffee.erb +++ b/app/assets/javascripts/controllers/trainings.coffee.erb @@ -1,40 +1,99 @@ 'use strict' +## +# Public listing of the trainings +## +Application.Controllers.controller "TrainingsController", ['$scope', '$state', 'trainingsPromise', ($scope, $state, trainingsPromise) -> + + ## List of trainings + $scope.trainings = trainingsPromise + + ## + # Callback for the 'reserve' button + ## + $scope.reserveTraining = (training, event) -> + $state.go('app.logged.trainings_reserve', {id: training.slug}) + + ## + # Callback for the 'show' button + ## + $scope.showTraining = (training) -> + $state.go('app.public.training_show', {id: training.slug}) +] + + + +## +# Public view of a specific training +## +Application.Controllers.controller "ShowTrainingController", ['$scope', '$state', 'trainingPromise', 'growl', '_t', 'dialogs', ($scope, $state, trainingPromise, growl, _t, dialogs) -> + + ## Current training + $scope.training = trainingPromise + + + + ## + # Callback to delete the current training (admins only) + ## + $scope.delete = (training) -> + # check the permissions + if $scope.currentUser.role isnt 'admin' + console.error _t('unauthorized_operation') + else + dialogs.confirm + resolve: + object: -> + title: _t('confirmation_required') + msg: _t('do_you_really_want_to_delete_this_training') + , -> # deletion confirmed + # delete the training then redirect to the trainings listing + training.$delete -> + $state.go('app.public.trainings_list') + , (error)-> + growl.warning(_t('the_training_cant_be_deleted_because_it_is_already_reserved_by_some_users')) + + + + ## + # Callback for the 'reserve' button + ## + $scope.reserveTraining = (training, event) -> + $state.go('app.logged.trainings_reserve', {id: training.id}) + + + + ## + # Revert view to the full list of trainings ("<-" button) + ## + $scope.cancel = (event) -> + $state.go('app.public.trainings_list') +] + + ## # Controller used in the training reservation agenda page. # 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) -> +Application.Controllers.controller "ReserveTrainingController", ["$scope", "$state", '$stateParams', '$filter', '$compile', "$uibModal", 'Auth', 'dialogs', '$timeout', 'Price', 'Availability', 'Slot', 'Member', 'Setting', 'CustomAsset', 'availabilityTrainingsPromise', 'plansPromise', 'groupsPromise', 'growl', 'settingsPromise', 'trainingPromise', '_t', 'Wallet', 'helpers', 'uiCalendarConfig', 'CalendarConfig' +($scope, $state, $stateParams, $filter, $compile, $uibModal, Auth, dialogs, $timeout, Price, Availability, Slot, Member, Setting, CustomAsset, availabilityTrainingsPromise, plansPromise, groupsPromise, growl, settingsPromise, trainingPromise, _t, Wallet, helpers, uiCalendarConfig, CalendarConfig) -> ### 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' + FREE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::TRAINING_COLOR %>' ### 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' } ] @@ -71,36 +130,21 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta ## 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' + ## Selected training unless 'all' trainings are displayed + $scope.training = trainingPromise - allDaySlot: false - defaultView: 'agendaWeek' - editable: false + ## Discount coupon to apply to the basket, if any + $scope.coupon = + applied: null + + ## fullCalendar (v2) configuration + $scope.calendarConfig = CalendarConfig + minTime: moment.duration(moment(settingsPromise.booking_window_start).format('HH:mm:ss')) + maxTime: moment.duration(moment(settingsPromise.booking_window_end).format('HH:mm:ss')) eventClick: (event, jsEvent, view) -> calendarEventClickCb(event, jsEvent, view) eventAfterAllRender: (view)-> - $scope.events = $scope.calendar.fullCalendar 'clientEvents' + $scope.events = uiCalendarConfig.calendars.calendar.fullCalendar 'clientEvents' eventRender: (event, element, view) -> eventRenderCb(event, element, view) @@ -112,8 +156,6 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta $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')) @@ -125,8 +167,8 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta if $scope.ctrl.member Member.get {id: $scope.ctrl.member.id}, (member) -> $scope.ctrl.member = member - Availability.trainings {member_id: $scope.ctrl.member.id}, (trainings) -> - $scope.calendar.fullCalendar 'removeEvents' + Availability.trainings {trainingId: $stateParams.id, member_id: $scope.ctrl.member.id}, (trainings) -> + uiCalendarConfig.calendars.calendar.fullCalendar 'removeEvents' $scope.eventSources.push events: trainings textColor: 'black' @@ -162,8 +204,8 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta $scope.selectedPlan = null $scope.trainingIsValid = false $timeout -> - $scope.calendar.fullCalendar 'refetchEvents' - $scope.calendar.fullCalendar 'rerenderEvents' + uiCalendarConfig.calendars.fullCalendar 'refetchEvents' + uiCalendarConfig.calendars.fullCalendar 'rerenderEvents' @@ -176,11 +218,13 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta 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) + Wallet.getWalletByUser {user_id: $scope.ctrl.member.id}, (wallet) -> + amountToPay = helpers.getAmountToPay($scope.amountTotal, wallet.amount) + if $scope.currentUser.role isnt 'admin' and amountToPay > 0 + payByStripe(reservation) + else + if $scope.currentUser.role is 'admin' or amountToPay 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')) @@ -235,7 +279,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta $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' + uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' @@ -248,7 +292,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta $scope.slotToPlace.backgroundColor = 'white' $scope.slotToPlace.title = $scope.slotToPlace.training.name $scope.slotToPlace = null - $scope.calendar.fullCalendar 'rerenderEvents' + uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' @@ -281,7 +325,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta $scope.slotToModify.can_modify = false $scope.slotToModify.is_completed = false if $scope.slotToModify.is_completed $scope.slotToModify = null - $scope.calendar.fullCalendar 'rerenderEvents' + uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' , -> # failure growl.error('an_error_occured_preventing_the_booked_slot_from_being_modified') @@ -297,7 +341,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta $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' + uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' @@ -307,7 +351,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta $scope.updatePrices = -> if Object.keys($scope.ctrl.member).length > 0 r = mkReservation($scope.ctrl.member, $scope.selectedTraining, $scope.selectedPlan) - Price.compute {reservation: r}, (res) -> + Price.compute mkRequestParams(r, $scope.coupon.applied), (res) -> $scope.amountTotal = res.price else $scope.amountTotal = null @@ -324,6 +368,11 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta Member.get id: $scope.currentUser.id, (member) -> $scope.ctrl.member = member + # watch when a coupon is applied to re-compute the total price + $scope.$watch 'coupon.applied', (newValue, oldValue) -> + unless newValue == null and oldValue == null + $scope.updatePrices() + ## @@ -351,6 +400,21 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta + ## + # Format the parameters expected by /api/prices/compute or /api/reservations and return the resulting object + # @param reservation {Object} as returned by mkReservation() + # @param coupon {Object} Coupon as returned from the API + # @return {{reservation:Object, coupon_code:string}} + ## + mkRequestParams = (reservation, coupon) -> + params = + reservation: reservation + coupon_code: (coupon.code if coupon) + + params + + + ## # 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 ...), @@ -362,6 +426,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta 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 + $scope.coupon.applied = null if event != $scope.selectedTraining $scope.selectedTraining = event $scope.selectedTraining.offered = false @@ -378,7 +443,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta angular.forEach $scope.events, (e)-> if event.id != e.id e.backgroundColor = 'white' - $scope.calendar.fullCalendar 'rerenderEvents' + uiCalendarConfig.calendars.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) @@ -388,7 +453,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta $scope.slotToPlace = event event.backgroundColor = '#bbb' event.title = event.training.name + ' - ' + _t('i_shift') - $scope.calendar.fullCalendar 'rerenderEvents' + uiCalendarConfig.calendars.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) @@ -406,7 +471,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta $scope.slotToModify = event event.backgroundColor = '#eee' event.title = event.training.name + ' - ' + _t('i_change') - $scope.calendar.fullCalendar 'rerenderEvents' + uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' else if type == 'cancel' dialogs.confirm resolve: @@ -425,7 +490,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta $scope.canceledSlot.can_modify = false $scope.canceledSlot.is_completed = false if event.is_completed $scope.canceledSlot = null - $scope.calendar.fullCalendar 'rerenderEvents' + uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' , -> # error while canceling growl.error _t('cancellation_failed') , -> # canceled @@ -440,11 +505,12 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta # @see http://fullcalendar.io/docs/event_rendering/eventRender/ ## eventRenderCb = (event, element, view)-> - element.attr( - 'uib-popover': event.training.description - 'popover-trigger': 'mouseenter' - ) - $compile(element)($scope) + # Comment these codes for show a popup of description, because we add feature page of training + #element.attr( + # 'uib-popover': $filter('humanize')($filter('simpleText')(event.training.description), 70) + # 'popover-trigger': 'mouseenter' + #) + #$compile(element)($scope) @@ -460,12 +526,20 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta reservation: -> reservation price: -> - Price.compute({reservation: reservation}).$promise + Price.compute(mkRequestParams(reservation, $scope.coupon.applied)).$promise + wallet: -> + Wallet.getWalletByUser({user_id: reservation.user_id}).$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) -> + coupon: -> + $scope.coupon.applied + controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'wallet', 'cgv', 'Auth', 'Reservation', '$locale', 'helpers', '$filter', 'coupon' + ($scope, $uibModalInstance, $state, reservation, price, wallet, cgv, Auth, Reservation, $locale, helpers, $filter, coupon) -> + # user wallet amount + $scope.walletAmount = wallet.amount + # Price - $scope.amount = price.price + $scope.amount = helpers.getAmountToPay(price.price, wallet.amount) # CGV $scope.cgv = cgv.custom_asset @@ -473,6 +547,10 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta # Reservation $scope.reservation = reservation + $scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM + + $scope.numberFilter = $filter('number') + ## # Callback to process the payment with Stripe, triggered on button click ## @@ -482,7 +560,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta else $scope.attempting = true $scope.reservation.card_token = response.id - Reservation.save reservation: $scope.reservation, (reservation) -> + Reservation.save mkRequestParams($scope.reservation, coupon), (reservation) -> $uibModalInstance.close(reservation) , (response)-> $scope.alerts = [] @@ -511,26 +589,44 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta reservation: -> reservation price: -> - Price.compute({reservation: reservation}).$promise - controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', ($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation) -> + Price.compute(mkRequestParams(reservation, $scope.coupon.applied)).$promise + wallet: -> + Wallet.getWalletByUser({user_id: reservation.user_id}).$promise + coupon: -> + $scope.coupon.applied + controller: ['$scope', '$uibModalInstance', '$state', '$filter', 'reservation', 'price', 'wallet', 'Auth', 'Reservation', '$locale', 'helpers', 'coupon' + ($scope, $uibModalInstance, $state, $filter, reservation, price, wallet, Auth, Reservation, $locale, helpers, coupon) -> + # user wallet amount + $scope.walletAmount = wallet.amount + # Price - $scope.amount = price.price + $scope.price = price.price + + # price to pay + $scope.amount = helpers.getAmountToPay(price.price, wallet.amount) # Reservation $scope.reservation = reservation + $scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM + + $scope.numberFilter = $filter('number') + # Button label if $scope.amount > 0 - $scope.validButtonName = _t('confirm_(payment_on_site)') + $scope.validButtonName = _t('confirm_payment_of_html', {ROLE:$scope.currentUser.role, AMOUNT:$filter('currency')($scope.amount)}, "messageformat") else - $scope.validButtonName = _t('confirm') + if price.price > 0 and $scope.walletAmount == 0 + $scope.validButtonName = _t('confirm_payment_of_html', {ROLE:$scope.currentUser.role, AMOUNT:$filter('currency')(price.price)}, "messageformat") + 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) -> + Reservation.save mkRequestParams($scope.reservation, coupon), (reservation) -> $uibModalInstance.close(reservation) $scope.attempting = true , (response)-> @@ -553,7 +649,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta # 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) -> + Price.compute mkRequestParams(r, $scope.coupon.applied), (res) -> $scope.selectedTrainingAmount = res.price else $scope.selectedTrainingAmount = null @@ -587,8 +683,8 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta 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' + uiCalendarConfig.calendars.calendar.fullCalendar 'refetchEvents' + uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' diff --git a/app/assets/javascripts/controllers/wallet.coffee b/app/assets/javascripts/controllers/wallet.coffee new file mode 100644 index 000000000..9ab033672 --- /dev/null +++ b/app/assets/javascripts/controllers/wallet.coffee @@ -0,0 +1,12 @@ +'use strict' + +Application.Controllers.controller "WalletController", ['$scope', 'walletPromise', 'transactionsPromise', ($scope, walletPromise, transactionsPromise)-> + + ### PUBLIC SCOPE ### + + ## current user wallet + $scope.wallet = walletPromise + + ## current wallet transactions + $scope.transactions = transactionsPromise +] diff --git a/app/assets/javascripts/directives/coupon.coffee.erb b/app/assets/javascripts/directives/coupon.coffee.erb new file mode 100644 index 000000000..2d1aa5812 --- /dev/null +++ b/app/assets/javascripts/directives/coupon.coffee.erb @@ -0,0 +1,48 @@ +Application.Directives.directive 'coupon', [ 'Coupon', 'growl', '_t', (Coupon, growl, _t) -> + { + restrict: 'E' + scope: + show: '=' + coupon: '=' + userId: '@' + hasSelectSlot: '=' + templateUrl: '<%= asset_path "shared/_coupon.html" %>' + link: ($scope, element, attributes) -> + + # Whether code input is shown or not (ie. the link 'I have a coupon' is shown) + $scope.code = + input: false + + # Available status are: 'pending', 'valid', 'invalid' + $scope.status = 'pending' + + # Binding for the code inputed + $scope.couponCode = null + + $scope.$watch 'hasSelectSlot', (newValue) -> + unless newValue + $scope.coupon = null + $scope.couponCode = null + $scope.code.input = false + + + ## + # Callback to validate the code + ## + $scope.validateCode = -> + if $scope.couponCode == '' + $scope.status = 'pending' + $scope.coupon = null + else + Coupon.validate {code: $scope.couponCode, user_id: $scope.userId}, (res) -> + $scope.status = 'valid' + $scope.coupon = res + growl.success(_t('the_coupon_has_been_applied_you_get_PERCENT_discount', {PERCENT: res.percent_off})) + , (err) -> + $scope.status = 'invalid' + $scope.coupon = null + growl.error(_t('unable_to_apply_the_coupon_because_'+err.data.status)) + } +] + + diff --git a/app/assets/javascripts/directives/validators.coffee b/app/assets/javascripts/directives/validators.coffee index 71f19c80a..871b6e947 100644 --- a/app/assets/javascripts/directives/validators.coffee +++ b/app/assets/javascripts/directives/validators.coffee @@ -18,7 +18,7 @@ Application.Directives.directive 'url', [ -> Application.Directives.directive 'endpoint', [ -> - ENDPOINT_REGEXP = /^\/([-._~:?#\[\]@!$&'()*+,;=%\w]+\/?)*$/ + ENDPOINT_REGEXP = /^\/?([-._~:?#\[\]@!$&'()*+,;=%\w]+\/?)*$/ { require: 'ngModel' link: (scope, element, attributes, ctrl) -> diff --git a/app/assets/javascripts/filters/filters.coffee b/app/assets/javascripts/filters/filters.coffee index 9b6334d91..a6420f896 100644 --- a/app/assets/javascripts/filters/filters.coffee +++ b/app/assets/javascripts/filters/filters.coffee @@ -98,10 +98,25 @@ Application.Filters.filter "humanize", [ -> Humanize.truncate(element, param, null) ] +## +# This filter will convert ASCII carriage-return character to the HTML break-line tag +## Application.Filters.filter "breakFilter", [ -> (text) -> - if text != undefined - text.replace(/\n/g, '
') + if text? + text.replace(/\n+/g, '
') +] + +## +# This filter will take a HTML text as input and will return it without the html tags +## +Application.Filters.filter "simpleText", [ -> + (text) -> + if text? + text = text.replace(//g, '\n') + text.replace(/<\/?\w+[^>]*>/g, '') + else + "" ] Application.Filters.filter "toTrusted", [ "$sce", ($sce) -> @@ -217,4 +232,29 @@ Application.Filters.filter 'toIsoDate', [ -> return date unless (date instanceof Date || moment.isMoment(date)) moment(date).format('YYYY-MM-DD') -] \ No newline at end of file +] + +Application.Filters.filter 'booleanFormat', [ '_t', (_t) -> + (boolean) -> + if boolean or boolean == 'true' + _t('yes') + else + _t('no') +] + +Application.Filters.filter 'booleanFormat', [ '_t', (_t) -> + (boolean) -> + if (typeof boolean == 'boolean' and boolean) or (typeof boolean == 'string' and boolean == 'true') + _t('yes') + else + _t('no') +] + +Application.Filters.filter 'maxCount', [ '_t', (_t) -> + (max) -> + if typeof max == 'undefined' or max == null or (typeof max == 'number' and max == 0) + _t('unlimited') + else + max +] + diff --git a/app/assets/javascripts/router.coffee.erb b/app/assets/javascripts/router.coffee.erb index 978705d54..5d7f16cc0 100644 --- a/app/assets/javascripts/router.coffee.erb +++ b/app/assets/javascripts/router.coffee.erb @@ -197,6 +197,22 @@ angular.module('application.router', ['ui.router']). translations: [ 'Translations', (Translations) -> Translations.query('app.logged.dashboard.invoices').$promise ] + .state 'app.logged.dashboard.wallet', + url: '/wallet' + views: + 'main@': + templateUrl: '<%= asset_path "dashboard/wallet.html" %>' + controller: 'WalletController' + resolve: + walletPromise: ['Wallet', 'currentUser', (Wallet, currentUser)-> + Wallet.getWalletByUser(user_id: currentUser.id).$promise + ] + transactionsPromise: ['Wallet', 'walletPromise', (Wallet, walletPromise)-> + Wallet.transactions(id: walletPromise.id).$promise + ] + translations: [ 'Translations', (Translations) -> + Translations.query(['app.shared.wallet']).$promise + ] # members @@ -232,7 +248,7 @@ angular.module('application.router', ['ui.router']). url: '/projects?q&page&theme_id&component_id&machine_id&from&whole_network' views: 'main@': - templateUrl: '<%= asset_path "projects/index.html" %>' + templateUrl: '<%= asset_path "projects/index.html.erb" %>' controller: 'ProjectsController' resolve: themesPromise: ['Theme', (Theme)-> @@ -254,6 +270,9 @@ angular.module('application.router', ['ui.router']). templateUrl: '<%= asset_path "projects/new.html" %>' controller: 'NewProjectController' resolve: + allowedExtensions: ['Project', (Project)-> + Project.allowedExtensions().$promise + ] translations: [ 'Translations', (Translations) -> Translations.query(['app.logged.projects_new', 'app.shared.project']).$promise ] @@ -280,6 +299,9 @@ angular.module('application.router', ['ui.router']). projectPromise: ['$stateParams', 'Project', ($stateParams, Project)-> Project.get(id: $stateParams.id).$promise ] + allowedExtensions: ['Project', (Project)-> + Project.allowedExtensions().$promise + ] translations: [ 'Translations', (Translations) -> Translations.query(['app.logged.projects_edit', 'app.shared.project']).$promise ] @@ -290,7 +312,7 @@ angular.module('application.router', ['ui.router']). url: '/machines' views: 'main@': - templateUrl: '<%= asset_path "machines/index.html" %>' + templateUrl: '<%= asset_path "machines/index.html.erb" %>' controller: 'MachinesController' resolve: machinesPromise: ['Machine', (Machine)-> @@ -335,6 +357,9 @@ angular.module('application.router', ['ui.router']). groupsPromise: ['Group', (Group)-> Group.query().$promise ] + machinePromise: ['Machine', '$stateParams', (Machine, $stateParams)-> + Machine.get(id: $stateParams.id).$promise + ] settingsPromise: ['Setting', (Setting)-> Setting.query(names: "['machine_explications_alert', 'booking_window_start', @@ -347,7 +372,8 @@ angular.module('application.router', ['ui.router']). ] 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 + 'app.shared.stripe', 'app.shared.valid_reservation_modal', 'app.shared.confirm_modify_slot_modal', + 'app.shared.wallet', 'app.shared.coupon_input']).$promise ] .state 'app.admin.machines_edit', url: '/machines/:id/edit' @@ -363,8 +389,34 @@ angular.module('application.router', ['ui.router']). Translations.query(['app.admin.machines_edit', 'app.shared.machine']).$promise ] # trainings + .state 'app.public.trainings_list', + url: '/trainings' + views: + 'main@': + templateUrl: '<%= asset_path "trainings/index.html.erb" %>' + controller: 'TrainingsController' + resolve: + trainingsPromise: ['Training', (Training)-> + Training.query({ public_page: true }).$promise + ] + translations: [ 'Translations', (Translations) -> + Translations.query(['app.public.trainings_list']).$promise + ] + .state 'app.public.training_show', + url: '/trainings/:id' + views: + 'main@': + templateUrl: '<%= asset_path "trainings/show.html" %>' + controller: 'ShowTrainingController' + resolve: + trainingPromise: ['Training', '$stateParams', (Training, $stateParams)-> + Training.get({id: $stateParams.id}).$promise + ] + translations: [ 'Translations', (Translations) -> + Translations.query(['app.public.training_show']).$promise + ] .state 'app.logged.trainings_reserve', - url: '/trainings/reserve' + url: '/trainings/:id/reserve' views: 'main@': templateUrl: '<%= asset_path "trainings/reserve.html" %>' @@ -379,8 +431,11 @@ angular.module('application.router', ['ui.router']). groupsPromise: ['Group', (Group)-> Group.query().$promise ] - availabilityTrainingsPromise: ['Availability', (Availability)-> - Availability.trainings().$promise + availabilityTrainingsPromise: ['Availability', '$stateParams', (Availability, $stateParams)-> + Availability.trainings({trainingId: $stateParams.id}).$promise + ] + trainingPromise: ['Training', '$stateParams', (Training, $stateParams)-> + Training.get({id: $stateParams.id}).$promise unless $stateParams.id == 'all' ] settingsPromise: ['Setting', (Setting)-> Setting.query(names: "['booking_window_start', @@ -395,14 +450,15 @@ angular.module('application.router', ['ui.router']). ] 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 + 'app.shared.stripe', 'app.shared.valid_reservation_modal', 'app.shared.confirm_modify_slot_modal', + 'app.shared.wallet', 'app.shared.coupon_input']).$promise ] # notifications .state 'app.logged.notifications', url: '/notifications' views: 'main@': - templateUrl: '<%= asset_path "notifications/index.html" %>' + templateUrl: '<%= asset_path "notifications/index.html.erb" %>' controller: 'NotificationsController' resolve: translations: [ 'Translations', (Translations) -> @@ -415,7 +471,7 @@ angular.module('application.router', ['ui.router']). abstract: Fablab.withoutPlans views: 'main@': - templateUrl: '<%= asset_path "plans/index.html" %>' + templateUrl: '<%= asset_path "plans/index.html.erb" %>' controller: 'PlansIndexController' resolve: subscriptionExplicationsPromise: ['Setting', (Setting)-> @@ -428,7 +484,8 @@ angular.module('application.router', ['ui.router']). Group.query().$promise ] translations: [ 'Translations', (Translations) -> - Translations.query(['app.public.plans', 'app.shared.member_select', 'app.shared.stripe']).$promise + Translations.query(['app.public.plans', 'app.shared.member_select', 'app.shared.stripe', 'app.shared.wallet', + 'app.shared.coupon_input']).$promise ] # events @@ -436,9 +493,18 @@ angular.module('application.router', ['ui.router']). url: '/events' views: 'main@': - templateUrl: '<%= asset_path "events/index.html" %>' + templateUrl: '<%= asset_path "events/index.html.erb" %>' controller: 'EventsController' resolve: + categoriesPromise: ['Category', (Category) -> + Category.query().$promise + ] + themesPromise: ['EventTheme', (EventTheme) -> + EventTheme.query().$promise + ] + ageRangesPromise: ['AgeRange', (AgeRange) -> + AgeRange.query().$promise + ] translations: [ 'Translations', (Translations) -> Translations.query('app.public.events_list').$promise ] @@ -452,11 +518,39 @@ angular.module('application.router', ['ui.router']). eventPromise: ['Event', '$stateParams', (Event, $stateParams)-> Event.get(id: $stateParams.id).$promise ] - reducedAmountAlert: ['Setting', (Setting)-> - Setting.get(name: 'event_reduced_amount_alert').$promise + priceCategoriesPromise: ['PriceCategory', (PriceCategory) -> + PriceCategory.query().$promise + ] + settingsPromise: ['Setting', (Setting)-> + Setting.query(names: "['booking_move_enable', 'booking_move_delay']").$promise ] translations: [ 'Translations', (Translations) -> - Translations.query(['app.public.events_show', 'app.shared.member_select', 'app.shared.stripe', 'app.shared.valid_reservation_modal']).$promise + Translations.query(['app.public.events_show', 'app.shared.member_select', 'app.shared.stripe', + 'app.shared.valid_reservation_modal', 'app.shared.wallet', 'app.shared.coupon_input']).$promise + ] + + # global calendar (trainings, machines and events) + .state 'app.public.calendar', + url: '/calendar' + views: + 'main@': + templateUrl: '<%= asset_path "calendar/calendar.html" %>' + controller: 'CalendarController' + resolve: + bookingWindowStart: ['Setting', (Setting)-> + Setting.get(name: 'booking_window_start').$promise + ] + bookingWindowEnd: ['Setting', (Setting)-> + Setting.get(name: 'booking_window_end').$promise + ] + trainingsPromise: ['Training', (Training)-> + Training.query().$promise + ] + machinesPromise: ['Machine', (Machine)-> + Machine.query().$promise + ] + translations: [ 'Translations', (Translations) -> + Translations.query(['app.public.calendar']).$promise ] # --- namespace /admin/... --- @@ -468,9 +562,6 @@ angular.module('application.router', ['ui.router']). 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 ] @@ -489,7 +580,7 @@ angular.module('application.router', ['ui.router']). url: '/admin/project_elements' views: 'main@': - templateUrl: '<%= asset_path "admin/project_elements/index.html" %>' + templateUrl: '<%= asset_path "admin/project_elements/index.html.erb" %>' controller: 'ProjectElementsController' resolve: componentsPromise: ['Component', (Component)-> @@ -510,8 +601,8 @@ angular.module('application.router', ['ui.router']). url: '/admin/trainings' views: 'main@': - templateUrl: '<%= asset_path "admin/trainings/index.html" %>' - controller: 'TrainingsController' + templateUrl: '<%= asset_path "admin/trainings/index.html.erb" %>' + controller: 'TrainingsAdminController' resolve: trainingsPromise: ['Training', (Training)-> Training.query().$promise @@ -520,20 +611,60 @@ angular.module('application.router', ['ui.router']). Machine.query().$promise ] translations: [ 'Translations', (Translations) -> - Translations.query('app.admin.trainings').$promise + Translations.query(['app.admin.trainings', 'app.shared.trainings']).$promise + ] + .state 'app.admin.trainings_new', + url: '/admin/trainings/new' + views: + 'main@': + templateUrl: '<%= asset_path "admin/trainings/new.html" %>' + controller: 'NewTrainingController' + resolve: + machinesPromise: ['Machine', (Machine)-> + Machine.query().$promise + ] + translations: [ 'Translations', (Translations) -> + Translations.query(['app.admin.trainings_new', 'app.shared.trainings']).$promise + ] + .state 'app.admin.trainings_edit', + url: '/admin/trainings/:id/edit' + views: + 'main@': + templateUrl: '<%= asset_path "admin/trainings/edit.html" %>' + controller: 'EditTrainingController' + resolve: + trainingPromise: ['Training', '$stateParams', (Training, $stateParams)-> + Training.get(id: $stateParams.id).$promise + ] + machinesPromise: ['Machine', (Machine)-> + Machine.query().$promise + ] + translations: [ 'Translations', (Translations) -> + Translations.query('app.shared.trainings').$promise ] - # events .state 'app.admin.events', url: '/admin/events' views: 'main@': - templateUrl: '<%= asset_path "admin/events/index.html" %>' + templateUrl: '<%= asset_path "admin/events/index.html.erb" %>' controller: 'AdminEventsController' resolve: eventsPromise: ['Event', (Event)-> Event.query(page: 1).$promise ] + categoriesPromise: ['Category', (Category) -> + Category.query().$promise + ] + themesPromise: ['EventTheme', (EventTheme) -> + EventTheme.query().$promise + ] + ageRangesPromise: ['AgeRange', (AgeRange) -> + AgeRange.query().$promise + ] + priceCategoriesPromise: ['PriceCategory', (PriceCategory) -> + PriceCategory.query().$promise + ] translations: [ 'Translations', (Translations) -> Translations.query('app.admin.events').$promise ] @@ -544,6 +675,18 @@ angular.module('application.router', ['ui.router']). templateUrl: '<%= asset_path "events/new.html" %>' controller: 'NewEventController' resolve: + categoriesPromise: ['Category', (Category) -> + Category.query().$promise + ] + themesPromise: ['EventTheme', (EventTheme) -> + EventTheme.query().$promise + ] + ageRangesPromise: ['AgeRange', (AgeRange) -> + AgeRange.query().$promise + ] + priceCategoriesPromise: ['PriceCategory', (PriceCategory) -> + PriceCategory.query().$promise + ] translations: [ 'Translations', (Translations) -> Translations.query(['app.admin.events_new', 'app.shared.event']).$promise ] @@ -557,6 +700,18 @@ angular.module('application.router', ['ui.router']). eventPromise: ['Event', '$stateParams', (Event, $stateParams)-> Event.get(id: $stateParams.id).$promise ] + categoriesPromise: ['Category', (Category) -> + Category.query().$promise + ] + themesPromise: ['EventTheme', (EventTheme) -> + EventTheme.query().$promise + ] + ageRangesPromise: ['AgeRange', (AgeRange) -> + AgeRange.query().$promise + ] + priceCategoriesPromise: ['PriceCategory', (PriceCategory) -> + PriceCategory.query().$promise + ] translations: [ 'Translations', (Translations) -> Translations.query(['app.admin.events_edit', 'app.shared.event']).$promise ] @@ -582,7 +737,7 @@ angular.module('application.router', ['ui.router']). url: '/admin/pricing' views: 'main@': - templateUrl: '<%= asset_path "admin/pricing/index.html" %>' + templateUrl: '<%= asset_path "admin/pricing/index.html.erb" %>' controller: 'EditPricingController' resolve: plans: ['Plan', (Plan) -> @@ -598,7 +753,7 @@ angular.module('application.router', ['ui.router']). TrainingsPricing.query().$promise ] translations: [ 'Translations', (Translations) -> - Translations.query('app.admin.pricing').$promise + Translations.query(['app.admin.pricing', 'app.shared.member_select', 'app.shared.coupon']).$promise ] trainingsPromise: ['Training', (Training) -> Training.query().$promise @@ -612,6 +767,9 @@ angular.module('application.router', ['ui.router']). trainingCreditsPromise: ['Credit', (Credit) -> Credit.query({creditable_type: 'Training'}).$promise ] + couponsPromise: ['Coupon', (Coupon) -> + Coupon.query().$promise + ] # plans .state 'app.admin.plans', @@ -656,6 +814,30 @@ angular.module('application.router', ['ui.router']). Translations.query(['app.admin.plans.edit', 'app.shared.plan']).$promise ] + # coupons + .state 'app.admin.coupons_new', + url: '/admin/coupons/new' + views: + 'main@': + templateUrl: '<%= asset_path "admin/coupons/new.html" %>' + controller: 'NewCouponController' + resolve: + translations: [ 'Translations', (Translations) -> + Translations.query(['app.admin.coupons_new', 'app.shared.coupon']).$promise + ] + .state 'app.admin.coupons_edit', + url: '/admin/coupons/:id/edit' + views: + 'main@': + templateUrl: '<%= asset_path "admin/coupons/edit.html" %>' + controller: 'EditCouponController' + resolve: + couponPromise: ['Coupon', '$stateParams', (Coupon, $stateParams) -> + Coupon.get({id: $stateParams.id}).$promise + ] + translations: [ 'Translations', (Translations) -> + Translations.query(['app.admin.coupons_edit', 'app.shared.coupon']).$promise + ] @@ -664,7 +846,7 @@ angular.module('application.router', ['ui.router']). url: '/admin/invoices' views: 'main@': - templateUrl: '<%= asset_path "admin/invoices/index.html" %>' + templateUrl: '<%= asset_path "admin/invoices/index.html.erb" %>' controller: 'InvoicesController' resolve: settings: ['Setting', (Setting)-> @@ -695,16 +877,16 @@ angular.module('application.router', ['ui.router']). url: '/admin/members' views: 'main@': - templateUrl: '<%= asset_path "admin/members/index.html" %>' + templateUrl: '<%= asset_path "admin/members/index.html.erb" %>' controller: 'AdminMembersController' 'groups@app.admin.members': - templateUrl: '<%= asset_path "admin/groups/index.html" %>' + templateUrl: '<%= asset_path "admin/groups/index.html.erb" %>' controller: 'GroupsController' 'tags@app.admin.members': - templateUrl: '<%= asset_path "admin/tags/index.html" %>' + templateUrl: '<%= asset_path "admin/tags/index.html.erb" %>' controller: 'TagsController' 'authentification@app.admin.members': - templateUrl: '<%= asset_path "admin/authentications/index.html" %>' + templateUrl: '<%= asset_path "admin/authentications/index.html.erb" %>' controller: 'AuthentificationController' resolve: membersPromise: ['Member', (Member)-> @@ -745,11 +927,20 @@ angular.module('application.router', ['ui.router']). memberPromise: ['Member', '$stateParams', (Member, $stateParams)-> Member.get(id: $stateParams.id).$promise ] + activeProviderPromise: ['AuthProvider', (AuthProvider) -> + AuthProvider.active().$promise + ] + walletPromise: ['Wallet', '$stateParams', (Wallet, $stateParams)-> + Wallet.getWalletByUser(user_id: $stateParams.id).$promise + ] + transactionsPromise: ['Wallet', 'walletPromise', (Wallet, walletPromise)-> + Wallet.transactions(id: walletPromise.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 + Translations.query(['app.admin.members_edit', 'app.shared.user', 'app.shared.user_admin', 'app.shared.wallet']).$promise ] .state 'app.admin.admins_new', url: '/admin/admins/new' @@ -804,7 +995,7 @@ angular.module('application.router', ['ui.router']). url: '/admin/statistics' views: 'main@': - templateUrl: '<%= asset_path "admin/statistics/index.html" %>' + templateUrl: '<%= asset_path "admin/statistics/index.html.erb" %>' controller: 'StatisticsController' resolve: membersPromise: ['Member', (Member) -> @@ -832,7 +1023,7 @@ angular.module('application.router', ['ui.router']). url: '/admin/settings' views: 'main@': - templateUrl: '<%= asset_path "admin/settings/index.html" %>' + templateUrl: '<%= asset_path "admin/settings/index.html.erb" %>' controller: 'SettingsController' resolve: settingsPromise: ['Setting', (Setting)-> @@ -846,7 +1037,6 @@ angular.module('application.router', ['ui.router']). 'training_explications_alert', 'training_information_message', 'subscription_explications_alert', - 'event_reduced_amount_alert', 'booking_window_start', 'booking_window_end', 'booking_move_enable', @@ -856,7 +1046,9 @@ angular.module('application.router', ['ui.router']). 'main_color', 'secondary_color', 'fablab_name', - 'name_genre' + 'name_genre', + 'reminder_enable', + 'reminder_delay' ]").$promise ] cguFile: ['CustomAsset', (CustomAsset) -> @@ -880,7 +1072,7 @@ angular.module('application.router', ['ui.router']). url: '/open_api_clients' views: 'main@': - templateUrl: '<%= asset_path "admin/open_api_clients/index.html" %>' + templateUrl: '<%= asset_path "admin/open_api_clients/index.html.erb" %>' controller: 'OpenAPIClientsController' resolve: clientsPromise: ['OpenAPIClient', (OpenAPIClient)-> diff --git a/app/assets/javascripts/services/age_range.coffee b/app/assets/javascripts/services/age_range.coffee new file mode 100644 index 000000000..e60d5b9d8 --- /dev/null +++ b/app/assets/javascripts/services/age_range.coffee @@ -0,0 +1,8 @@ +'use strict' + +Application.Services.factory 'AgeRange', ["$resource", ($resource)-> + $resource "/api/age_ranges/:id", + {id: "@id"}, + update: + method: 'PUT' +] diff --git a/app/assets/javascripts/services/availability.coffee b/app/assets/javascripts/services/availability.coffee index c96ca8e7b..8ef8b53a7 100644 --- a/app/assets/javascripts/services/availability.coffee +++ b/app/assets/javascripts/services/availability.coffee @@ -14,7 +14,8 @@ Application.Services.factory 'Availability', ["$resource", ($resource)-> isArray: true trainings: method: 'GET' - url: '/api/availabilities/trainings' + url: '/api/availabilities/trainings/:trainingId' + params: {trainingId: "@trainingId"} isArray: true update: method: 'PUT' diff --git a/app/assets/javascripts/services/calendar.coffee b/app/assets/javascripts/services/calendar.coffee new file mode 100644 index 000000000..61af184d6 --- /dev/null +++ b/app/assets/javascripts/services/calendar.coffee @@ -0,0 +1,38 @@ +'use strict' + +Application.Services.factory 'CalendarConfig', [-> + (options = {}) -> + # The calendar is divided in slots of 1 hour + BASE_SLOT = '01:00:00' + + # The calendar will be initialized positioned under 9:00 AM + DEFAULT_CALENDAR_POSITION = '09:00:00' + + defaultOptions = + 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 + + Object.assign({}, defaultOptions, options) +] diff --git a/app/assets/javascripts/services/coupon.coffee b/app/assets/javascripts/services/coupon.coffee new file mode 100644 index 000000000..291397aaf --- /dev/null +++ b/app/assets/javascripts/services/coupon.coffee @@ -0,0 +1,14 @@ +'use strict' + +Application.Services.factory 'Coupon', ["$resource", ($resource)-> + $resource "/api/coupons/:id", + {id: "@id"}, + update: + method: 'PUT' + validate: + method: 'POST' + url: '/api/coupons/validate' + send: + method: 'POST' + url: '/api/coupons/send' +] diff --git a/app/assets/javascripts/services/event_theme.coffee b/app/assets/javascripts/services/event_theme.coffee new file mode 100644 index 000000000..934a54f25 --- /dev/null +++ b/app/assets/javascripts/services/event_theme.coffee @@ -0,0 +1,8 @@ +'use strict' + +Application.Services.factory 'EventTheme', ["$resource", ($resource)-> + $resource "/api/event_themes/:id", + {id: "@id"}, + update: + method: 'PUT' +] diff --git a/app/assets/javascripts/services/export.coffee b/app/assets/javascripts/services/export.coffee new file mode 100644 index 000000000..3dd4dafa1 --- /dev/null +++ b/app/assets/javascripts/services/export.coffee @@ -0,0 +1,6 @@ +'use strict' + +Application.Services.factory 'Export', ["$http", ($http)-> + status: (query) -> + $http.post('/api/exports/status', query) +] diff --git a/app/assets/javascripts/services/helpers.coffee b/app/assets/javascripts/services/helpers.coffee new file mode 100644 index 000000000..488f12852 --- /dev/null +++ b/app/assets/javascripts/services/helpers.coffee @@ -0,0 +1,6 @@ +'use strict' + +Application.Services.factory 'helpers', [()-> + getAmountToPay: (price, walletAmount)-> + if walletAmount > price then 0 else price - walletAmount + ] diff --git a/app/assets/javascripts/services/price_category.coffee b/app/assets/javascripts/services/price_category.coffee new file mode 100644 index 000000000..6577fc8e0 --- /dev/null +++ b/app/assets/javascripts/services/price_category.coffee @@ -0,0 +1,8 @@ +'use strict' + +Application.Services.factory 'PriceCategory', ["$resource", ($resource)-> + $resource "/api/price_categories/:id", + {id: "@id"}, + update: + method: 'PUT' +] diff --git a/app/assets/javascripts/services/project.coffee b/app/assets/javascripts/services/project.coffee index 96c993506..7cfbaeab6 100644 --- a/app/assets/javascripts/services/project.coffee +++ b/app/assets/javascripts/services/project.coffee @@ -11,4 +11,8 @@ Application.Services.factory 'Project', ["$resource", ($resource)-> method: 'GET' url: '/api/projects/search' isArray: false + allowedExtensions: + method: 'GET' + url: '/api/projects/allowed_extensions' + isArray: true ] diff --git a/app/assets/javascripts/services/training.coffee b/app/assets/javascripts/services/training.coffee index c62f277d0..6647f0b12 100644 --- a/app/assets/javascripts/services/training.coffee +++ b/app/assets/javascripts/services/training.coffee @@ -5,4 +5,7 @@ Application.Services.factory 'Training', ["$resource", ($resource)-> {id: "@id"}, update: method: 'PUT' + availabilities: + method: 'GET' + url: "/api/trainings/:id/availabilities" ] diff --git a/app/assets/javascripts/services/version.coffee b/app/assets/javascripts/services/version.coffee new file mode 100644 index 000000000..dbed564b3 --- /dev/null +++ b/app/assets/javascripts/services/version.coffee @@ -0,0 +1,5 @@ +'use strict' + +Application.Services.factory 'Version', ["$resource", ($resource)-> + $resource "/api/version" +] diff --git a/app/assets/javascripts/services/wallet.coffee b/app/assets/javascripts/services/wallet.coffee new file mode 100644 index 000000000..44c414fdb --- /dev/null +++ b/app/assets/javascripts/services/wallet.coffee @@ -0,0 +1,18 @@ +'use strict' + +Application.Services.factory 'Wallet', ["$resource", ($resource)-> + $resource "/api/wallet", + {}, + getWalletByUser: + method: 'GET' + url: '/api/wallet/by_user/:user_id' + isArray: false + transactions: + method: 'GET' + url: '/api/wallet/:id/transactions' + isArray: true + credit: + method: 'PUT' + url: '/api/wallet/:id/credit' + isArray: false +] diff --git a/app/assets/stylesheets/app.base.scss b/app/assets/stylesheets/app.base.scss index 168c90161..b2ea363f4 100644 --- a/app/assets/stylesheets/app.base.scss +++ b/app/assets/stylesheets/app.base.scss @@ -62,11 +62,9 @@ p { line-height: rem-calc(24); &.intro, .intro { - font-family: $font-proxima-condensed; font-size: rem-calc(16); line-height: rem-calc(24); margin: 1.038em 0 30px 0; - font-weight: 600; } } diff --git a/app/assets/stylesheets/app.buttons.scss b/app/assets/stylesheets/app.buttons.scss index 49f97dd1c..5f8c187ad 100644 --- a/app/assets/stylesheets/app.buttons.scss +++ b/app/assets/stylesheets/app.buttons.scss @@ -2,7 +2,7 @@ .btn-default:hover, .btn-default:focus, .btn-default:active, .btn-default.active, .open > .btn-default.dropdown-toggle { background-color: #f2f2f2; - + } .btn{ @@ -24,6 +24,30 @@ } } +.btn-facebook { + border: 1px solid #8b9dc3; + background-color: #3b5998; + color: white; + + &:hover { + border: 1px solid #7d8fb4; + background-color: #394c89; + color: white; + } +} + +.btn-twitter { + border: 1px solid #ccd6dd; + background-color: #55acee; + color: white; + + &:hover { + border: 1px solid #bdc7ce; + background-color: #539fdf; + color: white; + } +} + .btn-block { padding-left: 12px; diff --git a/app/assets/stylesheets/app.colors.scss b/app/assets/stylesheets/app.colors.scss index 41a7bcbd5..e0c6d7d63 100644 --- a/app/assets/stylesheets/app.colors.scss +++ b/app/assets/stylesheets/app.colors.scss @@ -6,6 +6,7 @@ .bg-token { background-color: rgba(230, 208, 137, 0.49); } .bg-machine { background-color: $beige; } .bg-formation { background-color: $violet; } +.bg-event { background-color: $japonica; } .bg-atelier { background-color: $blue; } .bg-stage { background-color: $violet; } .bg-success { background-color: $brand-success; } @@ -35,3 +36,7 @@ .text-blue { color: $blue; } .text-muted { color: $text-muted; } .text-danger, .red { color: $red !important; } +.text-purple { color: $violet !important; } +.text-japonica { color: $japonica !important; } +.text-beige { color: $beige !important; } +.text-green, .green { color: #79C84A !important; } diff --git a/app/assets/stylesheets/app.components.scss b/app/assets/stylesheets/app.components.scss index 830632eb4..1fc857f85 100644 --- a/app/assets/stylesheets/app.components.scss +++ b/app/assets/stylesheets/app.components.scss @@ -154,8 +154,11 @@ } .article-thumbnail { - // max-height: 400px; overflow: hidden; + + img { + height: 400px; + } } } @@ -417,22 +420,23 @@ .event { transition: all 0.07s linear; + overflow: hidden; } .event:hover { -//background-color: #cb1117; -color: white; + // background-color: #cb1117; + color: white; } .event:hover * { -color: #eee; -border-color: #eee; + color: #eee !important; + border-color: #eee; } .box-h-m { - height: 150px; - max-height: 150px; + height: 175px; + max-height: 175px; } .half-w { @@ -446,25 +450,31 @@ border-color: #d0d0d0; padding: 10px; } -.crop-130 { - height: 130px; - width: 130px; - max-width: 130px; - max-height: 130px; +.crop-155 { + height: 155px; + width: 155px; + max-width: 155px; + max-height: 155px; overflow: hidden; vertical-align: bottom; } -.crop-130 img { - height: 130px; +.crop-155 img { + height: 155px; width: auto; } -@media only screen and (max-width: 1280px) and (min-width: 770px) { - .crop-130 { +@media only screen and (max-width: 1375px) and (min-width: 770px) { + .crop-155 { height: 90px; width: 90px; - margin-top: 25px; + margin-top: 35px; + } +} + +@media only screen and (max-width: 1375px) and (min-width: 1125px) { + .half-w { + width: 60%; } } @@ -489,6 +499,11 @@ padding: 10px; @media only screen and (max-width: 768px) { display: none; } + + .app-version { + margin-right: 10px; + color: #999; + } } .disabling-overlay { @@ -503,4 +518,78 @@ padding: 10px; z-index:10; border-radius: 3px; } +} + +.wallet-amount-container { + padding: 20px 0; + border-top: 2px dotted $border-color; + border-bottom: 2px dotted $border-color; + margin-bottom: 20px; + text-align: center; + + .wallet-amount { + font-size: rem-calc(40); + font-weight: 700; + font-style: italic; + color: #616161; + + span { + font-weight: 500; + font-size: .7em; + } + + &.cr-green { + color: $green; + } + } +} + +.amountGroup { + input { + display: inline-block; + width: 100px; + margin-left: 5px; + padding-right: 6px; + font-weight: bold; + color: $green; + font-size: 1.2em; + line-height: 0; + } + .afterAmount { + margin-left: -35px; + font-weight: bold; + color: $green; + font-size: 1.2em; + line-height: 0; + } +} + +.checkbox-group { + display: flex; + justify-content: flex-start; + + input[type=checkbox] { + font-size: 16px; + width: 2em; + } +} + + +.link-icon { + color: #1c94c4; + i { margin: 0 5px 0 10px; } + span { + border-bottom: 1px dashed #00b3ee; + text-decoration: none; + cursor: pointer; + } +} + + +.description-hover { + span { + display: inline-block; + border-bottom: 1px dashed #00b3ee; + cursor: help; + } } \ No newline at end of file diff --git a/app/assets/stylesheets/app.layout.scss b/app/assets/stylesheets/app.layout.scss index 68850aa24..bd693d343 100644 --- a/app/assets/stylesheets/app.layout.scss +++ b/app/assets/stylesheets/app.layout.scss @@ -88,7 +88,7 @@ color: black; } .heading-title { - overflow: hidden; + //overflow: hidden; height: 94px; h1 { margin: 0 0 0 15px; @@ -604,4 +604,14 @@ body.container{ display: inherit; text-align: center; height: 50px; -} \ No newline at end of file +} + +.calendar-filter { + h3 { + line-height: 2.1rem !important; + } +} + +.calendar-filter-aside { + padding: 20px; +} diff --git a/app/assets/stylesheets/app.plugins.scss b/app/assets/stylesheets/app.plugins.scss index 9d39c291c..c4f57ebbf 100644 --- a/app/assets/stylesheets/app.plugins.scss +++ b/app/assets/stylesheets/app.plugins.scss @@ -91,7 +91,7 @@ cursor: pointer; z-index: 9999; text-align: right; - .training-reserve &, .machine-reserve & { display: none; } + .training-reserve &, .machine-reserve &, .public-calendar & { display: none; } } .fc-v-event.fc-end { @@ -102,6 +102,15 @@ display: none !important; } +.calendar-filter { + .badge { + cursor: pointer; + + &.inactive { + opacity: 0.2; + } + } +} diff --git a/app/assets/stylesheets/app.utilities.scss b/app/assets/stylesheets/app.utilities.scss index b8c0e6aa5..eca31e638 100644 --- a/app/assets/stylesheets/app.utilities.scss +++ b/app/assets/stylesheets/app.utilities.scss @@ -102,6 +102,7 @@ p, .widget p { .text-italic { font-style: italic; } .text-center { text-align: center; } +.text-right { text-align: right; } .text-active, .active > .text, .active > .auto .text{display: none !important;} .active > .text-active, .active > .auto .text-active{display: inline-block !important;} @@ -125,7 +126,9 @@ p, .widget p { .pull-in{margin-left: -15px;margin-right: -15px;} .pull-out{margin:-10px -15px;} +.width-35 { width: 35% !important; } .width-70 { width: 70%; } +.width-90 { width: 90%; } .b{border: 1px solid rgba(0, 0, 0, 0.05)} .b-a{border: 1px solid $border-color} @@ -171,6 +174,7 @@ p, .widget p { } .r-n { border-radius: 0 0 0 0; } +.p-xs { padding: 5px;} .p-lg { padding: 30px; } .p-l { padding: 16px; } @@ -342,6 +346,7 @@ p, .widget p { @media screen and (min-width: $screen-lg-min) { .b-r-lg {border-right: 1px solid $border-color; } + .hide-b-r-lg { border: none !important; } } diff --git a/app/assets/stylesheets/application.scss.erb b/app/assets/stylesheets/application.scss.erb index 2ae151239..4cbd1ff78 100644 --- a/app/assets/stylesheets/application.scss.erb +++ b/app/assets/stylesheets/application.scss.erb @@ -13,6 +13,7 @@ *= require bootstrap-switch/dist/css/bootstrap3/bootstrap-switch.min *= require summernote/dist/summernote *= require jquery-minicolors/jquery.minicolors.css + *= require angular-aside/dist/css/angular-aside */ @import "app.functions"; diff --git a/app/assets/stylesheets/bootstrap_and_overrides.scss b/app/assets/stylesheets/bootstrap_and_overrides.scss index 4eb38226e..e4081ff3a 100644 --- a/app/assets/stylesheets/bootstrap_and_overrides.scss +++ b/app/assets/stylesheets/bootstrap_and_overrides.scss @@ -43,6 +43,7 @@ $blue: $brand-info; $green: $brand-success; $beige: #e4cd78; $violet: #bd7ae9; +$japonica: #dd7e6b; $border-color: #dddddd; $header-bg: $bg-gray; diff --git a/app/assets/templates/admin/authentications/_data_mapping.html.erb b/app/assets/templates/admin/authentications/_data_mapping.html.erb new file mode 100644 index 000000000..6fd8376ea --- /dev/null +++ b/app/assets/templates/admin/authentications/_data_mapping.html.erb @@ -0,0 +1,65 @@ + + + \ No newline at end of file diff --git a/app/assets/templates/admin/authentications/_oauth2_mapping.html.erb b/app/assets/templates/admin/authentications/_oauth2_mapping.html.erb index bb16e52f1..7756608d0 100644 --- a/app/assets/templates/admin/authentications/_oauth2_mapping.html.erb +++ b/app/assets/templates/admin/authentications/_oauth2_mapping.html.erb @@ -21,6 +21,9 @@ {{m.api_data_type}} {{m.api_field}} + @@ -38,7 +41,7 @@ diff --git a/app/assets/templates/admin/authentications/index.html.erb b/app/assets/templates/admin/authentications/index.html.erb index d0d9b5313..4b0a75bc5 100644 --- a/app/assets/templates/admin/authentications/index.html.erb +++ b/app/assets/templates/admin/authentications/index.html.erb @@ -14,6 +14,8 @@ {{ 'name' }} + {{ 'strategy_name' }} + {{ 'type' }} {{ 'state' }} @@ -24,6 +26,7 @@ {{ provider.name }} + {{ provider.strategy_name }} {{ getType(provider.providable_type) }} {{ getState(provider.status) }} diff --git a/app/assets/templates/admin/coupons/_form.html.erb b/app/assets/templates/admin/coupons/_form.html.erb new file mode 100644 index 000000000..81274bcc2 --- /dev/null +++ b/app/assets/templates/admin/coupons/_form.html.erb @@ -0,0 +1,103 @@ +
+ + + {{ 'name_is_required' }} +
+ +
+ + + {{ 'code_is_required' }} + {{ 'code_must_be_composed_of_capital_letters_digits_and_or_dashes' }} +
+ +
+ +
+ + +
+ {{ 'percent_off_is_required' }} + {{ 'percentage_must_be_between_0_and_100' }} +
+ +
+ + + {{ 'validity_per_user_is_required' }} +
+ +
+ +
+ + + + +
+ + + {{ 'leave_empty_for_no_limit' | translate }} + +
+ +
+ + + {{ 'max_usages_must_be_equal_or_greater_than_0' }} + + + {{ 'leave_empty_for_no_limit' | translate }} + +
+ +
+ + + +
diff --git a/app/assets/templates/admin/coupons/edit.html.erb b/app/assets/templates/admin/coupons/edit.html.erb new file mode 100644 index 000000000..b391f8dbc --- /dev/null +++ b/app/assets/templates/admin/coupons/edit.html.erb @@ -0,0 +1,40 @@ +
+
+
+
+ +
+
+
+
+

{{ 'coupon' | translate }} : {{ coupon.name }}

+
+
+ +
+
+ {{ 'cancel' }} +
+ +
+ + +
+
+ +
+
+ +
+
+ + + + +
+
+ +
+
diff --git a/app/assets/templates/admin/coupons/new.html.erb b/app/assets/templates/admin/coupons/new.html.erb new file mode 100644 index 000000000..16bf475cc --- /dev/null +++ b/app/assets/templates/admin/coupons/new.html.erb @@ -0,0 +1,33 @@ +
+
+
+
+ +
+
+
+
+

{{ 'add_a_coupon' }}

+
+
+ +
+
+ +
+
+ +
+
+ + + + + +
+
+ +
+
diff --git a/app/assets/templates/admin/events/filters.html.erb b/app/assets/templates/admin/events/filters.html.erb new file mode 100644 index 000000000..7a2e5d130 --- /dev/null +++ b/app/assets/templates/admin/events/filters.html.erb @@ -0,0 +1,120 @@ +
+

{{ 'categories' }}

+

{{ 'at_least_one_category_is_required' }}

+ + + + + + + + + + + + + + +
{{ 'name' }}
+ + {{ category.name }} + + + +
+ + +
+
+ + +
+
+ +

{{ 'themes' }}

+ + + + + + + + + + + + + + +
{{ 'name' }}
+ + {{ theme.name }} + + + +
+ + +
+
+ + +
+
+ +

{{ 'age_ranges' }}

+ + + + + + + + + + + + + + +
{{ 'name' }}
+ + {{ range.name }} + + + +
+ + +
+
+ + +
+
+ +
\ No newline at end of file diff --git a/app/assets/templates/admin/events/index.html.erb b/app/assets/templates/admin/events/index.html.erb index 1a38d12d3..d4647980b 100644 --- a/app/assets/templates/admin/events/index.html.erb +++ b/app/assets/templates/admin/events/index.html.erb @@ -7,7 +7,7 @@
-

{{ 'fablab_courses_and_workshops' }}

+

{{ 'fablab_events' }}

@@ -21,57 +21,19 @@
+ + + + -
- -
+ + + - - - - - - - - - - - - - - - -
{{ 'title' }}{{ 'dates' }}
- {{ event.title }} - - {{ 'from_DATE' | translate:{DATE:(event.start_date | amDateFormat:'LL')} }} {{ 'to_date' }} {{event.end_date | amDateFormat:'LL'}} -
- {{ 'all_day' }} - - {{ 'from_TIME' | translate:{TIME:(event.start_date | amDateFormat:'LT')} }} - {{ 'to_time' }} - {{event.end_date | amDateFormat:'LT'}} - -
-
- - -
-
-
-
- -
diff --git a/app/assets/templates/admin/events/monitoring.html.erb b/app/assets/templates/admin/events/monitoring.html.erb new file mode 100644 index 000000000..99a97720b --- /dev/null +++ b/app/assets/templates/admin/events/monitoring.html.erb @@ -0,0 +1,50 @@ +
+ +
+ + + + + + + + + + + + + + + + +
{{ 'title' }}{{ 'dates' }}
+ {{ event.title }} + + {{ 'from_DATE' | translate:{DATE:(event.start_date | amDateFormat:'LL')} }} {{ 'to_date' }} {{event.end_date | amDateFormat:'LL'}} +
+ {{ 'all_day' }} + + {{ 'from_TIME' | translate:{TIME:(event.start_date | amDateFormat:'LT')} }} + {{ 'to_time' }} + {{event.end_date | amDateFormat:'LT'}} + +
+
+ + +
+
+ + \ No newline at end of file diff --git a/app/assets/templates/admin/events/price_form.html b/app/assets/templates/admin/events/price_form.html new file mode 100644 index 000000000..2aa84572d --- /dev/null +++ b/app/assets/templates/admin/events/price_form.html @@ -0,0 +1,40 @@ + + + diff --git a/app/assets/templates/admin/events/prices.html.erb b/app/assets/templates/admin/events/prices.html.erb new file mode 100644 index 000000000..fcca1229b --- /dev/null +++ b/app/assets/templates/admin/events/prices.html.erb @@ -0,0 +1,31 @@ +
+

{{ 'prices_categories' }}

+ + + + + + + + + + + + + + + + + +
{{ 'name' }}{{ 'usages_count' }}
{{ category.name }}{{ category.events }} +
+ + +
+
+ +
\ No newline at end of file diff --git a/app/assets/templates/admin/events/reservations.html.erb b/app/assets/templates/admin/events/reservations.html.erb index f21bad55f..45d4c47a8 100644 --- a/app/assets/templates/admin/events/reservations.html.erb +++ b/app/assets/templates/admin/events/reservations.html.erb @@ -32,7 +32,10 @@ {{ reservation.user_full_name }} {{ reservation.created_at | amDateFormat:'LL LTS' }} - {{ 'full_price_' | translate }} {{reservation.nb_reserve_places}}
{{ 'reduced_rate_' | translate }} {{reservation.nb_reserve_reduced_places}} + + {{ 'full_price_' | translate }} {{reservation.nb_reserve_places}}
+ {{ticket.event_price_category.price_category.name}} : {{ticket.booked}} +
@@ -279,6 +280,12 @@ + + \ No newline at end of file + diff --git a/app/assets/templates/admin/members/administrators.html.erb b/app/assets/templates/admin/members/administrators.html.erb new file mode 100644 index 000000000..fa2cc4c79 --- /dev/null +++ b/app/assets/templates/admin/members/administrators.html.erb @@ -0,0 +1,39 @@ +
+
+
+ + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'surname' | translate }} {{ 'first_name' | translate }} {{ 'email' | translate }} {{ 'phone' | translate }}
{{ admin.profile_attributes.last_name }}{{ admin.profile_attributes.first_name }}{{ admin.email }}{{ admin.profile_attributes.phone }} + +
+
\ No newline at end of file diff --git a/app/assets/templates/admin/members/edit.html.erb b/app/assets/templates/admin/members/edit.html.erb index 92107c960..099fc7745 100644 --- a/app/assets/templates/admin/members/edit.html.erb +++ b/app/assets/templates/admin/members/edit.html.erb @@ -9,7 +9,8 @@
-

{{ 'user' | translate }} {{ user.name }}

+

{{ 'user' | translate }} {{ user.name }}

+ {{ 'incomplete_profile' }}
@@ -33,7 +34,14 @@ - + + +
+
+ {{ 'warning_incomplete_user_profile_probably_imported_from_sso' }} +
+
+
@@ -138,11 +146,11 @@ - +
-

{{ 'next_courses_and_workshops' | translate }}

+

{{ 'next_events' | translate }}

    @@ -152,20 +160,20 @@
    {{ 'NUMBER_full_price_tickets_reserved' }} - +
    - {{ 'NUMBER_reduced_rate_tickets_reserved' }} + {{ 'NUMBER_NAME_tickets_reserved' }}
-
{{ 'no_upcomning_courses_or_workshops'}}
+
{{ 'no_upcoming_events' }}
-

{{ 'passed_courses_and_workshops' | translate }}

+

{{ 'passed_events' | translate }}

    @@ -173,7 +181,7 @@ {{r.reservable.title}} - {{ r.start_at | amDateFormat:'LLL' }} - {{ r.end_at | amDateFormat:'LT' }}
-
{{ 'no_passed_courses_or_workshop' }}
+
{{ 'no_passed_events' }}
@@ -215,6 +223,22 @@
+ + +
+ + +
+
+ +
+ +
+ +
+ +
+
diff --git a/app/assets/templates/admin/members/index.html.erb b/app/assets/templates/admin/members/index.html.erb index f1ea808bd..1137646e2 100644 --- a/app/assets/templates/admin/members/index.html.erb +++ b/app/assets/templates/admin/members/index.html.erb @@ -16,130 +16,29 @@
-
- +
+ - -
-
-
- - -
-
-
-
- - + + + + + + - - - - + +
+
- + +
+
- - - - - - - - - - - - - - - - - - - - - - -
{{ 'surname' | translate }} {{ 'first_name' | translate }} {{ 'email' | translate }} {{ 'phone' | translate }} {{ 'user_type' | translate }} {{ 'subscription' | translate }}
{{ m.profile.last_name }}{{ m.profile.first_name }}{{ m.email }}{{ m.profile.phone }}{{ m.group.name }}{{ m.subscribed_plan | humanReadablePlanName }} -
- -
-
-
- -
-
-
- - -
-
-
- - -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - -
{{ 'surname' | translate }} {{ 'first_name' | translate }} {{ 'email' | translate }} {{ 'phone' | translate }}
{{ admin.profile_attributes.last_name }}{{ admin.profile_attributes.first_name }}{{ admin.email }}{{ admin.profile_attributes.phone }} - -
-
-
- - -
-
- - -
-
- - - -
-
-
-
+ +
+
+
+
diff --git a/app/assets/templates/admin/members/members.html.erb b/app/assets/templates/admin/members/members.html.erb new file mode 100644 index 000000000..8da4bf427 --- /dev/null +++ b/app/assets/templates/admin/members/members.html.erb @@ -0,0 +1,59 @@ +
+
+
+ + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'surname' | translate }} {{ 'first_name' | translate }}
{{ m.profile.last_name }}{{ m.profile.first_name }} +
+ + {{ 'incomplete_profile' }} +
+
+
+ +
+
\ No newline at end of file diff --git a/app/assets/templates/admin/plans/edit.html.erb b/app/assets/templates/admin/plans/edit.html.erb index 5c9020129..cca5e564e 100644 --- a/app/assets/templates/admin/plans/edit.html.erb +++ b/app/assets/templates/admin/plans/edit.html.erb @@ -47,7 +47,7 @@ - {{ price.priceable_name }} (id {{ price.priceable_id }}) * + {{ getMachineName(price.priceable_id) }} (id {{ price.priceable_id }}) *
{{currencySymbol}} diff --git a/app/assets/templates/admin/plans/new.html.erb b/app/assets/templates/admin/plans/new.html.erb index bd5621fb8..f8c3d3a19 100644 --- a/app/assets/templates/admin/plans/new.html.erb +++ b/app/assets/templates/admin/plans/new.html.erb @@ -23,7 +23,7 @@ diff --git a/app/assets/templates/admin/pricing/coupons.html.erb b/app/assets/templates/admin/pricing/coupons.html.erb new file mode 100644 index 000000000..61c97dff1 --- /dev/null +++ b/app/assets/templates/admin/pricing/coupons.html.erb @@ -0,0 +1,27 @@ +

{{ 'list_of_the_coupons' }}

+ + + + + + + + + + + + + + + + + + + + + +
{{ 'name' }}{{ 'percentage_off' }}{{ 'nb_of_usages' }}{{ 'status' }}
{{coupon.name}}{{coupon.percent_off}} %{{coupon.usages}}{{coupon.status}} + + + +
\ No newline at end of file diff --git a/app/assets/templates/admin/pricing/credits.html.erb b/app/assets/templates/admin/pricing/credits.html.erb new file mode 100644 index 000000000..0ba6ea08d --- /dev/null +++ b/app/assets/templates/admin/pricing/credits.html.erb @@ -0,0 +1,97 @@ +

{{ 'trainings' }}

+ + + + + + + + + + + + + + + + + + +
{{ 'subscription' }}{{ 'credits' }}{{ 'related_trainings' }}
+ {{ plan | humanReadablePlanName: groups }} + + + {{ plan.training_credit_nb }} + + + + {{ showTrainings(trainingIds) }} + + +
+ + +
+
+ +
+
+ +

{{ 'machines' }}

+
+ +
+ + + + + + + + + + + + + + + + + + +
{{ 'machine' }}{{ 'hours' }}{{ 'related_subscriptions' }}
+ + {{ showCreditableName(mc) }} + + + + {{ mc.hours }} + + + + {{ getPlanFromId(mc.plan_id) | humanReadablePlanName: groups: 'short' }} + + +
+ + +
+
+ + +
+
\ No newline at end of file diff --git a/app/assets/templates/admin/pricing/index.html.erb b/app/assets/templates/admin/pricing/index.html.erb index 9f771348f..df110a925 100644 --- a/app/assets/templates/admin/pricing/index.html.erb +++ b/app/assets/templates/admin/pricing/index.html.erb @@ -22,195 +22,23 @@ -

{{ 'list_of_the_subscription_plans' }}

- -
- {{ 'beware_the_subscriptions_are_disabled_on_this_application' | translate }} - {{ 'you_can_create_some_but_they_wont_be_available_until_the_project_is_redeployed_by_the_server_manager' | translate }} -
{{ 'for_safety_reasons_please_dont_create_subscriptions_if_you_dont_want_intend_to_use_them_later' | translate }} -
- - - - - - - - - - - - - - - - - - - - - - - - - -
{{ 'type' | translate }} {{ 'name' | translate }} {{ 'duration' | translate }} {{ 'group' | translate }} {{ 'prominence' | translate }} {{ 'price' | translate }}
{{getPlanType(plan.type)}}{{plan.base_name}}{{ plan.interval | planIntervalFilter:plan.interval_count }}{{getGroupFromId(groups, plan.group_id).name}}{{plan.ui_weight}}{{plan.amount | currency}}
+
- - - - - - - - - - - - - - -
{{ 'trainings' }} - {{group.name}} -
- {{ training.name }} - - - {{ findTrainingsPricing(trainingsPricings, training.id, group.id).amount | currency}} - -
+
-
- {{ 'these_prices_match_machine_hours_rates_' | translate }} {{ '_without_subscriptions' }}. -
- - - - - - - - - - - - - -
{{ 'machines' }} - {{group.name}} -
- {{ machine.name }} - - - {{ findPriceBy(machinesPrices, machine.id, group.id).amount | currency}} - -
+
-

{{ 'trainings' }}

- - - - - - - - - - + + - - - - - - - -
{{ 'subscription' }}{{ 'credits' }}{{ 'related_trainings' }}
- {{ plan | humanReadablePlanName: groups }} - - - {{ plan.training_credit_nb }} - - - - {{ showTrainings(trainingIds) }} - - -
- - -
-
- -
-
- -

{{ 'machines' }}

-
- -
- - - - - - - - - - - - - - - - - - -
{{ 'machine' }}{{ 'hours' }}{{ 'related_subscriptions' }}
- - {{ showCreditableName(mc) }} - - - - {{ mc.hours }} - - - - {{ getPlanFromId(mc.plan_id) | humanReadablePlanName: groups: 'short' }} - - -
- - -
-
- - -
-
+ +
diff --git a/app/assets/templates/admin/pricing/machine_hours.html.erb b/app/assets/templates/admin/pricing/machine_hours.html.erb new file mode 100644 index 000000000..d846d9574 --- /dev/null +++ b/app/assets/templates/admin/pricing/machine_hours.html.erb @@ -0,0 +1,26 @@ +
+ {{ 'these_prices_match_machine_hours_rates_' | translate }} {{ '_without_subscriptions' }}. +
+ + + + + + + + + + + + + +
{{ 'machines' }} + {{group.name}} +
+ {{ machine.name }} + + + {{ findPriceBy(machinesPrices, machine.id, group.id).amount | currency}} + +
\ No newline at end of file diff --git a/app/assets/templates/admin/pricing/sendCoupon.html.erb b/app/assets/templates/admin/pricing/sendCoupon.html.erb new file mode 100644 index 000000000..54cbbdcb3 --- /dev/null +++ b/app/assets/templates/admin/pricing/sendCoupon.html.erb @@ -0,0 +1,30 @@ + + + \ No newline at end of file diff --git a/app/assets/templates/admin/pricing/subscriptions.html.erb b/app/assets/templates/admin/pricing/subscriptions.html.erb new file mode 100644 index 000000000..792934cb3 --- /dev/null +++ b/app/assets/templates/admin/pricing/subscriptions.html.erb @@ -0,0 +1,33 @@ +

{{ 'list_of_the_subscription_plans' }}

+ +
+ {{ 'beware_the_subscriptions_are_disabled_on_this_application' | translate }} + {{ 'you_can_create_some_but_they_wont_be_available_until_the_project_is_redeployed_by_the_server_manager' | translate }} +
{{ 'for_safety_reasons_please_dont_create_subscriptions_if_you_dont_want_intend_to_use_them_later' | translate }} +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'type' | translate }} {{ 'name' | translate }} {{ 'duration' | translate }} {{ 'group' | translate }} {{ 'price' | translate }}
{{getPlanType(plan.type)}}{{plan.base_name}}{{ plan.interval | planIntervalFilter:plan.interval_count }}{{getGroupFromId(groups, plan.group_id).name}}{{plan.amount | currency}}
\ No newline at end of file diff --git a/app/assets/templates/admin/pricing/trainings.html.erb b/app/assets/templates/admin/pricing/trainings.html.erb new file mode 100644 index 000000000..d93f42c8e --- /dev/null +++ b/app/assets/templates/admin/pricing/trainings.html.erb @@ -0,0 +1,23 @@ + + + + + + + + + + + + + +
{{ 'trainings' }} + {{group.name}} +
+ {{ training.name }} + + + {{ findTrainingsPricing(trainingsPricings, training.id, group.id).amount | currency}} + +
\ No newline at end of file diff --git a/app/assets/templates/admin/project_elements/index.html.erb b/app/assets/templates/admin/project_elements/index.html.erb index b4264d2bc..8e8da7799 100644 --- a/app/assets/templates/admin/project_elements/index.html.erb +++ b/app/assets/templates/admin/project_elements/index.html.erb @@ -21,130 +21,13 @@
- - - - - - - - - - - - - - - -
{{ 'name' }}
- - {{ component.name }} - - - -
- - -
-
- - -
-
+
- - - - - - - - - - - - - - - -
{{ 'name' }}
- - {{ theme.name }} - - - -
- - -
-
- - -
-
+
- - - - - - - - - - - - - - - - - -
{{ 'name' }}
- - {{ licence.name }} - - - -
- - -
-
- - -
-
+
diff --git a/app/assets/templates/admin/project_elements/licences.html.erb b/app/assets/templates/admin/project_elements/licences.html.erb new file mode 100644 index 000000000..78c8c82e7 --- /dev/null +++ b/app/assets/templates/admin/project_elements/licences.html.erb @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + +
{{ 'name' }}
+ + {{ licence.name }} + + + +
+ + +
+
+ + +
+
\ No newline at end of file diff --git a/app/assets/templates/admin/project_elements/materials.html.erb b/app/assets/templates/admin/project_elements/materials.html.erb new file mode 100644 index 000000000..320fdc238 --- /dev/null +++ b/app/assets/templates/admin/project_elements/materials.html.erb @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + +
{{ 'name' }}
+ + {{ component.name }} + + + +
+ + +
+
+ + +
+
\ No newline at end of file diff --git a/app/assets/templates/admin/project_elements/themes.html.erb b/app/assets/templates/admin/project_elements/themes.html.erb new file mode 100644 index 000000000..3be7eaa0b --- /dev/null +++ b/app/assets/templates/admin/project_elements/themes.html.erb @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + +
{{ 'name' }}
+ + {{ theme.name }} + + + +
+ + +
+
+ + +
+
\ No newline at end of file diff --git a/app/assets/templates/admin/settings/about.html b/app/assets/templates/admin/settings/about.html new file mode 100644 index 000000000..9adf2b614 --- /dev/null +++ b/app/assets/templates/admin/settings/about.html @@ -0,0 +1,35 @@ +
+
+ +
+
+

+ {{ 'shift_enter_to_force_carriage_return' | translate }} + +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ {{ 'shift_enter_to_force_carriage_return' | translate }} + +
+ +
+ +
+
\ No newline at end of file diff --git a/app/assets/templates/admin/settings/general.html b/app/assets/templates/admin/settings/general.html new file mode 100644 index 000000000..cceea1e2a --- /dev/null +++ b/app/assets/templates/admin/settings/general.html @@ -0,0 +1,297 @@ +
+
+ {{ 'title' }} +
+
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+

{{ 'title_concordance' }}

+
+ + +
+ + +
+ +
+
+
+
+
+ +
+
+ {{ 'customize_information_messages' }} +
+
+ + +
+
+

{{ 'message_of_the_machine_booking_page' }}

+
+ +
+ +
+
+

{{ 'warning_message_of_the_training_booking_page'}}

+
+ +
+ +
+
+

{{ 'information_message_of_the_training_reservation_page'}}

+
+ +
+ +
+
+

{{ 'message_of_the_subscriptions_page' }}

+
+
+ +
+
+
+
+ +
+
+ {{ 'legal_documents'}} +
+
+
+ {{ 'if_these_documents_are_not_filled_no_consent_about_them_will_be_asked_to_the_user' }} +
+
+
+ + + +
+
+
+ {{cgvFile.custom_asset_file_attributes.attachment}} +
+ + {{ 'browse' }} + {{ 'change' }} + + +
+
+ +
+
+
+
+ + + +
+
+
+ {{cguFile.custom_asset_file_attributes.attachment}} +
+ + {{ 'browse' }} + {{ 'change' }} + + +
+
+ +
+
+
+
+ +
+
+ {{ 'customize_the_graphics' }} +
+
+
+ {{ 'for_an_optimal_rendering_the_logo_image_must_be_at_the_PNG_format_with_a_transparent_background_and_with_an_aspect_ratio_3.5_times_wider_than_the_height' }}
+ {{ 'concerning_the_favicon_it_must_be_at_ICO_format_with_a_size_of_16x16_pixels' }}
+
+ {{ 'remember_to_refresh_the_page_for_the_changes_to_take_effect' }} +
+
+
+
+ + +

{{ 'logo_(white_background)' }}

+ + +
+
+
+
+ + +

{{ 'logo_(black_background)' }}

+ + +
+
+
+
+ + +

{{ 'favicon' }}

+
+ + + {{customFavicon.custom_asset_file_attributes.attachment}} +
+
+
+ {{ 'change_the_favicon' | translate }} + +
+
+
+
+ +
+
+
+
+
+

{{ 'main_colour' }}

+
+
+
+
+ +
+ +
+
+
+ +
+
+
+
+

{{ 'secondary_colour' }}

+
+
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+
+
+ + +

{{ 'background_picture_of_the_profile_banner' }}

+
+ + + {{profileImage.custom_asset_file_attributes.attachment}} +
+
+
+ {{ 'change_the_profile_banner' | translate }} + +
+
+
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/app/assets/templates/admin/settings/home_page.html b/app/assets/templates/admin/settings/home_page.html new file mode 100644 index 000000000..3f6ee30a1 --- /dev/null +++ b/app/assets/templates/admin/settings/home_page.html @@ -0,0 +1,29 @@ +
+
+
+
+

{{ 'news_of_the_home_page' }}

+
+ {{ 'leave_it_empty_to_not_bring_up_any_news_on_the_home_page' | translate }} + +
+
+

{{ 'twitter_stream' }}

+
+
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+
\ No newline at end of file diff --git a/app/assets/templates/admin/settings/index.html b/app/assets/templates/admin/settings/index.html deleted file mode 100644 index 821060501..000000000 --- a/app/assets/templates/admin/settings/index.html +++ /dev/null @@ -1,491 +0,0 @@ -
-
-
-
- -
-
-
-
-

{{ 'customize_the_application' }}

-
-
- -
-
- -
-
- -
- - - -
-
- {{ 'title' }} -
-
-
-
-
- -
-
-
- -
-
- -
-
- -
-
-

{{ 'title_concordance' }}

-
- - -
- - -
- -
-
-
-
-
- -
-
- {{ 'customize_information_messages' }} -
-
- - -
-
-

{{ 'message_of_the_machine_booking_page' }}

-
- -
- -
-
-

{{ 'warning_message_of_the_training_booking_page'}}

-
- -
- -
-
-

{{ 'information_message_of_the_training_reservation_page'}}

-
- -
- -
-
-

{{ 'message_of_the_subscriptions_page' }}

-
-
- -
-
-
-
-

{{ 'message_of_the_event_page_relative_to_the_reduced_rate_availability_conditions' }}

-
-
- -
-
- -
-
- -
-
- {{ 'legal_documents'}} -
-
-
- {{ 'if_these_documents_are_not_filled_no_consent_about_them_will_be_asked_to_the_user' }} -
-
-
- - - -
-
-
- {{cgvFile.custom_asset_file_attributes.attachment}} -
- - {{ 'browse' }} - {{ 'change' }} - - -
-
- -
-
-
-
- - - -
-
-
- {{cguFile.custom_asset_file_attributes.attachment}} -
- - {{ 'browse' }} - {{ 'change' }} - - -
-
- -
-
-
-
- -
-
- {{ 'customize_the_graphics' }} -
-
-
- {{ 'for_an_optimal_rendering_the_logo_image_must_be_at_the_PNG_format_with_a_transparent_background_and_with_an_aspect_ratio_3.5_times_wider_than_the_height' }}
- {{ 'concerning_the_favicon_it_must_be_at_ICO_format_with_a_size_of_16x16_pixels' }}
-
- {{ 'remember_to_refresh_the_page_for_the_changes_to_take_effect' }} -
-
-
-
- - -

{{ 'logo_(white_background)' }}

- - -
-
-
-
- - -

{{ 'logo_(black_background)' }}

- - -
-
-
-
- - -

{{ 'favicon' }}

-
- - - {{customFavicon.custom_asset_file_attributes.attachment}} -
-
-
- {{ 'change_the_favicon' | translate }} - -
-
-
-
- -
-
-
-
-
-

{{ 'main_colour' }}

-
-
-
-
- -
- -
-
-
- -
-
-
-
-

{{ 'secondary_colour' }}

-
-
-
-
- -
- -
-
-
- -
-
-
-
-
-
-
- - -

{{ 'background_picture_of_the_profile_banner' }}

-
- - - {{profileImage.custom_asset_file_attributes.attachment}} -
-
-
- {{ 'change_the_profile_banner' | translate }} - -
-
-
-
- -
-
-
-
-
-
- - -
-
-
-
-

{{ 'news_of_the_home_page' }}

-
- {{ 'leave_it_empty_to_not_bring_up_any_news_on_the_home_page' | translate }} - -
-
-

{{ 'twitter_stream' }}

-
-
-
-
- -
- -
-
-
- -
-
-
-
-
-
-
- - -
-
- -
-
-

- {{ 'shift_enter_to_force_carriage_return' | translate }} - -
- -
- -
-
-
- -
- -
-
-
- -
- {{ 'shift_enter_to_force_carriage_return' | translate }} - -
- -
- -
-
-
- -
-
- {{ 'reservations_parameters' }} -
-
-
-
-

{{ 'confine_the_booking_agenda' }}

-
-

{{ 'opening_time' }}

- -
-
- -
-
-

{{ 'closing_time' }}

- -
-
- -
-
-
-

{{ 'ability_for_the_users_to_move_their_reservations' }}

-
- - - -
-
-
-
- -
-
-
- -
- -
-
- -
-
-
-

{{ 'ability_for_the_users_to_cancel_their_reservations' }}

-
- - - -
-
-
-
- -
-
-
- -
- -
-
- -
-
-
-
-
-
-
-
- -
-
diff --git a/app/assets/templates/admin/settings/index.html.erb b/app/assets/templates/admin/settings/index.html.erb new file mode 100644 index 000000000..aacf87196 --- /dev/null +++ b/app/assets/templates/admin/settings/index.html.erb @@ -0,0 +1,41 @@ +
+
+
+
+ +
+
+
+
+

{{ 'customize_the_application' }}

+
+
+ +
+
+ +
+
+ +
+ + + + + + + + + + + + + + + + + +
+ +
+
diff --git a/app/assets/templates/admin/settings/reservations.html b/app/assets/templates/admin/settings/reservations.html new file mode 100644 index 000000000..00cc92454 --- /dev/null +++ b/app/assets/templates/admin/settings/reservations.html @@ -0,0 +1,124 @@ +
+
+ {{ 'reservations_parameters' }} +
+
+
+
+

{{ 'confine_the_booking_agenda' }}

+
+

{{ 'opening_time' }}

+ +
+
+ +
+
+

{{ 'closing_time' }}

+ +
+
+ +
+
+
+

{{ 'ability_for_the_users_to_move_their_reservations' }}

+
+ + + +
+
+
+
+ +
+
+
+ +
+ +
+
+ +
+
+
+

{{ 'ability_for_the_users_to_cancel_their_reservations' }}

+
+ + + +
+
+
+
+ +
+
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+
+ {{ 'reservations_reminders' }} +
+
+
+

{{ 'notification_sending_before_the_reservation_occurs' }}

+
+ + + +
+
+
+
+ +
+
+
+ +
+ +
+ + {{ 'default_value_is_24_hours' | translate }} + +
+ +
+
+
+
\ No newline at end of file diff --git a/app/assets/templates/admin/statistics/export.html.erb b/app/assets/templates/admin/statistics/export.html.erb new file mode 100644 index 000000000..93ab32932 --- /dev/null +++ b/app/assets/templates/admin/statistics/export.html.erb @@ -0,0 +1,76 @@ + + + diff --git a/app/assets/templates/admin/statistics/index.html.erb b/app/assets/templates/admin/statistics/index.html.erb index 9ffe47fba..1226f531b 100644 --- a/app/assets/templates/admin/statistics/index.html.erb +++ b/app/assets/templates/admin/statistics/index.html.erb @@ -12,6 +12,8 @@ @@ -26,7 +28,7 @@
-
+
-
+
-
    +
    • {{ 'start' }}
      @@ -233,6 +235,7 @@
    • {{ 'revenue_' | translate }} {{sumCA | currency}}
    • {{ 'average_age' | translate }} {{averageAge}} {{ 'years_old' | translate }}
    • {{ 'total' | translate }} {{type.active.label}} : {{sumStat}}
    • +
    • {{ custom.field | translate }} {{customAggs[custom.field]}}
@@ -287,4 +290,3 @@
- diff --git a/app/assets/templates/admin/trainings/_form.html.erb b/app/assets/templates/admin/trainings/_form.html.erb new file mode 100644 index 000000000..91e09131b --- /dev/null +++ b/app/assets/templates/admin/trainings/_form.html.erb @@ -0,0 +1,122 @@ + + + + +
+
+ + {{alert.msg}} + +
+ +
+ + {{ 'name_is_required' }} +
+
+ +
+ +
+
+
+ +
+
+ +
+
+ + {{ 'add_an_illustration' | translate }} + {{ 'change' }} + + + {{ 'delete' }} +
+
+
+
+ + +
+ +
+ + + {{ 'description_is_required' }} +
+
+ +
+ +
+ + + + + + + + + +
+
+ +
+ +
+ +
+
+ + +
+ +
+ + +
+
+ +
+ + +
+ diff --git a/app/assets/templates/admin/trainings/edit.html.erb b/app/assets/templates/admin/trainings/edit.html.erb new file mode 100644 index 000000000..d02b1f22e --- /dev/null +++ b/app/assets/templates/admin/trainings/edit.html.erb @@ -0,0 +1,27 @@ +
+
+
+
+ +
+
+
+
+

{{ training.name }}

+
+
+ +
+
+
{{ 'cancel' }}
+
+
+
+
+ + +
+
+ +
+
diff --git a/app/assets/templates/admin/trainings/index.html.erb b/app/assets/templates/admin/trainings/index.html.erb index f70eb43c6..93f2cb190 100644 --- a/app/assets/templates/admin/trainings/index.html.erb +++ b/app/assets/templates/admin/trainings/index.html.erb @@ -21,11 +21,7 @@
- - + @@ -38,35 +34,12 @@ + + + - - -
{{ training.name }}{{ showMachines(training) }}{{ training.nb_total_places }} - - {{ training.name }} - - - - {{ showMachines(training) }} - - - - {{ training.nb_total_places }} - - -
- - -
-
- - +
+ + + + + + +
+ + +
+
+
+ + + +
+ + + diff --git a/app/assets/templates/calendar/filter.html.erb b/app/assets/templates/calendar/filter.html.erb new file mode 100644 index 000000000..cea225689 --- /dev/null +++ b/app/assets/templates/calendar/filter.html.erb @@ -0,0 +1,28 @@ +
+
+

{{ 'trainings' }}

+ +
+
+ {{::t.name}} + +
+
+
+
+

{{ 'machines' }}

+ +
+
+ {{::m.name}} + +
+
+
+

{{ 'events' }}

+ +
+
+

{{ 'show_no_disponible' }}

+ +
diff --git a/app/assets/templates/dashboard/events.html.erb b/app/assets/templates/dashboard/events.html.erb index 0903ba9dd..2c199becf 100644 --- a/app/assets/templates/dashboard/events.html.erb +++ b/app/assets/templates/dashboard/events.html.erb @@ -13,24 +13,38 @@
-

{{ 'your_next_courses_and_workshops' | translate }}

+

{{ 'your_next_events' | translate }}

  • - {{r.reservable.title}} - {{ r.start_at | amDateFormat:'LLL' }} - {{ r.end_at | amDateFormat:'LT' }} -
    {{ 'NUMBER_normal_places_reserved' }} -
    {{ 'NUMBER_reduced_fare_places_reserved' }} + {{r.reservable.title}} + - + {{ r.start_at | amDateFormat:'LLL' }} - {{ r.end_at | amDateFormat:'LT' }} +
    + + {{ 'NUMBER_normal_places_reserved' }} + + +
    + + {{ 'NUMBER_of_NAME_places_reserved' }} + +
-
{{ 'no_courses_or_workshops_to_come' }}
+
{{ 'no_events_to_come' }}
-

{{ 'your_previous_courses_and_workshops' | translate }}

+

{{ 'your_previous_events' | translate }}

    @@ -38,7 +52,7 @@ {{r.reservable.title}} - {{ r.start_at | amDateFormat:'LLL' }} - {{ r.end_at | amDateFormat:'LT' }}
-
{{ 'no_passed_courses_or_workshops' }}
+
{{ 'no_passed_events' }}
diff --git a/app/assets/templates/dashboard/nav.html.erb b/app/assets/templates/dashboard/nav.html.erb index ee0c398c0..9d609a588 100644 --- a/app/assets/templates/dashboard/nav.html.erb +++ b/app/assets/templates/dashboard/nav.html.erb @@ -8,15 +8,16 @@ diff --git a/app/assets/templates/dashboard/projects.html.erb b/app/assets/templates/dashboard/projects.html.erb index 6648b9525..a91df72c8 100644 --- a/app/assets/templates/dashboard/projects.html.erb +++ b/app/assets/templates/dashboard/projects.html.erb @@ -9,10 +9,16 @@
+ +
{{ 'you_dont_have_any_projects' }}
-

{{project.name}}

{{project.author_id == currentUser.id ? 'author' : 'collaborator' | translate}} +

{{project.name}}

+ {{project.author_id == currentUser.id ? 'author' : 'collaborator' | translate}} + {{ 'rough_draft' }} diff --git a/app/assets/templates/dashboard/wallet.html.erb b/app/assets/templates/dashboard/wallet.html.erb new file mode 100644 index 000000000..175504c64 --- /dev/null +++ b/app/assets/templates/dashboard/wallet.html.erb @@ -0,0 +1,21 @@ +
+ +
+
+ +
+ +
+ + +
+
+ +
+ +
+ +
+
+
+
diff --git a/app/assets/templates/events/_form.html.erb b/app/assets/templates/events/_form.html.erb index 685ea8172..9d4500b93 100644 --- a/app/assets/templates/events/_form.html.erb +++ b/app/assets/templates/events/_form.html.erb @@ -69,7 +69,7 @@
@@ -80,14 +80,13 @@
-

{{ 'event_type' }}

+

{{ 'event_type' }} *

- - + - + @@ -96,6 +95,40 @@
+
+
+

{{ 'event_theme' }}

+
+
+ + + + + + + + + + +
+
+ +
+
+

{{ 'age_range' }}

+
+
+ + + + + + + + + +
+

{{ 'dates_and_opening_hours' }}

@@ -204,15 +237,30 @@ {{ '0_=_free' }}
-
- +
+
+ + +
- +
{{currencySymbol}}
+
diff --git a/app/assets/templates/events/index.html.erb b/app/assets/templates/events/index.html.erb index 9182191a4..f440f8abc 100644 --- a/app/assets/templates/events/index.html.erb +++ b/app/assets/templates/events/index.html.erb @@ -7,7 +7,7 @@
-

{{ 'the_fablab_s_courses_and_workshops' }}

+

{{ 'the_fablab_s_events' }}

@@ -20,38 +20,64 @@
-
-

{{month.split(',')[0]}}, {{month.split(',')[1]}}

+
+
+ +
- diff --git a/app/assets/templates/events/modify_event_reservation_modal.html.erb b/app/assets/templates/events/modify_event_reservation_modal.html.erb index 108397ae1..a8adc6fbc 100644 --- a/app/assets/templates/events/modify_event_reservation_modal.html.erb +++ b/app/assets/templates/events/modify_event_reservation_modal.html.erb @@ -4,8 +4,8 @@
-

{{ event.title }} {{event.categories[0].name}}

+

{{ event.title }} {{event.category.name}}

@@ -39,6 +39,12 @@

+ + +
@@ -67,9 +73,13 @@
-
{{event.categories[0].name}}
+
{{event.category.name}}
-
{{ 'dates' | translate }}
+
{{event.event_themes[0].name}}
+
{{event.age_range.name}}
+
+
+
{{ 'dates' | translate }}
{{ 'beginning' | translate }} {{event.start_date | amDateFormat:'L'}}
{{ 'ending' | translate }} {{event.end_date | amDateFormat:'L'}}
{{ 'opening_hours' | translate }}
{{ 'all_day' }}
@@ -78,7 +88,12 @@
{{ 'full_price_' | translate }} {{ event.amount | currency}}
-
{{ 'reduced_rate*' | translate }} {{ event.reduced_amount | currency}}
+
+ + {{price.category.name}} : + + {{price.amount | currency}} +
@@ -98,17 +113,16 @@
- {{ 'ticket' | translate:{NUMBER:reserve.nbReservePlaces}:"messageformat" }}
-
- +
+
- {{ 'ticket' | translate:{NUMBER:reserve.nbReserveReducedPlaces}:"messageformat" }} + {{ 'ticket' | translate:{NUMBER:reserve.tickets[price.id]}:"messageformat" }}
-
@@ -139,7 +153,9 @@
{{ 'you_booked_(DATE)' | translate:{DATE:(reservation.created_at | amDateFormat:'L LT')} }}
{{ 'full_price_' | translate }} {{reservation.nb_reserve_places}} {{ 'ticket' | translate:{NUMBER:reservation.nb_reserve_places}:"messageformat" }}
-
{{ 'reduced_rate*' | translate }} {{reservation.nb_reserve_reduced_places}} {{ 'ticket' | translate:{NUMBER:reservation.nb_reserve_reduced_places}:"messageformat" }}
+
+ {{ticket.event_price_category.price_category.name}} : {{ticket.booked}} {{ 'ticket' | translate:{NUMBER:ticket.booked}:"messageformat" }} +
@@ -148,22 +164,20 @@ +
- -
diff --git a/app/assets/templates/home.html.erb b/app/assets/templates/home.html.erb index 134e65afe..1e08bdcfd 100644 --- a/app/assets/templates/home.html.erb +++ b/app/assets/templates/home.html.erb @@ -80,7 +80,7 @@
-

{{ 'fablab_s_next_courses_and_workshops' | translate }} {{ 'every_events' | translate }}

+

{{ 'fablab_s_next_events' | translate }} {{ 'every_events' | translate }}

@@ -97,7 +97,7 @@

{{event.title}}

- {{event.categories[0].name}} + {{event.category.name}}

{{event.description | humanize : 500 }}

@@ -122,7 +122,6 @@ {{ 'free_entry' }} {{ 'free_admission' }} {{event.amount | currency}} {{ 'full_price' | translate }} -
{{event.reduced_amount | currency}} {{ 'reduced_rate' | translate }}
{{ 'event_full' }}
diff --git a/app/assets/templates/machines/_form.html.erb b/app/assets/templates/machines/_form.html.erb index 0aa424824..4cdbf785d 100644 --- a/app/assets/templates/machines/_form.html.erb +++ b/app/assets/templates/machines/_form.html.erb @@ -1,4 +1,11 @@ -
+ @@ -10,7 +17,13 @@
- + {{ 'name_is_required' }}
@@ -46,7 +59,16 @@
- + + + {{ 'description_is_required' }}
@@ -54,7 +76,16 @@
- + + + {{ 'technical_specifications_are_required' }}
@@ -83,7 +114,10 @@
diff --git a/app/assets/templates/machines/index.html.erb b/app/assets/templates/machines/index.html.erb index f6804be64..ffa7cbfac 100644 --- a/app/assets/templates/machines/index.html.erb +++ b/app/assets/templates/machines/index.html.erb @@ -27,15 +27,14 @@
-
-
+
+
-
+
-
-

{{machine.name}}

-

{{machine.description | humanize : 140 }}

+
+

{{machine.name}}

diff --git a/app/assets/templates/machines/reserve.html.erb b/app/assets/templates/machines/reserve.html.erb index b61a953e5..246df7ba5 100644 --- a/app/assets/templates/machines/reserve.html.erb +++ b/app/assets/templates/machines/reserve.html.erb @@ -78,6 +78,8 @@
+ +

{{ 'to_benefit_from_attractive_prices' }}

diff --git a/app/assets/templates/machines/show.html.erb b/app/assets/templates/machines/show.html.erb index 5d77707fe..9f20ecb6b 100644 --- a/app/assets/templates/machines/show.html.erb +++ b/app/assets/templates/machines/show.html.erb @@ -34,7 +34,7 @@ {{machine.name}}
-

+

@@ -48,7 +48,7 @@

-

{{machine.spec}}

+

diff --git a/app/assets/templates/plans/index.html.erb b/app/assets/templates/plans/index.html.erb index 7f30b641b..362fb93a3 100644 --- a/app/assets/templates/plans/index.html.erb +++ b/app/assets/templates/plans/index.html.erb @@ -97,7 +97,7 @@
@@ -140,15 +140,17 @@ {{ selectedPlan | humanReadablePlanName }}
{{ 'subscription_price' | translate }} {{selectedPlan.amount | currency}}
+ +
-
+

{{ 'summary' }}

@@ -157,8 +159,8 @@ {{ 'you_ve_just_payed_the_' | translate }} {{ '_subscription' }} :
- {{ paidPlan | humanReadablePlanName }} -
{{ 'subscription_price' | translate }} {{paidPlan.amount | currency}}
+ {{ paid.plan | humanReadablePlanName }} +
{{ 'subscription_price' | translate }} {{paid.plan.amount | currency}}
{{ 'thank_you_your_subscription_is_successful' | translate }}
diff --git a/app/assets/templates/plans/payment_modal.html.erb b/app/assets/templates/plans/payment_modal.html.erb index 42b33239f..7b1f97185 100644 --- a/app/assets/templates/plans/payment_modal.html.erb +++ b/app/assets/templates/plans/payment_modal.html.erb @@ -3,10 +3,13 @@

{{ 'subscription_confirmation' }}

diff --git a/app/assets/templates/projects/_form.html.erb b/app/assets/templates/projects/_form.html.erb index 0f00a6f6a..94a88a8a8 100644 --- a/app/assets/templates/projects/_form.html.erb +++ b/app/assets/templates/projects/_form.html.erb @@ -42,18 +42,22 @@
- - +
+ + -
-
- {{file.attachment}} +
+
+ {{file.attachment}} +
+ {{ 'browse' }} + {{ 'change' }} +
- {{ 'browse' }} - {{ 'change' }} -
- +
+ +
{{ 'add_a_new_file' | translate }}
@@ -72,11 +76,20 @@
-
+
- {{ 'step_N' | translate:{ INDEX:$index+1 } }}/{{project.project_steps_attributes.length}} +
+ + +
+ +
- {{ 'add_a_picture' }}{{ 'change_the_picture' }} + {{ 'add_a_picture' | translate }} {{ 'change_the_picture' }} {{step.project_step_image}} @@ -96,11 +109,13 @@
- {{ 'add_a_new_step' }} + + + {{ 'add_a_new_step' }}
diff --git a/app/assets/templates/projects/edit.html.erb b/app/assets/templates/projects/edit.html.erb index cb17f767c..e5e1a4b53 100644 --- a/app/assets/templates/projects/edit.html.erb +++ b/app/assets/templates/projects/edit.html.erb @@ -7,7 +7,7 @@
- +
diff --git a/app/assets/templates/projects/index.html.erb b/app/assets/templates/projects/index.html.erb index 221aa03fe..5baef2d01 100644 --- a/app/assets/templates/projects/index.html.erb +++ b/app/assets/templates/projects/index.html.erb @@ -90,14 +90,18 @@
- +
-

{{ project.app_name }}

+

{{ project.app_name }}

{{project.name}}

+
+ {{ 'rough_draft' }} +
+
diff --git a/app/assets/templates/projects/new.html.erb b/app/assets/templates/projects/new.html.erb index 528eea6c2..13a5d4239 100644 --- a/app/assets/templates/projects/new.html.erb +++ b/app/assets/templates/projects/new.html.erb @@ -4,7 +4,7 @@
- +
diff --git a/app/assets/templates/projects/show.html.erb b/app/assets/templates/projects/show.html.erb index 78de142d3..88343e8ed 100644 --- a/app/assets/templates/projects/show.html.erb +++ b/app/assets/templates/projects/show.html.erb @@ -31,7 +31,7 @@
- {{project.name}} + {{project.name}}

{{ 'project_description' }}

@@ -40,10 +40,10 @@
-

{{ 'step_N' | translate:{INDEX:$index+1} }} : {{step.title}}

+

{{ 'step_N' | translate:{INDEX:step.step_nb} }} : {{step.title}}

- {{step.title}} + {{step.title}}
@@ -56,6 +56,11 @@
+ +
diff --git a/app/assets/templates/shared/_coupon.html.erb b/app/assets/templates/shared/_coupon.html.erb new file mode 100644 index 000000000..7e00a29f5 --- /dev/null +++ b/app/assets/templates/shared/_coupon.html.erb @@ -0,0 +1,21 @@ +
+ {{ 'i_have_a_coupon' }} + +
+ +
+ + + + + + +
+
+
diff --git a/app/assets/templates/shared/_member_form.html.erb b/app/assets/templates/shared/_member_form.html.erb index 72ec5edcf..b6c6d278e 100644 --- a/app/assets/templates/shared/_member_form.html.erb +++ b/app/assets/templates/shared/_member_form.html.erb @@ -162,6 +162,40 @@ {{ 'confirmation_mismatch_with_password' }}
+
+
+ + + +
+ {{ 'organization_name_is_required' }} +
+ +
+
+ + + +
+ {{ 'organization_address_is_required' }} +
+
@@ -277,6 +311,20 @@
+ +
+ + + +
+
diff --git a/app/assets/templates/shared/_wallet_amount_info.html.erb b/app/assets/templates/shared/_wallet_amount_info.html.erb new file mode 100644 index 000000000..84c2d0bd5 --- /dev/null +++ b/app/assets/templates/shared/_wallet_amount_info.html.erb @@ -0,0 +1,10 @@ +
+

+

{{'wallet_pay_reservation' | translate}}

+

+
+
+

+

{{'client_wallet_pay_reservation' | translate}}

+

+
diff --git a/app/assets/templates/shared/header.html.erb b/app/assets/templates/shared/header.html.erb index 180fd7493..ed5bfef09 100644 --- a/app/assets/templates/shared/header.html.erb +++ b/app/assets/templates/shared/header.html.erb @@ -27,19 +27,20 @@
-
+
-

{{ 'trainings_planning' }}

+

{{ 'trainings_planning' }}

+

{{ 'planning_of' }} {{training.name}}

+
+
+
@@ -24,6 +33,10 @@
+
+ +
+
@@ -69,6 +82,8 @@ {{ 'remove_this_slot' }}
+ +

{{ 'to_benefit_from_attractive_prices_and_a_free_training' }}

diff --git a/app/assets/templates/trainings/show.html.erb b/app/assets/templates/trainings/show.html.erb new file mode 100644 index 000000000..c1ef7f3bc --- /dev/null +++ b/app/assets/templates/trainings/show.html.erb @@ -0,0 +1,45 @@ +
+ +
+
+
+
+ +
+
+
+
+

{{ training.name }}

+
+
+ + +
+
+ +
+
+ +
+ +
+ {{training.name}} +
+ +

+ +
+ +
+ + +
+ +
diff --git a/app/assets/templates/wallet/credit_modal.html.erb b/app/assets/templates/wallet/credit_modal.html.erb new file mode 100644 index 000000000..7624e12a9 --- /dev/null +++ b/app/assets/templates/wallet/credit_modal.html.erb @@ -0,0 +1,44 @@ + + + diff --git a/app/assets/templates/wallet/show.html.erb b/app/assets/templates/wallet/show.html.erb new file mode 100644 index 000000000..307d74e3c --- /dev/null +++ b/app/assets/templates/wallet/show.html.erb @@ -0,0 +1,7 @@ +
+

{{'your_wallet_amount'}}

+

{{'wallet_amount'}}

+
+
{{wallet.amount | currency}}
+
+
diff --git a/app/assets/templates/wallet/transactions.html.erb b/app/assets/templates/wallet/transactions.html.erb new file mode 100644 index 000000000..4bd4b35ea --- /dev/null +++ b/app/assets/templates/wallet/transactions.html.erb @@ -0,0 +1,31 @@ +
+ + + + + + + + + + + + + + + + + +
{{ 'date' }}{{ 'operation' }}{{ 'operator' }}{{ 'amount' }}
{{ ::t.created_at | amDateFormat:'L' }} + {{ 'credit' }} + {{ 'debit' }} + + {{::t.invoice.reference}} + + {{ ::t.user.full_name }} + + + - + {{ ::t.amount | currency }} +
+

{{ 'no_transactions_for_now' }}

+
diff --git a/app/controllers/api/age_ranges_controller.rb b/app/controllers/api/age_ranges_controller.rb new file mode 100644 index 000000000..03c3a5602 --- /dev/null +++ b/app/controllers/api/age_ranges_controller.rb @@ -0,0 +1,49 @@ +class API::AgeRangesController < API::ApiController + before_action :authenticate_user!, except: [:index] + before_action :set_age_range, only: [:show, :update, :destroy] + + def index + @age_ranges = AgeRange.all + end + + def show + end + + def create + authorize AgeRange + @age_range = AgeRange.new(age_range_params) + if @age_range.save + render :show, status: :created, location: @age_range + else + render json: @age_range.errors, status: :unprocessable_entity + end + end + + + def update + authorize AgeRange + if @age_range.update(age_range_params) + render :show, status: :ok, location: @age_range + else + render json: @age_range.errors, status: :unprocessable_entity + end + end + + def destroy + authorize AgeRange + if @age_range.safe_destroy + head :no_content + else + render json: @age_range.errors, status: :unprocessable_entity + end + end + + private + def set_age_range + @age_range = AgeRange.find(params[:id]) + end + + def age_range_params + params.require(:age_range).permit(:name) + end +end diff --git a/app/controllers/api/auth_providers_controller.rb b/app/controllers/api/auth_providers_controller.rb index 2e5cdad48..3797dc743 100644 --- a/app/controllers/api/auth_providers_controller.rb +++ b/app/controllers/api/auth_providers_controller.rb @@ -31,8 +31,11 @@ class API::AuthProvidersController < API::ApiController def destroy authorize AuthProvider - @provider.destroy - head :no_content + if @provider.safe_destroy + head :no_content + else + render json: @provider.errors, status: :unprocessable_entity + end end def mapping_fields @@ -56,9 +59,10 @@ class API::AuthProvidersController < API::ApiController params.require(:auth_provider).permit(:name, :providable_type) elsif params['auth_provider']['providable_type'] == OAuth2Provider.name params.require(:auth_provider).permit(:name, :providable_type, providable_attributes: [ - :id, :base_url, :token_endpoint, :authorization_endpoint, :profile_url, :client_id, :client_secret, + :id, :base_url, :token_endpoint, :authorization_endpoint, :logout_endpoint, :profile_url, :client_id, :client_secret, o_auth2_mappings_attributes: [ - :id, :local_model, :local_field, :api_field, :api_endpoint, :api_data_type, :_destroy + :id, :local_model, :local_field, :api_field, :api_endpoint, :api_data_type, :_destroy, + transformation: [:type, :format, :true_value, :false_value, mapping: [:from, :to]] ] ]) end diff --git a/app/controllers/api/availabilities_controller.rb b/app/controllers/api/availabilities_controller.rb index aa27c5420..088e98bb6 100644 --- a/app/controllers/api/availabilities_controller.rb +++ b/app/controllers/api/availabilities_controller.rb @@ -1,5 +1,5 @@ class API::AvailabilitiesController < API::ApiController - before_action :authenticate_user! + before_action :authenticate_user!, except: [:public] before_action :set_availability, only: [:show, :update, :destroy, :reservations] respond_to :json @@ -8,7 +8,47 @@ class API::AvailabilitiesController < API::ApiController def index authorize Availability + start_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:start]) + end_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:end]).end_of_day @availabilities = Availability.includes(:machines,:tags,:trainings).where.not(available_type: 'event') + .where('start_at >= ? AND end_at <= ?', start_date, end_date) + end + + def public + start_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:start]) + end_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:end]).end_of_day + @reservations = Reservation.includes(:slots, user: [:profile]).references(:slots, :user).where('slots.start_at >= ? AND slots.end_at <= ?', start_date, end_date) + if in_same_day(start_date, end_date) + @training_and_event_availabilities = Availability.includes(:tags, :trainings, :event, :slots).where(available_type: ['training', 'event']) + .where('start_at >= ? AND end_at <= ?', start_date, end_date) + @machine_availabilities = Availability.includes(:tags, :machines).where(available_type: 'machines') + .where('start_at >= ? AND end_at <= ?', start_date, end_date) + @machine_slots = [] + @machine_availabilities.each do |a| + a.machines.each do |machine| + if params[:m] and params[:m].include?(machine.id.to_s) + ((a.end_at - a.start_at)/SLOT_DURATION.minutes).to_i.times do |i| + slot = Slot.new(start_at: a.start_at + (i*SLOT_DURATION).minutes, end_at: a.start_at + (i*SLOT_DURATION).minutes + SLOT_DURATION.minutes, availability_id: a.id, availability: a, machine: machine, title: machine.name) + slot = verify_machine_is_reserved(slot, @reservations, current_user, '') + @machine_slots << slot + end + end + end + end + @availabilities = [].concat(@training_and_event_availabilities).concat(@machine_slots) + else + + @availabilities = Availability.includes(:tags, :machines, :trainings, :event, :slots) + .where('start_at >= ? AND end_at <= ?', start_date, end_date) + @availabilities.each do |a| + if a.available_type != 'machines' + a = verify_training_event_is_reserved(a, @reservations) + end + end + end + machine_ids = params[:m] || [] + @title_filter = {machine_ids: machine_ids.map(&:to_i)} + @availabilities = filter_availabilites(@availabilities) end def show @@ -78,16 +118,35 @@ class API::AvailabilitiesController < API::ApiController @user = current_user end @slots = [] - @reservations = @user.reservations.includes(:slots).references(:slots).where("reservable_type = 'Training' AND slots.start_at > ?", Time.now) + + # first, we get the already-made reservations + @reservations = @user.reservations.where("reservable_type = 'Training'") + @reservations = @reservations.where('reservable_id = :id', id: params[:training_id].to_i) if params[:training_id].is_number? + @reservations = @reservations.joins(:slots).where('slots.start_at > ?', Time.now) + + # what is requested? + # 1) a single training + if params[:training_id].is_number? + @availabilities = Training.find(params[:training_id]).availabilities + # 2) all trainings + else + @availabilities = Availability.trainings + end + + # who made the request? + # 1) an admin (he can see all future availabilities) if @user.is_admin? - @availabilities = Availability.includes(:tags, :slots, trainings: [:machines]).trainings.where('availabilities.start_at > ?', Time.now) + @availabilities = @availabilities.includes(:tags, :slots, trainings: [:machines]).where('availabilities.start_at > ?', Time.now) + # 2) an user (he cannot see availabilities further than 1 (or 3) months) else end_at = 1.month.since end_at = 3.months.since if can_show_slot_plus_three_months(@user) - @availabilities = Availability.includes(:tags, :slots, trainings: [:machines]).trainings.where('availabilities.start_at > ? AND availabilities.start_at < ?', Time.now, end_at).where('availability_tags.tag_id' => @user.tag_ids.concat([nil])) + @availabilities = @availabilities.includes(:tags, :slots, :availability_tags, trainings: [:machines]).where('availabilities.start_at > ? AND availabilities.start_at < ?', Time.now, end_at).where('availability_tags.tag_id' => @user.tag_ids.concat([nil])) end + + # finally, we merge the availabilities with the reservations @availabilities.each do |a| - a = verify_training_is_reserved(a, @reservations) + a = verify_training_event_is_reserved(a, @reservations) end end @@ -119,31 +178,35 @@ class API::AvailabilitiesController < API::ApiController def verify_machine_is_reserved(slot, reservations, user, user_role) reservations.each do |r| r.slots.each do |s| - if s.start_at == slot.start_at and s.canceled_at == nil - slot.id = s.id - slot.is_reserved = true - slot.title = t('availabilities.not_available') - slot.can_modify = true if user_role === 'admin' - slot.reservation = r - end - if s.start_at == slot.start_at and r.user == user and s.canceled_at == nil - slot.title = t('availabilities.i_ve_reserved') - slot.can_modify = true - slot.is_reserved_by_current_user = true + if slot.machine.id == r.reservable_id + if s.start_at == slot.start_at and s.canceled_at == nil + slot.id = s.id + slot.is_reserved = true + slot.title = "#{slot.machine.name} - #{t('availabilities.not_available')}" + slot.can_modify = true if user_role === 'admin' + slot.reservation = r + end + if s.start_at == slot.start_at and r.user == user and s.canceled_at == nil + slot.title = "#{slot.machine.name} - #{t('availabilities.i_ve_reserved')}" + slot.can_modify = true + slot.is_reserved_by_current_user = true + end end end end slot end - def verify_training_is_reserved(availability, reservations) + def verify_training_event_is_reserved(availability, reservations) user = current_user reservations.each do |r| r.slots.each do |s| - if s.start_at == availability.start_at and s.canceled_at == nil and availability.trainings.first.id == r.reservable_id + if ((availability.available_type == 'training' and availability.trainings.first.id == r.reservable_id) or (availability.available_type == 'event' and availability.event.id == r.reservable_id)) and s.start_at == availability.start_at and s.canceled_at == nil availability.slot_id = s.id - availability.is_reserved = true - availability.can_modify = true if r.user == user + if r.user == user + availability.is_reserved = true + availability.can_modify = true + end end end end @@ -158,4 +221,40 @@ class API::AvailabilitiesController < API::ApiController def is_subscription_year(user) user.subscription and user.subscription.plan.interval == 'year' and user.subscription.expired_at >= Time.now end + + def in_same_day(start_date, end_date) + (end_date.to_date - start_date.to_date).to_i == 1 + end + + def filter_availabilites(availabilities) + availabilities_filtered = [] + availabilities.to_ary.each do |a| + # machine slot + if !a.try(:available_type) + availabilities_filtered << a + else + # training + if params[:t] and a.available_type == 'training' + if params[:t].include?(a.trainings.first.id.to_s) + availabilities_filtered << a + end + end + # machines + if params[:m] and a.available_type == 'machines' + if (params[:m].map(&:to_i) & a.machine_ids).any? + availabilities_filtered << a + end + end + # event + if params[:evt] and params[:evt] == 'true' and a.available_type == 'event' + availabilities_filtered << a + end + end + end + availabilities_filtered.delete_if do |a| + if params[:dispo] == 'false' + a.is_reserved or (a.try(:is_completed) and a.is_completed) + end + end + end end diff --git a/app/controllers/api/categories_controller.rb b/app/controllers/api/categories_controller.rb index 39419deb4..e6c840c41 100644 --- a/app/controllers/api/categories_controller.rb +++ b/app/controllers/api/categories_controller.rb @@ -1,5 +1,49 @@ class API::CategoriesController < API::ApiController + before_action :authenticate_user!, except: [:index] + before_action :set_category, only: [:show, :update, :destroy] + def index @categories = Category.all end + + def show + end + + def create + authorize Category + @category = Category.new(category_params) + if @category.save + render :show, status: :created, location: @category + else + render json: @category.errors, status: :unprocessable_entity + end + end + + + def update + authorize Category + if @category.update(category_params) + render :show, status: :ok, location: @category + else + render json: @category.errors, status: :unprocessable_entity + end + end + + def destroy + authorize Category + if @category.safe_destroy + head :no_content + else + render json: @category.errors, status: :unprocessable_entity + end + end + + private + def set_category + @category = Category.find(params[:id]) + end + + def category_params + params.require(:category).permit(:name) + end end diff --git a/app/controllers/api/coupons_controller.rb b/app/controllers/api/coupons_controller.rb new file mode 100644 index 000000000..164fb2c22 --- /dev/null +++ b/app/controllers/api/coupons_controller.rb @@ -0,0 +1,87 @@ +class API::CouponsController < API::ApiController + before_action :authenticate_user! + before_action :set_coupon, only: [:show, :update, :destroy] + + def index + @coupons = Coupon.all + end + + def show + end + + def create + authorize Coupon + @coupon = Coupon.new(coupon_params) + if @coupon.save + render :show, status: :created, location: @coupon + else + render json: @coupon.errors, status: :unprocessable_entity + end + end + + def validate + @coupon = Coupon.find_by_code(params[:code]) + if @coupon.nil? + render json: {status: 'rejected'}, status: :not_found + else + if !current_user.is_admin? + _user_id = current_user.id + else + _user_id = params[:user_id] + end + + status = @coupon.status(_user_id) + if status != 'active' + render json: {status: status}, status: :unprocessable_entity + else + render :validate, status: :ok, location: @coupon + end + end + end + + def update + authorize Coupon + if @coupon.update(coupon_editable_params) + render :show, status: :ok, location: @coupon + else + render json: @coupon.errors, status: :unprocessable_entity + end + end + + def destroy + authorize Coupon + if @coupon.safe_destroy + head :no_content + else + head :unprocessable_entity + end + end + + def send_to + authorize Coupon + + @coupon = Coupon.find_by_code(params[:coupon_code]) + if @coupon.nil? + render json: {error: "no coupon with code #{params[:coupon_code]}"}, status: :not_found + else + if @coupon.send_to(params[:user_id]) + render :show, status: :ok, location: @coupon + else + render json: @coupon.errors, status: :unprocessable_entity + end + end + end + + private + def set_coupon + @coupon = Coupon.find(params[:id]) + end + + def coupon_params + params.require(:coupon).permit(:name, :code, :percent_off, :validity_per_user, :valid_until, :max_usages, :active) + end + + def coupon_editable_params + params.require(:coupon).permit(:name, :active) + end +end diff --git a/app/controllers/api/event_themes_controller.rb b/app/controllers/api/event_themes_controller.rb new file mode 100644 index 000000000..2ba64e0a7 --- /dev/null +++ b/app/controllers/api/event_themes_controller.rb @@ -0,0 +1,49 @@ +class API::EventThemesController < API::ApiController + before_action :authenticate_user!, except: [:index] + before_action :set_event_theme, only: [:show, :update, :destroy] + + def index + @event_themes = EventTheme.all + end + + def show + end + + def create + authorize EventTheme + @event_theme = EventTheme.new(event_theme_params) + if @event_theme.save + render :show, status: :created, location: @event_theme + else + render json: @event_theme.errors, status: :unprocessable_entity + end + end + + + def update + authorize EventTheme + if @event_theme.update(event_theme_params) + render :show, status: :ok, location: @event_theme + else + render json: @event_theme.errors, status: :unprocessable_entity + end + end + + def destroy + authorize EventTheme + if @event_theme.safe_destroy + head :no_content + else + render json: @event_theme.errors, status: :unprocessable_entity + end + end + + private + def set_event_theme + @event_theme = EventTheme.find(params[:id]) + end + + def event_theme_params + params.require(:event_theme).permit(:name) + end +end diff --git a/app/controllers/api/events_controller.rb b/app/controllers/api/events_controller.rb index b17dd3b6f..a02590a9b 100644 --- a/app/controllers/api/events_controller.rb +++ b/app/controllers/api/events_controller.rb @@ -1,17 +1,24 @@ class API::EventsController < API::ApiController - before_action :set_event, only: [:show, :edit, :update, :destroy] + before_action :set_event, only: [:show, :update, :destroy] def index @events = policy_scope(Event) - @total = @events.count @page = params[:page] + + # filters + @events = @events.joins(:category).where('categories.id = :category', category: params[:category_id]) if params[:category_id] + @events = @events.joins(:event_themes).where('event_themes.id = :theme', theme: params[:theme_id]) if params[:theme_id] + @events = @events.where('age_range_id = :age_range', age_range: params[:age_range_id]) if params[:age_range_id] + + # paginate @events = @events.page(@page).per(12) + end # GET /events/upcoming/:limit def upcoming limit = params[:limit] - @events = Event.includes(:event_image, :event_files, :availability, :categories) + @events = Event.includes(:event_image, :event_files, :availability, :category) .where('availabilities.start_at >= ?', Time.now) .order('availabilities.start_at ASC').references(:availabilities).limit(limit) end @@ -55,10 +62,16 @@ class API::EventsController < API::ApiController # Never trust parameters from the scary internet, only allow the white list through. def event_params + # handle general properties event_preparams = params.required(:event).permit(:title, :description, :start_date, :start_time, :end_date, :end_time, - :amount, :reduced_amount, :nb_total_places, :availability_id, - :all_day, :recurrence, :recurrence_end_at, :category_ids, category_ids: [], - event_image_attributes: [:attachment], event_files_attributes: [:id, :attachment, :_destroy]) + :amount, :nb_total_places, :availability_id, + :all_day, :recurrence, :recurrence_end_at, :category_id, :event_theme_ids, + :age_range_id, event_theme_ids: [], + event_image_attributes: [:attachment], + event_files_attributes: [:id, :attachment, :_destroy], + event_price_categories_attributes: [:id, :price_category_id, :amount] + ) + # handle dates & times (whole-day events or not, maybe during many days) start_date = Time.zone.parse(event_preparams[:start_date]) end_date = Time.zone.parse(event_preparams[:end_date]) start_time = Time.parse(event_preparams[:start_time]) if event_preparams[:start_time] @@ -72,8 +85,17 @@ class API::EventsController < API::ApiController end event_preparams.merge!(availability_attributes: {id: event_preparams[:availability_id], start_at: start_at, end_at: end_at, available_type: 'event'}) .except!(:start_date, :end_date, :start_time, :end_time, :all_day) - event_preparams.merge!(amount: (event_preparams[:amount].to_i * 100 if event_preparams[:amount].present?), - reduced_amount: (event_preparams[:reduced_amount].to_i * 100 if event_preparams[:reduced_amount].present?)) + # convert main price to centimes + event_preparams.merge!(amount: (event_preparams[:amount].to_i * 100 if event_preparams[:amount].present?)) + # delete non-complete "other" prices and convert them to centimes + unless event_preparams[:event_price_categories_attributes].nil? + event_preparams[:event_price_categories_attributes].delete_if { |price_cat| price_cat[:price_category_id].empty? or price_cat[:amount].empty? } + event_preparams[:event_price_categories_attributes].each do |price_cat| + price_cat[:amount] = price_cat[:amount].to_i * 100 + end + end + # return the resulting params object + event_preparams end diff --git a/app/controllers/api/exports_controller.rb b/app/controllers/api/exports_controller.rb new file mode 100644 index 000000000..0ec8dff1c --- /dev/null +++ b/app/controllers/api/exports_controller.rb @@ -0,0 +1,41 @@ +class API::ExportsController < API::ApiController + before_action :authenticate_user! + before_action :set_export, only: [:download] + + def download + authorize @export + + send_file File.join(Rails.root, @export.file), :type => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', :disposition => 'attachment' + end + + def status + authorize Export + + export = Export.where({category: params[:category], export_type: params[:type], query: params[:query], key: params[:key]}) + + if params[:category] === 'users' + case params[:type] + when 'subscriptions' + export = export.where('created_at > ?', Subscription.maximum('updated_at')) + when 'reservations' + export = export.where('created_at > ?', Reservation.maximum('updated_at')) + when 'members' + export = export.where('created_at > ?', User.with_role(:member).maximum('updated_at')) + else + raise ArgumentError, "Unknown type #{params[:type]}" + end + end + export = export.last + + if export.nil? || !FileTest.exist?(export.file) + render json: {exists: false, id: nil}, status: :ok + else + render json: {exists: true, id: export.id}, status: :ok + end + end + + private + def set_export + @export = Export.find(params[:id]) + end +end \ No newline at end of file diff --git a/app/controllers/api/members_controller.rb b/app/controllers/api/members_controller.rb index b234b0d5c..9a71b1348 100644 --- a/app/controllers/api/members_controller.rb +++ b/app/controllers/api/members_controller.rb @@ -63,7 +63,7 @@ class API::MembersController < API::ApiController def update authorize @member - @flow_worker = MembersFlowWorker.new(@member) + @flow_worker = MembersProcessor.new(@member) if user_params[:group_id] and @member.group_id != user_params[:group_id].to_i and @member.subscribed_plan != nil # here a group change is requested but unprocessable, handle the exception @@ -91,29 +91,50 @@ class API::MembersController < API::ApiController # export subscriptions def export_subscriptions authorize :export - @datas = Subscription.includes(:plan, :user).all - respond_to do |format| - format.html - format.xls + + export = Export.where({category:'users', export_type: 'subscriptions'}).where('created_at > ?', Subscription.maximum('updated_at')).last + if export.nil? || !FileTest.exist?(export.file) + @export = Export.new({category:'users', export_type: 'subscriptions', user: current_user}) + if @export.save + render json: {export_id: @export.id}, status: :ok + else + render json: @export.errors, status: :unprocessable_entity + end + else + send_file File.join(Rails.root, export.file), :type => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', :disposition => 'attachment' end end # export reservations def export_reservations authorize :export - @datas = Reservation.includes(:user, :slots).all - respond_to do |format| - format.html - format.xls + + export = Export.where({category:'users', export_type: 'reservations'}).where('created_at > ?', Reservation.maximum('updated_at')).last + if export.nil? || !FileTest.exist?(export.file) + @export = Export.new({category:'users', export_type: 'reservations', user: current_user}) + if @export.save + render json: {export_id: @export.id}, status: :ok + else + render json: @export.errors, status: :unprocessable_entity + end + else + send_file File.join(Rails.root, export.file), :type => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', :disposition => 'attachment' end end def export_members authorize :export - @datas = User.with_role(:member).includes(:group, :subscriptions, :profile) - respond_to do |format| - format.html - format.xls + + export = Export.where({category:'users', export_type: 'members'}).where('created_at > ?', User.with_role(:member).maximum('updated_at')).last + if export.nil? || !FileTest.exist?(export.file) + @export = Export.new({category:'users', export_type: 'members', user: current_user}) + if @export.save + render json: {export_id: @export.id}, status: :ok + else + render json: @export.errors, status: :unprocessable_entity + end + else + send_file File.join(Rails.root, export.file), :type => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', :disposition => 'attachment' end end @@ -126,7 +147,7 @@ class API::MembersController < API::ApiController @account = User.find_by_auth_token(token) if @account - @flow_worker = MembersFlowWorker.new(@account) + @flow_worker = MembersProcessor.new(@account) begin if @flow_worker.merge_from_sso(@member) @member = @account @@ -188,11 +209,7 @@ class API::MembersController < API::ApiController # ILIKE => PostgreSQL case-insensitive LIKE @query = @query.where('profiles.first_name ILIKE :search OR profiles.last_name ILIKE :search OR profiles.phone ILIKE :search OR email ILIKE :search OR groups.name ILIKE :search OR plans.base_name ILIKE :search', search: "%#{p[:search]}%") if p[:search].size > 0 - # remove unmerged profiles from list @members = @query.to_a - @members.delete_if { |u| u.need_completion? } - - @members end @@ -214,11 +231,7 @@ class API::MembersController < API::ApiController end end - # remove unmerged profiles from list @members = @members.to_a - @members.delete_if { |u| u.need_completion? } - - @members end def mapping @@ -234,17 +247,19 @@ class API::MembersController < API::ApiController def user_params if current_user.id == params[:id].to_i - params.require(:user).permit(:username, :email, :password, :password_confirmation, :group_id, :is_allow_contact, + params.require(:user).permit(:username, :email, :password, :password_confirmation, :group_id, :is_allow_contact, :is_allow_newsletter, profile_attributes: [:id, :first_name, :last_name, :gender, :birthday, :phone, :interest, :software_mastered, :website, :job, :facebook, :twitter, :google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo, :dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr, - :user_avatar_attributes => [:id, :attachment, :_destroy], :address_attributes => [:id, :address]]) + user_avatar_attributes: [:id, :attachment, :_destroy], address_attributes: [:id, :address], + organization_attributes: [:id, :name, address_attributes: [:id, :address]]]) elsif current_user.is_admin? - params.require(:user).permit(:username, :email, :password, :password_confirmation, :invoicing_disabled, :is_allow_contact, + params.require(:user).permit(:username, :email, :password, :password_confirmation, :invoicing_disabled, :is_allow_contact, :is_allow_newsletter, :group_id, training_ids: [], tag_ids: [], profile_attributes: [:id, :first_name, :last_name, :gender, :birthday, :phone, :interest, :software_mastered, :website, :job, :facebook, :twitter, :google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo, :dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr, - user_avatar_attributes: [:id, :attachment, :_destroy], address_attributes: [:id, :address]]) + user_avatar_attributes: [:id, :attachment, :_destroy], address_attributes: [:id, :address], + organization_attributes: [:id, :name, address_attributes: [:id, :address]]]) end end diff --git a/app/controllers/api/price_categories_controller.rb b/app/controllers/api/price_categories_controller.rb new file mode 100644 index 000000000..494795869 --- /dev/null +++ b/app/controllers/api/price_categories_controller.rb @@ -0,0 +1,48 @@ +class API::PriceCategoriesController < API::ApiController + before_action :authenticate_user!, only: [:update, :show, :create, :destroy] + before_action :set_price_category, only: [:show, :update, :destroy] + + def index + @price_categories = PriceCategory.all + end + + def update + authorize PriceCategory + if @price_category.update(price_category_params) + render :show, status: :ok, location: @price_category + else + render json: @price_category.errors, status: :unprocessable_entity + end + end + + def show + end + + def create + authorize PriceCategory + @price_category = PriceCategory.new(price_category_params) + if @price_category.save + render :show, status: :created, location: @price_category + else + render json: @price_category.errors, status: :unprocessable_entity + end + end + + def destroy + authorize PriceCategory + if @price_category.safe_destroy + head :no_content + else + render json: @price_category.errors, status: :unprocessable_entity + end + end + + private + def set_price_category + @price_category = PriceCategory.find(params[:id]) + end + + def price_category_params + params.require(:price_category).permit(:name, :conditions) + end +end \ No newline at end of file diff --git a/app/controllers/api/prices_controller.rb b/app/controllers/api/prices_controller.rb index ac7d2765f..2c9dacd10 100644 --- a/app/controllers/api/prices_controller.rb +++ b/app/controllers/api/prices_controller.rb @@ -44,7 +44,7 @@ class API::PricesController < API::ApiController @amount = {elements: nil, total: 0} else _reservable = _price_params[:reservable_type].constantize.find(_price_params[:reservable_id]) - @amount = Price.compute(current_user.is_admin?, _user, _reservable, _price_params[:slots_attributes], _price_params[:plan_id], _price_params[:nb_reserve_places], _price_params[:nb_reserve_reduced_places]) + @amount = Price.compute(current_user.is_admin?, _user, _reservable, _price_params[:slots_attributes], _price_params[:plan_id], _price_params[:nb_reserve_places], _price_params[:tickets_attributes], coupon_params[:coupon_code]) end @@ -61,7 +61,12 @@ class API::PricesController < API::ApiController end def compute_price_params - params.require(:reservation).permit(:reservable_id, :reservable_type, :plan_id, :user_id, :nb_reserve_places, :nb_reserve_reduced_places, + params.require(:reservation).permit(:reservable_id, :reservable_type, :plan_id, :user_id, :nb_reserve_places, + tickets_attributes: [:event_price_category_id, :booked], slots_attributes: [:id, :start_at, :end_at, :availability_id, :offered]) end + + def coupon_params + params.permit(:coupon_code) + end end diff --git a/app/controllers/api/projects_controller.rb b/app/controllers/api/projects_controller.rb index f78ffe555..6dcc23b7b 100644 --- a/app/controllers/api/projects_controller.rb +++ b/app/controllers/api/projects_controller.rb @@ -57,6 +57,10 @@ class API::ProjectsController < API::ApiController render :index end + def allowed_extensions + render json: ENV['ALLOWED_EXTENSIONS'].split(' '), status: :ok + end + private def set_project @project = Project.find(params[:id]) @@ -66,7 +70,7 @@ class API::ProjectsController < API::ApiController params.require(:project).permit(:name, :description, :tags, :machine_ids, :component_ids, :theme_ids, :licence_id, :author_id, :licence_id, :state, user_ids: [], machine_ids: [], component_ids: [], theme_ids: [], project_image_attributes: [:attachment], project_caos_attributes: [:id, :attachment, :_destroy], - project_steps_attributes: [:id, :description, :title, :_destroy, + project_steps_attributes: [:id, :description, :title, :_destroy, :step_nb, :project_step_image_attributes => :attachment]) end end diff --git a/app/controllers/api/reservations_controller.rb b/app/controllers/api/reservations_controller.rb index f7332d0aa..65ebfb9ec 100644 --- a/app/controllers/api/reservations_controller.rb +++ b/app/controllers/api/reservations_controller.rb @@ -20,19 +20,23 @@ class API::ReservationsController < API::ApiController end def create - if current_user.is_admin? - @reservation = Reservation.new(reservation_params) - is_reserve = @reservation.save_with_local_payment - else - @reservation = Reservation.new(reservation_params.merge(user_id: current_user.id)) - is_reserve = @reservation.save_with_payment - end - if is_reserve - SubscriptionExtensionAfterReservation.new(@reservation).extend_subscription_if_eligible + begin + if current_user.is_admin? + @reservation = Reservation.new(reservation_params) + is_reserve = @reservation.save_with_local_payment(coupon_params[:coupon_code]) + else + @reservation = Reservation.new(reservation_params.merge(user_id: current_user.id)) + is_reserve = @reservation.save_with_payment(coupon_params[:coupon_code]) + end + if is_reserve + SubscriptionExtensionAfterReservation.new(@reservation).extend_subscription_if_eligible - render :show, status: :created, location: @reservation - else - render json: @reservation.errors, status: :unprocessable_entity + render :show, status: :created, location: @reservation + else + render json: @reservation.errors, status: :unprocessable_entity + end + rescue InvalidCouponError + render json: {coupon_code: 'wrong coupon code or expired'}, status: :unprocessable_entity end end @@ -52,7 +56,12 @@ class API::ReservationsController < API::ApiController def reservation_params params.require(:reservation).permit(:user_id, :message, :reservable_id, :reservable_type, :card_token, :plan_id, - :nb_reserve_places, :nb_reserve_reduced_places, + :nb_reserve_places, + tickets_attributes: [:event_price_category_id, :booked], slots_attributes: [:id, :start_at, :end_at, :availability_id, :offered]) end + + def coupon_params + params.permit(:coupon_code) + end end diff --git a/app/controllers/api/settings_controller.rb b/app/controllers/api/settings_controller.rb index 3cac77033..bab41054b 100644 --- a/app/controllers/api/settings_controller.rb +++ b/app/controllers/api/settings_controller.rb @@ -7,7 +7,7 @@ class API::SettingsController < API::ApiController def update authorize Setting - @setting = Setting.find_by(name: params[:name]) + @setting = Setting.find_or_initialize_by(name: params[:name]) if @setting.update(setting_params) render status: :ok else diff --git a/app/controllers/api/statistics_controller.rb b/app/controllers/api/statistics_controller.rb index 7dca1b347..147d2c181 100644 --- a/app/controllers/api/statistics_controller.rb +++ b/app/controllers/api/statistics_controller.rb @@ -10,13 +10,64 @@ class API::StatisticsController < API::ApiController class_eval %{ def #{path} authorize :statistic, :#{path}? + + # remove additional parameters + statistic_type = request.query_parameters.delete('stat-type') + start_date = request.query_parameters.delete('start-date') + end_date = request.query_parameters.delete('end-date') + puts start_date, end_date + + # run main query in elasticSearch query = MultiJson.load(request.body.read) results = Stats::#{path.classify}.search(query, request.query_parameters.symbolize_keys).response + + # run additional custom aggregations, if any + stat_index = StatisticIndex.find_by_es_type_key("#{path}") + stat_type = StatisticType.where(statistic_index_id: stat_index.id, key: statistic_type).first + client = Elasticsearch::Model.client + stat_type.statistic_custom_aggregations.each do |custom| + c_res = client.search index: custom.es_index, type:custom.es_type, body:sprintf(custom.query, {aggs_name: custom.field, start_date: start_date, end_date: end_date}) + results['aggregations'][custom.field] = c_res['aggregations'][custom.field] + end + + # return result render json: results end + + def export_#{path} + authorize :statistic, :export_#{path}? + + export = Export.where({category:'statistics', export_type: '#{path}', query: params[:body], key: params[:type_key]}).last + if export.nil? || !FileTest.exist?(export.file) + @export = Export.new({category:'statistics', export_type: '#{path}', user: current_user, query: params[:body], key: params[:type_key]}) + if @export.save + render json: {export_id: @export.id}, status: :ok + else + render json: @export.errors, status: :unprocessable_entity + end + else + send_file File.join(Rails.root, export.file), :type => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', :disposition => 'attachment' + end + end } end + def export_global + authorize :statistic, :export_global? + + export = Export.where({category:'statistics', export_type: 'global', query: params[:body]}).last + if export.nil? || !FileTest.exist?(export.file) + @export = Export.new({category:'statistics', export_type: 'global', user: current_user, query: params[:body]}) + if @export.save + render json: {export_id: @export.id}, status: :ok + else + render json: @export.errors, status: :unprocessable_entity + end + else + send_file File.join(Rails.root, export.file), :type => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', :disposition => 'attachment' + end + end + def scroll authorize :statistic, :scroll? diff --git a/app/controllers/api/subscriptions_controller.rb b/app/controllers/api/subscriptions_controller.rb index 20bcdc709..cd16e617c 100644 --- a/app/controllers/api/subscriptions_controller.rb +++ b/app/controllers/api/subscriptions_controller.rb @@ -14,18 +14,12 @@ class API::SubscriptionsController < API::ApiController else if current_user.is_admin? @subscription = Subscription.find_or_initialize_by(user_id: subscription_params[:user_id]) - @subscription.update_column(:expired_at, nil) unless @subscription.new_record? # very important @subscription.attributes = subscription_params - is_subscribe = @subscription.save_with_local_payment(!User.find(subscription_params[:user_id]).invoicing_disabled?) + is_subscribe = @subscription.save_with_local_payment(!User.find(subscription_params[:user_id]).invoicing_disabled?, coupon_params[:coupon_code]) else @subscription = Subscription.find_or_initialize_by(user_id: current_user.id) - if valid_card_token?(subscription_params[:card_token]) - @subscription.update_column(:expired_at, nil) unless @subscription.new_record? # very important - @subscription.attributes = subscription_params.merge(user_id: current_user.id) - is_subscribe = @subscription.save_with_payment - else - is_subscribe = false - end + @subscription.attributes = subscription_params + is_subscribe = @subscription.save_with_payment(true, coupon_params[:coupon_code]) end if is_subscribe render :show, status: :created, location: @subscription @@ -60,6 +54,10 @@ class API::SubscriptionsController < API::ApiController params.require(:subscription).permit(:plan_id, :user_id, :card_token) end + def coupon_params + params.permit(:coupon_code) + end + def subscription_update_params params.require(:subscription).permit(:expired_at) end diff --git a/app/controllers/api/trainings_controller.rb b/app/controllers/api/trainings_controller.rb index e17c1ed4a..45ae39af5 100644 --- a/app/controllers/api/trainings_controller.rb +++ b/app/controllers/api/trainings_controller.rb @@ -7,6 +7,9 @@ class API::TrainingsController < API::ApiController def index @requested_attributes = params[:requested_attributes] @trainings = policy_scope(Training) + if params[:public_page] + @trainings = @trainings.where(public_page: true) + end if attribute_requested?(@requested_attributes, 'availabilities') @trainings = @trainings.includes(:availabilities => [:slots => [:reservation => [:user => [:profile, :trainings]]]]).order('availabilities.start_at DESC') @@ -14,8 +17,7 @@ class API::TrainingsController < API::ApiController end def show - @training = Training.includes(availabilities: {slots: {reservation: {user: [:profile, :trainings] }}}) - .where(id: params[:id]).first + @training = Training.friendly.find(params[:id]) end def create @@ -35,10 +37,15 @@ class API::TrainingsController < API::ApiController members.each do |m| m.trainings << @training end + + head :no_content else - @training.update(training_params) + if @training.update(training_params) + render :show, status: :ok, location: @training + else + render json: @training.errors, status: :unprocessable_entity + end end - head :no_content end def destroy @@ -47,6 +54,12 @@ class API::TrainingsController < API::ApiController head :no_content end + def availabilities + authorize Training + @training = Training.find(params[:id]) + @availabilities = @training.availabilities.includes(slots: {reservation: {user: [:profile, :trainings] }}).order('start_at DESC') + end + private def set_training @training = Training.find(params[:id]) @@ -57,6 +70,6 @@ class API::TrainingsController < API::ApiController end def training_params - params.require(:training).permit(:id, :name, :description, :machine_ids, :plan_ids, :nb_total_places, machine_ids: [], plan_ids: []) + params.require(:training).permit(:id, :name, :description, :machine_ids, :plan_ids, :nb_total_places, :public_page, training_image_attributes: [:attachment], machine_ids: [], plan_ids: []) end end diff --git a/app/controllers/api/version_controller.rb b/app/controllers/api/version_controller.rb new file mode 100644 index 000000000..be963ebdb --- /dev/null +++ b/app/controllers/api/version_controller.rb @@ -0,0 +1,10 @@ + +class API::VersionController < API::ApiController + before_action :authenticate_user! + + def show + authorize :version + version = File.read('.fabmanager-version') + render json: {version: version}, status: :ok + end +end \ No newline at end of file diff --git a/app/controllers/api/wallet_controller.rb b/app/controllers/api/wallet_controller.rb new file mode 100644 index 000000000..5ff142155 --- /dev/null +++ b/app/controllers/api/wallet_controller.rb @@ -0,0 +1,26 @@ +class API::WalletController < API::ApiController + before_action :authenticate_user! + + def by_user + @wallet = Wallet.find_by(user_id: params[:user_id]) + authorize @wallet + render :show + end + + def transactions + @wallet = Wallet.find(params[:id]) + authorize @wallet + @wallet_transactions = @wallet.wallet_transactions.includes(:invoice, user: [:profile]).order(created_at: :desc) + end + + def credit + @wallet = Wallet.find(params[:id]) + authorize @wallet + service = WalletService.new(user: current_user, wallet: @wallet) + if service.credit(params[:amount].to_f) + render :show + else + head 422 + end + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 8ca0b4026..8d8e8cc66 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -29,12 +29,13 @@ class ApplicationController < ActionController::Base def configure_permitted_parameters devise_parameter_sanitizer.for(:sign_up) << {profile_attributes: [:phone, :last_name, :first_name, - :gender, :birthday, :interest, :software_mastered]} - devise_parameter_sanitizer.for(:sign_up).concat [:username, :is_allow_contact, :cgu, :group_id] + :gender, :birthday, :interest, :software_mastered, + organization_attributes: [:name, address_attributes: [:address]]]} + devise_parameter_sanitizer.for(:sign_up).concat [:username, :is_allow_contact, :is_allow_newsletter, :cgu, :group_id] end def default_url_options - Rails.env.production? ? { protocol: 'https' } : {} + { :host => Rails.application.secrets.default_host, protocol: Rails.application.secrets.default_protocol } end def permission_denied diff --git a/app/controllers/rss/events_controller.rb b/app/controllers/rss/events_controller.rb new file mode 100644 index 000000000..735a707ec --- /dev/null +++ b/app/controllers/rss/events_controller.rb @@ -0,0 +1,9 @@ +class Rss::EventsController < Rss::RssController + + def index + @events = Event.includes(:event_image, :event_files, :availability, :category) + .where('availabilities.start_at >= ?', Time.now) + .order('availabilities.start_at ASC').references(:availabilities).limit(10) + @fab_name = Setting.find_by(name: 'fablab_name').value + end +end diff --git a/app/controllers/rss/projects_controller.rb b/app/controllers/rss/projects_controller.rb new file mode 100644 index 000000000..75a9736ea --- /dev/null +++ b/app/controllers/rss/projects_controller.rb @@ -0,0 +1,7 @@ +class Rss::ProjectsController < Rss::RssController + + def index + @projects = Project.includes(:project_image, :users).published.order('created_at desc').limit(10) + @fab_name = Setting.find_by(name: 'fablab_name').value + end +end diff --git a/app/controllers/rss/rss_controller.rb b/app/controllers/rss/rss_controller.rb new file mode 100644 index 000000000..d9e32442a --- /dev/null +++ b/app/controllers/rss/rss_controller.rb @@ -0,0 +1,4 @@ +class Rss::RssController < ApplicationController + + +end diff --git a/app/controllers/social_bot_controller.rb b/app/controllers/social_bot_controller.rb new file mode 100644 index 000000000..95b55b54a --- /dev/null +++ b/app/controllers/social_bot_controller.rb @@ -0,0 +1,15 @@ +class SocialBotController < ActionController::Base + def share + case request.original_fullpath + when /(=%2F|\/)projects(%2F|\/)([\-0-9a-z]+)/ + @project = Project.friendly.find("#{$3}") + render :project, status: :ok + when /(=%2F|\/)events(%2F|\/)([0-9]+)/ + @event = Event.find("#{$3}".to_i) + render :event, status: :ok + else + puts "unknown bot request : #{request.original_url}" + end + end + +end \ No newline at end of file diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index d15abe757..f11fe55c9 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -14,7 +14,7 @@ class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController @user.username = generate_unique_username(@user.username) end # If the email is mapped, we check its uniqueness. If the email is already in use, we mark it as duplicate with an - # unique random string because. + # unique random string, because: # - if it is the same user, his email will be filled from the SSO when he merge his accounts # - if it is not the same user, this will prevent the raise of PG::UniqueViolation if active_provider.sso_fields.include?('user.email') and email_exists?(@user.email) @@ -22,7 +22,7 @@ class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController @user.email = "<#{old_mail}>#{Devise.friendly_token}-duplicate" flash[:alert] = t('omniauth.email_already_linked_to_another_account_please_input_your_authentication_code', OLD_MAIL: old_mail) end - else + else # => update of an existing user if username_exists?(@user.username, @user.id) flash[:alert] = t('omniauth.your_username_is_already_linked_to_another_account_unable_to_update_it', USERNAME: @user.username) @user.username = User.find(@user.id).username diff --git a/app/doc/open_api/v1/events_doc.rb b/app/doc/open_api/v1/events_doc.rb index e7d1a640e..46d511838 100644 --- a/app/doc/open_api/v1/events_doc.rb +++ b/app/doc/open_api/v1/events_doc.rb @@ -22,10 +22,14 @@ class OpenAPI::V1::EventsDoc < OpenAPI::V1::BaseDoc "description": "Que vous soyez Fab user, visiteur, curieux ou bricoleur, l’atelier de fabrication numérique vous ouvre ses portes les mercredis soirs pour avancer vos projets ou rencontrer la «communauté» Fab Lab. \r\n\r\nCe soir, venez spécialement découvrir les machines à commandes numérique du Fab Lab de La Casemate, venez comprendre ce lieux ouvert à tous. \r\n\r\n\r\nVenez découvrir un concept, une organisation, des machines, pour stimuler votre sens de la créativité.", "updated_at": "2016-04-25T10:49:40.055+02:00", "created_at": "2016-04-25T10:49:40.055+02:00", - "amount": 0, - "reduced_amount": 0, "nb_total_places": 18, - "nb_free_places": 16 + "nb_free_places": 16, + "prices": { + "normal": { + "name": "Plein tarif", + "amount": 0 + } + } }, { "id": 182, @@ -33,10 +37,18 @@ class OpenAPI::V1::EventsDoc < OpenAPI::V1::BaseDoc "description": "Envie de rider à travers Grenoble sur une planche unique ? Envie de découvrir la fabrication éco-responsable d'un skate ? Alors bienvenue à l'atelier Skate Board du Fablab ! Encadré par Ivan Mago et l'équipe du FabLab, vous réaliserez votre planche (skate, longboard,...) depuis son design jusqu'à sa décoration sur 4 séances.\r\n\r\nLe tarif 50€ inclut la participation aux ateliers, l'utilisations des machines, et tout le matériel de fabrication (bois+colle+grip+vinyle).\r\n\r\nCette première séance sera consacré au design de votre planche et à la découpe des gabarits. N'hésitez pas à venir avec votre ordinateur et vos logiciels de création 2D si vous le souhaitez.\r\n\r\nNous vous attendons nombreux !", "updated_at": "2016-04-11T17:40:15.146+02:00", "created_at": "2016-04-11T17:40:15.146+02:00", - "amount": 5000, - "reduced_amount": null, "nb_total_places": 8, - "nb_free_places": 0 + "nb_free_places": 0, + "prices": { + "normal": { + "name": "Plein tarif", + "amount": 5000 + }, + "1": { + "name": "Tarif réduit", + "amount": 4000 + }, + } } ] } diff --git a/app/exceptions/invalid_coupon_error.rb b/app/exceptions/invalid_coupon_error.rb new file mode 100644 index 000000000..6ed9f0e1a --- /dev/null +++ b/app/exceptions/invalid_coupon_error.rb @@ -0,0 +1,3 @@ +# Raised when using a coupon expired or invalid +class InvalidCouponError < StandardError +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index c504024bc..e3591f91c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -52,6 +52,27 @@ module ApplicationHelper if (bool) then return :true else return :false end end + def amount_to_f(amount) + amount / 100.00 + end + + ## + # Retrieve an item in the given array of items + # by default, the "id" is expected to match the given parameter but + # this can be overridden by passing a third parameter to specify the + # property to match + ## + def get_item(array, id, key = nil) + array.each do |i| + if key.nil? + return i if i.id == id + else + return i if i[key] == id + end + end + nil + end + private ## inspired by gems/actionview-4.2.5/lib/action_view/helpers/translation_helper.rb diff --git a/app/helpers/availability_helper.rb b/app/helpers/availability_helper.rb new file mode 100644 index 000000000..69ed58e29 --- /dev/null +++ b/app/helpers/availability_helper.rb @@ -0,0 +1,36 @@ +module AvailabilityHelper + MACHINE_COLOR = '#e4cd78' + TRAINING_COLOR = '#bd7ae9' + EVENT_COLOR = '#dd7e6b' + IS_RESERVED_BY_CURRENT_USER = '#b2e774' + MACHINE_IS_RESERVED_BY_USER = '#1d98ec' + IS_COMPLETED = '#eeeeee' + + def availability_border_color(availability) + if availability.available_type == 'machines' + MACHINE_COLOR + elsif availability.available_type == 'training' + TRAINING_COLOR + else + EVENT_COLOR + end + end + + def machines_slot_border_color(slot) + slot.is_reserved ? (slot.is_reserved_by_current_user ? IS_RESERVED_BY_CURRENT_USER : IS_COMPLETED) : MACHINE_COLOR + end + + def trainings_events_border_color(availability) + if availability.is_reserved + IS_RESERVED_BY_CURRENT_USER + elsif availability.is_completed + IS_COMPLETED + else + if availability.available_type == 'training' + TRAINING_COLOR + else + EVENT_COLOR + end + end + end +end diff --git a/app/models/age_range.rb b/app/models/age_range.rb new file mode 100644 index 000000000..0b4c83b38 --- /dev/null +++ b/app/models/age_range.rb @@ -0,0 +1,14 @@ +class AgeRange < ActiveRecord::Base + extend FriendlyId + friendly_id :name, use: :slugged + + has_many :events, dependent: :nullify + + def safe_destroy + if self.events.count == 0 + destroy + else + false + end + end +end diff --git a/app/models/auth_provider.rb b/app/models/auth_provider.rb index a4fd39d00..4c5e547a3 100644 --- a/app/models/auth_provider.rb +++ b/app/models/auth_provider.rb @@ -80,6 +80,14 @@ class AuthProvider < ActiveRecord::Base providable.profile_url end + def safe_destroy + if self.status != 'active' + destroy + else + false + end + end + private def set_initial_state # the initial state of a new AuthProvider will be 'pending', except if there is currently diff --git a/app/models/availability.rb b/app/models/availability.rb index 03b8f2984..7e7dd4f4e 100644 --- a/app/models/availability.rb +++ b/app/models/availability.rb @@ -1,4 +1,10 @@ class Availability < ActiveRecord::Base + + # elastic initialisations + include Elasticsearch::Model + index_name 'fablab' + document_type 'availabilities' + has_many :machines_availabilities, dependent: :destroy has_many :machines, through: :machines_availabilities accepts_nested_attributes_for :machines, allow_destroy: true @@ -24,6 +30,17 @@ class Availability < ActiveRecord::Base validate :length_must_be_1h_minimum, unless: proc { end_at.blank? or start_at.blank? } validate :should_be_associated + ## elastic callbacks + after_save { AvailabilityIndexerWorker.perform_async(:index, self.id) } + after_destroy { AvailabilityIndexerWorker.perform_async(:delete, self.id) } + + # elastic mapping + settings do + mappings dynamic: 'true' do + indexes 'available_type', analyzer: 'simple' + end + end + def safe_destroy if available_type == 'machines' reservations = Reservation.where(reservable_type: 'Machine', reservable_id: machine_ids).joins(:slots).where('slots.availability_id = ?', id) @@ -39,9 +56,14 @@ class Availability < ActiveRecord::Base end end - def title + def title(filter = {}) if available_type == 'machines' - machines.map(&:name).join(' - ') + if filter[:machine_ids] + return machines.to_ary.delete_if {|m| !filter[:machine_ids].include?(m.id)}.map(&:name).join(' - ') + end + return machines.map(&:name).join(' - ') + elsif available_type == 'event' + event.name else trainings.map(&:name).join(' - ') end @@ -51,18 +73,33 @@ class Availability < ActiveRecord::Base # if haven't defined a nb_total_places, places are unlimited def is_completed return false if nb_total_places.blank? - nb_total_places <= slots.to_a.select {|s| s.canceled_at == nil }.size + if available_type == 'training' + nb_total_places <= slots.to_a.select {|s| s.canceled_at == nil }.size + elsif available_type == 'event' + event.nb_free_places == 0 + end end # TODO: refactoring this function for avoid N+1 query def nb_total_places - if read_attribute(:nb_total_places).present? - read_attribute(:nb_total_places) - else - trainings.first.nb_total_places unless trainings.empty? + if available_type == 'training' + if read_attribute(:nb_total_places).present? + read_attribute(:nb_total_places) + else + trainings.first.nb_total_places unless trainings.empty? + end + elsif available_type == 'event' + event.nb_total_places end end + + def as_indexed_json + json = JSON.parse(to_json) + json['hours_duration'] = (end_at - start_at) / (60*60) + json.to_json + end + private def length_must_be_1h_minimum if end_at < (start_at + 1.hour) diff --git a/app/models/avoir.rb b/app/models/avoir.rb index a24564718..212c0627e 100644 --- a/app/models/avoir.rb +++ b/app/models/avoir.rb @@ -2,7 +2,7 @@ class Avoir < Invoice belongs_to :invoice after_create :expire_subscription, if: :subscription_to_expire - validates :avoir_mode, :inclusion => {:in => %w(stripe cheque transfer none cash)} + validates :avoir_mode, :inclusion => {:in => %w(stripe cheque transfer none cash wallet)} attr_accessor :invoice_items_ids diff --git a/app/models/category.rb b/app/models/category.rb index 0ec92a142..f407c6366 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -1,3 +1,36 @@ class Category < ActiveRecord::Base - has_and_belongs_to_many :events, join_table: :events_categories + extend FriendlyId + friendly_id :name, use: :slugged + + has_many :events, dependent: :destroy + + after_create :create_statistic_subtype + after_update :update_statistic_subtype, if: :name_changed? + after_destroy :remove_statistic_subtype + + + def create_statistic_subtype + index = StatisticIndex.where(es_type_key: 'event') + StatisticSubType.create!({statistic_types: index.first.statistic_types, key: self.slug, label: self.name}) + end + + def update_statistic_subtype + index = StatisticIndex.where(es_type_key: 'event') + subtype = StatisticSubType.joins(statistic_type_sub_types: :statistic_type).where(key: self.slug, statistic_types: { statistic_index_id: index.first.id }).first + subtype.label = self.name + subtype.save! + end + + def remove_statistic_subtype + subtype = StatisticSubType.where(key: self.slug).first + subtype.destroy! + end + + def safe_destroy + if Category.count > 1 && self.events.count == 0 + destroy + else + false + end + end end diff --git a/app/models/concerns/amount_concern.rb b/app/models/concerns/amount_concern.rb new file mode 100644 index 000000000..b2d07d050 --- /dev/null +++ b/app/models/concerns/amount_concern.rb @@ -0,0 +1,23 @@ +module AmountConcern + extend ActiveSupport::Concern + + included do + validates_numericality_of :amount, greater_than_or_equal_to: 0 + + def amount=(amount) + if amount.nil? + write_attribute(:amount, amount) + else + write_attribute(:amount, (amount * 100).to_i) + end + end + + def amount + if read_attribute(:amount).blank? + read_attribute(:amount) + else + read_attribute(:amount) / 100.0 + end + end + end +end diff --git a/app/models/coupon.rb b/app/models/coupon.rb new file mode 100644 index 000000000..3031d027d --- /dev/null +++ b/app/models/coupon.rb @@ -0,0 +1,75 @@ +class Coupon < ActiveRecord::Base + has_many :invoices + + after_commit :create_stripe_coupon, on: [:create] + after_commit :delete_stripe_coupon, on: [:destroy] + + validates :name, presence: true + validates :code, presence: true + validates :code, format: { with: /\A[A-Z0-9\-]+\z/ ,message: 'only caps letters, numbers, and dashes'} + validates :code, uniqueness: true + validates :percent_off, presence: true + validates :percent_off, :inclusion => 0..100 + validates :validity_per_user, presence: true + validates :validity_per_user, inclusion: { in: %w(once forever) } + + def safe_destroy + if self.invoices.size == 0 + destroy + else + false + end + end + + ## + # Check the status of the current coupon. The coupon: + # - may have been disabled by an admin, + # - may has expired because the validity date has been reached, + # - may have been used the maximum number of times it was allowed + # - may have already been used by the provided user, if the coupon is configured to allow only one use per user, + # - may be available for use + # @param [user_id] {Number} if provided and if the current coupon's validity_per_user == 'once', check that the coupon + # was already used by the provided user + # @return {String} status identifier + ## + def status(user_id = nil) + if not active? + 'disabled' + elsif (!valid_until.nil?) and valid_until.at_end_of_day < DateTime.now + 'expired' + elsif (!max_usages.nil?) and invoices.count >= max_usages + 'sold_out' + elsif (!user_id.nil?) and validity_per_user == 'once' and users_ids.include?(user_id.to_i) + 'already_used' + else + 'active' + end + end + + def users + self.invoices.map do |i| + i.user + end + end + + def users_ids + users.map do |u| + u.id + end + end + + def send_to(user_id) + NotificationCenter.call type: 'notify_member_about_coupon', + receiver: User.find(user_id), + attached_object: self + end + + def create_stripe_coupon + StripeWorker.perform_async(:create_stripe_coupon, id) + end + + def delete_stripe_coupon + StripeWorker.perform_async(:delete_stripe_coupon, code) + end + +end diff --git a/app/models/event.rb b/app/models/event.rb index 8d42c9aec..95181599b 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -5,9 +5,16 @@ class Event < ActiveRecord::Base accepts_nested_attributes_for :event_image, allow_destroy: true has_many :event_files, as: :viewable, dependent: :destroy accepts_nested_attributes_for :event_files, allow_destroy: true, reject_if: :all_blank - has_and_belongs_to_many :categories, join_table: :events_categories - validates :categories, presence: true + belongs_to :category + validates :category, presence: true has_many :reservations, as: :reservable, dependent: :destroy + has_and_belongs_to_many :event_themes, join_table: :events_event_themes, dependent: :destroy + + has_many :event_price_categories + has_many :price_categories, through: :event_price_categories + accepts_nested_attributes_for :event_price_categories, allow_destroy: false + + belongs_to :age_range belongs_to :availability, dependent: :destroy accepts_nested_attributes_for :availability @@ -23,6 +30,10 @@ class Event < ActiveRecord::Base title end + def themes + self.event_themes + end + def recurrence_events Event.includes(:availability).where('events.recurrence_id = ? AND events.id != ? AND availabilities.start_at >= ?', recurrence_id, id, Time.now).references(:availabilities) end @@ -36,6 +47,24 @@ class Event < ActiveRecord::Base end end + ## + # @deprecated + # DEPRECATED: Please use event_price_categories instead. + # This method is for backward compatibility only, do not use in new code + def reduced_amount + if ActiveRecord::Base.connection.column_exists?(:events, :reduced_amount) + read_attribute(:reduced_amount) + else + pc = PriceCategory.find_by(name: I18n.t('price_category.reduced_fare')) + reduced_fare = event_price_categories.where(price_category: pc).first + if reduced_fare.nil? + nil + else + reduced_fare.amount + end + end + end + # def reservations # Reservation.where(reservable: self) # end @@ -66,6 +95,10 @@ class Event < ActiveRecord::Base efs = event_files.map do |f| EventFile.new(attachment: f.attachment) end + event_price_cats = [] + event_price_categories.each do |epc| + event_price_cats.push(EventPriceCategory.new(price_category_id: epc.price_category_id, amount: epc.amount)) + end event = Event.new({ recurrence: 'none', title: title, @@ -74,9 +107,9 @@ class Event < ActiveRecord::Base event_files: efs, availability: Availability.new(start_at: start_at, end_at: end_at, available_type: 'event'), availability_id: nil, - category_ids: category_ids, + category_id: category_id, amount: amount, - reduced_amount: reduced_amount, + event_price_categories: event_price_cats, nb_total_places: nb_total_places, recurrence_id: id }) @@ -90,7 +123,7 @@ class Event < ActiveRecord::Base if nb_total_places.nil? self.nb_free_places = nil else - reserved_places = reservations.map{|r| r.nb_reserve_places + r.nb_reserve_reduced_places}.inject(0){|sum, t| sum + t } + reserved_places = reservations.map(&:total_booked_seats).inject(0){|sum, t| sum + t } self.nb_free_places = (nb_total_places - reserved_places) end end diff --git a/app/models/event_price_category.rb b/app/models/event_price_category.rb new file mode 100644 index 000000000..843418d87 --- /dev/null +++ b/app/models/event_price_category.rb @@ -0,0 +1,9 @@ +class EventPriceCategory < ActiveRecord::Base + belongs_to :event + belongs_to :price_category + + has_many :tickets + + validates :price_category_id, presence: true + validates :amount, presence: true +end diff --git a/app/models/event_theme.rb b/app/models/event_theme.rb new file mode 100644 index 000000000..9fc600e7a --- /dev/null +++ b/app/models/event_theme.rb @@ -0,0 +1,14 @@ +class EventTheme < ActiveRecord::Base + extend FriendlyId + friendly_id :name, use: :slugged + + has_and_belongs_to_many :events, join_table: :events_event_themes, dependent: :destroy + + def safe_destroy + if self.events.count == 0 + destroy + else + false + end + end +end diff --git a/app/models/export.rb b/app/models/export.rb new file mode 100644 index 000000000..ef44aa74d --- /dev/null +++ b/app/models/export.rb @@ -0,0 +1,35 @@ +class Export < ActiveRecord::Base + require 'fileutils' + + belongs_to :user + + validates :category, presence: true + validates :export_type, presence: true + validates :user, presence: true + + after_commit :generate_and_send_export, on: [:create] + + def file + dir = "exports/#{category}/#{export_type}" + + # create directories if they doesn't exists (exports & type & id) + FileUtils::mkdir_p dir + "#{dir}/#{self.filename}" + end + + def filename + "#{export_type}-#{self.id}_#{self.created_at.strftime('%d%m%Y')}.xlsx" + end + + private + def generate_and_send_export + case category + when 'statistics' + StatisticsExportWorker.perform_async(self.id) + when 'users' + UsersExportWorker.perform_async(self.id) + else + raise NoMethodError, "Unknown export service for #{category}/#{export_type}" + end + end +end diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 787eec524..5970283df 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -7,6 +7,8 @@ class Invoice < ActiveRecord::Base has_many :invoice_items, dependent: :destroy accepts_nested_attributes_for :invoice_items belongs_to :user + belongs_to :wallet_transaction + belongs_to :coupon has_one :avoir, class_name: 'Invoice', foreign_key: :invoice_id, dependent: :destroy @@ -66,6 +68,9 @@ class Invoice < ActiveRecord::Base reference.gsub!(/X\[([^\]]+)\]/, ''.to_s) end + # information about wallet (W[text]) + #reference.gsub!(/W\[([^\]]+)\]/, ''.to_s) + # remove information about refunds (R[text]) reference.gsub!(/R\[([^\]]+)\]/, ''.to_s) @@ -142,6 +147,11 @@ class Invoice < ActiveRecord::Base avoir.total += avoir_ii.amount end end + # handle coupon + unless avoir.coupon_id.nil? + discount = avoir.total * avoir.coupon.percent_off / 100.0 + avoir.total -= discount + end avoir end @@ -169,7 +179,7 @@ class Invoice < ActiveRecord::Base ## # Check if the current invoice is about a training that was previously validated for the concerned user. - # In that case refunding the invoice must not be not allowed. + # In that case refunding the invoice shouldn't be allowed. # @return {Boolean} ## def prevent_refund? diff --git a/app/models/notification_type.rb b/app/models/notification_type.rb index 38e8376f8..6a43c4819 100644 --- a/app/models/notification_type.rb +++ b/app/models/notification_type.rb @@ -37,5 +37,10 @@ class NotificationType notify_admin_profile_complete notify_admin_abuse_reported notify_admin_invoicing_changed + notify_user_wallet_is_credited + notify_admin_user_wallet_is_credited + notify_admin_export_complete + notify_member_about_coupon + notify_member_reservation_reminder ) end diff --git a/app/models/organization.rb b/app/models/organization.rb new file mode 100644 index 000000000..e227ace37 --- /dev/null +++ b/app/models/organization.rb @@ -0,0 +1,8 @@ +class Organization < ActiveRecord::Base + belongs_to :profile + has_one :address, as: :placeable, dependent: :destroy + accepts_nested_attributes_for :address, allow_destroy: true + + validates :name, presence: true + validates :address, presence: true +end diff --git a/app/models/price.rb b/app/models/price.rb index f2b7c379c..5f675d3e8 100644 --- a/app/models/price.rb +++ b/app/models/price.rb @@ -13,10 +13,11 @@ class Price < ActiveRecord::Base # @param slots {Array} when did the reservation will occur # @param [plan_id] {Number} if the user is subscribing to a plan at the same time of his reservation, specify the plan's ID here # @param [nb_places] {Number} for _reservable_ of type Event, pass here the number of booked places - # @param [nb_reduced_places] {Number} for _reservable_ of type Event, pass here the number of booked places at reduced price + # @param [tickets] {Array} for _reservable_ of type Event, mapping of the number of seats booked per price's category + # @param [coupon_code] {String} Code of the coupon to apply to the total price # @return {Hash} total and price detail ## - def self.compute(admin, user, reservable, slots, plan_id = nil, nb_places = nil, nb_reduced_places = nil) + def self.compute(admin, user, reservable, slots, plan_id = nil, nb_places = nil, tickets = nil, coupon_code = nil) _amount = 0 _elements = Hash.new _elements[:slots] = Array.new @@ -71,7 +72,7 @@ class Price < ActiveRecord::Base training_is_creditable = plan.training_credits.select {|credit| credit.creditable_id == reservable.id}.size > 0 # Training reserved by the user is free when : - + # |-> the user already has a current subscription and if training_is_creditable is true and has at least one credit available. if !new_plan_being_bought if user.training_credits.size < plan.training_credit_nb and training_is_creditable @@ -90,11 +91,10 @@ class Price < ActiveRecord::Base # Event reservation when Event - if reservable.reduced_amount and nb_reduced_places - amount = reservable.amount * nb_places + (reservable.reduced_amount * nb_reduced_places) - else - amount = reservable.amount * nb_places - end + amount = reservable.amount * nb_places + tickets.each do |ticket| + amount += ticket[:booked] * EventPriceCategory.find(ticket[:event_price_category_id]).amount + end unless tickets.nil? slots.each do |slot| _amount += get_slot_price(amount, slot, admin, _elements) end @@ -110,6 +110,12 @@ class Price < ActiveRecord::Base _amount += plan.amount end + # === apply Coupon if any === + unless coupon_code.nil? + _coupon = Coupon.find_by_code(coupon_code) + _amount = _amount - (_amount * _coupon.percent_off / 100.0) + end + # return result {elements: _elements, total: _amount} end diff --git a/app/models/price_category.rb b/app/models/price_category.rb new file mode 100644 index 000000000..62036d790 --- /dev/null +++ b/app/models/price_category.rb @@ -0,0 +1,16 @@ +class PriceCategory < ActiveRecord::Base + has_many :event_price_category + has_many :events, through: :event_price_categories + + validates :name, :presence => true + validates :name, uniqueness: {case_sensitive: false} + validates :conditions, :presence => true + + def safe_destroy + if event_price_category.count == 0 + destroy + else + false + end + end +end diff --git a/app/models/profile.rb b/app/models/profile.rb index 0dcf5b8b5..483fbee71 100644 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -7,6 +7,9 @@ class Profile < ActiveRecord::Base has_one :address, as: :placeable, dependent: :destroy accepts_nested_attributes_for :address, allow_destroy: true + has_one :organization, dependent: :destroy + accepts_nested_attributes_for :organization, allow_destroy: false + validates :first_name, presence: true, length: { maximum: 30 } validates :last_name, presence: true, length: { maximum: 30 } validates :gender, :inclusion => {:in => [true, false]} @@ -34,4 +37,16 @@ class Profile < ActiveRecord::Base def str_gender gender ? 'male' : 'female' end + + def self.mapping + # we protect some fields as they are designed to be managed by the system and must not be updated externally + blacklist = %w(id user_id created_at updated_at) + # model-relationships must be added manually + additional = [%w(avatar string), %w(address string), %w(organization_name string), %w(organization_address string)] + Profile.column_types + .map{|k,v| [k, v.type.to_s]} + .delete_if { |col| blacklist.include?(col[0]) } + .concat(additional) + end + end diff --git a/app/models/project.rb b/app/models/project.rb index 36153f251..0c2caf996 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -52,8 +52,8 @@ class Project < ActiveRecord::Base ## elastic # callbacks - after_save { IndexerWorker.perform_async(:index, self.id) } - after_destroy { IndexerWorker.perform_async(:delete, self.id) } + after_save { ProjectIndexerWorker.perform_async(:index, self.id) } + after_destroy { ProjectIndexerWorker.perform_async(:delete, self.id) } # settings do diff --git a/app/models/project_cao.rb b/app/models/project_cao.rb index a4b0ecf49..a394a7811 100644 --- a/app/models/project_cao.rb +++ b/app/models/project_cao.rb @@ -2,4 +2,5 @@ class ProjectCao < Asset mount_uploader :attachment, ProjectCaoUploader validates :attachment, file_size: { maximum: 20.megabytes.to_i } + validates :attachment, :file_mime_type => { :content_type => ENV['ALLOWED_MIME_TYPES'].split(' ') } end diff --git a/app/models/reservation.rb b/app/models/reservation.rb index ab9f353d4..d17a2b98b 100644 --- a/app/models/reservation.rb +++ b/app/models/reservation.rb @@ -6,6 +6,9 @@ class Reservation < ActiveRecord::Base accepts_nested_attributes_for :slots, allow_destroy: true belongs_to :reservable, polymorphic: true + has_many :tickets + accepts_nested_attributes_for :tickets, allow_destroy: false + has_one :invoice, -> {where(type: nil)}, as: :invoiced, dependent: :destroy validates_presence_of :reservable_id, :reservable_type @@ -17,13 +20,15 @@ class Reservation < ActiveRecord::Base after_commit :notify_member_create_reservation, on: :create after_commit :notify_admin_member_create_reservation, on: :create after_save :update_event_nb_free_places, if: Proc.new { |reservation| reservation.reservable_type == 'Event' } + after_create :debit_user_wallet - # + ## # Generate an array of {Stripe::InvoiceItem} with the elements in the current reservation, price included. # The training/machine price is depending of the member's group, subscription and credits already used # @param on_site {Boolean} true if an admin triggered the call - # - def generate_invoice_items(on_site = false) + # @param coupon_code {String} pass a valid code to appy a coupon + ## + def generate_invoice_items(on_site = false, coupon_code = nil) # returning array invoice_items = [] @@ -97,10 +102,9 @@ class Reservation < ActiveRecord::Base # === Event reservation === when Event - if reservable.reduced_amount and nb_reserve_reduced_places - amount = reservable.amount * nb_reserve_places + (reservable.reduced_amount * nb_reserve_reduced_places) - else - amount = reservable.amount * nb_reserve_places + amount = reservable.amount * nb_reserve_places + tickets.each do |ticket| + amount += ticket.booked * ticket.event_price_category.amount end slots.each do |slot| description = reservable.name + " #{I18n.l slot.start_at, format: :long} - #{I18n.l slot.end_at, format: :hour_minute}" @@ -124,25 +128,53 @@ class Reservation < ActiveRecord::Base end + # === Coupon === + unless coupon_code.nil? + cp = Coupon.find_by_code(coupon_code) + if not cp.nil? and cp.status(user.id) == 'active' + @coupon = cp + total = invoice.invoice_items.map(&:amount).map(&:to_i).reduce(:+) + unless on_site + invoice_items << Stripe::InvoiceItem.create( + customer: user.stp_customer_id, + amount: -(total * cp.percent_off / 100).to_i, + currency: Rails.application.secrets.stripe_currency, + description: "coupon #{cp.code} - reservation" + ) + end + else + raise InvalidCouponError + end + end + + @wallet_amount_debit = get_wallet_amount_debit + if @wallet_amount_debit != 0 and !on_site + invoice_items << Stripe::InvoiceItem.create( + customer: user.stp_customer_id, + amount: -@wallet_amount_debit, + currency: Rails.application.secrets.stripe_currency, + description: "wallet -#{@wallet_amount_debit / 100.0}" + ) + end + # let's return the resulting array of items invoice_items end - def save_with_payment + def save_with_payment(coupon_code = nil) build_invoice(user: user) - invoice_items = generate_invoice_items + invoice_items = generate_invoice_items(false, coupon_code) if valid? # TODO: refactoring customer = Stripe::Customer.retrieve(user.stp_customer_id) if plan_id self.subscription = Subscription.find_or_initialize_by(user_id: user.id) self.subscription.attributes = {plan_id: plan_id, user_id: user.id, card_token: card_token, expired_at: nil} - if subscription.save_with_payment(false) + if subscription.save_with_payment(false, coupon_code) self.stp_invoice_id = invoice_items.first.refresh.invoice self.invoice.stp_invoice_id = invoice_items.first.refresh.invoice self.invoice.invoice_items.push InvoiceItem.new(amount: subscription.plan.amount, stp_invoice_item_id: subscription.stp_subscription_id, description: subscription.plan.name, subscription_id: subscription.id) - total = invoice.invoice_items.map(&:amount).map(&:to_i).reduce(:+) - self.invoice.total = total + set_total_and_coupon(coupon_code) save! # # IMPORTANT NOTE: here, we don't have to create a stripe::invoice and pay it @@ -170,49 +202,49 @@ class Reservation < ActiveRecord::Base # # IMPORTANT NOTE: here, we have to create an invoice manually and pay it to pay all waiting stripe invoice items # - invoice = Stripe::Invoice.create( + stp_invoice = Stripe::Invoice.create( customer: user.stp_customer_id, ) - invoice.pay + stp_invoice.pay card.delete if card - self.stp_invoice_id = invoice.id - self.invoice.stp_invoice_id = invoice.id - self.invoice.total = invoice.total + self.stp_invoice_id = stp_invoice.id + self.invoice.stp_invoice_id = stp_invoice.id + set_total_and_coupon(coupon_code) save! rescue Stripe::CardError => card_error - clear_payment_info(card, invoice, invoice_items) + clear_payment_info(card, stp_invoice, invoice_items) logger.info card_error errors[:card] << card_error.message return false rescue Stripe::InvalidRequestError => e # Invalid parameters were supplied to Stripe's API - clear_payment_info(card, invoice, invoice_items) + clear_payment_info(card, stp_invoice, invoice_items) logger.error e errors[:payment] << e.message return false rescue Stripe::AuthenticationError => e # Authentication with Stripe's API failed # (maybe you changed API keys recently) - clear_payment_info(card, invoice, invoice_items) + clear_payment_info(card, stp_invoice, invoice_items) logger.error e errors[:payment] << e.message return false rescue Stripe::APIConnectionError => e # Network communication with Stripe failed - clear_payment_info(card, invoice, invoice_items) + clear_payment_info(card, stp_invoice, invoice_items) logger.error e errors[:payment] << e.message return false rescue Stripe::StripeError => e # Display a very generic error to the user, and maybe send # yourself an email - clear_payment_info(card, invoice, invoice_items) + clear_payment_info(card, stp_invoice, invoice_items) logger.error e errors[:payment] << e.message return false rescue => e # Something else happened, completely unrelated to Stripe - clear_payment_info(card, invoice, invoice_items) + clear_payment_info(card, stp_invoice, invoice_items) logger.error e errors[:payment] << e.message return false @@ -248,16 +280,24 @@ class Reservation < ActiveRecord::Base end - def save_with_local_payment + def save_with_local_payment(coupon_code = nil) if user.invoicing_disabled? if valid? + + ### generate invoice only for calcul price, TODO refactor!! + build_invoice(user: user) + generate_invoice_items(true, coupon_code) + @wallet_amount_debit = get_wallet_amount_debit + self.invoice = nil + ### + save! UsersCredits::Manager.new(reservation: self).update_credits return true end else build_invoice(user: user) - generate_invoice_items(true) + generate_invoice_items(true, coupon_code) end if valid? @@ -266,16 +306,14 @@ class Reservation < ActiveRecord::Base self.subscription.attributes = {plan_id: plan_id, user_id: user.id, expired_at: nil} if subscription.save_with_local_payment(false) self.invoice.invoice_items.push InvoiceItem.new(amount: subscription.plan.amount, description: subscription.plan.name, subscription_id: subscription.id) - total = invoice.invoice_items.map(&:amount).map(&:to_i).reduce(:+) - self.invoice.total = total + set_total_and_coupon(coupon_code) save! else errors[:card] << subscription.errors[:card].join return false end else - total = invoice.invoice_items.map(&:amount).map(&:to_i).reduce(:+) - self.invoice.total = total + set_total_and_coupon(coupon_code) save! end @@ -284,6 +322,15 @@ class Reservation < ActiveRecord::Base end end + def total_booked_seats + total = nb_reserve_places + if tickets.count > 0 + total += tickets.map(&:booked).map(&:to_i).reduce(:+) + end + total + end + + private def machine_not_already_reserved already_reserved = false self.slots.each do |slot| @@ -323,13 +370,62 @@ class Reservation < ActiveRecord::Base def update_event_nb_free_places if reservable_id_was.blank? - nb_free_places = reservable.nb_free_places - nb_reserve_places - nb_reserve_reduced_places + # simple reservation creation, we subtract the number of booked seats from the previous number + nb_free_places = reservable.nb_free_places - total_booked_seats else + # reservation moved from another date (for recurring events) + seats = total_booked_seats + reservable_was = Event.find(reservable_id_was) - nb_free_places = reservable_was.nb_free_places + nb_reserve_places + nb_reserve_reduced_places + nb_free_places = reservable_was.nb_free_places + seats reservable_was.update_columns(nb_free_places: nb_free_places) - nb_free_places = reservable.nb_free_places - nb_reserve_places - nb_reserve_reduced_places + nb_free_places = reservable.nb_free_places - seats end reservable.update_columns(nb_free_places: nb_free_places) end + + def get_wallet_amount_debit + total = (self.invoice.invoice_items.map(&:amount).map(&:to_i).reduce(:+) or 0) + if plan_id.present? + plan = Plan.find(plan_id) + total += plan.amount + end + if @coupon + total = (total - (total * @coupon.percent_off / 100.0)).to_i + end + wallet_amount = (user.wallet.amount * 100).to_i + + wallet_amount >= total ? total : wallet_amount + end + + def debit_user_wallet + if @wallet_amount_debit.present? and @wallet_amount_debit != 0 + amount = @wallet_amount_debit / 100.0 + wallet_transaction = WalletService.new(user: user, wallet: user.wallet).debit(amount, self) + if !user.invoicing_disabled? and wallet_transaction + self.invoice.update_columns(wallet_amount: @wallet_amount_debit, wallet_transaction_id: wallet_transaction.id) + end + end + end + + ## + # Set the total price to the reservation's invoice, summing its whole items. + # Additionally a coupon may be applied to this invoice to make a discount on the total price + # @param [coupon_code] {String} optional coupon code to apply to the invoice + ## + def set_total_and_coupon(coupon_code = nil) + total = invoice.invoice_items.map(&:amount).map(&:to_i).reduce(:+) + + unless coupon_code.nil? + cp = Coupon.find_by_code(coupon_code) + if not cp.nil? and cp.status(user.id) == 'active' + total = total - (total * cp.percent_off / 100.0) + self.invoice.coupon_id = cp.id + else + raise InvalidCouponError + end + end + + self.invoice.total = total + end end diff --git a/app/models/setting.rb b/app/models/setting.rb index e156b0a66..49b02a568 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -9,7 +9,6 @@ class Setting < ActiveRecord::Base training_explications_alert training_information_message subscription_explications_alert - event_reduced_amount_alert invoice_logo invoice_reference invoice_code-active @@ -29,7 +28,9 @@ class Setting < ActiveRecord::Base main_color secondary_color fablab_name - name_genre) + name_genre + reminder_enable + reminder_delay ) } after_update :update_stylesheet if :value_changed? diff --git a/app/models/statistic_custom_aggregation.rb b/app/models/statistic_custom_aggregation.rb new file mode 100644 index 000000000..8abc66704 --- /dev/null +++ b/app/models/statistic_custom_aggregation.rb @@ -0,0 +1,3 @@ +class StatisticCustomAggregation < ActiveRecord::Base + belongs_to :statistic_type +end diff --git a/app/models/statistic_type.rb b/app/models/statistic_type.rb index 1a348668b..47909d644 100644 --- a/app/models/statistic_type.rb +++ b/app/models/statistic_type.rb @@ -2,4 +2,5 @@ class StatisticType < ActiveRecord::Base has_one :statistic_index has_many :statistic_type_sub_types has_many :statistic_sub_types, through: :statistic_type_sub_types + has_many :statistic_custom_aggregations end diff --git a/app/models/stats/event.rb b/app/models/stats/event.rb index e2d77cb1f..9ad73a6a7 100644 --- a/app/models/stats/event.rb +++ b/app/models/stats/event.rb @@ -6,5 +6,7 @@ module Stats attribute :eventId, Integer attribute :eventDate, String + attribute :ageRange, String + attribute :eventTheme, String end end diff --git a/app/models/subscription.rb b/app/models/subscription.rb index 04ecc8db3..76a4fcaeb 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -18,11 +18,47 @@ class Subscription < ActiveRecord::Base after_save :notify_partner_subscribed_plan, if: :of_partner_plan? # Stripe subscription payment - def save_with_payment(invoice = true) + def save_with_payment(invoice = true, coupon_code = nil) if valid? - customer = Stripe::Customer.retrieve(user.stp_customer_id) begin - new_subscription = customer.subscriptions.create(plan: plan.stp_plan_id, card: card_token) + customer = Stripe::Customer.retrieve(user.stp_customer_id) + invoice_items = [] + + unless coupon_code.nil? + cp = Coupon.find_by_code(coupon_code) + if not cp.nil? and cp.status(user.id) == 'active' + @coupon = cp + total = plan.amount + invoice_items << Stripe::InvoiceItem.create( + customer: user.stp_customer_id, + amount: -(total * cp.percent_off / 100.0).to_i, + currency: Rails.application.secrets.stripe_currency, + description: "coupon #{cp.code} - subscription" + ) + else + raise InvalidCouponError + end + end + + # only add a wallet invoice item if pay subscription + # dont add if pay subscription + reservation + if invoice + @wallet_amount_debit = get_wallet_amount_debit + if @wallet_amount_debit != 0 + invoice_items << Stripe::InvoiceItem.create( + customer: user.stp_customer_id, + amount: -@wallet_amount_debit, + currency: Rails.application.secrets.stripe_currency, + description: "wallet -#{@wallet_amount_debit / 100.0}" + ) + end + end + + new_subscription = customer.subscriptions.create(plan: plan.stp_plan_id, source: card_token) + # very important to set expired_at to nil that can allow method is_new? to return true + # for send the notification + # TODO: Refactoring + update_column(:expired_at, nil) unless new_record? self.stp_subscription_id = new_subscription.id self.canceled_at = nil self.expired_at = Time.at(new_subscription.current_period_end) @@ -32,37 +68,52 @@ class Subscription < ActiveRecord::Base # generate invoice stp_invoice = Stripe::Invoice.all(customer: user.stp_customer_id, limit: 1).data.first - generate_invoice(stp_invoice.id).save if invoice + if invoice + invoc = generate_invoice(stp_invoice.id, coupon_code) + # debit wallet + wallet_transaction = debit_user_wallet + if wallet_transaction + invoc.wallet_amount = @wallet_amount_debit + invoc.wallet_transaction_id = wallet_transaction.id + end + invoc.save + end # cancel subscription after create cancel return true rescue Stripe::CardError => card_error + clear_wallet_and_goupon_invoice_items(invoice_items) logger.error card_error errors[:card] << card_error.message return false rescue Stripe::InvalidRequestError => e + clear_wallet_and_goupon_invoice_items(invoice_items) # Invalid parameters were supplied to Stripe's API logger.error e errors[:payment] << e.message return false rescue Stripe::AuthenticationError => e + clear_wallet_and_goupon_invoice_items(invoice_items) # Authentication with Stripe's API failed # (maybe you changed API keys recently) logger.error e errors[:payment] << e.message return false rescue Stripe::APIConnectionError => e + clear_wallet_and_goupon_invoice_items(invoice_items) # Network communication with Stripe failed logger.error e errors[:payment] << e.message return false rescue Stripe::StripeError => e + clear_wallet_and_goupon_invoice_items(invoice_items) # Display a very generic error to the user, and maybe send # yourself an email logger.error e errors[:payment] << e.message return false rescue => e + clear_wallet_and_goupon_invoice_items(invoice_items) # Something else happened, completely unrelated to Stripe logger.error e errors[:payment] << e.message @@ -71,22 +122,52 @@ class Subscription < ActiveRecord::Base end end - def save_with_local_payment(invoice = true) + def save_with_local_payment(invoice = true, coupon_code = nil) if valid? + # very important to set expired_at to nil that can allow method is_new? to return true + # for send the notification + # TODO: Refactoring + update_column(:expired_at, nil) unless new_record? self.stp_subscription_id = nil self.canceled_at = nil set_expired_at - save! - UsersCredits::Manager.new(user: self.user).reset_credits if expired_date_changed - generate_invoice.save if invoice - return true + if save + UsersCredits::Manager.new(user: self.user).reset_credits if expired_date_changed + if invoice + invoc = generate_invoice(nil, coupon_code) + @wallet_amount_debit = get_wallet_amount_debit + + # debit wallet + wallet_transaction = debit_user_wallet + if wallet_transaction + invoc.wallet_amount = @wallet_amount_debit + invoc.wallet_transaction_id = wallet_transaction.id + end + invoc.save + end + return true + else + return false + end else return false end end - def generate_invoice(stp_invoice_id = nil) - invoice = Invoice.new(invoiced_id: id, invoiced_type: 'Subscription', user: user, total: plan.amount, stp_invoice_id: stp_invoice_id) + def generate_invoice(stp_invoice_id = nil, coupon_code = nil) + coupon_id = nil + total = plan.amount + + unless coupon_code.nil? + cp = Coupon.find_by_code(coupon_code) + if not cp.nil? and cp.status(user.id) == 'active' + @coupon = cp + coupon_id = cp.id + total = plan.amount - (plan.amount * cp.percent_off / 100.0) + end + end + + invoice = Invoice.new(invoiced_id: id, invoiced_type: 'Subscription', user: user, total: total, stp_invoice_id: stp_invoice_id, coupon_id: coupon_id) invoice.invoice_items.push InvoiceItem.new(amount: plan.amount, stp_invoice_item_id: stp_subscription_id, description: plan.name, subscription_id: self.id) invoice end @@ -209,4 +290,35 @@ class Subscription < ActiveRecord::Base plan.is_a?(PartnerPlan) end + def get_wallet_amount_debit + total = plan.amount + if @coupon + total = (total - (total * @coupon.percent_off / 100.0)).to_i + end + wallet_amount = (user.wallet.amount * 100).to_i + return wallet_amount >= total ? total : wallet_amount + end + + def debit_user_wallet + if @wallet_amount_debit.present? and @wallet_amount_debit != 0 + amount = @wallet_amount_debit / 100.0 + return WalletService.new(user: user, wallet: user.wallet).debit(amount, self) + end + end + + def clear_wallet_and_goupon_invoice_items(invoice_items) + begin + invoice_items.each(&:delete) + rescue Stripe::InvalidRequestError => e + logger.error e + rescue Stripe::AuthenticationError => e + logger.error e + rescue Stripe::APIConnectionError => e + logger.error e + rescue Stripe::StripeError => e + logger.error e + rescue => e + logger.error e + end + end end diff --git a/app/models/ticket.rb b/app/models/ticket.rb new file mode 100644 index 000000000..f98dae596 --- /dev/null +++ b/app/models/ticket.rb @@ -0,0 +1,8 @@ +class Ticket < ActiveRecord::Base + belongs_to :reservation + belongs_to :event_price_category + + validates :event_price_category_id, presence: true + validates :booked, presence: true + validates :booked, numericality: { only_integer: true, greater_than: 0 } +end diff --git a/app/models/training.rb b/app/models/training.rb index b2fa1463f..df2d962d9 100644 --- a/app/models/training.rb +++ b/app/models/training.rb @@ -2,6 +2,9 @@ class Training < ActiveRecord::Base extend FriendlyId friendly_id :name, use: :slugged + has_one :training_image, as: :viewable, dependent: :destroy + accepts_nested_attributes_for :training_image, allow_destroy: true + has_and_belongs_to_many :machines, join_table: :trainings_machines has_many :trainings_availabilities @@ -23,8 +26,6 @@ class Training < ActiveRecord::Base after_update :update_statistic_subtype, if: :name_changed? after_destroy :remove_statistic_subtype - validates :description, length: { maximum: 255 } - def amount_by_group(group) trainings_pricings.where(group_id: group).first end diff --git a/app/models/training_image.rb b/app/models/training_image.rb new file mode 100644 index 000000000..084f84789 --- /dev/null +++ b/app/models/training_image.rb @@ -0,0 +1,4 @@ + +class TrainingImage < Asset + mount_uploader :attachment, MachineImageUploader +end \ No newline at end of file diff --git a/app/models/user.rb b/app/models/user.rb index c3522a747..af040ab7c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -45,12 +45,17 @@ class User < ActiveRecord::Base has_many :tags, through: :user_tags accepts_nested_attributes_for :tags, allow_destroy: true + has_one :wallet, dependent: :destroy + + has_many :exports, dependent: :destroy + # fix for create admin user before_save do self.email.downcase! if self.email end before_create :assign_default_role + after_create :create_a_wallet after_commit :create_stripe_customer, on: [:create] after_commit :notify_admin_when_user_is_created, on: :create after_update :notify_admin_invoicing_changed, if: :invoicing_disabled_changed? @@ -229,7 +234,18 @@ class User < ActiveRecord::Base if parsed[1] == 'user' self[parsed[2].to_sym] elsif parsed[1] == 'profile' - self.profile[parsed[2].to_sym] + case sso_mapping + when 'profile.avatar' + self.profile.user_avatar.remote_attachment_url + when 'profile.address' + self.profile.address.address + when 'profile.organization_name' + self.profile.organization.name + when 'profile.organization_address' + self.profile.organization.address.address + else + self.profile[parsed[2].to_sym] + end end end @@ -241,11 +257,22 @@ class User < ActiveRecord::Base if sso_mapping.to_s.start_with? 'user.' self[sso_mapping[5..-1].to_sym] = data unless data.nil? elsif sso_mapping.to_s.start_with? 'profile.' - if sso_mapping.to_s == 'profile.avatar' - self.profile.user_avatar ||= UserAvatar.new - self.profile.user_avatar.remote_attachment_url = data - else - self.profile[sso_mapping[8..-1].to_sym] = data unless data.nil? + case sso_mapping.to_s + when 'profile.avatar' + self.profile.user_avatar ||= UserAvatar.new + self.profile.user_avatar.remote_attachment_url = data + when 'profile.address' + self.profile.address ||= Address.new + self.profile.address.address = data + when 'profile.organization_name' + self.profile.organization ||= Organization.new + self.profile.organization.name = data + when 'profile.organization_address' + self.profile.organization ||= Organization.new + self.profile.organization.address ||= Address.new + self.profile.organization.address.address = data + else + self.profile[sso_mapping[8..-1].to_sym] = data unless data.nil? end end end @@ -309,6 +336,17 @@ class User < ActiveRecord::Base end end + def self.mapping + # we protect some fields as they are designed to be managed by the system and must not be updated externally + blacklist = %w(id encrypted_password reset_password_token reset_password_sent_at remember_created_at + sign_in_count current_sign_in_at last_sign_in_at current_sign_in_ip last_sign_in_ip confirmation_token confirmed_at + confirmation_sent_at unconfirmed_email failed_attempts unlock_token locked_at created_at updated_at stp_customer_id slug + provider auth_token merged_at) + User.column_types + .map{|k,v| [k, v.type.to_s]} + .delete_if { |col| blacklist.include?(col[0]) } + end + protected def confirmation_required? false @@ -333,6 +371,10 @@ class User < ActiveRecord::Base StripeWorker.perform_async(:create_stripe_customer, id) end + def create_a_wallet + self.create_wallet + end + def notify_admin_when_user_is_created if need_completion? and not provider.nil? NotificationCenter.call type: 'notify_admin_when_user_is_imported', diff --git a/app/models/wallet.rb b/app/models/wallet.rb new file mode 100644 index 000000000..da8128eb7 --- /dev/null +++ b/app/models/wallet.rb @@ -0,0 +1,24 @@ +class Wallet < ActiveRecord::Base + include AmountConcern + + belongs_to :user + has_many :wallet_transactions, dependent: :destroy + + validates :user, presence: true + + def credit(amount) + if amount.is_a?(Numeric) and amount >= 0 + self.amount += amount + return save + end + false + end + + def debit(amount) + if amount.is_a?(Numeric) and amount >= 0 + self.amount -= amount + return save + end + false + end +end diff --git a/app/models/wallet_transaction.rb b/app/models/wallet_transaction.rb new file mode 100644 index 000000000..0de5feb3b --- /dev/null +++ b/app/models/wallet_transaction.rb @@ -0,0 +1,12 @@ +class WalletTransaction < ActiveRecord::Base + include AmountConcern + + belongs_to :user + belongs_to :wallet + belongs_to :reservation + belongs_to :transactable, polymorphic: true + has_one :invoice + + validates_inclusion_of :transaction_type, in: %w( credit debit ) + validates :user, :wallet, presence: true +end diff --git a/app/pdfs/pdf/invoice.rb b/app/pdfs/pdf/invoice.rb index 6e2120159..de9d6cdf4 100644 --- a/app/pdfs/pdf/invoice.rb +++ b/app/pdfs/pdf/invoice.rb @@ -47,13 +47,22 @@ module PDF text I18n.t('invoices.invoice_issued_on_DATE', DATE:I18n.l(invoice.created_at.to_date)) end - # user's informations - if invoice.user.profile.address + # user/organization's informations + if invoice&.user&.profile&.organization + name = invoice.user.profile.organization.name + else + name = invoice.user.profile.full_name + end + + if invoice&.user&.profile&.organization&.address + address = invoice.user.profile.organization.address.address + elsif invoice&.user&.profile&.address address = invoice.user.profile.address.address else address = '' end - text_box "#{invoice.user.profile.full_name}\n#{invoice.user.email}\n#{address}", :at => [bounds.width - 130, bounds.top - 49], :width => 130, :align => :right, :inline_format => true + + text_box "#{name}\n#{invoice.user.email}\n#{address}", :at => [bounds.width - 130, bounds.top - 49], :width => 130, :align => :right, :inline_format => true # object move_down 25 @@ -74,6 +83,8 @@ module PDF object = subscription_verbose(invoice.invoiced, invoice.user) when 'OfferDay' object = offer_day_verbose(invoice.invoiced, invoice.user) + else + puts "ERROR : specified invoiced type (#{invoice.invoiced_type}) is unknown" end end text I18n.t('invoices.object')+' '+object @@ -110,12 +121,14 @@ module PDF ### Training reservation when 'Training' details += I18n.t('invoices.training_reservation_DESCRIPTION', DESCRIPTION: item.description) - ### courses and workshops reservation + ### events reservation when 'Event' - details += I18n.t('invoices.courses_and_workshops_reservation_DESCRIPTION', DESCRIPTION: item.description) + details += I18n.t('invoices.event_reservation_DESCRIPTION', DESCRIPTION: item.description) # details of the number of tickets details += "\n "+I18n.t('invoices.full_price_ticket', count: invoice.invoiced.nb_reserve_places) if invoice.invoiced.nb_reserve_places > 0 - details += "\n "+I18n.t('invoices.reduced_rate_ticket', count: invoice.invoiced.nb_reserve_reduced_places) if invoice.invoiced.nb_reserve_reduced_places > 0 + invoice.invoiced.tickets.each do |t| + details += "\n "+I18n.t('invoices.other_rate_ticket', count: t.booked, NAME: t.event_price_category.price_category.name) + end ### Other cases (not expected) else @@ -127,6 +140,16 @@ module PDF total_calc += price end + # subtract the coupon, if any + unless invoice.coupon_id.nil? + cp = invoice.coupon + discount = total_calc * cp.percent_off / 100.0 + total_calc = total_calc - discount + + # add a row for the coupon + data += [ [I18n.t('invoices.coupon_CODE_discount_of_PERCENT', CODE: cp.code, PERCENT: cp.percent_off), number_to_currency(-discount)] ] + end + # total verification total = invoice.total / 100.0 if total_calc != total @@ -196,23 +219,45 @@ module PDF payment_verbose += I18n.t('invoices.by_transfer') when 'cash' payment_verbose += I18n.t('invoices.by_cash') + when 'wallet' + payment_verbose += I18n.t('invoices.by_wallet') when 'none' payment_verbose = I18n.t('invoices.no_refund') else puts "ERROR : specified refunding method (#{payment_verbose}) is unknown" end + payment_verbose += ' '+I18n.t('invoices.for_an_amount_of_AMOUNT', AMOUNT: number_to_currency(total)) + else + # subtract the wallet amount for this invoice from the total + if invoice.wallet_amount + wallet_amount = invoice.wallet_amount / 100.0 + total = total - wallet_amount + end + + # payment method if invoice.stp_invoice_id payment_verbose = I18n.t('invoices.settlement_by_debit_card') else payment_verbose = I18n.t('invoices.settlement_done_at_the_reception') end - end - unless invoice.is_a?(Avoir) + + # if the invoice was 100% payed with the wallet ... + if total == 0 and wallet_amount + payment_verbose = I18n.t('invoices.settlement_by_wallet') + end + payment_verbose += ' '+I18n.t('invoices.on_DATE_at_TIME', DATE: I18n.l(invoice.created_at.to_date), TIME:I18n.l(invoice.created_at, format: :hour_minute)) - end - unless invoice.is_a?(Avoir) and invoice.avoir_mode == 'none' - payment_verbose += ' '+I18n.t('invoices.for_an_amount_of_AMOUNT', AMOUNT: number_to_currency(total)) if invoice.avoir_mode != 'none' + if total > 0 or !invoice.wallet_amount + payment_verbose += ' '+I18n.t('invoices.for_an_amount_of_AMOUNT', AMOUNT: number_to_currency(total)) + end + if invoice.wallet_amount + if total > 0 + payment_verbose += ' '+I18n.t('invoices.and') + ' ' + I18n.t('invoices.by_wallet') + ' ' + I18n.t('invoices.for_an_amount_of_AMOUNT', AMOUNT: number_to_currency(wallet_amount)) + else + payment_verbose += ' '+I18n.t('invoices.for_an_amount_of_AMOUNT', AMOUNT: number_to_currency(wallet_amount)) + end + end end text payment_verbose diff --git a/app/policies/age_range_policy.rb b/app/policies/age_range_policy.rb new file mode 100644 index 000000000..51c14003a --- /dev/null +++ b/app/policies/age_range_policy.rb @@ -0,0 +1,7 @@ +class AgeRangePolicy < ApplicationPolicy + %w(create update destroy show).each do |action| + define_method "#{action}?" do + user.is_admin? + end + end +end diff --git a/app/policies/category_policy.rb b/app/policies/category_policy.rb new file mode 100644 index 000000000..40ef36813 --- /dev/null +++ b/app/policies/category_policy.rb @@ -0,0 +1,7 @@ +class CategoryPolicy < ApplicationPolicy + %w(create update destroy show).each do |action| + define_method "#{action}?" do + user.is_admin? + end + end +end diff --git a/app/policies/coupon_policy.rb b/app/policies/coupon_policy.rb new file mode 100644 index 000000000..3f08ae304 --- /dev/null +++ b/app/policies/coupon_policy.rb @@ -0,0 +1,7 @@ +class CouponPolicy < ApplicationPolicy + %w(index show create update destroy send_to).each do |action| + define_method "#{action}?" do + user.is_admin? + end + end +end diff --git a/app/policies/event_policy.rb b/app/policies/event_policy.rb index d317c1b96..9b5be79e4 100644 --- a/app/policies/event_policy.rb +++ b/app/policies/event_policy.rb @@ -2,12 +2,12 @@ class EventPolicy < ApplicationPolicy class Scope < Scope def resolve if user.nil? or (user and !user.is_admin?) - scope.includes(:event_image, :event_files, :availability, :categories) + scope.includes(:event_image, :event_files, :availability, :category) .where('availabilities.start_at >= ?', Time.now) .order('availabilities.start_at ASC') .references(:availabilities) else - scope.includes(:event_image, :event_files, :availability, :categories) + scope.includes(:event_image, :event_files, :availability, :category) .order('availabilities.start_at DESC') .references(:availabilities) end diff --git a/app/policies/event_theme_policy.rb b/app/policies/event_theme_policy.rb new file mode 100644 index 000000000..750790c51 --- /dev/null +++ b/app/policies/event_theme_policy.rb @@ -0,0 +1,7 @@ +class EventThemePolicy < ApplicationPolicy + %w(create update destroy show).each do |action| + define_method "#{action}?" do + user.is_admin? + end + end +end diff --git a/app/policies/export_policy.rb b/app/policies/export_policy.rb index 18cd7c81d..a29ac0e20 100644 --- a/app/policies/export_policy.rb +++ b/app/policies/export_policy.rb @@ -1,5 +1,5 @@ class ExportPolicy < Struct.new(:user, :export) - %w(export_reservations export_members export_subscriptions).each do |action| + %w(export_reservations export_members export_subscriptions download status).each do |action| define_method "#{action}?" do user.is_admin? end diff --git a/app/policies/price_category_policy.rb b/app/policies/price_category_policy.rb new file mode 100644 index 000000000..7e3881ce5 --- /dev/null +++ b/app/policies/price_category_policy.rb @@ -0,0 +1,7 @@ +class PriceCategoryPolicy < ApplicationPolicy + %w(show create update destroy).each do |action| + define_method "#{action}?" do + user.is_admin? + end + end +end diff --git a/app/policies/statistic_policy.rb b/app/policies/statistic_policy.rb index faec07341..617e6f931 100644 --- a/app/policies/statistic_policy.rb +++ b/app/policies/statistic_policy.rb @@ -1,5 +1,6 @@ class StatisticPolicy < ApplicationPolicy - %w(index account event machine project subscription training user scroll).each do |action| + %w(index account event machine project subscription training user scroll export_subscription export_machine + export_training export_event export_account export_project export_global).each do |action| define_method "#{action}?" do user.is_admin? end diff --git a/app/policies/training_policy.rb b/app/policies/training_policy.rb index 043817e4a..0a095bd5f 100644 --- a/app/policies/training_policy.rb +++ b/app/policies/training_policy.rb @@ -5,7 +5,7 @@ class TrainingPolicy < ApplicationPolicy end end - %w(show create update).each do |action| + %w(create update).each do |action| define_method "#{action}?" do user.is_admin? end @@ -14,4 +14,8 @@ class TrainingPolicy < ApplicationPolicy def destroy? user.is_admin? and record.destroyable? end + + def availabilities? + user.is_admin? + end end diff --git a/app/policies/version_policy.rb b/app/policies/version_policy.rb new file mode 100644 index 000000000..e3f8d38ec --- /dev/null +++ b/app/policies/version_policy.rb @@ -0,0 +1,5 @@ +class VersionPolicy < ApplicationPolicy + def show? + user.is_admin? + end +end diff --git a/app/policies/wallet_policy.rb b/app/policies/wallet_policy.rb new file mode 100644 index 000000000..028fb54f9 --- /dev/null +++ b/app/policies/wallet_policy.rb @@ -0,0 +1,13 @@ +class WalletPolicy < ApplicationPolicy + def by_user? + user.is_admin? or user == record.user + end + + def transactions? + user.is_admin? or user == record.user + end + + def credit? + user.is_admin? + end +end diff --git a/app/flow_workers/members_flow_worker.rb b/app/processors/members_processor.rb similarity index 97% rename from app/flow_workers/members_flow_worker.rb rename to app/processors/members_processor.rb index e929b0ea7..ee9ddc6e5 100644 --- a/app/flow_workers/members_flow_worker.rb +++ b/app/processors/members_processor.rb @@ -1,5 +1,5 @@ -class MembersFlowWorker +class MembersProcessor attr_accessor :member diff --git a/app/services/statistic_service.rb b/app/services/statistic_service.rb index cdd42a100..913ccabc9 100644 --- a/app/services/statistic_service.rb +++ b/app/services/statistic_service.rb @@ -68,7 +68,9 @@ class StatisticService eventId: r.event_id, name: r.event_name, eventDate: r.event_date, - reservationId: r.reservation_id + reservationId: r.reservation_id, + eventTheme: r.event_theme, + ageRange: r.age_range }.merge(user_info_stat(r))) stat.stat = (type == 'booking' ? r.nb_places : r.nb_hours) stat.save @@ -119,10 +121,14 @@ class StatisticService def subscriptions_list(options = default_options) result = [] InvoiceItem.where('invoice_items.created_at >= :start_date AND invoice_items.created_at <= :end_date', options) - .eager_load(subscription: [:plan, user: [:profile, :group]]).each do |i| + .eager_load(invoice: [:coupon], subscription: [:plan, user: [:profile, :group]]).each do |i| unless i.invoice.is_a?(Avoir) sub = i.subscription if sub + ca = i.amount.to_i / 100.0 + unless i.invoice.coupon_id.nil? + ca = ca - ( ca * i.invoice.coupon.percent_off / 100.0 ) + end u = sub.user p = sub.plan result.push OpenStruct.new({ @@ -136,7 +142,7 @@ class StatisticService duration: p.duration.to_i, subscription_id: sub.id, invoice_item_id: i.id, - ca: i.amount.to_i / 100.0 + ca: ca }.merge(user_info(u))) end end @@ -198,10 +204,12 @@ class StatisticService date: options[:start_date].to_date, reservation_id: r.id, event_id: r.reservable.id, - event_type: r.reservable.categories.first.name, + event_type: r.reservable.category.name, event_name: r.reservable.name, event_date: slot.start_at.to_date, - nb_places: r.nb_reserve_places + r.nb_reserve_reduced_places, + event_theme: (r.reservable.event_themes.first ? r.reservable.event_themes.first.name : ''), + age_range: (r.reservable.age_range_id ? r.reservable.age_range.name : ''), + nb_places: r.total_booked_seats, nb_hours: difference_in_hours(slot.start_at, slot.end_at), ca: calcul_ca(r.invoice) }.merge(user_info(u))) if r.reservable @@ -333,6 +341,7 @@ class StatisticService def calcul_ca(invoice) return nil unless invoice ca = 0 + # sum each items in the invoice (+ for invoices/- for refunds) invoice.invoice_items.each do |ii| unless ii.subscription_id if invoice.is_a?(Avoir) @@ -342,6 +351,11 @@ class StatisticService end end end + # subtract coupon discount from invoices and refunds + unless invoice.coupon_id.nil? + ca = ca - ( ca * invoice.coupon.percent_off / 100.0 ) + end + # divide the result by 100 to convert from centimes to monetary unit ca == 0 ? ca : ca / 100.0 end @@ -350,6 +364,10 @@ class StatisticService invoice.invoice_items.each do |ii| ca = ca - ii.amount.to_i end + # subtract coupon discount from the refund + unless invoice.coupon_id.nil? + ca = ca - ( ca * invoice.coupon.percent_off / 100.0 ) + end ca == 0 ? ca : ca / 100.0 end diff --git a/app/services/statistics_export_service.rb b/app/services/statistics_export_service.rb new file mode 100644 index 000000000..a1c73ad68 --- /dev/null +++ b/app/services/statistics_export_service.rb @@ -0,0 +1,82 @@ +require 'abstract_controller' +require 'action_controller' +require 'action_view' +require 'active_record' + +# require any helpers +require './app/helpers/application_helper' + +class StatisticsExportService + + def export_global(export) + + # query all stats with range arguments + query = MultiJson.load(export.query) + + @results = Elasticsearch::Model.client.search({index: 'stats', scroll: '30s', body: query}) + scroll_id = @results['_scroll_id'] + while @results['hits']['hits'].size != @results['hits']['total'] + scroll_res = Elasticsearch::Model.client.scroll(scroll: '30s', scroll_id: scroll_id) + @results['hits']['hits'].concat(scroll_res['hits']['hits']) + scroll_id = scroll_res['_scroll_id'] + end + + ids = @results['hits']['hits'].map { |u| u['_source']['userId'] } + @users = User.includes(:profile).where(:id => ids) + + @indices = StatisticIndex.all.includes(:statistic_fields, :statistic_types => [:statistic_sub_types]) + + ActionController::Base.prepend_view_path './app/views/' + # place data in view_assigns + view_assigns = {results: @results, users: @users, indices: @indices} + av = ActionView::Base.new(ActionController::Base.view_paths, view_assigns) + av.class_eval do + # include any needed helpers (for the view) + include ApplicationHelper + end + + content = av.render template: 'exports/statistics_global.xlsx.axlsx' + # write content to file + File.open(export.file,"w+b") {|f| f.puts content } + end + + %w(account event machine project subscription training).each do |path| + class_eval %{ + def export_#{path}(export) + + query = MultiJson.load(export.query) + type_key = export.key + + @results = Elasticsearch::Model.client.search({index: 'stats', type: '#{path}', scroll: '30s', body: query}) + scroll_id = @results['_scroll_id'] + while @results['hits']['hits'].size != @results['hits']['total'] + scroll_res = Elasticsearch::Model.client.scroll(scroll: '30s', scroll_id: scroll_id) + @results['hits']['hits'].concat(scroll_res['hits']['hits']) + scroll_id = scroll_res['_scroll_id'] + end + + ids = @results['hits']['hits'].map { |u| u['_source']['userId'] } + @users = User.includes(:profile).where(:id => ids) + + @index = StatisticIndex.find_by(es_type_key: '#{path}') + @type = StatisticType.find_by(key: type_key, statistic_index_id: @index.id) + @subtypes = @type.statistic_sub_types + @fields = @index.statistic_fields + + ActionController::Base.prepend_view_path './app/views/' + # place data in view_assigns + view_assigns = {results: @results, users: @users, index: @index, type: @type, subtypes: @subtypes, fields: @fields} + av = ActionView::Base.new(ActionController::Base.view_paths, view_assigns) + av.class_eval do + # include any needed helpers (for the view) + include ApplicationHelper + end + + content = av.render template: 'exports/statistics_current.xlsx.axlsx' + # write content to file + File.open(export.file,"w+b") {|f| f.puts content } + end + } + end + +end \ No newline at end of file diff --git a/app/services/users_export_service.rb b/app/services/users_export_service.rb new file mode 100644 index 000000000..80b31d754 --- /dev/null +++ b/app/services/users_export_service.rb @@ -0,0 +1,65 @@ +require 'abstract_controller' +require 'action_controller' +require 'action_view' +require 'active_record' + +# require any helpers +require './app/helpers/application_helper' + +class UsersExportService + + # export subscriptions + def export_subscriptions(export) + @subscriptions = Subscription.all.includes(:plan, :user => [:profile]) + + ActionController::Base.prepend_view_path './app/views/' + # place data in view_assigns + view_assigns = {subscriptions: @subscriptions} + av = ActionView::Base.new(ActionController::Base.view_paths, view_assigns) + av.class_eval do + # include any needed helpers (for the view) + include ApplicationHelper + end + + content = av.render template: 'exports/users_subscriptions.xlsx.axlsx' + # write content to file + File.open(export.file,"w+b") {|f| f.puts content } + end + + # export reservations + def export_reservations(export) + @reservations = Reservation.all.includes(:slots, :reservable, :user => [:profile]) + + ActionController::Base.prepend_view_path './app/views/' + # place data in view_assigns + view_assigns = {reservations: @reservations} + av = ActionView::Base.new(ActionController::Base.view_paths, view_assigns) + av.class_eval do + # include any needed helpers (for the view) + include ApplicationHelper + end + + content = av.render template: 'exports/users_reservations.xlsx.axlsx' + # write content to file + File.open(export.file,"w+b") {|f| f.puts content } + end + + # export members + def export_members(export) + @members = User.with_role(:member).includes(:group, :trainings, :tags, :invoices, :projects, :subscriptions => [:plan], :profile => [:address, :organization => [:address]]) + + ActionController::Base.prepend_view_path './app/views/' + # place data in view_assigns + view_assigns = {members: @members} + av = ActionView::Base.new(ActionController::Base.view_paths, view_assigns) + av.class_eval do + # include any needed helpers (for the view) + include ApplicationHelper + end + + content = av.render template: 'exports/users_members.xlsx.axlsx' + # write content to file + File.open(export.file,"w+b") {|f| f.puts content } + end + +end \ No newline at end of file diff --git a/app/services/wallet_service.rb b/app/services/wallet_service.rb new file mode 100644 index 000000000..23a824529 --- /dev/null +++ b/app/services/wallet_service.rb @@ -0,0 +1,40 @@ +class WalletService + def initialize(user: nil, wallet: nil) + @user = user + @wallet = wallet + end + + ## credit an amount to wallet, if credit success then return a wallet transaction and notify to admin + def credit(amount) + ActiveRecord::Base.transaction do + if @wallet.credit(amount) + transaction = WalletTransaction.new(user: @user, wallet: @wallet, transaction_type: 'credit', amount: amount) + if transaction.save + NotificationCenter.call type: 'notify_user_wallet_is_credited', + receiver: @wallet.user, + attached_object: transaction + NotificationCenter.call type: 'notify_admin_user_wallet_is_credited', + receiver: User.admins, + attached_object: transaction + return transaction + end + end + raise ActiveRecord::Rollback + end + return false + end + + ## debit an amount to wallet, if debit success then return a wallet transaction + def debit(amount, transactable) + ActiveRecord::Base.transaction do + if @wallet.debit(amount) + transaction = WalletTransaction.new(user: @user, wallet: @wallet, transaction_type: 'debit', amount: amount, transactable: transactable) + if transaction.save + return transaction + end + end + raise ActiveRecord::Rollback + end + return false + end +end diff --git a/app/uploaders/event_image_uploader.rb b/app/uploaders/event_image_uploader.rb index 8ed9e67b2..26a3b8529 100644 --- a/app/uploaders/event_image_uploader.rb +++ b/app/uploaders/event_image_uploader.rb @@ -63,4 +63,9 @@ class EventImageUploader < CarrierWave::Uploader::Base def filename "#{model.class.to_s.underscore}.#{file.extension}" if original_filename end + + # return an array like [width, height] + def dimensions + ::MiniMagick::Image.open(file.file)[:dimensions] + end end diff --git a/app/uploaders/project_cao_uploader.rb b/app/uploaders/project_cao_uploader.rb index e97577878..fb5e4903e 100644 --- a/app/uploaders/project_cao_uploader.rb +++ b/app/uploaders/project_cao_uploader.rb @@ -39,7 +39,7 @@ class ProjectCaoUploader < CarrierWave::Uploader::Base # Add a white list of extensions which are allowed to be uploaded. # For images you might use something like this: def extension_white_list - %w(pdf ai eps cad math svg stl dxf dwg obj step iges 3dm doc docx png) + ENV['ALLOWED_EXTENSIONS'].split(' ') end # Override the filename of the uploaded files: diff --git a/app/uploaders/project_image_uploader.rb b/app/uploaders/project_image_uploader.rb index 7a5f9e10c..30eac1a35 100644 --- a/app/uploaders/project_image_uploader.rb +++ b/app/uploaders/project_image_uploader.rb @@ -59,4 +59,9 @@ class ProjectImageUploader < CarrierWave::Uploader::Base def filename "#{model.class.to_s.underscore}.#{file.extension}" if original_filename end + + # return an array like [width, height] + def dimensions + ::MiniMagick::Image.open(file.file)[:dimensions] + end end diff --git a/app/validators/file_mime_type_validator.rb b/app/validators/file_mime_type_validator.rb new file mode 100644 index 000000000..43c384521 --- /dev/null +++ b/app/validators/file_mime_type_validator.rb @@ -0,0 +1,81 @@ +# Based on: https://gist.github.com/1298417 + +class FileMimeTypeValidator < ActiveModel::EachValidator + MESSAGES = { :content_type => :wrong_content_type }.freeze + CHECKS = [ :content_type ].freeze + + DEFAULT_TOKENIZER = lambda { |value| value.split(//) } + RESERVED_OPTIONS = [:content_type, :tokenizer] + + def initialize(options) + super + + end + + def check_validity! + keys = CHECKS & options.keys + + if keys.empty? + raise ArgumentError, 'Patterns unspecified. Specify the :content_type option.' + end + + keys.each do |key| + value = options[key] + + unless valid_content_type_option?(value) + raise ArgumentError, ":#{key} must be a String or a Regexp or an Array" + end + + if key.is_a?(Array) && key == :content_type + options[key].each do |val| + raise ArgumentError, "#{val} must be a String or a Regexp" unless val.is_a?(String) || val.is_a?(Regexp) + end + end + end + end + + def validate_each(record, attribute, value) + raise(ArgumentError, 'A CarrierWave::Uploader::Base object was expected') unless value.kind_of? CarrierWave::Uploader::Base + value = (options[:tokenizer] || DEFAULT_TOKENIZER).call(value) if value.kind_of?(String) + return if value.length == 0 + + CHECKS.each do |key| + next unless check_value = options[key] + do_validation(value, check_value, key, record, attribute) if key == :content_type + end + end + + def help + Helper.instance + end + + class Helper + include Singleton + include ActionView::Helpers::NumberHelper + end + + private + + def valid_content_type_option?(content_type) + return true if %w{Array String Regexp}.include?(content_type.class.to_s) + false + end + + def do_validation(value, pattern, key, record, attribute) + if pattern.is_a?(String) || pattern.is_a?(Regexp) + return if value.file.content_type.send((pattern.is_a?(String) ? '==' : '=~' ), pattern) + else + valid_list = pattern.map do |p| + value.file.content_type.send((p.is_a?(String) ? '==' : '=~' ), p) + end + return if valid_list.include?(true) + end + + errors_options = options.except(*RESERVED_OPTIONS) + + default_message = options[MESSAGES[key]] + errors_options[:message] ||= default_message if default_message + + record.errors.add(attribute, MESSAGES[key], errors_options) + end +end \ No newline at end of file diff --git a/app/views/api/age_ranges/index.json.jbuilder b/app/views/api/age_ranges/index.json.jbuilder new file mode 100644 index 000000000..548c78dd1 --- /dev/null +++ b/app/views/api/age_ranges/index.json.jbuilder @@ -0,0 +1,6 @@ +user_is_admin = (current_user and current_user.is_admin?) + +json.array!(@age_ranges) do |ar| + json.extract! ar, :id, :name + json.related_to ar.events.count if user_is_admin +end diff --git a/app/views/api/age_ranges/show.json.jbuilder b/app/views/api/age_ranges/show.json.jbuilder new file mode 100644 index 000000000..5f77a2b46 --- /dev/null +++ b/app/views/api/age_ranges/show.json.jbuilder @@ -0,0 +1 @@ +json.extract! @age_range, :id, :name \ No newline at end of file diff --git a/app/views/api/auth_providers/_auth_provider.json.jbuilder b/app/views/api/auth_providers/_auth_provider.json.jbuilder index 6ff652c0b..70eb85290 100644 --- a/app/views/api/auth_providers/_auth_provider.json.jbuilder +++ b/app/views/api/auth_providers/_auth_provider.json.jbuilder @@ -1 +1 @@ -json.extract! auth_provider, :id, :name, :status, :providable_type \ No newline at end of file +json.extract! auth_provider, :id, :name, :status, :providable_type, :strategy_name \ No newline at end of file diff --git a/app/views/api/auth_providers/mapping_fields.json.jbuilder b/app/views/api/auth_providers/mapping_fields.json.jbuilder index 7416aa3cc..7eb9615bf 100644 --- a/app/views/api/auth_providers/mapping_fields.json.jbuilder +++ b/app/views/api/auth_providers/mapping_fields.json.jbuilder @@ -1,9 +1,4 @@ -# we protect some fields are they are designed to be managed by the system and must not be updated externally +json.user User.mapping -json.user User.column_names - %w(id encrypted_password reset_password_token reset_password_sent_at remember_created_at -sign_in_count current_sign_in_at last_sign_in_at current_sign_in_ip last_sign_in_ip confirmation_token confirmed_at -confirmation_sent_at unconfirmed_email failed_attempts unlock_token locked_at created_at updated_at stp_customer_id slug -provider auth_token merged_at) - -json.profile Profile.column_names - %w(id user_id created_at updated_at) + %w(avatar) \ No newline at end of file +json.profile Profile.mapping \ No newline at end of file diff --git a/app/views/api/auth_providers/show.json.jbuilder b/app/views/api/auth_providers/show.json.jbuilder index 665f76237..7bb61df29 100644 --- a/app/views/api/auth_providers/show.json.jbuilder +++ b/app/views/api/auth_providers/show.json.jbuilder @@ -6,7 +6,7 @@ if @provider.providable_type == OAuth2Provider.name json.providable_attributes do json.extract! @provider.providable, :id, :base_url, :token_endpoint, :authorization_endpoint, :profile_url, :client_id, :client_secret json.o_auth2_mappings_attributes @provider.providable.o_auth2_mappings do |m| - json.extract! m, :id, :local_model, :local_field, :api_field, :api_endpoint, :api_data_type + json.extract! m, :id, :local_model, :local_field, :api_field, :api_endpoint, :api_data_type, :transformation end end end \ No newline at end of file diff --git a/app/views/api/availabilities/index.json.jbuilder b/app/views/api/availabilities/index.json.jbuilder index eaeda36e6..606f92dee 100644 --- a/app/views/api/availabilities/index.json.jbuilder +++ b/app/views/api/availabilities/index.json.jbuilder @@ -7,6 +7,10 @@ json.array!(@availabilities) do |availability| json.machine_ids availability.machine_ids json.training_ids availability.training_ids json.backgroundColor 'white' - json.borderColor availability.available_type == 'machines' ? '#e4cd78' : '#bd7ae9' + json.borderColor availability_border_color(availability) json.tag_ids availability.tag_ids + json.tags availability.tags do |t| + json.id t.id + json.name t.name + end end diff --git a/app/views/api/availabilities/machine.json.jbuilder b/app/views/api/availabilities/machine.json.jbuilder index c068a649d..255b9153b 100644 --- a/app/views/api/availabilities/machine.json.jbuilder +++ b/app/views/api/availabilities/machine.json.jbuilder @@ -6,7 +6,7 @@ json.array!(@slots) do |slot| json.end slot.end_at.iso8601 json.is_reserved slot.is_reserved json.backgroundColor 'white' - json.borderColor slot.is_reserved ? (slot.is_reserved_by_current_user ? '#b2e774' : '#1d98ec') : '#e4cd78' + json.borderColor machines_slot_border_color(slot) json.availability_id slot.availability_id json.machine do diff --git a/app/views/api/availabilities/public.json.jbuilder b/app/views/api/availabilities/public.json.jbuilder new file mode 100644 index 000000000..89e47cad8 --- /dev/null +++ b/app/views/api/availabilities/public.json.jbuilder @@ -0,0 +1,48 @@ +json.array!(@availabilities) do |availability| + json.id availability.id + json.start availability.start_at.iso8601 + json.end availability.end_at.iso8601 + json.textColor 'black' + json.backgroundColor 'white' + # availability object + if availability.try(:available_type) + json.title availability.title(@title_filter) + if availability.available_type == 'event' + json.event_id availability.event.id + end + if availability.available_type == 'training' + json.training_id availability.trainings.first.id + end + json.available_type availability.available_type + json.tag_ids availability.tag_ids + json.tags availability.tags do |t| + json.id t.id + json.name t.name + end + + if availability.available_type != 'machines' + json.borderColor trainings_events_border_color(availability) + if availability.is_reserved + json.is_reserved true + json.title "#{availability.title}' - #{t('trainings.i_ve_reserved')}" + elsif availability.is_completed + json.is_completed true + json.title "#{availability.title} - #{t('trainings.completed')}" + end + else + json.borderColor availability_border_color(availability) + end + + # machine slot object ( here => availability = slot ) + else + json.title availability.title + json.machine_id availability.machine.id + json.borderColor machines_slot_border_color(availability) + json.tag_ids availability.availability.tag_ids + json.tags availability.availability.tags do |t| + json.id t.id + json.name t.name + end + json.is_reserved availability.is_reserved + end +end diff --git a/app/views/api/availabilities/show.json.jbuilder b/app/views/api/availabilities/show.json.jbuilder index 279d43a37..5c97a67b4 100644 --- a/app/views/api/availabilities/show.json.jbuilder +++ b/app/views/api/availabilities/show.json.jbuilder @@ -4,7 +4,7 @@ json.end_at @availability.end_at.iso8601 json.available_type @availability.available_type json.machine_ids @availability.machine_ids json.backgroundColor 'white' -json.borderColor @availability.available_type == 'machines' ? '#e4cd78' : '#bd7ae9' +json.borderColor availability_border_color(@availability) json.title @availability.title json.tag_ids @availability.tag_ids json.tags @availability.tags do |t| diff --git a/app/views/api/availabilities/trainings.json.jbuilder b/app/views/api/availabilities/trainings.json.jbuilder index 1cbee8169..3a23ac7d2 100644 --- a/app/views/api/availabilities/trainings.json.jbuilder +++ b/app/views/api/availabilities/trainings.json.jbuilder @@ -4,15 +4,13 @@ json.array!(@availabilities) do |a| if a.is_reserved json.is_reserved true json.title "#{a.trainings[0].name}' - #{t('trainings.i_ve_reserved')}" - json.borderColor '#b2e774' elsif a.is_completed json.is_completed true json.title "#{a.trainings[0].name} - #{t('trainings.completed')}" - json.borderColor '#eeeeee' else json.title a.trainings[0].name - json.borderColor '#bd7ae9' end + json.borderColor trainings_events_border_color(a) json.start a.start_at.iso8601 json.end a.end_at.iso8601 json.backgroundColor 'white' diff --git a/app/views/api/categories/index.json.jbuilder b/app/views/api/categories/index.json.jbuilder index 45039014d..e61789fd4 100644 --- a/app/views/api/categories/index.json.jbuilder +++ b/app/views/api/categories/index.json.jbuilder @@ -1,3 +1,6 @@ +user_is_admin = (current_user and current_user.is_admin?) + json.array!(@categories) do |category| json.extract! category, :id, :name + json.related_to category.events.count if user_is_admin end diff --git a/app/views/api/categories/show.json.jbuilder b/app/views/api/categories/show.json.jbuilder new file mode 100644 index 000000000..0501fe17e --- /dev/null +++ b/app/views/api/categories/show.json.jbuilder @@ -0,0 +1 @@ +json.extract! @category, :id, :name \ No newline at end of file diff --git a/app/views/api/coupons/_coupon.json.jbuilder b/app/views/api/coupons/_coupon.json.jbuilder new file mode 100644 index 000000000..52f84198c --- /dev/null +++ b/app/views/api/coupons/_coupon.json.jbuilder @@ -0,0 +1,3 @@ +json.extract! coupon, :id, :name, :code, :percent_off, :valid_until, :validity_per_user, :max_usages, :active, :created_at +json.usages coupon.invoices.count +json.status coupon.status \ No newline at end of file diff --git a/app/views/api/coupons/index.json.jbuilder b/app/views/api/coupons/index.json.jbuilder new file mode 100644 index 000000000..e2c1763b5 --- /dev/null +++ b/app/views/api/coupons/index.json.jbuilder @@ -0,0 +1,3 @@ +json.array!(@coupons) do |coupon| + json.partial! 'api/coupons/coupon', coupon: coupon +end diff --git a/app/views/api/coupons/show.json.jbuilder b/app/views/api/coupons/show.json.jbuilder new file mode 100644 index 000000000..902f63a97 --- /dev/null +++ b/app/views/api/coupons/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'api/coupons/coupon', coupon: @coupon \ No newline at end of file diff --git a/app/views/api/coupons/validate.json.jbuilder b/app/views/api/coupons/validate.json.jbuilder new file mode 100644 index 000000000..5ed96af7a --- /dev/null +++ b/app/views/api/coupons/validate.json.jbuilder @@ -0,0 +1 @@ +json.extract! @coupon, :id, :code, :percent_off \ No newline at end of file diff --git a/app/views/api/event_themes/index.json.jbuilder b/app/views/api/event_themes/index.json.jbuilder new file mode 100644 index 000000000..ab115d91b --- /dev/null +++ b/app/views/api/event_themes/index.json.jbuilder @@ -0,0 +1,6 @@ +user_is_admin = (current_user and current_user.is_admin?) + +json.array!(@event_themes) do |theme| + json.extract! theme, :id, :name + json.related_to theme.events.count if user_is_admin +end diff --git a/app/views/api/event_themes/show.json.jbuilder b/app/views/api/event_themes/show.json.jbuilder new file mode 100644 index 000000000..138759e87 --- /dev/null +++ b/app/views/api/event_themes/show.json.jbuilder @@ -0,0 +1 @@ +json.extract! @event_theme, :id, :name \ No newline at end of file diff --git a/app/views/api/events/_event.json.jbuilder b/app/views/api/events/_event.json.jbuilder index e630941b6..d3f0499ee 100644 --- a/app/views/api/events/_event.json.jbuilder +++ b/app/views/api/events/_event.json.jbuilder @@ -1,15 +1,23 @@ -json.extract! event, :id, :title, :description +json.extract! event, :id, :title, :description, :age_range_id json.event_image event.event_image.attachment_url if event.event_image json.event_files_attributes event.event_files do |f| json.id f.id json.attachment f.attachment_identifier json.attachment_url f.attachment_url end -json.category_ids event.category_ids -json.categories event.categories do |c| - json.id c.id - json.name c.name +json.category_id event.category_id +json.category do + json.id event.category.id + json.name event.category.name +end if event.category +json.event_theme_ids event.event_theme_ids +json.event_themes event.event_themes do |e| + json.name e.name end +json.age_range_id event.age_range_id +json.age_range do + json.name event.age_range.name +end if event.age_range json.start_date event.availability.start_at json.start_time event.availability.start_at json.end_date event.availability.end_at @@ -25,6 +33,13 @@ json.availability do end json.availability_id event.availability_id json.amount (event.amount / 100.0) if event.amount -json.reduced_amount (event.reduced_amount / 100.0) if event.reduced_amount +json.prices event.event_price_categories do |p_cat| + json.id p_cat.id + json.amount (p_cat.amount / 100.0) + json.category do + json.extract! p_cat.price_category, :id, :name + end +end json.nb_total_places event.nb_total_places json.nb_free_places event.nb_free_places || event.nb_total_places + diff --git a/app/views/api/events/index.json.jbuilder b/app/views/api/events/index.json.jbuilder index c95bf00a7..83f10f52a 100644 --- a/app/views/api/events/index.json.jbuilder +++ b/app/views/api/events/index.json.jbuilder @@ -1,8 +1,10 @@ +total = @events.except(:offset, :limit, :order).count + json.cache! [@events, @page] do json.array!(@events) do |event| json.partial! 'api/events/event', event: event json.event_image_small event.event_image.attachment.small.url if event.event_image json.url event_url(event, format: :json) - json.nb_total_events @total + json.nb_total_events total end end diff --git a/app/views/api/invoices/list.json.jbuilder b/app/views/api/invoices/list.json.jbuilder index 4604e4604..97795c6ad 100644 --- a/app/views/api/invoices/list.json.jbuilder +++ b/app/views/api/invoices/list.json.jbuilder @@ -1,7 +1,7 @@ -maxInvoices = @invoices.except(:offset, :limit, :order).count +max_invoices = @invoices.except(:offset, :limit, :order).count json.array!(@invoices) do |invoice| - json.maxInvoices maxInvoices + json.maxInvoices max_invoices json.extract! invoice, :id, :created_at, :reference, :invoiced_type, :user_id, :avoir_date json.total (invoice.total / 100.00) json.url invoice_url(invoice, format: :json) diff --git a/app/views/api/machines/index.json.jbuilder b/app/views/api/machines/index.json.jbuilder index c45e7965e..5f642ebe2 100644 --- a/app/views/api/machines/index.json.jbuilder +++ b/app/views/api/machines/index.json.jbuilder @@ -1,7 +1,5 @@ -json.cache! @machines do - json.array!(@machines) do |machine| - json.extract! machine, :id, :name, :description, :spec, :slug - json.url machine_url(machine, format: :json) - json.machine_image machine.machine_image.attachment.medium.url if machine.machine_image - end +json.array!(@machines) do |machine| + json.extract! machine, :id, :name, :description, :spec, :slug + json.url machine_url(machine, format: :json) + json.machine_image machine.machine_image.attachment.medium.url if machine.machine_image end diff --git a/app/views/api/members/export_members.xls.erb b/app/views/api/members/export_members.xls.erb deleted file mode 100644 index 3f1735f9f..000000000 --- a/app/views/api/members/export_members.xls.erb +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - <% @datas.each do |data| %> - - - - - - - - - - - - - - <% end %> -
<%=t('export_members.id')%><%=t('export_members.surname')%><%=t('export_members.first_name')%><%=t('export_members.email')%><%=t('export_members.gender')%><%=t('export_members.age')%><%=t('export_members.phone')%><%=t('export_members.group')%><%=t('export_members.subscription')%><%=t('export_members.subscription_end_date')%><%=t('export_members.validated_trainings')%>
<%= data.id %><%= data.profile.last_name %><%= data.profile.first_name %><%= data.email %><%= data.profile.gender ? t('export_members.man') : t('export_members.woman') %><%= data.profile.age %><%= data.profile.phone %><%= data.group.name %><%= (data.subscription and data.subscription.expired_at > Time.now) ? data.subscription.plan.name : t('export_members.without_subscriptions') %><%= (data.subscription and data.subscription.expired_at > Time.now) ? data.subscription.expired_at : '' %><%= raw data.trainings.map(&:name).join(' ') %>
diff --git a/app/views/api/members/export_reservations.xls.erb b/app/views/api/members/export_reservations.xls.erb deleted file mode 100644 index 7b117c592..000000000 --- a/app/views/api/members/export_reservations.xls.erb +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - <% @datas.each do |d| %> - - - - - - - - - - - <% end %> -
<%=t('export_reservations.customer_id')%><%=t('export_reservations.customer')%><%=t('export_reservations.email')%><%=t('export_reservations.reservation_date')%><%=t('export_reservations.reservation_type')%><%=t('export_reservations.reservation_object')%><%=t('export_reservations.slots_number_hours_tickets')%>
<%= d.user.id %><%= d.user.profile.full_name %><%= d.user.email %><%= d.created_at %><%= d.reservable_type %><%= d.reservable.name if !d.reservable.nil? %><%= d.slots.count %><%= (d.stp_invoice_id.nil?)? t('export_reservations.local_payment') : t('export_reservations.online_payment') %>
\ No newline at end of file diff --git a/app/views/api/members/export_subscriptions.xls.erb b/app/views/api/members/export_subscriptions.xls.erb deleted file mode 100644 index 64805d1fb..000000000 --- a/app/views/api/members/export_subscriptions.xls.erb +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - <% @datas.each do |data| %> - - - - - - - - - - - - <% end %> -
<%=t('export_subscriptions.id')%><%=t('export_subscriptions.customer')%><%=t('export_subscriptions.email')%><%=t('export_subscriptions.subscription')%><%=t('export_subscriptions.period')%><%=t('export_subscriptions.start_date')%><%=t('export_subscriptions.expiration_date')%><%=t('export_subscriptions.amount')%><%=t('export_subscriptions.payment_method')%>
<%= data.user.id %><%= data.user.profile.full_name %><%= data.user.email %><%= data.plan.human_readable_name(group: true) %><%= t("duration.#{data.plan.interval}", count: data.plan.interval_count) %><%= data.created_at %><%= data.expired_at %><%= number_to_currency(data.plan.amount / 100) %><%= (data.stp_subscription_id.nil?)? t('export_subscriptions.local_payment') : t('export_subscriptions.online_payment') %>
diff --git a/app/views/api/members/index.json.jbuilder b/app/views/api/members/index.json.jbuilder index 89167ffe2..1cedb2a80 100644 --- a/app/views/api/members/index.json.jbuilder +++ b/app/views/api/members/index.json.jbuilder @@ -1,8 +1,8 @@ user_is_admin = (current_user and current_user.is_admin?) -maxMembers = @query.except(:offset, :limit, :order).count +max_members = @query.except(:offset, :limit, :order).count json.array!(@members) do |member| - json.maxMembers maxMembers + json.maxMembers max_members json.id member.id json.username member.username json.slug member.slug @@ -23,6 +23,7 @@ json.array!(@members) do |member| json.birthday member.profile.birthday.iso8601 if member.profile.birthday end end if attribute_requested?(@requested_attributes, 'profile') + json.need_completion member.need_completion? json.group_id member.group_id json.group do json.id member.group.id diff --git a/app/views/api/members/list.json.jbuilder b/app/views/api/members/list.json.jbuilder index 15ab9a462..7fde599a3 100644 --- a/app/views/api/members/list.json.jbuilder +++ b/app/views/api/members/list.json.jbuilder @@ -1,7 +1,7 @@ -maxMembers = @query.except(:offset, :limit, :order).count +max_members = @query.except(:offset, :limit, :order).count json.array!(@members) do |member| - json.maxMembers maxMembers + json.maxMembers max_members json.id member.id json.email member.email if current_user json.profile do @@ -9,6 +9,7 @@ json.array!(@members) do |member| json.last_name member.profile.last_name json.phone member.profile.phone end + json.need_completion member.need_completion? json.group do json.name member.group.name end diff --git a/app/views/api/members/search.json.jbuilder b/app/views/api/members/search.json.jbuilder index cd63c3d25..c2ef52edc 100644 --- a/app/views/api/members/search.json.jbuilder +++ b/app/views/api/members/search.json.jbuilder @@ -2,4 +2,5 @@ json.array!(@members) do |member| json.id member.id json.name "#{member.profile.first_name} #{member.profile.last_name}" json.group_id member.group_id + json.need_completion member.need_completion? end \ No newline at end of file diff --git a/app/views/api/members/show.json.jbuilder b/app/views/api/members/show.json.jbuilder index bdc715b02..ece8b562b 100644 --- a/app/views/api/members/show.json.jbuilder +++ b/app/views/api/members/show.json.jbuilder @@ -1,4 +1,6 @@ -json.extract! @member, :id, :uid, :username, :email, :group_id, :slug, :invoicing_disabled, :is_allow_contact +requested_current = (current_user and current_user.id == @member.id) + +json.extract! @member, :id, :uid, :username, :email, :group_id, :slug, :invoicing_disabled, :is_allow_contact, :is_allow_newsletter json.role @member.roles.first.name json.name @member.profile.full_name json.need_completion @member.need_completion? @@ -22,6 +24,15 @@ json.profile do json.website @member.profile.website json.job @member.profile.job json.extract! @member.profile, :facebook, :twitter, :google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo, :dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr + json.organization do + json.id @member.profile.organization.id + json.name @member.profile.organization.name + json.address do + json.id @member.profile.organization.address.id + json.address @member.profile.organization.address.address + end + end if @member.profile.organization + end json.subscribed_plan do json.partial! 'api/shared/plan', plan: @member.subscribed_plan @@ -62,33 +73,35 @@ json.machine_credits @member.machine_credits do |mc| end json.last_sign_in_at @member.last_sign_in_at.iso8601 if @member.last_sign_in_at json.all_projects @member.all_projects do |project| - json.extract! project, :id, :name, :description, :author_id, :licence_id, :slug - json.url project_url(project, format: :json) - json.project_image project.project_image.attachment.large.url if project.project_image - json.machine_ids project.machine_ids - json.machines project.machines do |m| - json.id m.id - json.name m.name - end - json.author_id project.author_id - json.user_ids project.user_ids - json.component_ids project.component_ids - json.components project.components do |c| - json.id c.id - json.name c.name - end - json.project_users project.project_users do |pu| - json.id pu.user.id - json.first_name pu.user.profile.first_name - json.last_name pu.user.profile.last_name - json.full_name pu.user.profile.full_name - json.user_avatar do - json.id pu.user.profile.user_avatar.id - json.attachment_url pu.user.profile.user_avatar.attachment_url - end if pu.user.profile.user_avatar - json.username pu.user.username - json.is_valid pu.is_valid - json.valid_token pu.valid_token if !pu.is_valid and @member == pu.user + if requested_current || project.state == 'published' + json.extract! project, :id, :name, :description, :author_id, :licence_id, :slug, :state + json.url project_url(project, format: :json) + json.project_image project.project_image.attachment.large.url if project.project_image + json.machine_ids project.machine_ids + json.machines project.machines do |m| + json.id m.id + json.name m.name + end + json.author_id project.author_id + json.user_ids project.user_ids + json.component_ids project.component_ids + json.components project.components do |c| + json.id c.id + json.name c.name + end + json.project_users project.project_users do |pu| + json.id pu.user.id + json.first_name pu.user.profile.first_name + json.last_name pu.user.profile.last_name + json.full_name pu.user.profile.full_name + json.user_avatar do + json.id pu.user.profile.user_avatar.id + json.attachment_url pu.user.profile.user_avatar.attachment_url + end if pu.user.profile.user_avatar + json.username pu.user.username + json.is_valid pu.is_valid + json.valid_token pu.valid_token if !pu.is_valid and @member == pu.user + end end end json.events_reservations @member.reservations.where(reservable_type: 'Event').joins(:slots).order('slots.start_at asc') do |r| @@ -96,7 +109,12 @@ json.events_reservations @member.reservations.where(reservable_type: 'Event').jo json.start_at r.slots.first.start_at json.end_at r.slots.first.end_at json.nb_reserve_places r.nb_reserve_places - json.nb_reserve_reduced_places r.nb_reserve_reduced_places + json.tickets r.tickets do |t| + json.booked t.booked + json.price_category do + json.name t.event_price_category.price_category.name + end + end json.reservable r.reservable end json.invoices @member.invoices.order('reference DESC') do |i| diff --git a/app/views/api/notifications/_notify_admin_export_complete.json.jbuilder b/app/views/api/notifications/_notify_admin_export_complete.json.jbuilder new file mode 100644 index 000000000..02f24df29 --- /dev/null +++ b/app/views/api/notifications/_notify_admin_export_complete.json.jbuilder @@ -0,0 +1,6 @@ +json.title notification.notification_type +json.description t('.export')+' '+ + t(".#{notification.attached_object.category}_#{notification.attached_object.export_type}")+' '+ + t('.is_over')+' '+ + link_to( t('.download_here'), "api/exports/#{notification.attached_object.id}/download" )+'.' +json.url notification_url(notification, format: :json) \ No newline at end of file diff --git a/app/views/api/notifications/_notify_admin_user_wallet_is_credited.json.jbuilder b/app/views/api/notifications/_notify_admin_user_wallet_is_credited.json.jbuilder new file mode 100644 index 000000000..6b692d542 --- /dev/null +++ b/app/views/api/notifications/_notify_admin_user_wallet_is_credited.json.jbuilder @@ -0,0 +1,7 @@ +json.title notification.notification_type +amount = notification.attached_object.amount +json.description t('.wallet_is_credited', + AMOUNT: number_to_currency(amount), + USER: notification.attached_object.wallet.user.profile.full_name, + ADMIN: notification.attached_object.user.profile.full_name) +json.url notification_url(notification, format: :json) diff --git a/app/views/api/notifications/_notify_admin_when_project_published.json.jbuilder b/app/views/api/notifications/_notify_admin_when_project_published.json.jbuilder index e90c6daf0..63bee71e1 100644 --- a/app/views/api/notifications/_notify_admin_when_project_published.json.jbuilder +++ b/app/views/api/notifications/_notify_admin_when_project_published.json.jbuilder @@ -1,5 +1,5 @@ json.title notification.notification_type json.description t('.project_NAME_has_been_published_html', - ID: notification.attached_object.id, + ID: notification.attached_object.slug, NAME: notification.attached_object.name) json.url notification_url(notification, format: :json) diff --git a/app/views/api/notifications/_notify_member_about_coupon.json.jbuilder b/app/views/api/notifications/_notify_member_about_coupon.json.jbuilder new file mode 100644 index 000000000..674a0dd53 --- /dev/null +++ b/app/views/api/notifications/_notify_member_about_coupon.json.jbuilder @@ -0,0 +1,5 @@ +json.title notification.notification_type +json.description t('.enjoy_a_discount_of_PERCENT_with_code_CODE', + PERCENT: notification.attached_object.percent_off, + CODE: notification.attached_object.code) +json.url notification_url(notification, format: :json) diff --git a/app/views/api/notifications/_notify_member_reservation_reminder.json.jbuilder b/app/views/api/notifications/_notify_member_reservation_reminder.json.jbuilder new file mode 100644 index 000000000..dffc4e533 --- /dev/null +++ b/app/views/api/notifications/_notify_member_reservation_reminder.json.jbuilder @@ -0,0 +1,5 @@ +json.title notification.notification_type +json.description t('.reminder_you_have_a_reservation_RESERVABLE_to_be_held_on_DATE_html', + RESERVABLE: notification.attached_object.reservable.name, + DATE: I18n.l(notification.attached_object.slots.order(:start_at).first.start_at, format: :long)) +json.url notification_url(notification, format: :json) diff --git a/app/views/api/notifications/_notify_project_author_when_collaborator_valid.json.jbuilder b/app/views/api/notifications/_notify_project_author_when_collaborator_valid.json.jbuilder index 58209d776..d5dc45864 100644 --- a/app/views/api/notifications/_notify_project_author_when_collaborator_valid.json.jbuilder +++ b/app/views/api/notifications/_notify_project_author_when_collaborator_valid.json.jbuilder @@ -1,5 +1,5 @@ json.title notification.notification_type json.description t('.USER_became_collaborator_of_your_project', USER: notification.attached_object.user.profile.full_name) + - " #{notification.attached_object.project.name}." + " #{notification.attached_object.project.name}." json.url notification_url(notification, format: :json) diff --git a/app/views/api/notifications/_notify_project_collaborator_to_valid.json.jbuilder b/app/views/api/notifications/_notify_project_collaborator_to_valid.json.jbuilder index f656f668d..2615f16c6 100644 --- a/app/views/api/notifications/_notify_project_collaborator_to_valid.json.jbuilder +++ b/app/views/api/notifications/_notify_project_collaborator_to_valid.json.jbuilder @@ -1,4 +1,4 @@ json.title notification.notification_type json.description t('.you_are_invited_to_collaborate_on_the_project') + - "#{notification.attached_object.project.name}." + "#{notification.attached_object.project.name}." json.url notification_url(notification, format: :json) diff --git a/app/views/api/notifications/_notify_user_wallet_is_credited.json.jbuilder b/app/views/api/notifications/_notify_user_wallet_is_credited.json.jbuilder new file mode 100644 index 000000000..836492cf5 --- /dev/null +++ b/app/views/api/notifications/_notify_user_wallet_is_credited.json.jbuilder @@ -0,0 +1,5 @@ +json.title notification.notification_type +amount = notification.attached_object.amount +json.description t('.your_wallet_is_credited', + AMOUNT: number_to_currency(amount)) +json.url notification_url(notification, format: :json) diff --git a/app/views/api/price_categories/index.json.jbuilder b/app/views/api/price_categories/index.json.jbuilder new file mode 100644 index 000000000..80def7181 --- /dev/null +++ b/app/views/api/price_categories/index.json.jbuilder @@ -0,0 +1,6 @@ +user_is_admin = (current_user and current_user.is_admin?) + +json.array!(@price_categories) do |category| + json.extract! category, :id, :name, :conditions + json.events category.event_price_category.count if user_is_admin +end diff --git a/app/views/api/price_categories/show.json.jbuilder b/app/views/api/price_categories/show.json.jbuilder new file mode 100644 index 000000000..8e38da3bf --- /dev/null +++ b/app/views/api/price_categories/show.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! @price_category, :id, :name, :conditions, :created_at +json.events @price_category.event_price_category.count \ No newline at end of file diff --git a/app/views/api/projects/index.json.jbuilder b/app/views/api/projects/index.json.jbuilder index 6f5e5e249..d9bfd00ed 100644 --- a/app/views/api/projects/index.json.jbuilder +++ b/app/views/api/projects/index.json.jbuilder @@ -1,5 +1,5 @@ json.projects @projects do |project| - json.extract! project, :id, :name, :description, :author_id, :licence_id, :slug + json.extract! project, :id, :name, :description, :author_id, :licence_id, :slug, :state json.url project_url(project, format: :json) json.project_image project.project_image.attachment.medium.url if project.project_image json.author_id project.author_id diff --git a/app/views/api/projects/show.json.jbuilder b/app/views/api/projects/show.json.jbuilder index 495a9c42c..3e9cb1d83 100644 --- a/app/views/api/projects/show.json.jbuilder +++ b/app/views/api/projects/show.json.jbuilder @@ -1,5 +1,6 @@ json.extract! @project, :id, :name, :description, :tags, :created_at, :updated_at, :author_id, :licence_id, :slug json.project_image @project.project_image.attachment.large.url if @project.project_image +json.project_full_image @project.project_image.attachment.url if @project.project_image json.author do json.id @project.author_id json.first_name @project.author.profile.first_name @@ -46,12 +47,14 @@ json.project_users @project.project_users do |pu| json.slug pu.user.slug json.is_valid pu.is_valid end -json.project_steps_attributes @project.project_steps.order('project_steps.created_at ASC') do |s| +json.project_steps_attributes @project.project_steps.order('project_steps.step_nb ASC') do |s| json.id s.id json.description s.description json.title s.title json.project_step_image s.project_step_image.attachment_identifier if s.project_step_image json.project_step_image_url s.project_step_image.attachment.medium.url if s.project_step_image + json.project_step_full_image_url s.project_step_image.attachment.url if s.project_step_image + json.step_nb s.step_nb end json.state @project.state json.licence do diff --git a/app/views/api/reservations/_reservation.json.jbuilder b/app/views/api/reservations/_reservation.json.jbuilder index fc724eb87..fce8ca460 100644 --- a/app/views/api/reservations/_reservation.json.jbuilder +++ b/app/views/api/reservations/_reservation.json.jbuilder @@ -8,7 +8,16 @@ json.slots reservation.slots do |s| json.end_at s.end_at.iso8601 end json.nb_reserve_places reservation.nb_reserve_places -json.nb_reserve_reduced_places reservation.nb_reserve_reduced_places +json.tickets reservation.tickets do |t| + json.extract! t, :booked, :created_at + json.event_price_category do + json.extract! t.event_price_category, :id, :price_category_id + json.price_category do + json.extract! t.event_price_category.price_category, :id, :name + end + end +end +json.total_booked_seats reservation.total_booked_seats json.created_at reservation.created_at.iso8601 json.reservable_id reservation.reservable_id json.reservable_type reservation.reservable_type diff --git a/app/views/api/reservations/show.json.jbuilder b/app/views/api/reservations/show.json.jbuilder index e3c9c832d..cf62a5a88 100644 --- a/app/views/api/reservations/show.json.jbuilder +++ b/app/views/api/reservations/show.json.jbuilder @@ -25,5 +25,14 @@ json.reservable do json.name @reservation.reservable.name end json.nb_reserve_places @reservation.nb_reserve_places -json.nb_reserve_reduced_places @reservation.nb_reserve_reduced_places +json.tickets @reservation.tickets do |t| + json.extract! t, :booked, :created_at + json.event_price_category do + json.extract! t.event_price_category, :id, :price_category_id + json.price_category do + json.extract! t.event_price_category.price_category, :id, :name + end + end +end +json.total_booked_seats @reservation.total_booked_seats json.created_at @reservation.created_at.iso8601 diff --git a/app/views/api/statistics/index.json.jbuilder b/app/views/api/statistics/index.json.jbuilder index e5f78eedf..d0f638d66 100644 --- a/app/views/api/statistics/index.json.jbuilder +++ b/app/views/api/statistics/index.json.jbuilder @@ -5,6 +5,9 @@ json.array!(@statistics) do |s| end json.types s.statistic_types do |t| json.extract! t, :id, :key, :label, :graph, :simple + json.custom_aggregations t.statistic_custom_aggregations do |c| + json.extract! c, :id, :field + end json.subtypes t.statistic_sub_types do |st| json.extract! st, :id, :key, :label end diff --git a/app/views/api/trainings/availabilities.json.jbuilder b/app/views/api/trainings/availabilities.json.jbuilder new file mode 100644 index 000000000..e40db7827 --- /dev/null +++ b/app/views/api/trainings/availabilities.json.jbuilder @@ -0,0 +1,11 @@ +json.extract! @training, :id, :name, :description, :machine_ids, :nb_total_places, :public_page +json.availabilities @availabilities do |a| + json.id a.id + json.start_at a.start_at.iso8601 + json.end_at a.end_at.iso8601 + json.reservation_users a.slots.map do |slot| + json.id slot.reservation.user.id + json.full_name slot.reservation.user.profile.full_name + json.is_valid slot.reservation.user.trainings.include?(@training) + end +end diff --git a/app/views/api/trainings/index.json.jbuilder b/app/views/api/trainings/index.json.jbuilder index 02cdd42b4..51550ce1b 100644 --- a/app/views/api/trainings/index.json.jbuilder +++ b/app/views/api/trainings/index.json.jbuilder @@ -2,12 +2,8 @@ role = (current_user and current_user.is_admin?) ? 'admin' : 'user' json.cache! [@trainings, role] do json.array!(@trainings) do |training| - json.id training.id - json.name training.name - json.description training.description - json.machine_ids training.machine_ids - json.nb_total_places training.nb_total_places - + json.extract! training, :id, :name, :description, :machine_ids, :nb_total_places, :slug + json.training_image training.training_image.attachment.large.url if training.training_image json.plan_ids training.plan_ids if role === 'admin' end end diff --git a/app/views/api/trainings/show.json.jbuilder b/app/views/api/trainings/show.json.jbuilder index fbc0587b2..5d69b4223 100644 --- a/app/views/api/trainings/show.json.jbuilder +++ b/app/views/api/trainings/show.json.jbuilder @@ -1,11 +1,2 @@ -json.extract! @training, :id, :name, :machine_ids, :nb_total_places -json.availabilities @training.availabilities do |a| - json.id a.id - json.start_at a.start_at.iso8601 - json.end_at a.end_at.iso8601 - json.reservation_users a.slots.map do |slot| - json.id slot.reservation.user.id - json.full_name slot.reservation.user.profile.full_name - json.is_valid slot.reservation.user.trainings.include?(@training) - end -end +json.extract! @training, :id, :name, :description, :machine_ids, :nb_total_places, :public_page +json.training_image @training.training_image.attachment.large.url if @training.training_image diff --git a/app/views/api/wallet/show.json.jbuilder b/app/views/api/wallet/show.json.jbuilder new file mode 100644 index 000000000..ef0b12454 --- /dev/null +++ b/app/views/api/wallet/show.json.jbuilder @@ -0,0 +1 @@ +json.extract! @wallet, :id, :user_id, :amount diff --git a/app/views/api/wallet/transactions.json.jbuilder b/app/views/api/wallet/transactions.json.jbuilder new file mode 100644 index 000000000..14a484915 --- /dev/null +++ b/app/views/api/wallet/transactions.json.jbuilder @@ -0,0 +1,11 @@ +json.array!(@wallet_transactions) do |t| + json.extract! t, :id, :transaction_type, :created_at, :amount, :transactable_type + json.user do + json.id t.user.id + json.full_name t.user.profile.full_name + end + json.invoice do + json.id t.invoice.id + json.reference t.invoice.reference + end if t.invoice +end diff --git a/app/views/application/index.html.erb b/app/views/application/index.html.erb index 0d1b9d1da..e505d194a 100644 --- a/app/views/application/index.html.erb +++ b/app/views/application/index.html.erb @@ -65,6 +65,9 @@ <% end %> + + + -
Powered by Fab Manager
+
+ + + + Powered by Fab Manager +
<%= javascript_include_tag 'application' %> diff --git a/app/views/exports/statistics_current.xlsx.axlsx b/app/views/exports/statistics_current.xlsx.axlsx new file mode 100644 index 000000000..76c676854 --- /dev/null +++ b/app/views/exports/statistics_current.xlsx.axlsx @@ -0,0 +1,79 @@ +wb = xlsx_package.workbook + +bold = wb.styles.add_style :b => true +header = wb.styles.add_style :b => true, :bg_color => Stylesheet.primary.upcase.gsub('#', 'FF'), :fg_color => 'FFFFFFFF' +date = wb.styles.add_style :format_code => Rails.application.secrets.excel_date_format + +wb.add_worksheet(name: @index.label) do |sheet| + ## heading stats for the current page + sheet.add_row [t('export.entries'), @results['hits']['total']], :style => [bold, nil], :types => [:string, :integer] + if @index.ca + sheet.add_row [t('export.revenue'), @results['aggregations']['total_ca']['value']], :style => [bold, nil], :types => [:string, :float] + end + sheet.add_row [t('export.average_age'), @results['aggregations']['average_age']['value']], :style => [bold, nil], :types => [:string, :float] + unless @type.simple + sheet.add_row ["#{t('export.total')} #{@type.label}", @results['aggregations']['total_stat']['value']], :style => [bold, nil], :types => [:string, :integer] + end + sheet.add_row [] + + ## data table + # heading labels + columns = [t('export.date'), t('export.user'), t('export.email'), t('export.phone'), t('export.gender'), t('export.age'), t('export.type')] + columns.push @type.label unless @type.simple + @fields.each do |f| + columns.push f.label + end + columns.push t('export.revenue') if @index.ca + sheet.add_row columns, :style => header + + # data rows + @results['hits']['hits'].each do |hit| + user = get_item(@users, hit['_source']['userId']) + subtype = get_item(@subtypes, hit['_source']['subType'], 'key') + data = [ + Date::strptime(hit['_source']['date'],'%Y-%m-%d'), + (user ? user.profile.full_name : "ID #{hit['_source']['userId']}"), + (user ? user.email : ''), + (user ? user.profile.phone : ''), + t("export.#{hit['_source']['gender']}"), + hit['_source']['age'], + subtype.nil? ? "" : subtype.label + ] + styles = [date, nil, nil, nil, nil, nil, nil] + types = [:date, :string, :string, :string, :string, :integer, :string] + unless @type.simple + data.push hit['_source']['stat'] + styles.push nil + types.push :string + end + @fields.each do |f| + field_data = hit['_source'][f.key] + case f.data_type + when 'date' + data.push Date::strptime(field_data, '%Y-%m-%d') + styles.push date + types.push :date + when 'list' + data.push field_data.map{|e| e['name'] }.join(', ') + styles.push nil + types.push :string + when 'number' + data.push field_data + styles.push nil + types.push :float + else + data.push field_data + styles.push nil + types.push :string + end + + end + if @index.ca + data.push hit['_source']['ca'] + styles.push nil + types.push :float + end + + sheet.add_row data, :style => styles, :types => types + end +end \ No newline at end of file diff --git a/app/views/exports/statistics_global.xlsx.axlsx b/app/views/exports/statistics_global.xlsx.axlsx new file mode 100644 index 000000000..dd00a277f --- /dev/null +++ b/app/views/exports/statistics_global.xlsx.axlsx @@ -0,0 +1,84 @@ +wb = xlsx_package.workbook + +header = wb.styles.add_style :b => true, :bg_color => Stylesheet.primary.upcase.gsub('#', 'FF'), :fg_color => 'FFFFFFFF' +date = wb.styles.add_style :format_code => Rails.application.secrets.excel_date_format + +@indices.each do |index| + if index.table + index.statistic_types.each do |type| + # see https://msdn.microsoft.com/fr-fr/library/c6bdca6y(v=vs.90).aspx for unauthorized character list + sheet_name = "#{index.label} - #{type.label}".gsub(/[*|\\:"<>?\/]/,'').truncate(31) + wb.add_worksheet(name: sheet_name) do |sheet| + + ## data table + # heading labels + columns = [t('export.date'), t('export.user'), t('export.email'), t('export.phone'), t('export.gender'), t('export.age'), t('export.type')] + columns.push type.label unless type.simple + index.statistic_fields.each do |f| + columns.push f.label + end + columns.push t('export.revenue') if index.ca + sheet.add_row columns, :style => header + + # data rows + @results['hits']['hits'].each do |hit| + # check that the current result is for the given index and type + if hit['_type'] == index.es_type_key and hit['_source']['type'] == type.key + # get matching objects + user = get_item(@users, hit['_source']['userId']) + subtype = get_item(type.statistic_sub_types, hit['_source']['subType'], 'key') + # start to fill data and associated styles and data-types + data = [ + Date::strptime(hit['_source']['date'],'%Y-%m-%d'), + (user ? user.profile.full_name : "ID #{hit['_source']['userId']}"), + (user ? user.email : ''), + (user ? user.profile.phone : ''), + t("export.#{hit['_source']['gender']}"), + hit['_source']['age'], + subtype.nil? ? "" : subtype.label + ] + styles = [date, nil, nil, nil, nil, nil, nil] + types = [:date, :string, :string, :string, :string, :integer, :string] + # do not proceed with the 'stat' field if the type is declared as 'simple' + unless type.simple + data.push hit['_source']['stat'] + styles.push nil + types.push :string + end + # proceed additional fields + index.statistic_fields.each do |f| + field_data = hit['_source'][f.key] + case f.data_type + when 'date' + data.push Date::strptime(field_data, '%Y-%m-%d') + styles.push date + types.push :date + when 'list' + data.push field_data.map{|e| e['name'] }.join(', ') + styles.push nil + types.push :string + when 'number' + data.push field_data + styles.push nil + types.push :float + else + data.push field_data + styles.push nil + types.push :string + end + + end + # proceed teh 'ca' field if requested + if index.ca + data.push hit['_source']['ca'] + styles.push nil + types.push :float + end + # finally, add the data row to the workbook's sheet + sheet.add_row data, :style => styles, :types => types + end + end + end + end + end +end \ No newline at end of file diff --git a/app/views/exports/users_members.xlsx.axlsx b/app/views/exports/users_members.xlsx.axlsx new file mode 100644 index 000000000..3888a9b5d --- /dev/null +++ b/app/views/exports/users_members.xlsx.axlsx @@ -0,0 +1,58 @@ +wb = xlsx_package.workbook + +header = wb.styles.add_style :b => true, :bg_color => Stylesheet.primary.upcase.gsub('#', 'FF'), :fg_color => 'FFFFFFFF' +date = wb.styles.add_style :format_code => Rails.application.secrets.excel_date_format + +wb.add_worksheet(name: t('export_members.members')) do |sheet| + + ## data table + # heading labels + columns = [t('export_members.id'), t('export_members.surname'), t('export_members.first_name'), t('export_members.email'), + t('export_members.newsletter'), t('export_members.gender'), t('export_members.age'), t('export_members.address'), t('export_members.phone'), + t('export_members.website'), t('export_members.job'), t('export_members.interests'), + t('export_members.cad_software_mastered'), t('export_members.group'), t('export_members.subscription'), + t('export_members.subscription_end_date'), t('export_members.validated_trainings'), t('export_members.tags'), + t('export_members.number_of_invoices'), t('export_members.projects'), t('export_members.facebook'), + t('export_members.twitter'), t('export_members.echo_sciences'), + t('export_members.organization'), t('export_members.organization_address')] + sheet.add_row columns, :style => header + + # data rows + @members.each do |member| + data = [ + member.id, member.profile.last_name, member.profile.first_name, + member.email, member.is_allow_newsletter, member.profile.gender ? t('export_members.man') : t('export_members.woman'), member.profile.age, + member.profile.address ? member.profile.address.address : '', member.profile.phone, member.profile.website, + member.profile.job, member.profile.interest, member.profile.software_mastered, member.group.name, + (member.subscription and member.subscription.expired_at > Time.now) ? member.subscription.plan.name : t('export_members.without_subscriptions'), + (member.subscription and member.subscription.expired_at > Time.now) ? member.subscription.expired_at.to_date : nil, + member.trainings.map(&:name).join("\n"), member.tags.map(&:name).join("\n"), member.invoices.size, + member.projects.map(&:name).join("\n"), member.profile.facebook || '', member.profile.twitter || '', + member.profile.echosciences || '', + member.profile.organization ? member.profile.organization.name : '', member.profile.organization ? member.profile.organization.address.address : '' + ] + styles = [nil, nil, nil, + nil, nil, nil, nil, + nil, nil, nil, + nil, nil, nil, nil, + nil, + date, + nil, nil, nil, + nil, nil, nil, + nil, + nil, nil + ] + types = [:integer, :string, :string, + :string, :boolean, :string, :integer, + :string, :string, :string, + :string, :string, :string, :string, + :string, + :date, + :string, :string, :integer, + :string, :string, :string, + :string, + :string, :string] + + sheet.add_row data, :style => styles, :types => types + end +end diff --git a/app/views/exports/users_reservations.xlsx.axlsx b/app/views/exports/users_reservations.xlsx.axlsx new file mode 100644 index 000000000..54bbb0e90 --- /dev/null +++ b/app/views/exports/users_reservations.xlsx.axlsx @@ -0,0 +1,32 @@ +wb = xlsx_package.workbook + +header = wb.styles.add_style :b => true, :bg_color => Stylesheet.primary.upcase.gsub('#', 'FF'), :fg_color => 'FFFFFFFF' +date = wb.styles.add_style :format_code => Rails.application.secrets.excel_date_format + +wb.add_worksheet(name: t('export_reservations.reservations')) do |sheet| + + ## data table + # heading labels + columns = [t('export_reservations.customer_id'), t('export_reservations.customer'), t('export_reservations.email'), + t('export_reservations.reservation_date'), t('export_reservations.reservation_type'), t('export_reservations.reservation_object'), + t('export_reservations.slots_number_hours_tickets'), t('export_reservations.payment_method')] + sheet.add_row columns, :style => header + + # data rows + @reservations.each do |resrv| + data = [ + resrv.user.id, + resrv.user.profile.full_name, + resrv.user.email, + resrv.created_at.to_date, + resrv.reservable_type, + (resrv.reservable.nil? ? '' : resrv.reservable.name), + resrv.slots.count, + (resrv.stp_invoice_id.nil?)? t('export_reservations.local_payment') : t('export_reservations.online_payment') + ] + styles = [nil, nil, nil, date, nil, nil, nil, nil] + types = [:integer, :string, :string, :date, :string, :string, :integer, :string] + + sheet.add_row data, :style => styles, :types => types + end +end \ No newline at end of file diff --git a/app/views/exports/users_subscriptions.xlsx.axlsx b/app/views/exports/users_subscriptions.xlsx.axlsx new file mode 100644 index 000000000..5b1a08d38 --- /dev/null +++ b/app/views/exports/users_subscriptions.xlsx.axlsx @@ -0,0 +1,33 @@ +wb = xlsx_package.workbook + +header = wb.styles.add_style :b => true, :bg_color => Stylesheet.primary.upcase.gsub('#', 'FF'), :fg_color => 'FFFFFFFF' +date = wb.styles.add_style :format_code => Rails.application.secrets.excel_date_format + +wb.add_worksheet(name: t('export_subscriptions.subscriptions')) do |sheet| + + ## data table + # heading labels + columns = [t('export_subscriptions.id'), t('export_subscriptions.customer'), t('export_subscriptions.email'), + t('export_subscriptions.subscription'), t('export_subscriptions.period'), t('export_subscriptions.start_date'), + t('export_subscriptions.expiration_date'), t('export_subscriptions.amount'), t('export_subscriptions.payment_method')] + sheet.add_row columns, :style => header + + # data rows + @subscriptions.each do |sub| + data = [ + sub.user.id, + sub.user.profile.full_name, + sub.user.email, + sub.plan.human_readable_name(group: true), + t("duration.#{sub.plan.interval}", count: sub.plan.interval_count), + sub.created_at.to_date, + sub.expired_at.to_date, + number_to_currency(sub.plan.amount / 100), + (sub.stp_subscription_id.nil?)? t('export_subscriptions.local_payment') : t('export_subscriptions.online_payment') + ] + styles = [nil, nil, nil, nil, nil, date, date, nil, nil] + types = [:integer, :string, :string, :string, :string, :date, :date, :string, :string] + + sheet.add_row data, :style => styles, :types => types + end +end \ No newline at end of file diff --git a/app/views/notifications_mailer/notify_admin_export_complete.html.erb b/app/views/notifications_mailer/notify_admin_export_complete.html.erb new file mode 100644 index 000000000..9097d833c --- /dev/null +++ b/app/views/notifications_mailer/notify_admin_export_complete.html.erb @@ -0,0 +1,10 @@ +<%= render 'notifications_mailer/shared/hello', recipient: @recipient %> + +

+ <%= t('.body.you_asked_for_an_export') %> + <%= t(".body.#{@attached_object.category}_#{@attached_object.export_type}") %>. +

+

+ <%= t('.body.click_to_download') %> + <%=link_to( t('.body.here'), "#{root_url}api/exports/#{@attached_object.id}/download", target: "_blank" )%> +

\ No newline at end of file diff --git a/app/views/notifications_mailer/notify_admin_user_wallet_is_credited.html.erb b/app/views/notifications_mailer/notify_admin_user_wallet_is_credited.html.erb new file mode 100644 index 000000000..8f758a2c8 --- /dev/null +++ b/app/views/notifications_mailer/notify_admin_user_wallet_is_credited.html.erb @@ -0,0 +1,9 @@ +<%# this is a mail template of notifcation notify_admin_user_wallet_is_credited %> +<%= render 'notifications_mailer/shared/hello', recipient: @recipient %> +

+ <%= t('.body.wallet_credit_html', + AMOUNT: number_to_currency(@attached_object.amount), + USER: @attached_object.wallet.user.profile.full_name, + ADMIN: @attached_object.user.profile.full_name) + %> +

diff --git a/app/views/notifications_mailer/notify_admin_when_project_published.html.erb b/app/views/notifications_mailer/notify_admin_when_project_published.html.erb index 0c3486e53..add5528e8 100644 --- a/app/views/notifications_mailer/notify_admin_when_project_published.html.erb +++ b/app/views/notifications_mailer/notify_admin_when_project_published.html.erb @@ -1,3 +1,3 @@ <%= render 'notifications_mailer/shared/hello', recipient: @recipient %> -

<%= t('.body.new_project_published') %> "<%= link_to @attached_object.name, "#{root_url}#!/projects/#{@attached_object.id}" %>"

+

<%= t('.body.new_project_published') %> "<%= link_to @attached_object.name, "#{root_url}#!/projects/#{@attached_object.slug}" %>"

diff --git a/app/views/notifications_mailer/notify_admin_when_user_is_created.html.erb b/app/views/notifications_mailer/notify_admin_when_user_is_created.html.erb index cadac8f8c..503ec5aed 100644 --- a/app/views/notifications_mailer/notify_admin_when_user_is_created.html.erb +++ b/app/views/notifications_mailer/notify_admin_when_user_is_created.html.erb @@ -2,6 +2,10 @@

<%= t('.body.new_account_created') %> "<%= @attached_object.profile.full_name %> <<%= @attached_object.email%>>"

+<% if @attached_object.profile.organization %> +

<%= t('.body.account_for_organization') %> <%= @attached_object.profile.organization.name %>

+<% end %> + <% if @attached_object.invoicing_disabled? %>

<%= t('.body.invoicing_disabled_html') %>

<% end %> \ No newline at end of file diff --git a/app/views/notifications_mailer/notify_member_about_coupon.html.erb b/app/views/notifications_mailer/notify_member_about_coupon.html.erb new file mode 100644 index 000000000..08b3ccd8d --- /dev/null +++ b/app/views/notifications_mailer/notify_member_about_coupon.html.erb @@ -0,0 +1,28 @@ +<%= render 'notifications_mailer/shared/hello', recipient: @recipient %> + +

<%= t('.body.enjoy_a_discount_of_PERCENT_with_code_CODE', + PERCENT: @attached_object.percent_off, + CODE: @attached_object.code + ) %> +

+ +<% + # we must tell the use if he could use the code just once or many times (in case we won't specify) + usages = 999 # just a number > 1 + if @attached_object.validity_per_user == 'once' + usages = 1 + else + unless @attached_object.max_usages.nil? + usages = @attached_object.max_usages + end + end +%> +

+ <%= _t('.body.this_coupon_is_valid_USAGE_times_until_DATE_for_all_your_purchases', + { + USAGE: usages, + DATE: @attached_object.valid_until.nil? ? 'NO-DATE' : I18n.l(@attached_object.valid_until.to_date) + }) + # messageFormat + %> +

diff --git a/app/views/notifications_mailer/notify_member_reservation_reminder.html.erb b/app/views/notifications_mailer/notify_member_reservation_reminder.html.erb new file mode 100644 index 000000000..091eb2591 --- /dev/null +++ b/app/views/notifications_mailer/notify_member_reservation_reminder.html.erb @@ -0,0 +1,18 @@ +<%= render 'notifications_mailer/shared/hello', recipient: @recipient %> + +

+ <%= t('.body.this_is_a_reminder_about_your_reservation_RESERVABLE_to_be_held_on_DATE_html', + RESERVABLE: @attached_object.reservable.name, + DATE: I18n.l(@attached_object.slots.order(:start_at).first.start_at, format: :long) + ) + %> +

+ +

+ <%= t('.body.this_reservation_concerns_the_following_slots') %> +

    + <% @attached_object.slots.order(:start_at).each do |slot| %> +
  • <%= "#{I18n.l slot.start_at, format: :long} - #{I18n.l slot.end_at, format: :hour_minute}" %>
  • + <% end %> +
+

diff --git a/app/views/notifications_mailer/notify_project_author_when_collaborator_valid.html.erb b/app/views/notifications_mailer/notify_project_author_when_collaborator_valid.html.erb index c5141f2fc..1ea34bed0 100644 --- a/app/views/notifications_mailer/notify_project_author_when_collaborator_valid.html.erb +++ b/app/views/notifications_mailer/notify_project_author_when_collaborator_valid.html.erb @@ -1,3 +1,3 @@ <%= render 'notifications_mailer/shared/hello', recipient: @recipient %> -

<%= t('.body.the_member') %> <%= @attached_object.user.profile.full_name %> <%= t(".body.accepted_your_invitation_to_take_part_in_the_project" ) %> <%= link_to @attached_object.project.name, "#{root_url}#!/projects/#{@attached_object.project.id}" %>.

+

<%= t('.body.the_member') %> <%= @attached_object.user.profile.full_name %> <%= t(".body.accepted_your_invitation_to_take_part_in_the_project" ) %> <%= link_to @attached_object.project.name, "#{root_url}#!/projects/#{@attached_object.project.slug}" %>.

diff --git a/app/views/notifications_mailer/notify_project_collaborator_to_valid.html.erb b/app/views/notifications_mailer/notify_project_collaborator_to_valid.html.erb index 1bb99907c..26e5a72ce 100644 --- a/app/views/notifications_mailer/notify_project_collaborator_to_valid.html.erb +++ b/app/views/notifications_mailer/notify_project_collaborator_to_valid.html.erb @@ -1,5 +1,5 @@ <%= render 'notifications_mailer/shared/hello', recipient: @recipient %> -

<%= t(".body.your_are_invited_to_take_part_in_a_project") %> <%= link_to @attached_object.project.name, "#{root_url}#!/projects/#{@attached_object.project.id}" %>.

+

<%= t(".body.your_are_invited_to_take_part_in_a_project") %> <%= link_to @attached_object.project.name, "#{root_url}#!/projects/#{@attached_object.project.slug}" %>.

<%= t(".body.to_accept_the_invitation_click_on_following_link") %> <%= link_to "#{root_url}project_collaborator/#{@attached_object.valid_token}", "#{root_url}project_collaborator/#{@attached_object.valid_token}" %>

diff --git a/app/views/notifications_mailer/notify_user_wallet_is_credited.html.erb b/app/views/notifications_mailer/notify_user_wallet_is_credited.html.erb new file mode 100644 index 000000000..a2894c454 --- /dev/null +++ b/app/views/notifications_mailer/notify_user_wallet_is_credited.html.erb @@ -0,0 +1,2 @@ +<%= render 'notifications_mailer/shared/hello', recipient: @recipient %> +

<%= t('.body.wallet_credit_html', AMOUNT: number_to_currency(@attached_object.amount)) %>

diff --git a/app/views/open_api/v1/events/index.json.jbuilder b/app/views/open_api/v1/events/index.json.jbuilder index 7cc7c2db8..33478cd38 100644 --- a/app/views/open_api/v1/events/index.json.jbuilder +++ b/app/views/open_api/v1/events/index.json.jbuilder @@ -1,4 +1,17 @@ json.events @events do |event| json.partial! 'open_api/v1/events/event', event: event - json.extract! event, :amount, :reduced_amount, :nb_total_places, :nb_free_places + json.extract! event, :nb_total_places, :nb_free_places + json.prices do + json.normal do + json.name I18n.t('app.public.home.full_price') + json.amount event.amount + end + event.event_price_categories.each do |epc| + pc = epc.price_category + json.set! pc.id do + json.name pc.name + json.amount epc.amount + end + end + end end diff --git a/app/views/rss/events/index.xml.builder b/app/views/rss/events/index.xml.builder new file mode 100644 index 000000000..bb5653122 --- /dev/null +++ b/app/views/rss/events/index.xml.builder @@ -0,0 +1,22 @@ +#encoding: UTF-8 + +xml.instruct! :xml, version: '1.0' +xml.rss version: '2.0' do + xml.channel do + xml.title "#{t('app.public.events_list.the_fablab_s_events')} - #{@fab_name}" + xml.description t('app.public.home.fablab_s_next_events') + xml.author @fab_name + xml.link root_url + '#!/events' + xml.language I18n.locale.to_s + + @events.each do |event| + xml.item do + xml.guid event.id + xml.pubDate event.created_at.strftime('%F %T') + xml.title event.name + xml.link root_url + '#!/events/' + event.id.to_s + xml.description event.description + end + end + end +end diff --git a/app/views/rss/projects/index.xml.builder b/app/views/rss/projects/index.xml.builder new file mode 100644 index 000000000..bc7ac0c92 --- /dev/null +++ b/app/views/rss/projects/index.xml.builder @@ -0,0 +1,23 @@ +#encoding: UTF-8 + +xml.instruct! :xml, version: '1.0' +xml.rss version: '2.0' do + xml.channel do + xml.title "#{t('app.public.projects_list.the_fablab_projects')} - #{@fab_name}" + xml.description t('app.public.home.latest_documented_projects') + xml.author @fab_name + xml.link root_url + '#!/projects' + xml.language I18n.locale.to_s + + @projects.each do |project| + xml.item do + xml.guid project.id + xml.pubDate project.created_at.strftime('%F %T') + xml.title project.name + xml.link root_url + '#!/projects/' + project.slug + xml.author project.author.first_name + xml.description project.description + end + end + end +end diff --git a/app/views/social_bot/event.html.erb b/app/views/social_bot/event.html.erb new file mode 100644 index 000000000..8fdb64506 --- /dev/null +++ b/app/views/social_bot/event.html.erb @@ -0,0 +1,26 @@ +<% image = @event.event_image.attachment.medium %> +<% width, height = image.dimensions %> + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/views/social_bot/project.html.erb b/app/views/social_bot/project.html.erb new file mode 100644 index 000000000..9f94dcd46 --- /dev/null +++ b/app/views/social_bot/project.html.erb @@ -0,0 +1,26 @@ +<% image = @project.project_image.attachment.medium %> +<% width, height = image.dimensions %> + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/workers/availability_indexer_worker.rb b/app/workers/availability_indexer_worker.rb new file mode 100644 index 000000000..a902ed718 --- /dev/null +++ b/app/workers/availability_indexer_worker.rb @@ -0,0 +1,21 @@ +class AvailabilityIndexerWorker + include Sidekiq::Worker + sidekiq_options queue: 'elasticsearch', retry: true + + Logger = Sidekiq.logger.level == Logger::DEBUG ? Sidekiq.logger : nil + Client = Elasticsearch::Model.client + + def perform(operation, record_id) + logger.debug [operation, "ID: #{record_id}"] + + case operation.to_s + when /index/ + record = Availability.find(record_id) + Client.index index: Availability.index_name, type: Availability.document_type, id: record.id, body: record.as_indexed_json + #puts record.as_indexed_json + when /delete/ + Client.delete index: Availability.index_name, type: Availability.document_type, id: record_id + else raise ArgumentError, "Unknown operation '#{operation}'" + end + end +end diff --git a/app/workers/indexer_worker.rb b/app/workers/project_indexer_worker.rb similarity index 82% rename from app/workers/indexer_worker.rb rename to app/workers/project_indexer_worker.rb index e08e21d3a..7dce4fa47 100644 --- a/app/workers/indexer_worker.rb +++ b/app/workers/project_indexer_worker.rb @@ -1,4 +1,4 @@ -class IndexerWorker +class ProjectIndexerWorker include Sidekiq::Worker sidekiq_options queue: 'elasticsearch', retry: true @@ -12,9 +12,8 @@ class IndexerWorker when /index/ record = Project.find(record_id) Client.index index: Project.index_name, type: Project.document_type, id: record.id, body: record.as_indexed_json - #puts record.as_indexed_json when /delete/ - Client.delete index: 'fablab', type: 'projects', id: record_id + Client.delete index: Project.index_name, type: Project.document_type, id: record_id else raise ArgumentError, "Unknown operation '#{operation}'" end end diff --git a/app/workers/reservation_reminder_worker.rb b/app/workers/reservation_reminder_worker.rb new file mode 100644 index 000000000..5aff73feb --- /dev/null +++ b/app/workers/reservation_reminder_worker.rb @@ -0,0 +1,29 @@ +class ReservationReminderWorker + include Sidekiq::Worker + + ## In case the reminder is enabled but no delay were configured, we use this default value + DEFAULT_REMINDER_DELAY = 24.hours + + def perform + enabled = Setting.find_by(name: 'reminder_enable').try(:value) + if enabled == 'true' + delay = Setting.find_by(name: 'reminder_delay').try(:value).try(:to_i).try(:hours) || DEFAULT_REMINDER_DELAY + + starting = Time.now.beginning_of_hour + delay + ending = starting + 1.hour + + Reservation.joins(:slots).where('slots.start_at >= ? AND slots.start_at <= ?', starting, ending).each do |r| + already_sent = Notification.where( + attached_object_type: Reservation.name, + attached_object_id: r.id, + notification_type_id: NotificationType.find_by_name('notify_member_reservation_reminder') + ).count + unless already_sent > 0 + NotificationCenter.call type: 'notify_member_reservation_reminder', + receiver: r.user, + attached_object: r + end + end + end + end +end \ No newline at end of file diff --git a/app/workers/statistics_export_worker.rb b/app/workers/statistics_export_worker.rb new file mode 100644 index 000000000..0f5e97e3f --- /dev/null +++ b/app/workers/statistics_export_worker.rb @@ -0,0 +1,27 @@ +class StatisticsExportWorker + include Sidekiq::Worker + + def perform(export_id) + export = Export.find(export_id) + + unless export.user.is_admin? + raise SecurityError, 'Not allowed to export' + end + + unless export.category == 'statistics' + raise KeyError, 'Wrong worker called' + end + + service = StatisticsExportService.new + method_name = "export_#{export.export_type}" + + if %w(account event machine project subscription training global).include?(export.export_type) and service.respond_to?(method_name) + service.public_send(method_name, export) + + NotificationCenter.call type: :notify_admin_export_complete, + receiver: export.user, + attached_object: export + end + + end +end diff --git a/app/workers/stripe_worker.rb b/app/workers/stripe_worker.rb index feff2281e..a7bfd8e49 100644 --- a/app/workers/stripe_worker.rb +++ b/app/workers/stripe_worker.rb @@ -14,4 +14,27 @@ class StripeWorker ) user.update_columns(stp_customer_id: customer.id) end + + def create_stripe_coupon(coupon_id) + coupon = Coupon.find(coupon_id) + stp_coupon = { + id: coupon.code, + duration: coupon.validity_per_user, + percent_off: coupon.percent_off, + } + unless coupon.valid_until.nil? + stp_coupon[:redeem_by] = coupon.valid_until.to_i + end + stp_coupon + unless coupon.max_usages.nil? + stp_coupon[:max_redemptions] = coupon.max_usages + end + + Stripe::Coupon.create(stp_coupon) + end + + def delete_stripe_coupon(coupon_code) + cpn = Stripe::Coupon.retrieve(coupon_code) + cpn.delete + end end diff --git a/app/workers/users_export_worker.rb b/app/workers/users_export_worker.rb new file mode 100644 index 000000000..95857f94e --- /dev/null +++ b/app/workers/users_export_worker.rb @@ -0,0 +1,27 @@ +class UsersExportWorker + include Sidekiq::Worker + + def perform(export_id) + export = Export.find(export_id) + + unless export.user.is_admin? + raise SecurityError, 'Not allowed to export' + end + + unless export.category == 'users' + raise KeyError, 'Wrong worker called' + end + + service = UsersExportService.new + method_name = "export_#{export.export_type}" + + if %w(members subscriptions reservations).include?(export.export_type) and service.respond_to?(method_name) + service.public_send(method_name, export) + + NotificationCenter.call type: :notify_admin_export_complete, + receiver: export.user, + attached_object: export + end + + end +end diff --git a/bower.json b/bower.json index 4ba28caa6..d245b9573 100644 --- a/bower.json +++ b/bower.json @@ -18,7 +18,7 @@ "angular-bootstrap": "~0.14.3", "angular-ui-router": ">=0.2.15", "fullcalendar": "=2.3.1", - "angular-ui-calendar": "0.9.0-beta.1", + "angular-ui-calendar": "1.0.1", "moment": "=2.10.6", "angular-moment": ">=0.10.3", "ngUpload": ">=0.5.11", @@ -53,7 +53,8 @@ "angular-translate-interpolation-messageformat": "~2.8.1", "messageformat": "=0.1.8", "moment-timezone": "~0.5.0", - "ngFitText": "~4.1.1" + "ngFitText": "~4.1.1", + "angular-aside": "^1.3.2" }, "resolutions": { "jquery": ">=1.10.2", diff --git a/config/application.rb b/config/application.rb index e1dba87a2..c692e74af 100644 --- a/config/application.rb +++ b/config/application.rb @@ -63,6 +63,9 @@ module Fablab config.web_console.whitelisted_ips << '10.0.2.2' #vagrant end + # load locales for subdirectories + config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**/*.yml').to_s] + # enable the app to find locales in plugins locales directory config.i18n.load_path += Dir["#{Rails.root}/plugins/*/config/locales/*.yml"] diff --git a/config/application.yml.default b/config/application.yml.default index 4facf7172..c46ee4815 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -34,6 +34,8 @@ TWITTER_CONSUMER_SECRET: '' TWITTER_ACCESS_TOKEN: '' TWITTER_ACCESS_TOKEN_SECRET: '' +FACEBOOK_APP_ID: '' + RAILS_LOCALE: 'fr' MOMENT_LOCALE: 'fr' SUMMERNOTE_LOCALE: 'fr-FR' @@ -47,7 +49,13 @@ TIME_ZONE: 'Paris' WEEK_STARTING_DAY: 'monday' D3_DATE_FORMAT: '%d/%m/%y' UIB_DATE_FORMAT: 'dd/MM/yyyy' +EXCEL_DATE_FORMAT: 'dd/mm/yyyy' OPENLAB_APP_SECRET: OPENLAB_APP_ID: OPENLAB_BASE_URI: 'https://openprojects.fab-manager.com' + +LOG_LEVEL: 'debug' + +ALLOWED_EXTENSIONS: pdf ai eps cad math svg stl dxf dwg obj step iges igs 3dm 3dmf doc docx png ino scad fcad skp sldprt sldasm slddrw slddrt tex latex ps +ALLOWED_MIME_TYPES: application/pdf application/postscript application/illustrator image/x-eps image/svg+xml application/sla application/dxf application/acad application/dwg application/octet-stream application/step application/iges model/iges x-world/x-3dmf application/ application/vnd.openxmlformats-officedocument.wordprocessingml.document image/png text/x-arduino text/plain application/scad application/vnd.sketchup.skp application/x-koan application/vnd-koan koan/x-skm application/vnd.koan application/x-tex application/x-latex diff --git a/config/deploy.rb b/config/deploy.rb index 00b46b0a9..d9f16dcf3 100644 --- a/config/deploy.rb +++ b/config/deploy.rb @@ -42,6 +42,8 @@ namespace :deploy do run "mkdir -p #{shared_path}/config" run "mkdir -p #{shared_path}/uploads" run "mkdir -p #{shared_path}/invoices" + run "mkdir -p #{shared_path}/exports" + run "mkdir -p #{shared_path}/plugins" put File.read("config/database.yml"), "#{shared_path}/config/database.yml" puts "Now edit #{shared_path}/config/database.yml and add your username and password" put File.read("config/application.yml"), "#{shared_path}/config/application.yml" @@ -92,6 +94,20 @@ namespace :deploy do end after "deploy:finalize_update", 'deploy:symlink_invoices_dir' + desc "Symlinks the exports dir" + task :symlink_exports_dir, :roles => :app do + run "rm -rf #{release_path}/exports" + run "ln -nfs #{shared_path}/exports/ #{release_path}/" + end + after "deploy:finalize_update", 'deploy:symlink_exports_dir' + + desc "Symlinks the plugins dir" + task :symlink_plugins_dir, :roles => :app do + run "rm -rf #{release_path}/plugins" + run "ln -nfs #{shared_path}/plugins/ #{release_path}/" + end + after "deploy:finalize_update", 'deploy:symlink_plugins_dir' + namespace :assets do desc 'Run the precompile task locally and rsync with shared' task :precompile, :roles => :web, :except => { :no_release => true } do diff --git a/config/environments/development.rb b/config/environments/development.rb index 2e1262529..074739368 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -41,4 +41,6 @@ Rails.application.configure do config.action_mailer.delivery_method = :letter_opener config.action_mailer.default_url_options = { :host => 'localhost:5000' } + + config.log_level = Rails.application.secrets.log_level || :debug end diff --git a/config/environments/production.rb b/config/environments/production.rb index c0285ae84..5175c8719 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -46,7 +46,7 @@ Rails.application.configure do # config.force_ssl = true # Set to :debug to see everything in the log. - config.log_level = :debug + config.log_level = Rails.application.secrets.log_level.blank? ? :debug : Rails.application.secrets.log_level # Prepend all log lines with the following tags. # config.log_tags = [ :subdomain, :uuid ] diff --git a/config/environments/staging.rb b/config/environments/staging.rb index 1db32f4e9..067ea2d1d 100644 --- a/config/environments/staging.rb +++ b/config/environments/staging.rb @@ -46,7 +46,7 @@ Rails.application.configure do # config.force_ssl = true # Set to :debug to see everything in the log. - config.log_level = :debug + config.log_level = Rails.application.secrets.log_level.blank? ? :debug : Rails.application.secrets.log_level # Prepend all log lines with the following tags. # config.log_tags = [ :subdomain, :uuid ] diff --git a/config/environments/test.rb b/config/environments/test.rb index 224c4f69f..23135c381 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -41,4 +41,5 @@ Rails.application.configure do config.active_support.test_order = :random + config.log_level = Rails.application.secrets.log_level.blank? ? :debug : Rails.application.secrets.log_level end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 687eb1039..1e82761d9 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -7,7 +7,7 @@ Devise.setup do |config| # Devise will use the `secret_key_base` on Rails 4+ applications as its `secret_key` # by default. You can change it below and use your own secret key. # config.secret_key = 'f0ad7aadec8086b90c0427e734602262e5d211147f3d93b5b94b5263ffd245e9fd9fcd672dcadea1d9ee2b1bffbf2712cdb013883d66943ef5bed93a263fe11a' - + # ==> Mailer Configuration # Configure the e-mail address which will be shown in Devise::Mailer, # note that it will be overwritten if you use your own mailer class diff --git a/config/initializers/elasticsearch.rb b/config/initializers/elasticsearch.rb index 16fdd2ce3..5f8f7d446 100644 --- a/config/initializers/elasticsearch.rb +++ b/config/initializers/elasticsearch.rb @@ -1,3 +1,3 @@ -client = Elasticsearch::Client.new host: "http://#{ENV["ELASTICSEARCH_HOST"]}:9200", log: true +client = Elasticsearch::Client.new host: "http://#{Rails.application.secrets.elaticsearch_host}:9200", log: true Elasticsearch::Model.client = client Elasticsearch::Persistence.client = client diff --git a/config/initializers/is_number.rb b/config/initializers/is_number.rb new file mode 100644 index 000000000..83daa6e93 --- /dev/null +++ b/config/initializers/is_number.rb @@ -0,0 +1,12 @@ +## Helper method: will return true if the current string +## can be parsed as a number (float or integer), false otherwise +# exemples: +# "2" => true +# "4.5" => true +# "hello" => false +# "" => false +class String + def is_number? + true if Float(self) rescue false + end +end \ No newline at end of file diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index 5c668f460..570833c41 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -46,11 +46,6 @@ en: trainings: # track and monitor the trainings - add_a_new_training: "Add a new training" - beware_when_creating_a_training_its_reservation_prices_are_initialized_to_zero: "Beware, when creating a training, its reservation prices are initialized at zero." - dont_forget_to_change_them_before_creating_slots_for_this_training: "Don't forget to change them before creating slots for this training." - associated_machines: "Associated machines" - number_of_tickets: "Number of tickets" training: "Training" year_NUMBER: "Year {{NUMBER}}" # angular interpolation month_of_NAME: "Month of {{NAME}}" # angular interpolation @@ -67,19 +62,55 @@ en: description_was_successfully_saved: "Description was successfully saved." training_successfully_deleted: "Training successfully deleted." unable_to_delete_the_training_because_some_users_alredy_booked_it: "Unable to delete the training because some users already booked it." + do_you_really_want_to_delete_this_training: "Do you really want to delete this training?" + + trainings_new: + # create a new training + beware_when_creating_a_training_its_reservation_prices_are_initialized_to_zero: "Beware, when creating a training, its reservation prices are initialized at zero." + dont_forget_to_change_them_before_creating_slots_for_this_training: "Don't forget to change them before creating slots for this training." events: - # courses and workshops tracking and management - fablab_courses_and_workshops: "Fablab courses and workshops" + # events tracking and management + events_monitoring: "Events monitoring" + manage_filters: "Manage filters" + fablab_events: "Fablab events" all_events: "All events" passed_events: "Passed events" events_to_come: "Events to come" from_DATE: "From {{DATE}}" # angular interpolation from_TIME: "From {{TIME}}" # angular interpolation view_reservations: "View reservations" + categories: "Categories" + add_a_category: "Add a category" + add_a_theme: "Add a theme" + age_ranges: "Age ranges" + add_a_range: "Add a range" + do_you_really_want_to_delete_this_ELEMENT: "Do you really want to delete this {ELEMENT, select, category{category} theme{theme} age_range{age range} other{element}}?" # messageFormat interpolation + unable_to_delete_ELEMENT_already_in_use_NUMBER_times: "Unable to delete this {ELEMENT, select, category{category} theme{theme} age_range{age range} other{element}} because it is already associated with {NUMBER, plural, =0{no events} one{one event} other{{NUMBER} events}}." # messageFormat interpolation + at_least_one_category_is_required: "At least one category is required." + unable_to_delete_the_last_one: "Unable to delete the last one." + unable_to_delete_an_error_occured: "Unable to delete: an error occurred." + manage_prices_categories: "Manage prices' categories" + prices_categories: "Prices' categories" + add_a_price_category: "Add a price's category" + usages_count: "Usages count" + price_category: "Price category" + category_name: "Category's name" + category_name_is_required: "Category's name is required." + enter_here_the_conditions_under_which_this_price_is_applicable: "Enter here the conditions under which this price is applicable" + conditions_are_required: "Conditions are required." + price_category_successfully_created: "Price category successfully created." + unable_to_add_the_price_category_check_name_already_used: "Unable to add the price category, check that the name is not already used." + unexpected_error_occurred_please_refresh: "An unexpected error occurred, please refresh the page." + price_category_successfully_updated: "Price category successfully updated." + unable_to_update_the_price_category: "Unable to update the price category." + unable_to_delete_this_price_category_because_it_is_already_used: "Unable to delete this price category because it is already used." + do_you_really_want_to_delete_this_price_category: "Do you really want to delete this price category?" + price_category_successfully_deleted: "Price category successfully deleted." + price_category_deletion_failed: "Price category deletion failed." events_new: - # add a new workshop/course + # add a new event none: "None" every_days: "Every days" every_week: "Every week" @@ -100,7 +131,7 @@ en: back_to_monitoring: "Back to monitoring" pricing: - # subscriptions, prices and credits management + # subscriptions, prices, credits and coupons management pricing_management: "Pricing management" list_of_the_subscription_plans: "List of the subscription plans" beware_the_subscriptions_are_disabled_on_this_application: "Beware, the subscriptions are disabled on this application." @@ -129,8 +160,36 @@ en: changes_have_been_successfully_saved: "Changes have been successfully saved." credit_was_successfully_saved: "Credit was successfully saved." do_you_really_want_to_delete_this_subscription_plan: "Do you really want to delete this subscription plan?" - subscription_plan_was_successfully_deleted: "Subscription plan was successfully delete." + subscription_plan_was_successfully_deleted: "Subscription plan was successfully deleted." unable_to_delete_the_specified_subscription_an_error_occurred: "Unable to delete the specified subscription, an error occurred." + coupons: "Coupons" + list_of_the_coupons: "List of the coupons" + percentage_off: "Percentage off" + nb_of_usages: "Number of usages" + status: "Status" + add_a_new_coupon: "Add a new coupon" + disabled: "Disabled" + expired: "Expired" + sold_out: "Sold out" + active: "Active" + do_you_really_want_to_delete_this_coupon: "Do you really want to delete this coupon?" + coupon_was_successfully_deleted: "Coupon was successfully deleted." + unable_to_delete_the_specified_coupon_already_in_use: "Unable to delete the specified coupon: it is already used with some invoices." + unable_to_delete_the_specified_coupon_an_unexpected_error_occurred: "Unable to delete the specified coupon: an unexpected error occurred." + send_a_coupon: "Send a coupon" + coupon: "Coupon" + usages: "Usages" + coupon_successfully_sent_to_USER: "Coupon successfully sent to {{USER}}" # angular interpolation + an_error_occurred_unable_to_send_the_coupon: "An unexpected error prevent from sending the coupon." + + coupons_new: + # ajouter un code promotionnel + add_a_coupon: "Add a coupon" + unable_to_create_the_coupon_check_code_already_used: "Unable to create the coupon. Please check that the code is not already used" + + coupons_edit: + # mettre à jour un code promotionnel + unable_to_update_the_coupon_an_error_occurred: "Unable to update the coupon: an error occurred." plans: new: @@ -188,6 +247,7 @@ en: day: "Day" "#_of_invoice": "# of invoice" online_sales: "Online sales" + wallet: "Wallet" refund: "Refund" documentation: "Documentation" 2_digits_year_(eg_70): "2 digits year (eg. 70)" @@ -211,6 +271,8 @@ en: add_a_notice_regarding_refunds_only_if_the_invoice_is_concerned: "Add a notice regarding refunds, only if the invoice is concerned." this_will_never_be_added_when_an_online_sales_notice_is_present: "This will never be added when an online sales notice is present." (eg_R[/A]_will_add_/A_to_the_refund_invoices): '(ed. R[/A] will add "/A" to the refund invoices)' + add_a_notice_regarding_the_wallet_only_if_the_invoice_is_concerned: "Add a notice regarding the wallet, only if the invoice is concerned." + (eg_W[/PM]_will_add_/PM_to_the_invoices_settled_with_wallet): '(eg. W[/PM] will add "/PM" to the invoices settled with wallet)' code: "Code" enable_the_code: "Enable the code" enabled: "Enabled" @@ -233,6 +295,7 @@ en: by_cash: "By cash" by_cheque: "By cheque" by_transfer: "By transfer" + by_wallet: "By wallet" you_must_select_at_least_one_element_to_create_a_refund: "You must select at least one element, to create a refund." unable_to_create_the_refund: "Unable to create the refund" invoice_reference_successfully_saved: "Invoice reference successfully saved." @@ -291,6 +354,7 @@ en: an_error_occurred_and_the_tag_deletion_failed: "An error occurred and the tag deletion failed." search_for_an_authentication_provider: "Search for an authentication provider" add_a_new_authentication_provider: "Add a new authentication provider" + strategy_name: "Strategy's name" state: "State" unknown: "Unknown: " active: "Active" @@ -318,13 +382,12 @@ en: next_trainings: "Next trainings" passed_trainings: "Passed trainings" validated_trainings: "Validated trainings" - courses_and_workshops: "Courses and workshops" - next_courses_and_workshops: "Next courses and workshops" - no_upcomning_courses_or_workshops: "No upcoming courses and workshops" + events: "Events" + next_events: "Next events" + no_upcoming_events: "No upcoming events" NUMBER_full_price_tickets_reserved: "{NUMBER, plural, =0{} one{1 full price ticket reserved} other{{NUMBER} full price tickets reserved}}" # messageFormat interpolation - NUMBER_reduced_rate_tickets_reserved: "{NUMBER, plural, =0{} one{1 reduced rate ticket reserved} other{{NUMBER} reduced rate tickets reserved}}" # messageFormat interpolation - passed_courses_and_workshops: "Passed courses and workshops" - no_passed_courses_or_workshop: "No passed courses or workshops" + NUMBER_NAME_tickets_reserved: "{NUMBER, plural, =0{} one{1 {NAME} ticket reserved} other{{NUMBER} {NAME} tickets reserved}}" # messageFormat interpolation + passed_events: "Passed events" invoices: "Invoices" invoice_#: "Invoice #" download_the_refund_invoice: "Download the refund invoice" @@ -383,13 +446,18 @@ en: average_age: "Average age:" years_old: "Years old" total: "Total" + available_hours: "Hours available for booking:" + available_tickets: "Tickets available for booking:" gender: "Gender" age: "Age" revenue: "Revenue" unknown: "Unknown" user_id: "User ID" display_more_results: "Display more results" - + export_statistics_to_excel: "Export statistics to Excel" + export_all_statistics: "Export all statistics" + export_the_current_search_results: "Export the current search results" + export: "Export" stats_graphs: # statistics graphs @@ -424,7 +492,6 @@ en: warning_message_of_the_training_booking_page: "Warning message of the training booking page:" information_message_of_the_training_reservation_page: "Information message of the training reservation page:" message_of_the_subscriptions_page: "Message of the subscriptions page:" - message_of_the_event_page_relative_to_the_reduced_rate_availability_conditions: "Message of the event page, relative to the reduced rate availability conditions:" legal_documents: "Legal documents" if_these_documents_are_not_filled_no_consent_about_them_will_be_asked_to_the_user: "If these documents are not filled, no consent about them will be asked." general_terms_and_conditions_(T&C): "General terms and conditions (T&C)" @@ -466,8 +533,31 @@ en: disabled: "Disabled" ability_for_the_users_to_cancel_their_reservations: "Ability for the users to cancel their reservations" reservations_cancelling: "Reservations cancelling" - customization_of_SETTING_successfully_saved: "Customization of {{SETTING}} successfully saved." # angular interpolation + reservations_reminders: "Reservations reminders" + notification_sending_before_the_reservation_occurs: "Notification sending before the reservation occurs" + customization_of_SETTING_successfully_saved: "Customization of the {{SETTING}} successfully saved." # angular interpolation file_successfully_updated: "File successfully updated." + name_genre: "title concordance" + machine_explications_alert: "explanation message on the machine reservation page" + training_explications_alert: "explanation message on the training reservation page" + training_information_message: "information message on the machine reservation page" + subscription_explications_alert: "explanation message on the subscription page" + main_color: "main colour" + secondary_color: "secondary colour" + home_blogpost: "homepage's brief" + twitter_name: "Twitter feed name" + about_title: "\"About\" page title" + about_body: "\"About\" page content" + about_contacts: "\"About\" page contacts" + booking_window_start: "opening time" + booking_window_end: "closing time" + booking_move_enable: "reservation moving enabling" + booking_move_delay: "preventive delay of moving" + booking_cancel_enable: "reservation canceling enabling" + booking_cancel_delay: "preventive delay of canceling" + reminder_enable: "reservation reminding enabling" + reminder_delay: "delay before sending the reminder" + default_value_is_24_hours: "If the field is leaved empty: 24 hours." open_api_clients: add_new_client: "Create new API client" diff --git a/config/locales/app.admin.fr.yml b/config/locales/app.admin.fr.yml index ce66544db..8200c6990 100644 --- a/config/locales/app.admin.fr.yml +++ b/config/locales/app.admin.fr.yml @@ -46,11 +46,6 @@ fr: trainings: # suivre et surveiller les formations - add_a_new_training: "Ajouter une nouvelle formation" - beware_when_creating_a_training_its_reservation_prices_are_initialized_to_zero: "Attention, lors de la création d'une formation, ses tarifs de réservation sont initialisés à zero." - dont_forget_to_change_them_before_creating_slots_for_this_training: "Pensez à les modifier avant de créer des créneaux pour cette formation." - associated_machines: "Machines associées" - number_of_tickets: "Nombre de places" training: "Formation" year_NUMBER: "Année {{NUMBER}}" # angular interpolation month_of_NAME: "Mois de {{NAME}}" # angular interpolation @@ -67,19 +62,55 @@ fr: description_was_successfully_saved: "La description a bien été enregistrée." training_successfully_deleted: "La formation a bien été supprimée." unable_to_delete_the_training_because_some_users_alredy_booked_it: "La formation ne peut pas être supprimée car elle a déjà été réservée par des utilisateurs." + do_you_really_want_to_delete_this_training: "Êtes-vous sur de vouloir supprimer cette formation ?" + + trainings_new: + # créer une nouvelle formation + beware_when_creating_a_training_its_reservation_prices_are_initialized_to_zero: "Attention, lors de la création d'une formation, ses tarifs de réservation sont initialisés à zero." + dont_forget_to_change_them_before_creating_slots_for_this_training: "Pensez à les modifier avant de créer des créneaux pour cette formation." events: - # gestion et suivi des stages et ateliers - fablab_courses_and_workshops: "Les Stages et ateliers du Fab Lab" + # gestion et suivi des évènements + events_monitoring: "Suivi des évènements" + manage_filters: "Gérer les filtres" + fablab_events: "Les évènements du Fab Lab" all_events: "Tous les évènements" passed_events: "Les évènements déjà passés" events_to_come: "Les évènements à venir" from_DATE: "Du {{DATE}}" # angular interpolation from_TIME: "De {{TIME}}" # angular interpolation view_reservations: "Consulter les réservations" + categories: "Catégories" + add_a_category: "Ajouter une catégorie" + add_a_theme: "Ajouter une thématique" + age_ranges: "Tranches d'âge" + add_a_range: "Ajouter une tranche" + do_you_really_want_to_delete_this_ELEMENT: "Voulez-vous vraiment supprimer cette {ELEMENT, select, category{catégorie} theme{thématique} age_range{tranche d'âge} other{élément}} ?" # messageFormat interpolation + unable_to_delete_ELEMENT_already_in_use_NUMBER_times: "Impossible de supprimer cette {ELEMENT, select, category{catégorie} theme{thématique} age_range{tranche d'âge} other{élément}} car elle est actuellement associée à {NUMBER, plural, =0{aucun évènement} one{un évènement} other{{NUMBER} évènements}}." # messageFormat interpolation + at_least_one_category_is_required: "Au moins une catégorie est requise." + unable_to_delete_the_last_one: "Impossible de supprimer la dernière." + unable_to_delete_an_error_occured: "Impossible de supprimer : une erreur est survenue." + manage_prices_categories: "Gérer les catégories tarifaires" + prices_categories: "Catégories tarifaires" + add_a_price_category: "Ajouter une catégorie tarifaire" + usages_count: "Nombre d'utilisations" + price_category: "Catégorie tarifaire" + category_name: "Nom de la catégorie" + category_name_is_required: "Le nom de la catégorie est requis." + enter_here_the_conditions_under_which_this_price_is_applicable: "Saisissez ici les conditions d'application du tarif" + conditions_are_required: "Les conditions sont requises." + price_category_successfully_created: "La catégorie tarifaire a bien été créée." + unable_to_add_the_price_category_check_name_already_used: "Impossible d'ajouter la catégorie tarifaire, vérifiez que le nom n'est pas déjà utilisé." + unexpected_error_occurred_please_refresh: "Une erreur inattendue est survenue, merci de rafraîchir la page." + price_category_successfully_updated: "La catégorie tarifaire a bien été mise à jour." + unable_to_update_the_price_category: "Impossible de mettre à jour la catégorie tarifaire." + unable_to_delete_this_price_category_because_it_is_already_used: "Impossible de supprimer cette catégorie tarifaire car elle déjà utilisée." + do_you_really_want_to_delete_this_price_category: "Êtes vous sur de vouloir supprimer cette catégorie tarifaire ?" + price_category_successfully_deleted: "Catégorie tarifaire supprimée avec succès." + price_category_deletion_failed: "Échec de la suppression de la catégorie tarifaire." events_new: - # ajouter un nouveau atelier/stage + # ajouter un nouvel évènement none: "Aucune" every_days: "Tous les jours" every_week: "Chaque semaine" @@ -100,7 +131,7 @@ fr: back_to_monitoring: "Retour au suivi" pricing: - # gestion des abonnements, des tarifs et des crédits + # gestion des abonnements, des tarifs, des crédits et des codes promo pricing_management: "Gestion de la tarification" list_of_the_subscription_plans: "Liste des formules d'abonnements" beware_the_subscriptions_are_disabled_on_this_application: "Attention, les abonnements sont désactivés sur cette application." @@ -131,6 +162,34 @@ fr: do_you_really_want_to_delete_this_subscription_plan: "Êtes-vous sûr(e) de vouloir supprimer cette formule d'abonnement ?" subscription_plan_was_successfully_deleted: "La formule d'abonnement a bien été supprimée." unable_to_delete_the_specified_subscription_an_error_occurred: "Impossible de supprimer l'abonnement spécifié, une erreur s'est produite." + coupons: "Codes promotionnels" + list_of_the_coupons: "Liste des codes promotionnels" + percentage_off: "Pourcentage de réduction" + nb_of_usages: "Nombre d'utilisations" + status: "Statut" + add_a_new_coupon: "Ajouter un code promotionnel" + disabled: "Désactivé" + expired: "Expiré" + sold_out: "Épuisé" + active: "Actif" + do_you_really_want_to_delete_this_coupon: "Êtes-vous sûr(e) de vouloir supprimer ce code promotionnel ?" + coupon_was_successfully_deleted: "Le code promotionnel a bien été supprimé." + unable_to_delete_the_specified_coupon_already_in_use: "Impossible de supprimer le code promotionnel : il est utilisé dans des factures." + unable_to_delete_the_specified_coupon_an_unexpected_error_occurred: "Impossible de supprimer le code promotionnel : une erreur inattendue s'est produite." + send_a_coupon: "Envoyer un code promo" + coupon: "Code promo" + usages: "Utilisations" + coupon_successfully_sent_to_USER: "Le code promotionnel a bien été envoyé à {{USER}}" # angular interpolation + an_error_occurred_unable_to_send_the_coupon: "Une erreur inattendue a empêché l'envoi du code promotionnel." + + coupons_new: + # ajouter un code promotionnel + add_a_coupon: "Ajouter un code promotionnel" + unable_to_create_the_coupon_check_code_already_used: "Impossible de créer le code promotionnel. Vérifiez que le code n'est pas utilisé." + + coupons_edit: + # mettre à jour un code promotionnel + unable_to_update_the_coupon_an_error_occurred: "Impossible de mettre à jour le code promotionnel : une erreur est survenue." plans: new: @@ -188,6 +247,7 @@ fr: day: "Jour" "#_of_invoice": "N° de facture" online_sales: "Vente en ligne" + wallet: "Porte-monnaie" refund: "Remboursement" documentation: "Documentation" 2_digits_year_(eg_70): "Année sur 2 chiffres (ex. 70)" @@ -211,6 +271,8 @@ fr: add_a_notice_regarding_refunds_only_if_the_invoice_is_concerned: "Ajoute une information relative aux remboursements, uniquement si cela concerne la facture. " this_will_never_be_added_when_an_online_sales_notice_is_present: "Ceci ne sera jamais cumulé avec une information de vente en ligne." (eg_R[/A]_will_add_/A_to_the_refund_invoices): '(ex. R[/A] ajoutera "/A" aux factures de remboursement)' + add_a_notice_regarding_the_wallet_only_if_the_invoice_is_concerned: "Ajoute une information relative au paiement par le porte-monnaie, uniquement si cela concerne la facture." + (eg_W[/PM]_will_add_/PM_to_the_invoices_settled_with_wallet): '(ex. W[/PM] ajoutera "/PM" aux factures réglées avec porte-monnaie)' code: "Code" enable_the_code: "Activer le code" enabled: "Activé" @@ -233,6 +295,7 @@ fr: by_cash: "En espèces" by_cheque: "Par chèque" by_transfer: "Par virement" + by_wallet: "Par porte-monnaie" you_must_select_at_least_one_element_to_create_a_refund: "Vous devez sélectionner au moins un élément sur lequel créer un avoir." unable_to_create_the_refund: "Impossible de créer l'avoir" invoice_reference_successfully_saved: "La référence facture a bien été enregistrée." @@ -291,6 +354,7 @@ fr: an_error_occurred_and_the_tag_deletion_failed: "Une erreur est survenue et l'étiquette n'a pas pu être supprimé." search_for_an_authentication_provider: "Recherchez un fournisseur d'authentification" add_a_new_authentication_provider: "Ajouter un nouveau fournisseur d'authentification" + strategy_name: "Nom de la stratégie" state: "État" unknown: "Inconnu : " active: "Actif" @@ -318,13 +382,12 @@ fr: next_trainings: "Les prochaines formations" passed_trainings: "Les formations passées" validated_trainings: "Les formations validées" - courses_and_workshops: "Ateliers et stages" - next_courses_and_workshops: "Les prochains stages et ateliers" - no_upcomning_courses_or_workshops: "Aucun stage ou atelier à venir" + events: "Évènements" + next_events: "Les prochains évènements" + no_upcoming_events: "Aucun évènement à venir" NUMBER_full_price_tickets_reserved: "{NUMBER, plural, =0{} one{1 place plein tarif réservée} other{{NUMBER} places plein tarif réservées}}" # messageFormat interpolation - NUMBER_reduced_rate_tickets_reserved: "{NUMBER, plural, =0{} one{1 place à tarif réduit réservée} other{{NUMBER} places à tarif réduit réservées}}" # messageFormat interpolation - passed_courses_and_workshops: "Les stages et ateliers passés" - no_passed_courses_or_workshop: "Aucun stage ou atelier passé" + NUMBER_NAME_tickets_reserved: "{NUMBER, plural, =0{} one{1 place {NAME} réservée} other{{NUMBER} places {NAME} réservées}}" # messageFormat interpolation + passed_events: "Les évènements passés" invoices: "Factures" invoice_#: "Facture n°" download_the_refund_invoice: "Télécharger l'avoir" @@ -383,13 +446,18 @@ fr: average_age: "Âge moyen :" years_old: "ans" total: "Total" + available_hours: "Heures disponibles à la réservation :" + available_tickets: "Places disponibles à la reservation :" gender: "Genre" age: "Âge" revenue: "Chiffre d'affaires" unknown: "Inconnu" user_id: "ID Utilisateur" display_more_results: "Afficher plus de résultats" - + export_statistics_to_excel: "Exporter les statistiques vers Excel" + export_all_statistics: "Exporter toutes les statistiques" + export_the_current_search_results: "Exporter les résultats de la recherche courante" + export: "Exporter" stats_graphs: # graphiques de statistiques @@ -424,7 +492,6 @@ fr: warning_message_of_the_training_booking_page: "Message d'avertissement sur la page de réservation d'une formation :" information_message_of_the_training_reservation_page: "Message d'information sur la page de réservation d'une formation :" message_of_the_subscriptions_page: "Message sur la page des abonnements :" - message_of_the_event_page_relative_to_the_reduced_rate_availability_conditions: "Message sur la page d'un évènement, relatif aux conditions d'application du tarif réduit :" legal_documents: "Documents légaux" if_these_documents_are_not_filled_no_consent_about_them_will_be_asked_to_the_user: "Si ces documents ne sont pas renseignés, aucun consentement à leur sujet ne sera demandé à l'utilisateur." general_terms_and_conditions_(T&C): "Conditions générales de vente (CGV)" @@ -466,8 +533,31 @@ fr: disabled: "Désactivé" ability_for_the_users_to_cancel_their_reservations: "Possibilité pour l'utilisateur d'annuler ses réservations" reservations_cancelling: "Annulation des réservations" + reservations_reminders: "Rappel des réservations" + notification_sending_before_the_reservation_occurs: "Envoi de notification avant l'avènement de la réservation" customization_of_SETTING_successfully_saved: "La personnalisation de {{SETTING}} a bien été enregistrée." # angular interpolation file_successfully_updated: "Le fichier a bien été mis à jour." + name_genre: "l'accord du nom" + machine_explications_alert: "l'explication sur la page de réservation d'une machine" + training_explications_alert: "l'explication sur la page de réservation d'une formation" + training_information_message: "l'information sur la page de réservation d'une formation" + subscription_explications_alert: "l'explication sur la page de souscription à un abonnement" + main_color: "la couleur principale" + secondary_color: "la couleur secondaire" + home_blogpost: "la brève de la page d'accueil" + twitter_name: "nom du flux Twitter" + about_title: "titre de la page \"À propos\"" + about_body: "corps de la page \"À propos\"" + about_contacts: "contacts sur la page \"À propos\"" + booking_window_start: "l'heure d'ouverture" + booking_window_end: "l'heure de fermeture" + booking_move_enable: "l'activation du déplacement de réservation" + booking_move_delay: "délai préventif de déplacement" + booking_cancel_enable: "l'activation de l'annulation de réservation" + booking_cancel_delay: "délai préventif d'annulation" + reminder_enable: "l'activation du rappel de réservation" + reminder_delay: "délai avant envoi de la notification de rappel" + default_value_is_24_hours: "Si aucune valeur n'est renseignée : 24 heures." open_api_clients: add_new_client: "Créer un compte client" diff --git a/config/locales/app.logged.en.yml b/config/locales/app.logged.en.yml index 4a1217ce2..b0195a92f 100644 --- a/config/locales/app.logged.en.yml +++ b/config/locales/app.logged.en.yml @@ -59,12 +59,11 @@ en: your_approved_trainings: "Your approved trainings" events: # dashboard: my events - your_next_courses_and_workshops: "Your next courses and workshops" - no_courses_or_workshops_to_come: "No courses or workshops to come" - your_previous_courses_and_workshops: "Your previous courses and workshops" - no_passed_courses_or_workshops: "No passed courses or workshops" - NUMBER_normal_places_reserved: "{NUMBER} {NUMBER, plural, =1{normal place reserved}, other{normal places reserved}}" # messageFormat interpolation - NUMBER_reduced_fare_places_reserved: "{NUMBER} {NUMBER, plural, =1{reduced fare place reserved}, other{reduced fare places reserved}" # messageFormat interpolation + your_next_events: "Your next events" + no_events_to_come: "No events to come" + your_previous_events: "Your previous events" + NUMBER_normal_places_reserved: "{NUMBER} {NUMBER, plural, =0{} =1{normal place reserved} other{normal places reserved}}" # messageFormat interpolation + NUMBER_of_NAME_places_reserved: "{NUMBER} {NUMBER, plural, =0{} =1{of {NAME} place reserved} other{of {NAME} places reserved}}" # messageFormat interpolation invoices: # dashboard: my invoices reference_number: "Reference number" @@ -128,6 +127,8 @@ en: trainings_reserve: # book a training trainings_planning: "Trainings planning" + planning_of: "Planning of" # followed by the training name (eg. "Planning of 3d printer training") + all_trainings: "All trainings" select_a_slot_in_the_calendar: "Select a slot in the calendar" you_ve_just_selected_the_slot: "You've just selected the slot:" datetime_to_time: "{{START_DATETIME}} to {{END_TIME}}" # angular interpolation, eg: Thursday, September 4 1986 8:30 PM to 10:00 PM diff --git a/config/locales/app.logged.fr.yml b/config/locales/app.logged.fr.yml index 7636f11bc..84420ce1c 100644 --- a/config/locales/app.logged.fr.yml +++ b/config/locales/app.logged.fr.yml @@ -59,12 +59,11 @@ fr: your_approved_trainings: "Vos formations validées" events: # tableau de bord : mes évènements - your_next_courses_and_workshops: "Vos prochains stages et ateliers" - no_courses_or_workshops_to_come: "Aucun stage ou atelier à venir" - your_previous_courses_and_workshops: "Vos stages et ateliers passés" - no_passed_courses_or_workshops: "Aucun stage ou atelier passé" - NUMBER_normal_places_reserved: "{NUMBER} {NUMBER, plural, =1{place normale réservée}, other{places normales réservées}}" # messageFormat interpolation - NUMBER_reduced_fare_places_reserved: "{NUMBER} {NUMBER, plural, =1{place réservée à tarif réduit}, other{places réservées à tarif réduit}" # messageFormat interpolation + your_next_events: "Vos prochains évènements" + no_events_to_come: "Aucun évènement à venir" + your_previous_events: "Vos évènements passés" + NUMBER_normal_places_reserved: "{NUMBER} {NUMBER, plural, =0{} =1{place normale réservée} other{places normales réservées}}" # messageFormat interpolation + NUMBER_of_NAME_places_reserved: "{NUMBER} {NUMBER, plural, =0{} =1{place {NAME} réservée} other{places {NAME} réservées}}" # messageFormat interpolation invoices: # tableau de bord : mes factures reference_number: "Référence" @@ -127,7 +126,9 @@ fr: trainings_reserve: # réserver une formation - trainings_planning: "Planning formation" + trainings_planning: "Planning formations" + planning_of: "Planning de la" # suivi du nom de la formation (eg. "Planning de la formation imprimante 3d") + all_trainings: "Toutes les formations" select_a_slot_in_the_calendar: "Sélectionnez un créneau dans le calendrier" you_ve_just_selected_the_slot: "Vous venez de sélectionner le créneau :" datetime_to_time: "{{START_DATETIME}} à {{END_TIME}}" # angular interpolation, eg: Thursday, September 4 1986 8:30 PM to 10:00 PM diff --git a/config/locales/app.public.en.yml b/config/locales/app.public.en.yml index a68216ce7..296d8fa3d 100644 --- a/config/locales/app.public.en.yml +++ b/config/locales/app.public.en.yml @@ -12,8 +12,9 @@ en: my_settings: "My Settings" my_projects: "My Projects" my_trainings: "My Trainings" - my_courses_and_workshops: "My Courses and Workshops" + my_events: "My Events" my_invoices: "My Invoices" + my_wallet: "My Wallet" # login/logout sign_out: "Sign Out" @@ -29,9 +30,10 @@ en: home: "Home" reserve_a_machine: "Reserve a Machine" trainings_registrations: "Trainings registrations" - courses_and_workshops_registrations: "Courses and Workshops registrations" + events_registrations: "Events registrations" projects_gallery: "Projects gallery" subscriptions: "Subscriptions" + public_calendar: "Calendar" # left menu (admin) trainings_monitoring: "Trainings monitoring" @@ -40,7 +42,7 @@ en: manage_the_users: "Manage the Users" manage_the_invoices: "Manage the invoices" subscriptions_and_prices: "Subscriptions and Prices" - courses_and_workshops_monitoring: "Courses and Workshops monitoring" + manage_the_events: "Manage the events" manage_the_machines: "Manage the Machines" manage_the_projects_elements: "Manage the Projects Elements" statistics: "Statistics" @@ -66,6 +68,11 @@ en: type_your_password_again: "Type your password again" password_confirmation_is_required: "Password confirmation is required." password_does_not_match_with_confirmation: "Password does not match with confirmation." + i_am_an_organization: "I am an organization" + name_of_your_organization: "Name of your organization" + organization_name_is_required: "Organization name is required." + address_of_your_organization: "Address of your organization" + organization_address_is_required: "Organization address is required." your_user_s_profile: "Your user's profile" user_s_profile_is_required: "User's profile is required." birth_date: "Birth date" @@ -73,6 +80,7 @@ en: phone_number: "Phone number" phone_number_is_required: "Phone number is required." i_authorize_Fablab_users_registered_on_the_site_to_contact_me: "I authorize FabLab users, registered on the site, to contact me" + i_accept_to_receive_informations_from_the_fablab: "I accept to receive informations from the FabLab" i_ve_read_and_i_accept_: "I've read and I accept" _the_fablab_policy: "the FabLab policy" @@ -92,6 +100,9 @@ en: your_email_address_is_unknown: "Your e-mail address is unknown." you_will_receive_in_a_moment_an_email_with_instructions_to_reset_your_password: "You will receive in a moment, an e-mail with instructions to reset your password." + # Fab-manager's version + version: "Version:" + about: # about page read_the_fablab_policy: "Read the FabLab policy" @@ -108,7 +119,7 @@ en: discover_members: "Discover members" # next events summary on the home page - fablab_s_next_courses_and_workshops: "Fablab's next courses and workshops" + fablab_s_next_events: "Fablab's next events" every_events: "Every events" from_date_to_date: "From {{START}} to {{END}}" # angular interpolation on_the_date: "On the {{DATE}}" # angular interpolation @@ -125,14 +136,12 @@ en: tooltip_openlab_projects_switch: "The search over the whole network lets you search over the projects of every Fab-manager using this feature !" openlab_search_not_available_at_the_moment: "Search over the whole network is not available at the moment. You still can search over the projects of this platform." project_search_result_is_empty: "Sorry, we found no results matching your search criteria." - add_a_project: "Add a project" reset_all_filters: "Reset all filters" search: "Search" all_projects: "All projects" my_projects: "My projects" projects_to_whom_i_take_part_in: "Projects to whom I take part in" all_machines: "All machines" - all_themes: "All themes" all_materials: "All materials" load_next_projects: "Load next projects" @@ -162,7 +171,6 @@ en: # list of machines the_fablab_s_machines: "The FabLab's machines" add_a_machine: "Add a machine" - book: "Book" _or_the_: " or the " machines_show: @@ -171,9 +179,21 @@ en: files_to_download: "Files to download" projects_using_the_machine: "Projects using the machine" _or_the_: " or the " - unauthorized_operation: "Unauthoried operation" + do_you_really_want_to_delete_this_machine: "Do you really want to delete this machine?" + unauthorized_operation: "Unauthorized operation" the_machine_cant_be_deleted_because_it_is_already_reserved_by_some_users: "The machine can't be deleted because it's already reserved by some users." + trainings_list: + # list of trainings + the_trainings: "The trainings" + + training_show: + # details of a training + book_this_training: "Book this training" + do_you_really_want_to_delete_this_training: "Do you really want to delete this training?" + unauthorized_operation: "Unauthorized operation" + the_training_cant_be_deleted_because_it_is_already_reserved_by_some_users: "The training can't be deleted because it's already reserved by some users." + plans: # summary of the subscriptions subcriptions: "Subscriptions" @@ -199,7 +219,9 @@ en: events_list: # Fablab's events list - the_fablab_s_courses_and_workshops: "The Fablab's courses and workshops" + the_fablab_s_events: "The Fablab's events" + all_categories: "All categories" + for_all: "For all" events_show: # details and booking of an event @@ -209,7 +231,6 @@ en: beginning: "Beginning:" ending: "Ending:" opening_hours: "Opening hours:" - reduced_rate*: "Reduced rate*:" tickets_still_availables: "Tickets still available:" sold_out: "Sold out." free_entry: "Free entry" @@ -220,3 +241,8 @@ en: book: "Book" change_the_reservation: "Change the reservation" you_can_shift_this_reservation_on_the_following_slots: "You can shift this reservation on the following slots:" + + calendar: + calendar: "Calendar" + show_no_disponible: "Show the slots no disponibles" + filter-calendar: "Filter calendar" diff --git a/config/locales/app.public.fr.yml b/config/locales/app.public.fr.yml index 7cfa70309..d1a4420b3 100644 --- a/config/locales/app.public.fr.yml +++ b/config/locales/app.public.fr.yml @@ -12,8 +12,9 @@ fr: my_settings: "Mes paramètres" my_projects: "Mes projets" my_trainings: "Mes formations" - my_courses_and_workshops: "Mes stages et ateliers" + my_events: "Mes évènements" my_invoices: "Mes factures" + my_wallet: "Mon porte-monnaie" # connexion / déconnexion sign_out: "Se déconnecter" @@ -29,9 +30,10 @@ fr: home: "Accueil" reserve_a_machine: "Réserver une machine" trainings_registrations: "Inscriptions formations" - courses_and_workshops_registrations: "Inscriptions stages et ateliers" + events_registrations: "Inscriptions aux évènements" projects_gallery: "Galerie de projets" subscriptions: "Abonnements" + public_calendar: "Calendrier" # menu de gauche (partie admin) trainings_monitoring: "Suivi formations" @@ -40,7 +42,7 @@ fr: manage_the_users: "Gérer les utilisateurs" manage_the_invoices: "Gérer les factures" subscriptions_and_prices: "Abonnements & Tarifs" - courses_and_workshops_monitoring: "Suivi stages et ateliers" + manage_the_events: "Gérer les évènements" manage_the_machines: "Gérer les machines" manage_the_projects_elements: "Gérer les éléments projets" statistics: "Statistiques" @@ -66,6 +68,11 @@ fr: type_your_password_again: "Ressaisissez votre mot de passe" password_confirmation_is_required: "La confirmation du mot de passe est requise." password_does_not_match_with_confirmation: "Le mot de passe ne concorde pas avec la confirmation." + i_am_an_organization: "Je suis une structure" + name_of_your_organization: "Nom de votre structure" + organization_name_is_required: "Le nom de la structure est requis." + address_of_your_organization: "Adresse de votre structure" + organization_address_is_required: "L'adresse de la structure est requise." your_user_s_profile: "Votre profil utilisateur" user_s_profile_is_required: "Le profil utilisateur est requis." birth_date: "Date de naissance" @@ -73,6 +80,7 @@ fr: phone_number: "Numéro de téléphone" phone_number_is_required: "Le numéro de téléphone est requis." i_authorize_Fablab_users_registered_on_the_site_to_contact_me: "J'autorise les utilisateurs du Fab Lab inscrits sur le site à me contacter" + i_accept_to_receive_informations_from_the_fablab: "J'accepte de recevoir des informations du Fab Lab" i_ve_read_and_i_accept_: "J'ai lu et j'accepte" _the_fablab_policy: "la charte d'utilisation du Fab Lab" @@ -92,6 +100,8 @@ fr: your_email_address_is_unknown: "Votre adresse de courriel est inconnue." you_will_receive_in_a_moment_an_email_with_instructions_to_reset_your_password: "Vous allez recevoir sous quelques minutes un courriel vous indiquant comment réinitialiser votre mot de passe." + # Fab-manager's version + version: "Version :" about: # page à propos read_the_fablab_policy: "Consulter les règles d'utilisation du Fab Lab" @@ -108,7 +118,7 @@ fr: discover_members: "Découvrir les membres" # résumé des prochains évènements sur la page d'acceuil - fablab_s_next_courses_and_workshops: "Les prochains ateliers et stages du Fab Lab" + fablab_s_next_events: "Les prochains évènements du Fab Lab" every_events: "Tous les évènements" from_date_to_date: "Du {{START}} au {{END}}" # angular interpolation on_the_date: "Le {{DATE}}" # angular interpolation @@ -125,14 +135,12 @@ fr: tooltip_openlab_projects_switch: "La recherche sur tout le réseau vous permet de rechercher parmis les projets de tous les Fab-managers utilisant cette fonctionnalité !" openlab_search_not_available_at_the_moment: "La recherche sur tout le réseau n'est pas disponible pour le moment. Vous pouvez cependant effectuer une recherche parmis les projets de cette plateforme." project_search_result_is_empty: "Il n'y a pas de projets correspondant à vos critères de recherche." - add_a_project: "Ajouter un projet" reset_all_filters: "Réinitialiser tous les filtres" search: "Rechercher" all_projects: "Tous les projets" my_projects: "Mes projets" projects_to_whom_i_take_part_in: "Les projets auxquels je collabore" all_machines: "Toutes les machines" - all_themes: "Toutes les thématiques" all_materials: "Tous les matériaux" load_next_projects: "Charger les projets suivants" @@ -162,7 +170,6 @@ fr: # liste des machines the_fablab_s_machines: "Les machines du FabLab" add_a_machine: "Ajouter une machine" - book: "Réserver" _or_the_: " ou la " machines_show: @@ -171,9 +178,23 @@ fr: files_to_download: "Fichiers à télécharger" projects_using_the_machine: "Projets utilisant la machine" _or_the_: " ou la " + do_you_really_want_to_delete_this_machine: "Êtes-vous sur de vouloir supprimer cette machine ?" unauthorized_operation: "Opération non autorisée" the_machine_cant_be_deleted_because_it_is_already_reserved_by_some_users: "La machine ne peut pas être supprimée car elle a déjà été réservée par des utilisateurs." + + trainings_list: + # liste des formations + the_trainings: "Les formations" + + training_show: + # détails d'une formation + book_this_training: "Réserver cette formation" + do_you_really_want_to_delete_this_training: "Êtes-vous sur de vouloir supprimer cette formation ?" + unauthorized_operation: "Opération non autorisée" + the_training_cant_be_deleted_because_it_is_already_reserved_by_some_users: "La formation ne peut pas être supprimée car elle a déjà été réservée par des utilisateurs." + + plans: # page récapitulative des abonnements subcriptions: "Les abonnements" @@ -199,7 +220,9 @@ fr: events_list: # liste des évènements du fablab - the_fablab_s_courses_and_workshops: "Les Stages et ateliers du Fab Lab" + the_fablab_s_events: "Les évènements du Fab Lab" + all_categories: "Toutes les catégories" + for_all: "Tout public" events_show: # détails d'un événement et réservation @@ -209,7 +232,6 @@ fr: beginning: "Début :" ending: "Fin :" opening_hours: "Horaires :" - reduced_rate*: "Tarif réduit* :" tickets_still_availables: "Places encore disponibles :" sold_out: "Événement complet." free_entry: "Entrée libre" @@ -220,3 +242,8 @@ fr: book: "Réserver" change_the_reservation: "Modifier la réservation" you_can_shift_this_reservation_on_the_following_slots: "Vous pouvez déplacer cette réservation sur les créneaux suivants :" + + calendar: + calendar: "Calendrier" + show_no_disponible: "Afficher les crénaux non disponibles" + filter-calendar: "Filtrer le calendrier" diff --git a/config/locales/app.shared.en.yml b/config/locales/app.shared.en.yml index 3890f616a..bcdbc8723 100644 --- a/config/locales/app.shared.en.yml +++ b/config/locales/app.shared.en.yml @@ -29,6 +29,7 @@ en: confirmation_required: "Confirmation required" description: "Description" machines: "Machines" + events: "Events" materials: "Materials" date: "Date" price: "Price" @@ -43,8 +44,6 @@ en: title: "Title" total_: "TOTAL :" full_price_: "Full price:" - reduced_rate: "Reduced rate" - reduced_rate_: "Reduced rate:" rough_draft: "Rough draft" machines_and_materials: "Machines and materials" collaborators: "Collaborators" @@ -54,7 +53,8 @@ en: confirm_and_pay: "Confirm and pay" your_invoice_will_be_available_soon_from_your_: "Your invoice will be available soon form your" add_an_event: "Add an event" - load_the_next_courses_and_workshops: "Load the next courses and workshops..." + load_the_next_events: "Load the next events..." + no_passed_events: "No passed events" dates: "Dates:" thank_you_your_payment_has_been_successfully_registered: "Thank you. Your payment has been successfully registered !" surname: "Surname" @@ -86,6 +86,20 @@ en: _click_on_the_synchronization_button_opposite_: "click on the synchronization button opposite" _disconnect_then_reconnect_: "disconnect then reconnect" _for_your_changes_to_take_effect: "for your changes to take effect." + add_a_project: "Add a project" + illustration: "Illustration" + add_an_illustration: "Add an illustration." + book: "Book" + description_is_required: "Description is required." + name_is_required: "Name is required." + all_themes: "All themes" + filter: 'Filter' + confirm_payment_of_html: "{ROLE, select, admin{Payment on site} other{Pay}}: {AMOUNT}" # messageFormat interpolation (context: confirm my payment of $20.00) + export_is_running_you_ll_be_notified_when_its_ready: "Export is running. You'll be notified when it's ready." + share_on_facebook: "Share on Facebook" + share_on_twitter: "Share on Twitter" + incomplete_profile: "Incomplete profile" + unlimited: "Unlimited" messages: you_will_lose_any_unsaved_modification_if_you_quit_this_page: "You will lose any unsaved modification if you quit this page" @@ -102,6 +116,8 @@ en: confirmation_of_password_is_required: "Confirmation of password is required." confirmation_of_password_is_too_short_(minimum_8_characters): "Confirmation of password is too short (minimum 8 characters)." confirmation_mismatch_with_password: "Confirmation mismatch with password." + organization_name: "Organization name" + organization_address: "Organization address" date_of_birth: "Date of birth" date_of_birth_is_required: "Date of birth is required." website: "Website" @@ -109,17 +125,17 @@ en: project: # project edition form - name_is_required: "Name is required." illustration: "Illustration" add_an_illustration: "Add an illustration" CAD_file: "CAD file" + allowed_extensions: "Allowed extensions:" add_a_new_file: "Add a new file" - description_is_required: "Description is required." steps: "Steps" step_title: "Step title" add_a_picture: "Add a picture" change_the_picture: "Change the picture" delete_the_step: "Delete the step" + do_you_really_want_to_delete_this_step: "Do you really want to delete this step?" add_a_new_step: "Add a new step" publish_your_project: "Publish your project" employed_materials: "Employed materials" @@ -128,10 +144,6 @@ en: machine: # machine edition form - name_is_required: "The name is required." - illustration: "Illustration" - add_an_illustration: "Add an illustration." - description_is_required: "Description is required." technical_specifications_are_required: "Technical specifications are required." attached_files_(pdf): "Attached files (pdf)" attach_a_file: "Attach a file" @@ -154,7 +166,8 @@ en: i_have_read_and_accept_: "I have read, and accept" _the_general_terms_and_conditions: "the general terms and conditions." enter_your_card_number: "Enter your card number" - confirm_my_payment_of_: "Confirm my payment of" # context: confirm my payment of $20.00 + credit_amount_for_pay_reservation: "{{amount}} {{currency}} remains to be paid to confirm your reservation" + client_credit_amount_for_pay_reservation: "{{amount}} {{currency}} remains to be paid to confirm reservation of client" valid_reservation_modal: # dialog of on site payment for reservations @@ -162,11 +175,10 @@ en: here_is_the_summary_of_the_slots_to_book_for_the_current_user: "Here is the summary of the slots to book for the current user:" event: - # event edition form (courses/workshops) + # event edition form title_is_required: "Title is required." matching_visual: "Matching visual" choose_a_picture: "Choose a picture" - description_is_required: "Description is required." attachments: "Attachments" add_a_new_file: "Add a new file" event_type: "Event type" @@ -182,11 +194,12 @@ en: standard_rate: "Standard rate" 0_=_free: "0 = free" tickets_available: "Tickets available" + event_theme: "Event theme" + age_range: "Age range" plan: # subscription plan edition form general_informations: "General informations" - name_is_required: "Name is required." name_length_must_be_less_than_24_characters: "Name length must be less than 24 characters." type_is_required: "Type is required." group: "Group" @@ -211,8 +224,18 @@ en: new_partner: "New partner" email_address_is_required: "Email address is required." + trainings: + # training edition form + add_a_new_training: "Add a new training" + validate_your_training: "Validate your training" + associated_machines: "Associated machines" + number_of_tickets: "Number of tickets" + public_page: "Show in training lists" + user_admin: # partial form to edit/create an user (admin view) + user_profile: "Profil utilisateur" + warning_incomplete_user_profile_probably_imported_from_sso: "Warning: This user's profile is incomplete. As \"single sign-on\" (SSO) authentication is currently enabled, it may probably be an imported but non merged account. Do not modify it unless you know what your doing." group: "Group" group_is_required: "Group is required" disable_invoices_generation: "Disable invoices generation:" @@ -226,6 +249,10 @@ en: provider_name_is_required: "Provider name is required." authentication_type: "Authentication type" authentication_type_is_required: "Authentication type is required." + data_mapping: "Data mapping" + expected_data_type: "Expected data type" + input_format: "Input format" + mappings: "Mappings" oauth2: # edition/creation form of an OAuth2 authentication provider @@ -281,4 +308,64 @@ en: no_projects: "No projects" author: "Author" collaborator: "Collaborator" - private_profile: "Private profile" \ No newline at end of file + private_profile: "Private profile" + + wallet: + # wallet + wallet: 'Wallet' + your_wallet_amount: 'Your amount available' + wallet_amount: 'Amount available' + no_transactions_for_now: 'No transactions for now' + operation: 'Operation' + operator: 'Operator' + amount: 'Amount' + credit: 'Credit' + debit: 'Debit' + credit_title: 'Credit wallet' + credit_label: 'Set the amount to be credited' + confirm_credit_label: 'Confirm the amount to be credited' + to_credit: 'Credit' + wallet_credit_successfully: "Wallet of user is credited successfully." + a_problem_occurred_for_wallet_credit: "A problem is occurred while taking the credit of wallet" + amount_is_required: "The amount is required" + amount_minimum_1: "The amount minimum is 1" + amount_confirm_is_required: "The amount confirmation is required" + amount_confirm_does_not_match: "The amount confirmation does not match" + you_have_amount_in_wallet: "You have {{amount}} {{currency}} in your wallet" + client_have_amount_in_wallet: "Client has {{amount}} {{currency}} in wallet" + wallet_pay_reservation: "You can pay direct your reservation" + client_wallet_pay_reservation: "Client can pay direct reservation" + debit_subscription: "Debit by subscription" + debit_reservation_training: "Debit by reservation of training" + debit_reservation_machine: "Debit by reservation of machine" + debit_reservation_event: "Debit by reservation of event" + warning_uneditable_credit: "Warning: once validated, the credited amount won't be editable anymore." + + coupon: + # promotional coupon (creation/edition form) + code: "Code" + code_is_required: "Code is required." + code_must_be_composed_of_capital_letters_digits_and_or_dashes: "The code must be composed of capital letters, digits and/or dashes." + percent_off: "Percentage off" + percent_off_is_required: "Percentage off is required." + percentage_must_be_between_0_and_100: "Percentage must be between 0 and 100." + validity_per_user: "Validity per user" + once: "Just once" + forever: "Each use" + validity_per_user_is_required: "Validity per user is required." + valid_until: "Valid until (included)" + leave_empty_for_no_limit: "Do not specify any limit by leaving the field empty." + max_usages: "Maximum usages allowed" + max_usages_must_be_equal_or_greater_than_0: "The maximum usages allowed must be greater than 0." + enabled: "Active" + + coupon_input: + # coupon (input zone for users) + i_have_a_coupon: "I have a coupon!" + code_: "Code:" + the_coupon_has_been_applied_you_get_PERCENT_discount: "The coupon has been applied. You get {{PERCENT}}% discount." # angular interpolation + unable_to_apply_the_coupon_because_disabled: "Unable to apply the coupon: this code was disabled." + unable_to_apply_the_coupon_because_expired: "Unable to apply the coupon: this code has expired." + unable_to_apply_the_coupon_because_sold_out: "Unable to apply the coupon: this code reached its quota." + unable_to_apply_the_coupon_because_already_used: "Unable to apply the coupon: you have already used this code once before." + unable_to_apply_the_coupon_because_rejected: "This code does not exists." \ No newline at end of file diff --git a/config/locales/app.shared.fr.yml b/config/locales/app.shared.fr.yml index 5cd493971..e6ce0c8ea 100644 --- a/config/locales/app.shared.fr.yml +++ b/config/locales/app.shared.fr.yml @@ -29,6 +29,7 @@ fr: confirmation_required: "Confirmation requise" description: "Description" machines: "Machines" + events: "Évènements" materials: "Matériaux" date: "Date" price: "Prix" @@ -43,8 +44,6 @@ fr: title: "Titre" total_: "TOTAL :" full_price_: "Plein tarif :" - reduced_rate: "Tarif réduit" - reduced_rate_: "Tarif réduit :" rough_draft: "Brouillon" machines_and_materials: "Machines et matériaux" collaborators: "Les collaborateurs" @@ -54,7 +53,8 @@ fr: confirm_and_pay: "Valider et payer" your_invoice_will_be_available_soon_from_your_: "Votre facture sera bientôt disponible depuis votre" add_an_event: "Ajouter un évènement" - load_the_next_courses_and_workshops: "Charger les stages et ateliers suivants ..." + load_the_next_events: "Charger les évènements suivants ..." + no_passed_events: "Aucun évènement passé" dates: "Dates :" thank_you_your_payment_has_been_successfully_registered: "Merci. Votre paiement a bien été pris en compte !" surname: "Nom" @@ -86,6 +86,20 @@ fr: _click_on_the_synchronization_button_opposite_: "cliquez sur le bouton de synchronisation ci-contre" _disconnect_then_reconnect_: "déconnectez-vous puis re-connectez vous" _for_your_changes_to_take_effect: "pour que les modifications soient prises en compte." + add_a_project: "Ajouter un projet" + illustration: "Visuel" + add_an_illustration: "Ajouter un visuel" + book: "Réserver" + description_is_required: "La description est requise." + name_is_required: "Le nom est requis." + all_themes: "Toutes les thématiques" + filter: 'Filtre' + confirm_payment_of_html: "{ROLE, select, admin{Paiement sur place} other{Payer}} : {AMOUNT}" # messageFormat interpolation (contexte : valider mon paiement de 20,00 €) + export_is_running_you_ll_be_notified_when_its_ready: "L'export est en cours. Vous serez notifié lorsqu'il sera prêt." + share_on_facebook: "Partager sur Facebook" + share_on_twitter: "Partager sur Twitter" + incomplete_profile: "Profil incomplet" + unlimited: "Illimité" messages: you_will_lose_any_unsaved_modification_if_you_quit_this_page: "Vous perdrez les modifications non enregistrées si vous quittez cette page" @@ -102,6 +116,8 @@ fr: confirmation_of_password_is_required: "La confirmation du mot de passe est requise." confirmation_of_password_is_too_short_(minimum_8_characters): "La confirmation du mot de passe est trop courte (au moins 8 caractères)." confirmation_mismatch_with_password: "La confirmation ne concorde pas avec le mot de passe." + organization_name: "Nom de la structure" + organization_address: "Adresse de la structure" date_of_birth: "Date de naissance" date_of_birth_is_required: "La date de naissance est requise." website: "Site web" @@ -109,17 +125,17 @@ fr: project: # formulaire d'étition d'un projet - name_is_required: "Le nom est requis." illustration: "Illustration" add_an_illustration: "Ajouter un visuel" CAD_file: "Fichier CAO" + allowed_extensions: "Extensions autorisées :" add_a_new_file: "Ajouter un nouveau fichier" - description_is_required: "La description est requise." steps: "Étapes" step_title: "Titre de l'étape" add_a_picture: "Ajouter une image" change_the_picture: "Modifier l'image" delete_the_step: "Supprimer l'étape" + do_you_really_want_to_delete_this_step: "Êtes-vous sur de vouloir supprimer cette étape ?" add_a_new_step: "Ajouter une nouvelle étape" publish_your_project: "Publier votre projet" employed_materials: "Matériaux utilisés" @@ -128,10 +144,6 @@ fr: machine: # formulaire d'édition d'une machine - name_is_required: "Le nom est requis." - illustration: "Visuel" - add_an_illustration: "Ajouter un visuel" - description_is_required: "La description est requise." technical_specifications_are_required: "Les caractéristiques techniques sont requises." attached_files_(pdf): "Pièces jointes (pdf)" attach_a_file: "Joindre un fichier" @@ -154,7 +166,8 @@ fr: i_have_read_and_accept_: "J'ai bien pris connaissance, et accepte" _the_general_terms_and_conditions: "les conditions générales de vente." enter_your_card_number: "Saisissez votre numéro de carte" - confirm_my_payment_of_: "Valider mon paiement de" # contexte : valider mon paiement de 20,00 € + credit_amount_for_pay_reservation: "Il vous reste {{amount}} {{currency}} à payer pour valider votre réservation" + client_credit_amount_for_pay_reservation: "Il reste {{amount}} {{currency}} à payer pour valider la réservation" valid_reservation_modal: # fenêtre de paiement sur place d'une réservation @@ -162,11 +175,10 @@ fr: here_is_the_summary_of_the_slots_to_book_for_the_current_user: "Voici le récapitulatif des créneaux à réserver pour l'utilisateur courant :" event: - # formulaire d'édition d'un événement (stage/atelier) + # formulaire d'édition d'un événement title_is_required: "Le titre est requis." matching_visual: "Visuel associé" choose_a_picture: "Choisir une image" - description_is_required: "La description est requise." attachments: "Pièces jointes" add_a_new_file: "Ajouter un nouveau fichier" event_type: "Type d'évènement" @@ -182,11 +194,12 @@ fr: standard_rate: "Tarif standard" 0_=_free: "0 = gratuit" tickets_available: "Places disponibles" + event_theme: "Thème de l'évènement" + age_range: "Tranche d'âge" plan: # formulaire d'édition d'une formule d'abonnement general_informations: "Informations générales" - name_is_required: "Le nom est requis." name_length_must_be_less_than_24_characters: "Le nom doit faire moins de 24 caractères." type_is_required: "Le type est requis." group: "Groupe" @@ -211,8 +224,18 @@ fr: new_partner: "Nouveau partenaire" email_address_is_required: "L'adresse e-mail est requise." + trainings: + # formulaire d'édition d'une formation + add_a_new_training: "Ajouter une nouvelle formation" + validate_your_training: "Valider votre formation" + associated_machines: "Machines associées" + number_of_tickets: "Nombre de places" + public_page: "Afficher dans la liste de formation" + user_admin: # formulaire partiel d'édition/création utilisateur (vue admin) + user_profile: "Profil utilisateur" + warning_incomplete_user_profile_probably_imported_from_sso: "Attention : Le profil de cet utilisateur est incomplet. Comme l'authentification \"single sign-on\" (SSO) est actuellement activée, il s'agit probablement d'un compte importé mais non fusionné. N'y apportez aucune modification sauf si vous savez ce que vous faites." group: "Groupe" group_is_required: "Le groupe est requis." disable_invoices_generation: "Désactiver la génération de factures :" @@ -226,6 +249,10 @@ fr: provider_name_is_required: "Le nom du fournisseur est requis." authentication_type: "Type d'authentification" authentication_type_is_required: "Le type d'authentification est requis." + data_mapping: "Correspondance des données" + expected_data_type: "Type de données attendues" + input_format: "Format d'entrée" + mappings: "Correspondances" oauth2: # formulaire d'édition/création d'un fournisseur d'authentification de type OAuth2 @@ -282,3 +309,63 @@ fr: author: "Auteur" collaborator: "Collaborateur" private_profile: "Profil privé" + + wallet: + # porte-monnaie + wallet: 'Porte-monnaie' + your_wallet_amount: 'Votre montant disponible' + wallet_amount: 'Montant disponible' + no_transactions_for_now: 'Aucune transaction pour le moment' + operation: 'Opération' + operator: 'Opérateur' + amount: 'Montant' + credit: 'Crédit' + debit: 'Débit' + credit_title: 'Créditer le porte-monnaie' + credit_label: 'Indiquez le montant à créditer' + confirm_credit_label: 'Confirmez le montant à créditer' + to_credit: 'Créditer' + wallet_credit_successfully: "Le porte-monnaie de l'utilisateur a été chargé avec succès." + a_problem_occurred_for_wallet_credit: "Un problème est survenu lors du chargement du porte-monnaie." + amount_is_required: "Le montant est requis." + amount_minimum_1: "Le montant minimum est de 1" + amount_confirm_is_required: "La confirmation du montant est requise." + amount_confirm_does_not_match: "La confirmation du montant ne correspond pas." + you_have_amount_in_wallet: "Vous avez {{amount}} {{currency}} sur votre porte-monnaie" + client_have_amount_in_wallet: "Le client a {{amount}} {{currency}} sur son porte-monnaie" + wallet_pay_reservation: "Vous pouvez effectuer directement votre paiement de réservation" + client_wallet_pay_reservation: "Le client pouvez effectuer directement son paiement de réservation" + debit_subscription: "Payer un abonnement" + debit_reservation_training: "Payer un reservation de formation" + debit_reservation_machine: "Payer un reservation de machine" + debit_reservation_event: "Payer un reservation d'évenement" + warning_uneditable_credit: "Attention : une fois validé, le montant crédité ne sera plus modifiable." + + coupon: + # code promotionnel (formulaire de création/édition) + code: "Code" + code_is_required: "Le code est requis." + code_must_be_composed_of_capital_letters_digits_and_or_dashes: "Le code doit être composé de lettres majuscules, de chiffres et/ou de tirets." + percent_off: "Pourcentage de réduction" + percent_off_is_required: "Le pourcentage de réduction est requis." + percentage_must_be_between_0_and_100: "Le pourcentage doit être compris entre 0 et 100." + validity_per_user: "Validité par utilisateur" + once: "Une seule fois" + forever: "À chaque utilisation" + validity_per_user_is_required: "La validité par utilisateur est requise." + valid_until: "Valable jusqu'au (inclus)" + leave_empty_for_no_limit: "Laissez vide pour ne pas spécifier de limite." + max_usages: "Nombre maximum d'utilisations autorisées" + max_usages_must_be_equal_or_greater_than_0: "Le nombre d'utilisations maximum doit être supérieur ou égal à 0." + enabled: "Activé" + + coupon_input: + # code promotionnel (zone de saisie pour les utilisateurs) + i_have_a_coupon: "J'ai un code promo !" + code_: "Code :" + the_coupon_has_been_applied_you_get_PERCENT_discount: "Le code promo a bien été appliqué. Vous bénéficiez d'une remise de {{PERCENT}} %." # angular interpolation + unable_to_apply_the_coupon_because_disabled: "Impossible d'appliquer la réduction : ce code promo a été désactivé." + unable_to_apply_the_coupon_because_expired: "Impossible d'appliquer la réduction : ce code promo a expiré." + unable_to_apply_the_coupon_because_sold_out: "Impossible d'appliquer la réduction : ce code promo a atteint son quota." + unable_to_apply_the_coupon_because_already_used: "Impossible d'appliquer la réduction : vous avez déjà utilisé ce code promo par le passé." + unable_to_apply_the_coupon_because_rejected: "Ce code promo n'existe pas." \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index bb12272d4..87e55c020 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -36,6 +36,9 @@ en: errors: <<: *errors + apipie: + api_documentation: "API Documentation" + omniauth: # error messages when importing an account from a SSO email_already_linked_to_another_account_please_input_your_authentication_code: "E-mail address \"%{OLD_MAIL}\" is already linked to another account, please input your authentication code." @@ -75,14 +78,15 @@ en: subscription_NAME_from_START_to_END: "Subscription - From %{START} to %{END}" machine_reservation_DESCRIPTION: "Machine reservation - %{DESCRIPTION}" training_reservation_DESCRIPTION: "Training reservation - %{DESCRIPTION}" - courses_and_workshops_reservation_DESCRIPTION: "Courses and Workshops reservation - %{DESCRIPTION}" + event_reservation_DESCRIPTION: "Event reservation - %{DESCRIPTION}" full_price_ticket: one: "One full price ticket" other: "%{count} full price tickets" - reduced_rate_ticket: - one: "One reduced rate ticket" - other: "%{count} reduced rate tickets" + other_rate_ticket: + one: "One %{NAME} ticket" + other: "%{count} %{NAME} tickets" reservation_other: "Reservation (other)" + coupon_CODE_discount_of_PERCENT: "Coupon %{CODE}: discount of %{PERCENT}%" total_including_all_taxes: "Total incl. all taxes" including_VAT_RATE: "Including VAT %{RATE}%" including_total_excluding_taxes: "Including Total excl. taxes" @@ -94,14 +98,17 @@ en: by_transfer: "by transfer" by_cash: "by cash" no_refund: "No refund" + by_wallet: "by wallet" settlement_by_debit_card: "Settlement by debit card" settlement_done_at_the_reception: "Settlement done at the reception" + settlement_by_wallet: "Settlement by wallet" on_DATE_at_TIME: "on %{DATE} at %{TIME}," for_an_amount_of_AMOUNT: "for an amount of %{AMOUNT}" on_DATE_from_START_to_END: "On %{DATE} from %{START} to %{END}" # eg: on feb. 7 from 7AM to 9AM from_STARTDATE_to_ENDDATE_from_STARTTIME_to_ENDTIME: "From %{STARTDATE} to %{ENDDATE}, from %{STARTTIME} to %{ENDTIME}" # eg: from feb. 7 to feb. 10, from 6PM to 10PM subscription_of_NAME_for_DURATION_starting_from_DATE: "Subscription of %{NAME} for %{DURATION} starting from %{DATE}" subscription_of_NAME_extended_starting_from_STARTDATE_until_ENDDATE: "Subscription of %{NAME} extended (Free days) starting from %{STARTDATE} until %{ENDDATE}" + and: 'and' trainings: # training availabilities @@ -110,23 +117,39 @@ en: export_members: # members list export to EXCEL format + members: "Members" id: "ID" surname: "Surname" first_name: "First name" email: "E-mail" + newsletter: "Newsletter" gender: "Gender" age: "Age" + address: "Address" phone: "Phone" + website: "Website" + job: "Job" + interests: "Interests" + cad_software_mastered: "CAD Softwares mastered" group: "Group" subscription: "Subscription" subscription_end_date: "Subscription end date" validated_trainings: "Validated trainings" + tags: "Tags" + number_of_invoices: "Number of invoices" + projects: "Projects" + facebook: "Facebook" + twitter: "Twitter" + echo_sciences: "Echosciences" + organization: "Organization" + organization_address: "Organization address" man: "Man" woman: "Woman" without_subscriptions: "Without subscriptions" export_reservations: # machines/trainings/events reservations list to EXCEL format + reservations: "Reservations" customer_id: "Customer ID" customer: "Customer" email: "E-mail" @@ -140,6 +163,7 @@ en: export_subscriptions: # subscriptions list export to EXCEL format + subscriptions: "Subscriptions" id: "ID" customer: "Customer" email: "E-mail" @@ -189,6 +213,8 @@ en: a_new_user_account_has_been_imported_from_PROVIDER_(UID)_html: "A new user account has been imported from: %{PROVIDER} (%{UID})." notify_member_create_reservation: your_reservation_RESERVABLE_was_successfully_saved_html: "Your reservation %{RESERVABLE} was successfully saved." + notify_member_reservation_reminder: + reminder_you_have_a_reservation_RESERVABLE_to_be_held_on_DATE_html: "Reminder: You have a reservation %{RESERVABLE} to be held on %{DATE}" notify_member_slot_is_canceled: your_reservation_RESERVABLE_of_DATE_was_successfully_cancelled: "Your reservation %{RESERVABLE} of %{DATE} was successfully cancelled." notify_member_slot_is_modified: @@ -225,4 +251,73 @@ en: your_invoice_is_ready_html: "Your invoice #%{REFERENCE}, of %{AMOUNT} is ready. Click here to download." undefined_notification: unknown_notification: "Unknown notification" - notification_ID_wrong_type_TYPE_unknown: "Notification %{ID} wrong (type %{TYPE} unknown)" \ No newline at end of file + notification_ID_wrong_type_TYPE_unknown: "Notification %{ID} wrong (type %{TYPE} unknown)" + notify_user_wallet_is_credited: + your_wallet_is_credited: "Your wallet has been credited by administrator" + notify_admin_user_wallet_is_credited: + wallet_is_credited: "The wallet of member %{USER} has been credited %{AMOUNT}" + notify_admin_export_complete: + export: "The export" + statistics_global: "of all the statistics" + statistics_account: "of the registration statistics" + statistics_event: "of statistics about events" + statistics_machine: "of statistics about machine hours" + statistics_project: "of statistics about projects" + statistics_subscription: "of subscription statistics" + statistics_training: "of statistics about trainings" + users_members: "of the members' list" + users_subscriptions: "of the subscriptions' list" + users_reservations: "of the reservations' list" + is_over: "is over." + download_here: "Download here" + notify_member_about_coupon: + enjoy_a_discount_of_PERCENT_with_code_CODE: "Enjoy a discount of %{PERCENT}% with code %{CODE}" + + statistics: + # statistics tools for admins + subscriptions: "Subscriptions" + machines_hours: "Machines hours" + trainings: "Trainings" + events: "Events" + registrations: "Registrations" + projects: "Projects" + users: "Users" + training_id: "Training ID" + training_date: "Training Date" + event_id: "Event ID" + event_date: "Event Date" + event_name: "Event Name" + event_theme: "Theme" + age_range: "Age Range" + themes: "Themes" + components: "Components" + machines: "Machines" + user_id: "User ID" + bookings: "Bookings" + hours_number: "Hours number" + tickets_number: "Tickets number" + revenue: "Revenue" + account_creation: "Account creation" + project_publication: "Project publication" + + export: + # statistics exports to the excel file format + entries: "Entries" + revenue: "Revenue" + average_age: "Average Age" + total: "Total" + date: "Date" + user: "User" + email: "Email" + phone: "Phone" + gender: "Gender" + age: "Age" + type: "Type" + revenue: "Revenue" + male: "Man" + female: "Woman" + + price_category: + # initial price's category for events, created to replace the old "reduced amount" property + reduced_fare: "Reduced fare" + reduced_fare_if_you_are_under_25_student_or_unemployed: "Reduced fare if you are under 25, student or unemployed." \ No newline at end of file diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 6ee961dd9..1b6629cc1 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -36,6 +36,9 @@ fr: errors: <<: *errors + apipie: + api_documentation: "Documentation de l'API" + omniauth: # messages d'erreur lors de l'import d'un compte depuis un SSO email_already_linked_to_another_account_please_input_your_authentication_code: "L'adresse de courriel \"%{OLD_MAIL}\" est déjà associée à un compte utilisateur, merci de saisir votre code d'authentification." @@ -75,14 +78,15 @@ fr: subscription_NAME_from_START_to_END: "Abonnement - Du %{START} au %{END}" machine_reservation_DESCRIPTION: "Réservation Machine - %{DESCRIPTION}" training_reservation_DESCRIPTION: "Réservation Formation - %{DESCRIPTION}" - courses_and_workshops_reservation_DESCRIPTION: "Réservation Ateliers et Stages - %{DESCRIPTION}" + event_reservation_DESCRIPTION: "Réservation Évènement - %{DESCRIPTION}" full_price_ticket: one: "Une place plein tarif" other: "%{count} places plein tarif" - reduced_rate_ticket: - one: "Une place à tarif réduit" - other: "%{count} places à tarif réduit" + other_rate_ticket: + one: "Une place %{NAME}" + other: "%{count} places %{NAME}" reservation_other: "Réservation (autre)" + coupon_CODE_discount_of_PERCENT: "Code %{CODE} : remise de %{PERCENT} %" total_including_all_taxes: "Total TTC" including_VAT_RATE: "Dont TVA %{RATE}%" including_total_excluding_taxes: "Dont total HT" @@ -93,15 +97,18 @@ fr: by_cheque: "par chèque" by_transfer: "par virement" by_cash: "en espèces" + by_wallet: "par porte-monnaie" no_refund: "Pas de remboursement" settlement_by_debit_card: "Règlement effectué par carte bancaire" settlement_done_at_the_reception: "Règlement effectué à l'accueil" + settlement_by_wallet: "Règlement effectué par porte-monnaie" on_DATE_at_TIME: "le %{DATE} à %{TIME}," for_an_amount_of_AMOUNT: "pour un montant de %{AMOUNT}" on_DATE_from_START_to_END: "Le %{DATE} de %{START} à %{END}" # eg: on feb. 7 from 7AM to 9AM from_STARTDATE_to_ENDDATE_from_STARTTIME_to_ENDTIME: "Du %{STARTDATE} au %{ENDDATE}, de %{STARTTIME} à %{ENDTIME}" # eg: from feb. 7 to feb. 10, from 6PM to 10PM subscription_of_NAME_for_DURATION_starting_from_DATE: "Abonnement de %{NAME} pour %{DURATION} à compter du %{DATE}" subscription_of_NAME_extended_starting_from_STARTDATE_until_ENDDATE: "Prolongement Abonnement (Jours gratuits) de %{NAME} à compter du %{STARTDATE} jusqu'au %{ENDDATE}" + and: 'et' trainings: # disponibilités formations @@ -110,23 +117,39 @@ fr: export_members: # export de la liste des members au format EXCEL + members: "Membres" id: "ID" surname: "Nom" first_name: "Prénom" email: "Courriel" + newsletter: "Lettre d'informations" gender: "Genre" age: "Âge" + address: "Adresse" phone: "Tel." + website: "Site web" + job: "Profession" + interests: "Centres d'intérêts" + cad_software_mastered: "Logiciels de conception maîtrisés" group: "Groupe" subscription: "Abonnement" subscription_end_date: "Date de fin de l'abonnement" validated_trainings: "Formations validées" + tags: "Étiquettes" + number_of_invoices: "Nombre de factures" + projects: "Projets" + facebook: "Facebook" + twitter: "Twitter" + echo_sciences: "Echosciences" + organization: "Structure" + organization_address: "Adresse de la structure" man: "Homme" woman: "Femme" without_subscriptions: "Sans Abonnement" export_reservations: # export de la liste des réservations machines/formations/évènements au format EXCEL + reservations: "Réservations" customer_id: "ID client" customer: "Client" email: "Courriel" @@ -140,6 +163,7 @@ fr: export_subscriptions: # export de la liste des abonnements au format EXCEL + subscriptions: "Abonnements" id: "ID" customer: "Client" email: "Courriel" @@ -189,6 +213,8 @@ fr: a_new_user_account_has_been_imported_from_PROVIDER_(UID)_html: "Un nouveau compte utilisateur vient d'être importé depuis : %{PROVIDER} (%{UID})." notify_member_create_reservation: your_reservation_RESERVABLE_was_successfully_saved_html: "Votre réservation %{RESERVABLE} a bien été enregistrée." + notify_member_reservation_reminder: + reminder_you_have_a_reservation_RESERVABLE_to_be_held_on_DATE_html: "Rappel : Vous avez une réservation %{RESERVABLE} qui aura lieu le %{DATE}" notify_member_slot_is_canceled: your_reservation_RESERVABLE_of_DATE_was_successfully_cancelled: "Votre réservation %{RESERVABLE} du %{DATE} a bien été annulée." notify_member_slot_is_modified: @@ -225,4 +251,73 @@ fr: your_invoice_is_ready_html: "Votre facture n°%{REFERENCE}, d'un montant de %{AMOUNT}, est prête. Cliquez ici pour la télécharger." undefined_notification: unknown_notification: "Notification inconnue" - notification_ID_wrong_type_TYPE_unknown: "Notification {ID} erronée (type {TYPE} inconnu)." \ No newline at end of file + notification_ID_wrong_type_TYPE_unknown: "Notification {ID} erronée (type {TYPE} inconnu)." + notify_user_wallet_is_credited: + your_wallet_is_credited: "Votre porte-monnaie a bien été crédité de %{AMOUNT} par l'administrateur" + notify_admin_user_wallet_is_credited: + wallet_is_credited: "Le porte-monnaie du membre %{USER} a bien été crédité de %{AMOUNT}" + notify_admin_export_complete: + export: "L'export" + statistics_global: "de toutes les statistiques" + statistics_account: "des statistiques d'inscriptions" + statistics_event: "des statistiques sur les évènements" + statistics_machine: "des statistiques d'heures machines" + statistics_project: "des statistiques sur les projets" + statistics_subscription: "des statistiques d'abonnements" + statistics_training: "des statistiques sur les formations" + users_members: "de la liste des membres" + users_subscriptions: "de la liste des abonnements" + users_reservations: "de la liste des réservations" + is_over: "est terminé." + download_here: "Téléchargez ici" + notify_member_about_coupon: + enjoy_a_discount_of_PERCENT_with_code_CODE: "Bénéficiez d'une remise de %{PERCENT} % avec le code %{CODE}" + + statistics: + # outil de statistiques pour les administrateurs + subscriptions: "Abonnements" + machines_hours: "Heures machines" + trainings: "Formations" + events: "Évènements" + registrations: "Inscriptions" + projects: "Projets" + users: "Utilisateurs" + training_id: "ID Formation" + training_date: "Date Formation" + event_id: "ID Évènement" + event_date: "Date Évènement" + event_name: "Nom Évènement" + event_theme: "Thématique" + age_range: "Tranche d'âge" + themes: "Thèmes" + components: "Composants" + machines: "Machines" + user_id: "ID Utilisateur" + bookings: "Réservations" + hours_number: "Nombre d'heures" + tickets_number: "Nombre de places" + revenue: "Chiffre d'affaires" + account_creation: "Création de compte" + project_publication: "Publication de projet" + + export: + # export des statistiques au format excel + entries: "Entrées" + revenue: "Chiffre d'affaires" + average_age: "Âge moyen" + total: "Total" + date: "Date" + user: "Utilisateur" + email: "Courriel" + phone: "Téléphone" + gender: "Genre" + age: "Âge" + type: "Type" + revenue: "Chiffre d'affaires" + male: "Homme" + female: "Femme" + + price_category: + # catégorie initiale de prix pour les évènements, en remplacement de l'ancienne propriété "montant réduit" + reduced_fare: "Tarif réduit" + reduced_fare_if_you_are_under_25_student_or_unemployed: "Tarif réduit si vous avez moins de 25 ans, que vous êtes étudiant ou demandeur d'emploi." \ No newline at end of file diff --git a/config/locales/mails.en.yml b/config/locales/mails.en.yml index 8ec076374..e9e2e6071 100644 --- a/config/locales/mails.en.yml +++ b/config/locales/mails.en.yml @@ -1,6 +1,6 @@ en: layouts: - notifications_mailer: + notifications_mailer: see_you_later: "See you soon on {GENDER, select, other{the}}" # messageFormat interpolation sincerely: "Sincerely," signature: "The Fab Lab team." @@ -116,6 +116,7 @@ en: subject: "A user account has been created" body: new_account_created: "A new user account has been created on the website:" + account_for_organization: "This account manage an organization:" invoicing_disabled_html: "Invoicing generation is disabled for this user." notify_admin_subscribed_plan: @@ -130,6 +131,12 @@ en: invoice_in_your_dashboard_html: "You can access your invoice in %{DASHBOARD} on the Fab Lab website." your_dashboard: "your dashboard" + notify_member_reservation_reminder: + subject: "Reservation reminder" + body: + this_is_a_reminder_about_your_reservation_RESERVABLE_to_be_held_on_DATE_html: "This is a reminder about your reservation %{RESERVABLE} to be held on %{DATE}" + this_reservation_concerns_the_following_slots: "This reservation concerns the following slots:" + notify_member_avoir_ready: subject: "Your FabLab's refund invoice" body: @@ -207,7 +214,7 @@ en: is_changing_its_auth_system_and_will_now_use: "is actually changing its user identification system and will use" instead_of: "instead of" consequence_of_the_modification: "Because of this change you won't be able to login to the website with your actual usernames" - to_use_the_platform_thanks_for: "to keep on using the website, please" + to_use_the_platform_thanks_for: "To keep on using the website, please" create_an_account_on: "create an account on" or_use_an_existing_account_clicking_here: "or use an existing account by clicking here" in_case_of_problem_enter_the_following_code: "In case of problem with this link, you can enter the following code at your first connection attempt in order to migrate your actual account into the new authentification system:" @@ -240,5 +247,38 @@ en: disabled: "From now on, no invoice will be issued when the user pays at the reception." enabled: "From now on, all payments made by this user at the reception will lead to invoicing issuing. " + notify_user_wallet_is_credited: + subject: "Your wallet has been credited" + body: + wallet_credit_html: "Your wallet has been credited %{AMOUNT} by administrator." + + notify_admin_user_wallet_is_credited: + subject: "The wallet of an user has been credited" + body: + wallet_credit_html: "The wallet of member %{USER} has been credited %{AMOUNT} by administrator %{ADMIN}." + + notify_admin_export_complete: + subject: "Export completed" + body: + you_asked_for_an_export: "You asked for an export" + statistics_global: "of all the statistics" + statistics_account: "of the registration statistics" + statistics_event: "of statistics about events" + statistics_machine: "of statistics about machine hours" + statistics_project: "of statistics about projects" + statistics_subscription: "of subscription statistics" + statistics_training: "of statistics about trainings" + users_members: "of the members' list" + users_subscriptions: "of the subscriptions' list" + users_reservations: "of the reservations' list" + click_to_download: "Excel file generated successfully. To download it, click" + here: "here" + + notify_member_about_coupon: + subject: "Coupon" + body: + enjoy_a_discount_of_PERCENT_with_code_CODE: "Enjoy a discount of %{PERCENT}% on the whole site with the code %{CODE}." + this_coupon_is_valid_USAGE_times_until_DATE_for_all_your_purchases: "This coupon is valid {USAGE, plural, =1{just once} other{many times}}: for all your purchases, from now {DATE, select, NO-DATE{and without time limit} other{and until {DATE}}}." + shared: hello: "Hello %{user_name}" diff --git a/config/locales/mails.fr.yml b/config/locales/mails.fr.yml index 2f18c1ac3..eda12e67d 100644 --- a/config/locales/mails.fr.yml +++ b/config/locales/mails.fr.yml @@ -116,6 +116,7 @@ fr: subject: "Un compte utilisateur a été créé" body: new_account_created: "Un nouveau compte utilisateur vient d'être créé sur la plateforme :" + account_for_organization: "Ce compte gère une structure :" invoicing_disabled_html: "La génération de factures est désactivée pour cet utilisateur." notify_admin_subscribed_plan: @@ -130,6 +131,12 @@ fr: invoice_in_your_dashboard_html: "Vous pouvez à tout moment retrouver votre facture dans %{DASHBOARD} sur le site du Fab Lab." your_dashboard: "votre tableau de bord" + notify_member_reservation_reminder: + subject: "Rappel de réservation" + body: + this_is_a_reminder_about_your_reservation_RESERVABLE_to_be_held_on_DATE_html: "Ceci est un rappel concernant votre réservation %{RESERVABLE} qui aura lieu le %{DATE}" + this_reservation_concerns_the_following_slots: "Cette réservation concerne les créneaux suivants :" + notify_member_avoir_ready: subject: "Votre facture d'avoir du FabLab" body: @@ -240,5 +247,38 @@ fr: disabled: "Désormais, aucune facture ne sera générée pour les paiement de cet utilisateur effectués à l'accueil." enabled: "Désormais, tous les paiement de cet utilisateur effectués à l'accueil, donneront lieu à la génération d'une facture." + notify_user_wallet_is_credited: + subject: "Votre porte-monnaie a bien été crédité" + body: + wallet_credit_html: "Votre porte-monnaie a bien été crédité de %{AMOUNT} par l'administrateur." + + notify_admin_user_wallet_is_credited: + subject: "Le porte-monnaie d'un utilisateur a bien été crédité" + body: + wallet_credit_html: "Le porte-monnaie du membre %{USER} a bien été crédité de %{AMOUNT} par l'administrateur %{ADMIN}." + + notify_admin_export_complete: + subject: "Export terminé" + body: + you_asked_for_an_export: "Vous avez demandé un export" + statistics_global: "de toutes les statistiques" + statistics_account: "des statistiques d'inscriptions" + statistics_event: "des statistiques sur les évènements" + statistics_machine: "des statistiques d'heures machines" + statistics_project: "des statistiques sur les projets" + statistics_subscription: "des statistiques d'abonnements" + statistics_training: "des statistiques sur les formations" + users_members: "de la liste des membres" + users_subscriptions: "de la liste des abonnements" + users_reservations: "de la liste des réservations" + click_to_download: "La génération est terminée. Pour télécharger le fichier Excel, cliquez" + here: "ici" + + notify_member_about_coupon: + subject: "Code promo" + body: + enjoy_a_discount_of_PERCENT_with_code_CODE: "Bénéficiez d'une remise de %{PERCENT} % sur tout le site en utilisant le code promo %{CODE}." + this_coupon_is_valid_USAGE_times_until_DATE_for_all_your_purchases: "Ce code promo est valable {USAGE, plural, =1{une seule fois} other{plusieurs fois}} : pour tous vos achats, dès maintenant {DATE, select, NO-DATE{et sans limitation de durée} other{et jusqu'au {DATE}}}." + shared: hello: "Bonjour %{user_name}" diff --git a/config/locales/rails.en.yml b/config/locales/rails.en.yml index 43dd86e3c..42f0b3800 100644 --- a/config/locales/rails.en.yml +++ b/config/locales/rails.en.yml @@ -130,6 +130,7 @@ en: one: is the wrong length (should be 1 character) other: is the wrong length (should be %{count} characters) other_than: must be other than %{count} + wrong_content_type: "content type is not allowed" template: body: 'There were problems with the following fields:' header: diff --git a/config/locales/rails.fr.yml b/config/locales/rails.fr.yml index 26d2eb66f..a2952b5f3 100644 --- a/config/locales/rails.fr.yml +++ b/config/locales/rails.fr.yml @@ -132,6 +132,7 @@ fr: one: ne fait pas la bonne longueur (doit comporter un seul caractère) other: ne fait pas la bonne longueur (doit comporter %{count} caractères) other_than: doit être différent de %{count} + wrong_content_type: "ce type de contenu n'est pas autorisé" template: body: 'Veuillez vérifier les champs suivants : ' header: diff --git a/config/routes.rb b/config/routes.rb index 1226d6dbb..b6afce9ff 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -17,14 +17,20 @@ Rails.application.routes.draw do ## The priority is based upon order of creation: first created -> highest priority. ## See how all your routes lay out with "rake routes". + + constraints :user_agent => /facebookexternalhit\/[0-9]|Twitterbot|Pinterest|Google.*snippet/ do + root :to => 'social_bot#share', as: :bot_root + end + ## You can have the root of your site routed with "root" root 'application#index' namespace :api, as: nil, defaults: { format: :json } do - resources :projects, only: [:index, :last_published, :show, :create, :update, :destroy] do + resources :projects, only: [:index, :show, :create, :update, :destroy] do collection do get :last_published get :search + get :allowed_extensions end end resources :openlab_projects, only: :index @@ -48,24 +54,33 @@ Rails.application.routes.draw do resources :notifications, only: [:index, :show, :update] do match :update_all, path: '/', via: [:put, :patch], on: :collection end + resources :wallet, only: [] do + get '/by_user/:user_id', action: 'by_user', on: :collection + get :transactions, on: :member + put :credit, on: :member + end # for homepage get '/last_subscribed/:last' => "members#last_subscribed" get '/feeds/twitter_timelines' => "feeds#twitter_timelines" - get 'pricing' => "pricing#index" - put 'pricing' => "pricing#update" + get 'pricing' => 'pricing#index' + put 'pricing' => 'pricing#update' resources :prices, only: [:index, :update] do post 'compute', on: :collection end + resources :coupons + post 'coupons/validate' => 'coupons#validate' + post 'coupons/send' => 'coupons#send_to' resources :trainings_pricings, only: [:index, :update] resources :availabilities do get 'machines/:machine_id', action: 'machine', on: :collection - get 'trainings', on: :collection + get 'trainings/:training_id', action: 'trainings', on: :collection get 'reservations', on: :member + get 'public', on: :collection end resources :groups, only: [:index, :create, :update, :destroy] @@ -85,9 +100,13 @@ Rails.application.routes.draw do end # for admin - resources :trainings + resources :trainings do + get :availabilities, on: :member + end resources :credits - resources :categories, only: [:index] + resources :categories + resources :event_themes + resources :age_ranges resources :statistics, only: [:index] resources :custom_assets, only: [:show, :create, :update] resources :tags @@ -100,9 +119,24 @@ Rails.application.routes.draw do resources :open_api_clients, only: [:index, :create, :update, :destroy] do patch :reset_token, on: :member end + resources :price_categories # i18n get 'translations/:locale/:state' => 'translations#show', :constraints => { :state => /[^\/]+/ } # allow dots in URL for 'state' + + # XLSX exports + get 'exports/:id/download' => 'exports#download' + post 'exports/status' => 'exports#status' + + # Fab-manager's version + get 'version' => 'version#show' + end + + # rss + + namespace :rss, as: nil, defaults: { format: :xml } do + resources :projects, only: [:index], as: 'rss_projects' + resources :events, only: [:index], as: 'rss_events' end # open_api @@ -127,7 +161,9 @@ Rails.application.routes.draw do %w(account event machine project subscription training user).each do |path| post "/stats/#{path}/_search", to: "api/statistics##{path}" + post "/stats/#{path}/export", to: "api/statistics#export_#{path}" end + post '/stats/global/export', to: "api/statistics#export_global" post '_search/scroll', to: "api/statistics#scroll" match '/project_collaborator/:valid_token', to: 'api/projects#collaborator_valid', via: :get diff --git a/config/schedule.yml b/config/schedule.yml index 4d3404265..e70e4845a 100644 --- a/config/schedule.yml +++ b/config/schedule.yml @@ -19,4 +19,9 @@ open_api_trace_calls_count: cron: "0 4 * * 0" # every sunday at 4am class: "OpenAPITraceCallsCountWorker" +reservation_reminder: + cron: "1 * * * *" + class: "ReservationReminderWorker" + queue: default + <%= PluginRegistry.insert_code('yml.schedule') %> diff --git a/config/secrets.yml b/config/secrets.yml index 4425cc5fc..fc6fcaebe 100644 --- a/config/secrets.yml +++ b/config/secrets.yml @@ -17,10 +17,13 @@ development: stripe_currency: <%= ENV["STRIPE_CURRENCY"] %> disqus_shortname: <%= ENV["DISQUS_SHORTNAME"] %> fablab_without_plans: <%= ENV["FABLAB_WITHOUT_PLANS"] %> + default_host: <%= ENV["DEFAULT_HOST"] %> + default_protocol: <%= ENV["DEFAULT_PROTOCOL"] %> time_zone: <%= ENV["TIME_ZONE"] %> week_starting_day: <%= ENV["WEEK_STARTING_DAY"] %> d3_date_format: <%= ENV["D3_DATE_FORMAT"].dump %> # .dump is needed as the value may start by a '%', see https://github.com/tenderlove/psych/issues/75 uib_date_format: <%= ENV["UIB_DATE_FORMAT"] %> + excel_date_format: <%= ENV["EXCEL_DATE_FORMAT"]%> rails_locale: <%= ENV["RAILS_LOCALE"] %> moment_locale: <%= ENV["MOMENT_LOCALE"] %> summernote_locale: <%= ENV["SUMMERNOTE_LOCALE"] %> @@ -33,6 +36,9 @@ development: openlab_base_uri: <%= ENV["OPENLAB_BASE_URI"] %> navinum_api_login: <%= ENV["NAVINUM_API_LOGIN"] %> navinum_api_password: <%= ENV["NAVINUM_API_PASSWORD"] %> + log_level: <%= ENV["LOG_LEVEL"] %> + facebook_app_id: <%= ENV["FACEBOOK_APP_ID"] %> + elaticsearch_host: <%= ENV["ELASTICSEARCH_HOST"] %> test: secret_key_base: 83daf5e7b80d990f037407bab78dff9904aaf3c195a50f84fa8695a22287e707dfbd9524b403b1dcf116ae1d8c06844c3d7ed942564e5b46be6ae3ead93a9d30 @@ -41,10 +47,13 @@ test: stripe_currency: usd disqus_shortname: fablab-sleede fablab_without_plans: false + default_host: <%= ENV["DEFAULT_HOST"] %> + default_protocol: <%= ENV["DEFAULT_PROTOCOL"] %> time_zone: Paris week_starting_day: monday d3_date_format: '%d/%m/%y' uib_date_format: dd/MM/yyyy + excel_date_format: dd/mm/yyyy rails_locale: en moment_locale: en summernote_locale: en-US @@ -57,6 +66,9 @@ test: openlab_base_uri: navinum_api_login: navinum_api_password: + log_level: <%= ENV["LOG_LEVEL"] %> + facebook_app_id: <%= ENV["FACEBOOK_APP_ID"] %> + elaticsearch_host: localhost staging: secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> @@ -76,6 +88,7 @@ staging: week_starting_day: <%= ENV["WEEK_STARTING_DAY"] %> d3_date_format: <%= ENV["D3_DATE_FORMAT"].dump %> uib_date_format: <%= ENV["UIB_DATE_FORMAT"] %> + excel_date_format: <%= ENV["EXCEL_DATE_FORMAT"]%> rails_locale: <%= ENV["RAILS_LOCALE"] %> moment_locale: <%= ENV["MOMENT_LOCALE"] %> summernote_locale: <%= ENV["SUMMERNOTE_LOCALE"] %> @@ -88,6 +101,9 @@ staging: openlab_base_uri: <%= ENV["OPENLAB_BASE_URI"] %> navinum_api_login: <%= ENV["NAVINUM_API_LOGIN"] %> navinum_api_password: <%= ENV["NAVINUM_API_PASSWORD"] %> + log_level: <%= ENV["LOG_LEVEL"] %> + facebook_app_id: <%= ENV["FACEBOOK_APP_ID"] %> + elaticsearch_host: <%= ENV["ELASTICSEARCH_HOST"] %> # Do not keep production secrets in the repository, # instead read values from the environment. @@ -109,6 +125,7 @@ production: week_starting_day: <%= ENV["WEEK_STARTING_DAY"] %> d3_date_format: <%= ENV["D3_DATE_FORMAT"].dump %> uib_date_format: <%= ENV["UIB_DATE_FORMAT"] %> + excel_date_format: <%= ENV["EXCEL_DATE_FORMAT"]%> rails_locale: <%= ENV["RAILS_LOCALE"] %> moment_locale: <%= ENV["MOMENT_LOCALE"] %> summernote_locale: <%= ENV["SUMMERNOTE_LOCALE"] %> @@ -122,3 +139,6 @@ production: google_analytics_id: <%= ENV["GA_ID"] %> navinum_api_login: <%= ENV["NAVINUM_API_LOGIN"] %> navinum_api_password: <%= ENV["NAVINUM_API_PASSWORD"] %> + log_level: <%= ENV["LOG_LEVEL"] %> + facebook_app_id: <%= ENV["FACEBOOK_APP_ID"] %> + elaticsearch_host: <%= ENV["ELASTICSEARCH_HOST"] %> diff --git a/db/migrate/20160602075531_add_step_nb_to_project_step.rb b/db/migrate/20160602075531_add_step_nb_to_project_step.rb new file mode 100644 index 000000000..2f41200f8 --- /dev/null +++ b/db/migrate/20160602075531_add_step_nb_to_project_step.rb @@ -0,0 +1,19 @@ +class AddStepNbToProjectStep < ActiveRecord::Migration + def up + add_column :project_steps, :step_nb, :integer + execute 'UPDATE project_steps + SET step_nb = subquery.index + FROM ( + SELECT + id, project_id, created_at, + row_number() OVER (PARTITION BY project_id) AS index + FROM project_steps + ORDER BY created_at + ) AS subquery + WHERE project_steps.id = subquery.id;' + end + + def down + remove_column :project_steps, :step_nb + end +end diff --git a/db/migrate/20160628092931_rename_courses_workshops_to_events.rb b/db/migrate/20160628092931_rename_courses_workshops_to_events.rb new file mode 100644 index 000000000..74eea160b --- /dev/null +++ b/db/migrate/20160628092931_rename_courses_workshops_to_events.rb @@ -0,0 +1,13 @@ +class RenameCoursesWorkshopsToEvents < ActiveRecord::Migration + def up + execute "UPDATE statistic_indices + SET label='Évènements' + WHERE es_type_key='event';" + end + + def down + execute "UPDATE statistic_indices + SET label='Ateliers/Stages' + WHERE es_type_key='event';" + end +end diff --git a/db/migrate/20160628124538_create_event_themes.rb b/db/migrate/20160628124538_create_event_themes.rb new file mode 100644 index 000000000..7b6a6fd6d --- /dev/null +++ b/db/migrate/20160628124538_create_event_themes.rb @@ -0,0 +1,9 @@ +class CreateEventThemes < ActiveRecord::Migration + def change + create_table :event_themes do |t| + t.string :name + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20160628131408_create_events_event_themes.rb b/db/migrate/20160628131408_create_events_event_themes.rb new file mode 100644 index 000000000..0f9c299b0 --- /dev/null +++ b/db/migrate/20160628131408_create_events_event_themes.rb @@ -0,0 +1,8 @@ +class CreateEventsEventThemes < ActiveRecord::Migration + def change + create_table :events_event_themes do |t| + t.belongs_to :event, index: true, foreign_key: true + t.belongs_to :event_theme, index: true, foreign_key: true + end + end +end diff --git a/db/migrate/20160628134211_create_age_ranges.rb b/db/migrate/20160628134211_create_age_ranges.rb new file mode 100644 index 000000000..6a5b5c669 --- /dev/null +++ b/db/migrate/20160628134211_create_age_ranges.rb @@ -0,0 +1,9 @@ +class CreateAgeRanges < ActiveRecord::Migration + def change + create_table :age_ranges do |t| + t.string :range + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20160628134303_add_age_range_id_to_event.rb b/db/migrate/20160628134303_add_age_range_id_to_event.rb new file mode 100644 index 000000000..7e914274d --- /dev/null +++ b/db/migrate/20160628134303_add_age_range_id_to_event.rb @@ -0,0 +1,5 @@ +class AddAgeRangeIdToEvent < ActiveRecord::Migration + def change + add_column :events, :age_range_id, :integer + end +end diff --git a/db/migrate/20160629091649_rename_range_to_name_from_age_range.rb b/db/migrate/20160629091649_rename_range_to_name_from_age_range.rb new file mode 100644 index 000000000..3158b5a63 --- /dev/null +++ b/db/migrate/20160629091649_rename_range_to_name_from_age_range.rb @@ -0,0 +1,3 @@ +class RenameRangeToNameFromAgeRange < ActiveRecord::Migration + rename_column :age_ranges, :range, :name +end diff --git a/db/migrate/20160630083438_add_slug_to_categories.rb b/db/migrate/20160630083438_add_slug_to_categories.rb new file mode 100644 index 000000000..0914484ab --- /dev/null +++ b/db/migrate/20160630083438_add_slug_to_categories.rb @@ -0,0 +1,6 @@ +class AddSlugToCategories < ActiveRecord::Migration + def change + add_column :categories, :slug, :string + add_index :categories, :slug, unique: true + end +end diff --git a/db/migrate/20160630083556_add_slug_to_age_range.rb b/db/migrate/20160630083556_add_slug_to_age_range.rb new file mode 100644 index 000000000..1dcff4ee7 --- /dev/null +++ b/db/migrate/20160630083556_add_slug_to_age_range.rb @@ -0,0 +1,6 @@ +class AddSlugToAgeRange < ActiveRecord::Migration + def change + add_column :age_ranges, :slug, :string + add_index :age_ranges, :slug, unique: true + end +end diff --git a/db/migrate/20160630083759_add_slug_to_event_theme.rb b/db/migrate/20160630083759_add_slug_to_event_theme.rb new file mode 100644 index 000000000..6c608add6 --- /dev/null +++ b/db/migrate/20160630083759_add_slug_to_event_theme.rb @@ -0,0 +1,6 @@ +class AddSlugToEventTheme < ActiveRecord::Migration + def change + add_column :event_themes, :slug, :string + add_index :event_themes, :slug, unique: true + end +end diff --git a/db/migrate/20160630100137_add_event_theme_and_age_range_to_statistic_field.rb b/db/migrate/20160630100137_add_event_theme_and_age_range_to_statistic_field.rb new file mode 100644 index 000000000..79495cfdb --- /dev/null +++ b/db/migrate/20160630100137_add_event_theme_and_age_range_to_statistic_field.rb @@ -0,0 +1,6 @@ +class AddEventThemeAndAgeRangeToStatisticField < ActiveRecord::Migration + def change + StatisticField.create!({key:'eventTheme', label:I18n.t('statistics.event_theme'), statistic_index_id: 4, data_type: 'text'}) + StatisticField.create!({key:'ageRange', label:I18n.t('statistics.age_range'), statistic_index_id: 4, data_type: 'text'}) + end +end diff --git a/db/migrate/20160630140204_add_slugs_to_existing_categories.rb b/db/migrate/20160630140204_add_slugs_to_existing_categories.rb new file mode 100644 index 000000000..a6a4ed6cf --- /dev/null +++ b/db/migrate/20160630140204_add_slugs_to_existing_categories.rb @@ -0,0 +1,13 @@ +class AddSlugsToExistingCategories < ActiveRecord::Migration + def up + execute 'UPDATE categories + SET slug=name + WHERE slug IS NULL;' + end + + def down + execute 'UPDATE categories + SET slug=NULL + WHERE slug=name;' + end +end diff --git a/db/migrate/20160704095606_create_wallets.rb b/db/migrate/20160704095606_create_wallets.rb new file mode 100644 index 000000000..4bad09e11 --- /dev/null +++ b/db/migrate/20160704095606_create_wallets.rb @@ -0,0 +1,18 @@ +class CreateWallets < ActiveRecord::Migration + def up + create_table :wallets do |t| + t.belongs_to :user, index: true, foreign_key: true + t.integer :amount, default: 0 + + t.timestamps null: false + end + + User.all.each do |u| + Wallet.create(user: u) + end + end + + def down + drop_table :wallets + end +end diff --git a/db/migrate/20160704165139_create_wallet_transactions.rb b/db/migrate/20160704165139_create_wallet_transactions.rb new file mode 100644 index 000000000..a74b878ab --- /dev/null +++ b/db/migrate/20160704165139_create_wallet_transactions.rb @@ -0,0 +1,13 @@ +class CreateWalletTransactions < ActiveRecord::Migration + def change + create_table :wallet_transactions do |t| + t.belongs_to :user, index: true, foreign_key: true + t.belongs_to :wallet, index: true, foreign_key: true + t.references :transactable, polymorphic: true, index: {name: 'index_wallet_transactions_on_transactable'} + t.string :transaction_type + t.integer :amount + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20160714095018_add_public_page_to_training.rb b/db/migrate/20160714095018_add_public_page_to_training.rb new file mode 100644 index 000000000..5476f0b5e --- /dev/null +++ b/db/migrate/20160714095018_add_public_page_to_training.rb @@ -0,0 +1,5 @@ +class AddPublicPageToTraining < ActiveRecord::Migration + def change + add_column :trainings, :public_page, :boolean, default: true + end +end diff --git a/db/migrate/20160718165434_add_wallet_amount_to_invoice.rb b/db/migrate/20160718165434_add_wallet_amount_to_invoice.rb new file mode 100644 index 000000000..9900b3a5f --- /dev/null +++ b/db/migrate/20160718165434_add_wallet_amount_to_invoice.rb @@ -0,0 +1,5 @@ +class AddWalletAmountToInvoice < ActiveRecord::Migration + def change + add_column :invoices, :wallet_amount, :integer + end +end diff --git a/db/migrate/20160720124355_add_wallet_transaction_to_invoice.rb b/db/migrate/20160720124355_add_wallet_transaction_to_invoice.rb new file mode 100644 index 000000000..b0fdac46d --- /dev/null +++ b/db/migrate/20160720124355_add_wallet_transaction_to_invoice.rb @@ -0,0 +1,5 @@ +class AddWalletTransactionToInvoice < ActiveRecord::Migration + def change + add_reference :invoices, :wallet_transaction, index: true, foreign_key: true + end +end diff --git a/db/migrate/20160725131756_add_category_id_to_event.rb b/db/migrate/20160725131756_add_category_id_to_event.rb new file mode 100644 index 000000000..c6b22e620 --- /dev/null +++ b/db/migrate/20160725131756_add_category_id_to_event.rb @@ -0,0 +1,5 @@ +class AddCategoryIdToEvent < ActiveRecord::Migration + def change + add_reference :events, :category, index: true, foreign_key: true + end +end diff --git a/db/migrate/20160725131950_singleize_event_categories.rb b/db/migrate/20160725131950_singleize_event_categories.rb new file mode 100644 index 000000000..dde067224 --- /dev/null +++ b/db/migrate/20160725131950_singleize_event_categories.rb @@ -0,0 +1,18 @@ +class SingleizeEventCategories < ActiveRecord::Migration + def up + execute 'UPDATE events AS e + SET category_id = ec.category_id + FROM events_categories AS ec + WHERE e.id = ec.event_id;' + end + + def down + execute 'INSERT INTO events_categories + (event_id, category_id, created_at, updated_at) + SELECT id, category_id, now(), now() + FROM events;' + + execute 'UPDATE events + SET category_id = NULL;' + end +end diff --git a/db/migrate/20160725135112_drop_events_categories.rb b/db/migrate/20160725135112_drop_events_categories.rb new file mode 100644 index 000000000..9ccc2fef4 --- /dev/null +++ b/db/migrate/20160725135112_drop_events_categories.rb @@ -0,0 +1,14 @@ +class DropEventsCategories < ActiveRecord::Migration + def up + drop_table :events_categories + end + + def down + create_table :events_categories do |t| + t.belongs_to :event, index: true + t.belongs_to :category, index: true + + t.timestamps + end + end +end diff --git a/db/migrate/20160726081931_create_exports.rb b/db/migrate/20160726081931_create_exports.rb new file mode 100644 index 000000000..7a0b70314 --- /dev/null +++ b/db/migrate/20160726081931_create_exports.rb @@ -0,0 +1,11 @@ +class CreateExports < ActiveRecord::Migration + def change + create_table :exports do |t| + t.string :category + t.string :type + t.string :query + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20160726111509_add_user_to_export.rb b/db/migrate/20160726111509_add_user_to_export.rb new file mode 100644 index 000000000..6c63fb005 --- /dev/null +++ b/db/migrate/20160726111509_add_user_to_export.rb @@ -0,0 +1,5 @@ +class AddUserToExport < ActiveRecord::Migration + def change + add_reference :exports, :user, index: true, foreign_key: true + end +end diff --git a/db/migrate/20160726131152_rename_type_to_stat_type_from_export.rb b/db/migrate/20160726131152_rename_type_to_stat_type_from_export.rb new file mode 100644 index 000000000..2bda2fc6d --- /dev/null +++ b/db/migrate/20160726131152_rename_type_to_stat_type_from_export.rb @@ -0,0 +1,5 @@ +class RenameTypeToStatTypeFromExport < ActiveRecord::Migration + def change + rename_column :exports, :type, :export_type + end +end diff --git a/db/migrate/20160726144257_add_key_to_export.rb b/db/migrate/20160726144257_add_key_to_export.rb new file mode 100644 index 000000000..bfb79e2a9 --- /dev/null +++ b/db/migrate/20160726144257_add_key_to_export.rb @@ -0,0 +1,5 @@ +class AddKeyToExport < ActiveRecord::Migration + def change + add_column :exports, :key, :string + end +end diff --git a/db/migrate/20160728095026_add_is_allow_newsletter_to_users.rb b/db/migrate/20160728095026_add_is_allow_newsletter_to_users.rb new file mode 100644 index 000000000..f36d56b0f --- /dev/null +++ b/db/migrate/20160728095026_add_is_allow_newsletter_to_users.rb @@ -0,0 +1,5 @@ +class AddIsAllowNewsletterToUsers < ActiveRecord::Migration + def change + add_column :users, :is_allow_newsletter, :boolean + end +end diff --git a/db/migrate/20160801145502_create_organizations.rb b/db/migrate/20160801145502_create_organizations.rb new file mode 100644 index 000000000..e7fe75b6c --- /dev/null +++ b/db/migrate/20160801145502_create_organizations.rb @@ -0,0 +1,9 @@ +class CreateOrganizations < ActiveRecord::Migration + def change + create_table :organizations do |t| + t.string :name + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20160801153454_add_profile_to_organization.rb b/db/migrate/20160801153454_add_profile_to_organization.rb new file mode 100644 index 000000000..6d32841f2 --- /dev/null +++ b/db/migrate/20160801153454_add_profile_to_organization.rb @@ -0,0 +1,5 @@ +class AddProfileToOrganization < ActiveRecord::Migration + def change + add_reference :organizations, :profile, index: true, foreign_key: true + end +end diff --git a/db/migrate/20160803085201_create_coupons.rb b/db/migrate/20160803085201_create_coupons.rb new file mode 100644 index 000000000..fbfcf322e --- /dev/null +++ b/db/migrate/20160803085201_create_coupons.rb @@ -0,0 +1,16 @@ +class CreateCoupons < ActiveRecord::Migration + def change + create_table :coupons do |t| + t.string :name + t.string :code + t.integer :percent_off + t.datetime :valid_until + t.integer :max_usages + t.integer :usages + t.boolean :active + t.string :stp_coupon_id + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20160803104701_add_coupon_to_invoice.rb b/db/migrate/20160803104701_add_coupon_to_invoice.rb new file mode 100644 index 000000000..b58874bad --- /dev/null +++ b/db/migrate/20160803104701_add_coupon_to_invoice.rb @@ -0,0 +1,5 @@ +class AddCouponToInvoice < ActiveRecord::Migration + def change + add_reference :invoices, :coupon, index: true, foreign_key: true + end +end diff --git a/db/migrate/20160804073558_remove_usages_from_coupons.rb b/db/migrate/20160804073558_remove_usages_from_coupons.rb new file mode 100644 index 000000000..4988a01b6 --- /dev/null +++ b/db/migrate/20160804073558_remove_usages_from_coupons.rb @@ -0,0 +1,5 @@ +class RemoveUsagesFromCoupons < ActiveRecord::Migration + def change + remove_column :coupons, :usages, :integer + end +end diff --git a/db/migrate/20160808113850_remove_stp_coupon_id_from_coupons.rb b/db/migrate/20160808113850_remove_stp_coupon_id_from_coupons.rb new file mode 100644 index 000000000..180e49bef --- /dev/null +++ b/db/migrate/20160808113850_remove_stp_coupon_id_from_coupons.rb @@ -0,0 +1,5 @@ +class RemoveStpCouponIdFromCoupons < ActiveRecord::Migration + def change + remove_column :coupons, :stp_coupon_id, :string + end +end diff --git a/db/migrate/20160808113930_add_validity_per_user_to_coupon.rb b/db/migrate/20160808113930_add_validity_per_user_to_coupon.rb new file mode 100644 index 000000000..1172f100c --- /dev/null +++ b/db/migrate/20160808113930_add_validity_per_user_to_coupon.rb @@ -0,0 +1,5 @@ +class AddValidityPerUserToCoupon < ActiveRecord::Migration + def change + add_column :coupons, :validity_per_user, :string + end +end diff --git a/db/migrate/20160824080717_create_price_categories.rb b/db/migrate/20160824080717_create_price_categories.rb new file mode 100644 index 000000000..9f31dbcd5 --- /dev/null +++ b/db/migrate/20160824080717_create_price_categories.rb @@ -0,0 +1,10 @@ +class CreatePriceCategories < ActiveRecord::Migration + def change + create_table :price_categories do |t| + t.string :name + t.text :conditions + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20160824084111_create_event_price_categories.rb b/db/migrate/20160824084111_create_event_price_categories.rb new file mode 100644 index 000000000..25588ff14 --- /dev/null +++ b/db/migrate/20160824084111_create_event_price_categories.rb @@ -0,0 +1,11 @@ +class CreateEventPriceCategories < ActiveRecord::Migration + def change + create_table :event_price_categories do |t| + t.belongs_to :event, index: true, foreign_key: true + t.belongs_to :price_category, index: true, foreign_key: true + t.integer :amount + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20160825141326_create_tickets.rb b/db/migrate/20160825141326_create_tickets.rb new file mode 100644 index 000000000..576ca525b --- /dev/null +++ b/db/migrate/20160825141326_create_tickets.rb @@ -0,0 +1,11 @@ +class CreateTickets < ActiveRecord::Migration + def change + create_table :tickets do |t| + t.belongs_to :reservation, index: true, foreign_key: true + t.belongs_to :event_price_category, index: true, foreign_key: true + t.integer :booked + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20160830154719_migrate_event_reduced_amount_to_price_category.rb b/db/migrate/20160830154719_migrate_event_reduced_amount_to_price_category.rb new file mode 100644 index 000000000..7d4de6134 --- /dev/null +++ b/db/migrate/20160830154719_migrate_event_reduced_amount_to_price_category.rb @@ -0,0 +1,49 @@ +class MigrateEventReducedAmountToPriceCategory < ActiveRecord::Migration + def up + pc = PriceCategory.new( + name: I18n.t('price_category.reduced_fare'), + conditions: I18n.t('price_category.reduced_fare_if_you_are_under_25_student_or_unemployed') + ) + pc.save! + + Event.where.not(reduced_amount: nil).each do |event| + unless event.reduced_amount == 0 and event.amount == 0 + epc = EventPriceCategory.new( + event: event, + price_category: pc, + amount: event.reduced_amount + ) + epc.save! + + Reservation.where(reservable_type: 'Event', reservable_id: event.id).where('nb_reserve_reduced_places > 0').each do |r| + t = Ticket.new( + reservation: r, + event_price_category: epc, + booked: r.nb_reserve_reduced_places + ) + t.save! + end + end + end + end + + def down + pc = PriceCategory.find_by_name(I18n.t('price_category.reduced_fare')) + EventPriceCategory.where(price_category_id: pc.id).each do |epc| + epc.event.update_column(:reduced_amount, epc.amount) + + Reservation.where(reservable_type: 'Event', reservable_id: epc.event.id).each do |r| + r.tickets.each do |t| + if t.event_price_category_id == epc.id + r.update_column(:nb_reserve_reduced_places, t.booked) + t.destroy! + break + end + end + end + epc.destroy! + end + + pc.destroy! + end +end diff --git a/db/migrate/20160831084443_remove_reduced_amount_from_event.rb b/db/migrate/20160831084443_remove_reduced_amount_from_event.rb new file mode 100644 index 000000000..1dd959141 --- /dev/null +++ b/db/migrate/20160831084443_remove_reduced_amount_from_event.rb @@ -0,0 +1,5 @@ +class RemoveReducedAmountFromEvent < ActiveRecord::Migration + def change + remove_column :events, :reduced_amount, :integer + end +end diff --git a/db/migrate/20160831084519_remove_nb_reserve_reduced_places_from_reservation.rb b/db/migrate/20160831084519_remove_nb_reserve_reduced_places_from_reservation.rb new file mode 100644 index 000000000..287d11b7e --- /dev/null +++ b/db/migrate/20160831084519_remove_nb_reserve_reduced_places_from_reservation.rb @@ -0,0 +1,5 @@ +class RemoveNbReserveReducedPlacesFromReservation < ActiveRecord::Migration + def change + remove_column :reservations, :nb_reserve_reduced_places, :integer + end +end diff --git a/db/migrate/20160905141858_create_statistic_custom_aggregations.rb b/db/migrate/20160905141858_create_statistic_custom_aggregations.rb new file mode 100644 index 000000000..d1242f56a --- /dev/null +++ b/db/migrate/20160905141858_create_statistic_custom_aggregations.rb @@ -0,0 +1,11 @@ +class CreateStatisticCustomAggregations < ActiveRecord::Migration + def change + create_table :statistic_custom_aggregations do |t| + t.text :query + t.string :path + t.belongs_to :statistic_type, index: true, foreign_key: true + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20160905142700_add_field_to_statistic_custom_aggregation.rb b/db/migrate/20160905142700_add_field_to_statistic_custom_aggregation.rb new file mode 100644 index 000000000..8b0da532c --- /dev/null +++ b/db/migrate/20160905142700_add_field_to_statistic_custom_aggregation.rb @@ -0,0 +1,5 @@ +class AddFieldToStatisticCustomAggregation < ActiveRecord::Migration + def change + add_column :statistic_custom_aggregations, :field, :string + end +end diff --git a/db/migrate/20160906094739_remove_path_from_statistic_custom_aggregations.rb b/db/migrate/20160906094739_remove_path_from_statistic_custom_aggregations.rb new file mode 100644 index 000000000..f58d20a1c --- /dev/null +++ b/db/migrate/20160906094739_remove_path_from_statistic_custom_aggregations.rb @@ -0,0 +1,5 @@ +class RemovePathFromStatisticCustomAggregations < ActiveRecord::Migration + def change + remove_column :statistic_custom_aggregations, :path, :string + end +end diff --git a/db/migrate/20160906094847_add_es_index_es_type_to_statistic_custom_aggregations.rb b/db/migrate/20160906094847_add_es_index_es_type_to_statistic_custom_aggregations.rb new file mode 100644 index 000000000..894325854 --- /dev/null +++ b/db/migrate/20160906094847_add_es_index_es_type_to_statistic_custom_aggregations.rb @@ -0,0 +1,6 @@ +class AddEsIndexEsTypeToStatisticCustomAggregations < ActiveRecord::Migration + def change + add_column :statistic_custom_aggregations, :es_index, :string + add_column :statistic_custom_aggregations, :es_type, :string + end +end diff --git a/db/migrate/20160906145713_insert_custom_aggregations.rb b/db/migrate/20160906145713_insert_custom_aggregations.rb new file mode 100644 index 000000000..90c95e456 --- /dev/null +++ b/db/migrate/20160906145713_insert_custom_aggregations.rb @@ -0,0 +1,29 @@ +class InsertCustomAggregations < ActiveRecord::Migration + def change + # available reservations hours for machines + machine = StatisticIndex.find_by_es_type_key('machine') + machine_hours = StatisticType.find_by(key: 'hour', statistic_index_id: machine.id) + + available_hours = StatisticCustomAggregation.new({ + statistic_type_id: machine_hours.id, + es_index: 'fablab', + es_type: 'availabilities', + field: 'available_hours', + query: '{"size":0, "aggregations":{"%{aggs_name}":{"sum":{"field":"hours_duration"}}}, "query":{"bool":{"must":[{"range":{"start_at":{"gte":"%{start_date}", "lte":"%{end_date}"}}}, {"match":{"available_type":"machines"}}]}}}' + }) + available_hours.save! + + # available training tickets + training = StatisticIndex.find_by_es_type_key('training') + training_bookings = StatisticType.find_by(key: 'booking', statistic_index_id: training.id) + + available_tickets = StatisticCustomAggregation.new({ + statistic_type_id: training_bookings.id, + es_index: 'fablab', + es_type: 'availabilities', + field: 'available_tickets', + query: '{"size":0, "aggregations":{"%{aggs_name}":{"sum":{"field":"nb_total_places"}}}, "query":{"bool":{"must":[{"range":{"start_at":{"gte":"%{start_date}", "lte":"%{end_date}"}}}, {"match":{"available_type":"training"}}]}}}' + }) + available_tickets.save! + end +end diff --git a/db/migrate/20160915105234_add_transformation_to_o_auth2_mapping.rb b/db/migrate/20160915105234_add_transformation_to_o_auth2_mapping.rb new file mode 100644 index 000000000..0e4d44780 --- /dev/null +++ b/db/migrate/20160915105234_add_transformation_to_o_auth2_mapping.rb @@ -0,0 +1,5 @@ +class AddTransformationToOAuth2Mapping < ActiveRecord::Migration + def change + add_column :o_auth2_mappings, :transformation, :jsonb + end +end diff --git a/db/schema.rb b/db/schema.rb index 09055dee9..24d8a73cd 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160613093842) do +ActiveRecord::Schema.define(version: 20160915105234) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -44,6 +44,15 @@ ActiveRecord::Schema.define(version: 20160613093842) do t.datetime "updated_at" end + create_table "age_ranges", force: :cascade do |t| + t.string "name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "slug" + end + + add_index "age_ranges", ["slug"], name: "index_age_ranges_on_slug", unique: true, using: :btree + create_table "assets", force: :cascade do |t| t.integer "viewable_id" t.string "viewable_type", limit: 255 @@ -86,12 +95,27 @@ ActiveRecord::Schema.define(version: 20160613093842) do t.string "name", limit: 255 t.datetime "created_at" t.datetime "updated_at" + t.string "slug" end + add_index "categories", ["slug"], name: "index_categories_on_slug", unique: true, using: :btree + create_table "components", force: :cascade do |t| t.string "name", limit: 255, null: false end + create_table "coupons", force: :cascade do |t| + t.string "name" + t.string "code" + t.integer "percent_off" + t.datetime "valid_until" + t.integer "max_usages" + t.boolean "active" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "validity_per_user" + end + create_table "credits", force: :cascade do |t| t.integer "creditable_id" t.string "creditable_type", limit: 255 @@ -114,6 +138,26 @@ ActiveRecord::Schema.define(version: 20160613093842) do t.datetime "updated_at", null: false end + create_table "event_price_categories", force: :cascade do |t| + t.integer "event_id" + t.integer "price_category_id" + t.integer "amount" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "event_price_categories", ["event_id"], name: "index_event_price_categories_on_event_id", using: :btree + add_index "event_price_categories", ["price_category_id"], name: "index_event_price_categories_on_price_category_id", using: :btree + + create_table "event_themes", force: :cascade do |t| + t.string "name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "slug" + end + + add_index "event_themes", ["slug"], name: "index_event_themes_on_slug", unique: true, using: :btree + create_table "events", force: :cascade do |t| t.string "title", limit: 255 t.text "description" @@ -121,24 +165,36 @@ ActiveRecord::Schema.define(version: 20160613093842) do t.datetime "updated_at" t.integer "availability_id" t.integer "amount" - t.integer "reduced_amount" t.integer "nb_total_places" t.integer "nb_free_places" t.integer "recurrence_id" + t.integer "age_range_id" + t.integer "category_id" end add_index "events", ["availability_id"], name: "index_events_on_availability_id", using: :btree + add_index "events", ["category_id"], name: "index_events_on_category_id", using: :btree add_index "events", ["recurrence_id"], name: "index_events_on_recurrence_id", using: :btree - create_table "events_categories", force: :cascade do |t| - t.integer "event_id" - t.integer "category_id" - t.datetime "created_at" - t.datetime "updated_at" + create_table "events_event_themes", force: :cascade do |t| + t.integer "event_id" + t.integer "event_theme_id" end - add_index "events_categories", ["category_id"], name: "index_events_categories_on_category_id", using: :btree - add_index "events_categories", ["event_id"], name: "index_events_categories_on_event_id", using: :btree + add_index "events_event_themes", ["event_id"], name: "index_events_event_themes_on_event_id", using: :btree + add_index "events_event_themes", ["event_theme_id"], name: "index_events_event_themes_on_event_theme_id", using: :btree + + create_table "exports", force: :cascade do |t| + t.string "category" + t.string "export_type" + t.string "query" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "user_id" + t.string "key" + end + + add_index "exports", ["user_id"], name: "index_exports_on_user_id", using: :btree create_table "friendly_id_slugs", force: :cascade do |t| t.string "slug", limit: 255, null: false @@ -190,10 +246,15 @@ ActiveRecord::Schema.define(version: 20160613093842) do t.string "type", limit: 255 t.boolean "subscription_to_expire" t.text "description" + t.integer "wallet_amount" + t.integer "wallet_transaction_id" + t.integer "coupon_id" end + add_index "invoices", ["coupon_id"], name: "index_invoices_on_coupon_id", using: :btree add_index "invoices", ["invoice_id"], name: "index_invoices_on_invoice_id", using: :btree add_index "invoices", ["user_id"], name: "index_invoices_on_user_id", using: :btree + add_index "invoices", ["wallet_transaction_id"], name: "index_invoices_on_wallet_transaction_id", using: :btree create_table "licences", force: :cascade do |t| t.string "name", limit: 255, null: false @@ -244,6 +305,7 @@ ActiveRecord::Schema.define(version: 20160613093842) do t.string "local_model" t.string "api_endpoint" t.string "api_data_type" + t.jsonb "transformation" end add_index "o_auth2_mappings", ["o_auth2_provider_id"], name: "index_o_auth2_mappings_on_o_auth2_provider_id", using: :btree @@ -287,6 +349,15 @@ ActiveRecord::Schema.define(version: 20160613093842) do t.datetime "updated_at", null: false end + create_table "organizations", force: :cascade do |t| + t.string "name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "profile_id" + end + + add_index "organizations", ["profile_id"], name: "index_organizations_on_profile_id", using: :btree + create_table "plans", force: :cascade do |t| t.string "name", limit: 255 t.integer "amount" @@ -306,6 +377,13 @@ ActiveRecord::Schema.define(version: 20160613093842) do add_index "plans", ["group_id"], name: "index_plans_on_group_id", using: :btree + create_table "price_categories", force: :cascade do |t| + t.string "name" + t.text "conditions" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "prices", force: :cascade do |t| t.integer "group_id" t.integer "plan_id" @@ -357,6 +435,7 @@ ActiveRecord::Schema.define(version: 20160613093842) do t.datetime "created_at" t.datetime "updated_at" t.string "title", limit: 255 + t.integer "step_nb" end add_index "project_steps", ["project_id"], name: "index_project_steps_on_project_id", using: :btree @@ -418,10 +497,9 @@ ActiveRecord::Schema.define(version: 20160613093842) do t.datetime "created_at" t.datetime "updated_at" t.integer "reservable_id" - t.string "reservable_type", limit: 255 - t.string "stp_invoice_id", limit: 255 + t.string "reservable_type", limit: 255 + t.string "stp_invoice_id", limit: 255 t.integer "nb_reserve_places" - t.integer "nb_reserve_reduced_places" end add_index "reservations", ["reservable_id", "reservable_type"], name: "index_reservations_on_reservable_id_and_reservable_type", using: :btree @@ -464,6 +542,18 @@ ActiveRecord::Schema.define(version: 20160613093842) do add_index "slots", ["availability_id"], name: "index_slots_on_availability_id", using: :btree add_index "slots", ["reservation_id"], name: "index_slots_on_reservation_id", using: :btree + create_table "statistic_custom_aggregations", force: :cascade do |t| + t.text "query" + t.integer "statistic_type_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "field" + t.string "es_index" + t.string "es_type" + end + + add_index "statistic_custom_aggregations", ["statistic_type_id"], name: "index_statistic_custom_aggregations_on_statistic_type_id", using: :btree + create_table "statistic_fields", force: :cascade do |t| t.integer "statistic_index_id" t.string "key", limit: 255 @@ -554,6 +644,17 @@ ActiveRecord::Schema.define(version: 20160613093842) do t.string "name", limit: 255, null: false end + create_table "tickets", force: :cascade do |t| + t.integer "reservation_id" + t.integer "event_price_category_id" + t.integer "booked" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "tickets", ["event_price_category_id"], name: "index_tickets_on_event_price_category_id", using: :btree + add_index "tickets", ["reservation_id"], name: "index_tickets_on_reservation_id", using: :btree + create_table "trainings", force: :cascade do |t| t.string "name", limit: 255 t.datetime "created_at" @@ -561,6 +662,7 @@ ActiveRecord::Schema.define(version: 20160613093842) do t.integer "nb_total_places" t.string "slug", limit: 255 t.text "description" + t.boolean "public_page", default: true end add_index "trainings", ["slug"], name: "index_trainings_on_slug", unique: true, using: :btree @@ -645,6 +747,7 @@ ActiveRecord::Schema.define(version: 20160613093842) do t.string "uid" t.string "auth_token" t.datetime "merged_at" + t.boolean "is_allow_newsletter" end add_index "users", ["auth_token"], name: "index_users_on_auth_token", using: :btree @@ -676,12 +779,51 @@ ActiveRecord::Schema.define(version: 20160613093842) do add_index "users_roles", ["user_id", "role_id"], name: "index_users_roles_on_user_id_and_role_id", using: :btree + create_table "wallet_transactions", force: :cascade do |t| + t.integer "user_id" + t.integer "wallet_id" + t.integer "transactable_id" + t.string "transactable_type" + t.string "transaction_type" + t.integer "amount" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "wallet_transactions", ["transactable_type", "transactable_id"], name: "index_wallet_transactions_on_transactable", using: :btree + add_index "wallet_transactions", ["user_id"], name: "index_wallet_transactions_on_user_id", using: :btree + add_index "wallet_transactions", ["wallet_id"], name: "index_wallet_transactions_on_wallet_id", using: :btree + + create_table "wallets", force: :cascade do |t| + t.integer "user_id" + t.integer "amount", default: 0 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "wallets", ["user_id"], name: "index_wallets_on_user_id", using: :btree + add_foreign_key "availability_tags", "availabilities" add_foreign_key "availability_tags", "tags" + add_foreign_key "event_price_categories", "events" + add_foreign_key "event_price_categories", "price_categories" + add_foreign_key "events", "categories" + add_foreign_key "events_event_themes", "event_themes" + add_foreign_key "events_event_themes", "events" + add_foreign_key "exports", "users" + add_foreign_key "invoices", "coupons" + add_foreign_key "invoices", "wallet_transactions" add_foreign_key "o_auth2_mappings", "o_auth2_providers" add_foreign_key "open_api_calls_count_tracings", "open_api_clients" + add_foreign_key "organizations", "profiles" add_foreign_key "prices", "groups" add_foreign_key "prices", "plans" + add_foreign_key "statistic_custom_aggregations", "statistic_types" + add_foreign_key "tickets", "event_price_categories" + add_foreign_key "tickets", "reservations" add_foreign_key "user_tags", "tags" add_foreign_key "user_tags", "users" + add_foreign_key "wallet_transactions", "users" + add_foreign_key "wallet_transactions", "wallets" + add_foreign_key "wallets", "users" end diff --git a/db/seeds.rb b/db/seeds.rb index 3c36d25d0..33ee91648 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -2,13 +2,13 @@ if StatisticIndex.count == 0 StatisticIndex.create!([ - {id:1, es_type_key:'subscription', label:'Abonnements'}, - {id:2, es_type_key:'machine', label:'Heures machines'}, - {id:3, es_type_key:'training', label:'Formations'}, - {id:4, es_type_key:'event', label:'Ateliers/Stages'}, - {id:5, es_type_key:'account', label:'Inscriptions', ca: false}, - {id:6, es_type_key:'project', label:'Projets', ca: false}, - {id:7, es_type_key:'user', label:'Utilisateurs', table: false, ca: false} + {id:1, es_type_key:'subscription', label:I18n.t('statistics.subscriptions')}, + {id:2, es_type_key:'machine', label:I18n.t('statistics.machines_hours')}, + {id:3, es_type_key:'training', label:I18n.t('statistics.trainings')}, + {id:4, es_type_key:'event', label:I18n.t('statistics.events')}, + {id:5, es_type_key:'account', label:I18n.t('statistics.registrations'), ca: false}, + {id:6, es_type_key:'project', label:I18n.t('statistics.projects'), ca: false}, + {id:7, es_type_key:'user', label:I18n.t('statistics.users'), table: false, ca: false} ]) connection = ActiveRecord::Base.connection if connection.instance_values["config"][:adapter] == 'postgresql' @@ -19,15 +19,17 @@ end if StatisticField.count == 0 StatisticField.create!([ # available data_types : index, number, date, text, list - {key:'trainingId', label:'ID Formation', statistic_index_id: 3, data_type: 'index'}, - {key:'trainingDate', label:'Date Formation', statistic_index_id: 3, data_type: 'date'}, - {key:'eventId', label:'ID Évènement', statistic_index_id: 4, data_type: 'index'}, - {key:'eventDate', label:'Date Évènement', statistic_index_id: 4, data_type: 'date'}, - {key:'themes', label:'Thèmes', statistic_index_id: 6, data_type: 'list'}, - {key:'components', label:'Composants', statistic_index_id: 6, data_type: 'list'}, - {key:'machines', label:'Machines', statistic_index_id: 6, data_type: 'list'}, - {key:'name', label:'Nom Évènement', statistic_index_id: 4, data_type: 'text'}, - {key:'userId', label:'ID Utilisateur', statistic_index_id: 7, data_type: 'index'} + {key:'trainingId', label:I18n.t('statistics.training_id'), statistic_index_id: 3, data_type: 'index'}, + {key:'trainingDate', label:I18n.t('statistics.training_date'), statistic_index_id: 3, data_type: 'date'}, + {key:'eventId', label:I18n.t('statistics.event_id'), statistic_index_id: 4, data_type: 'index'}, + {key:'eventDate', label:I18n.t('statistics.event_date'), statistic_index_id: 4, data_type: 'date'}, + {key:'themes', label:I18n.t('statistics.themes'), statistic_index_id: 6, data_type: 'list'}, + {key:'components', label:I18n.t('statistics.components'), statistic_index_id: 6, data_type: 'list'}, + {key:'machines', label:I18n.t('statistics.machines'), statistic_index_id: 6, data_type: 'list'}, + {key:'name', label:I18n.t('statistics.event_name'), statistic_index_id: 4, data_type: 'text'}, + {key:'userId', label:I18n.t('statistics.user_id'), statistic_index_id: 7, data_type: 'index'}, + {key:'eventTheme', label:I18n.t('statistics.event_theme'), statistic_index_id: 4, data_type: 'text'}, + {key:'ageRange', label:I18n.t('statistics.age_range'), statistic_index_id: 4, data_type: 'text'} ]) end @@ -41,25 +43,22 @@ end if StatisticType.count == 0 StatisticType.create!([ - {statistic_index_id: 2, key: 'booking', label:'Réservations', graph: true, simple: true}, - {statistic_index_id: 2, key: 'hour', label:"Nombre d'heures", graph: true, simple: false}, - {statistic_index_id: 3, key: 'booking', label:'Réservations', graph: false, simple: true}, - {statistic_index_id: 3, key: 'hour', label:"Nombre d'heures", graph: false, simple: false}, - {statistic_index_id: 4, key: 'booking', label:'Nombre de places', graph: false, simple: false}, - {statistic_index_id: 4, key: 'hour', label:"Nombre d'heures", graph: false, simple: false}, - {statistic_index_id: 5, key: 'member', label:'Utilisateurs', graph: true, simple: true}, - {statistic_index_id: 6, key: 'project', label:'Projets', graph: false, simple: true}, - {statistic_index_id: 7, key: 'revenue', label:"Chiffre d'affaires", graph: false, simple: false} + {statistic_index_id: 2, key: 'booking', label:I18n.t('statistics.bookings'), graph: true, simple: true}, + {statistic_index_id: 2, key: 'hour', label:I18n.t('statistics.hours_number'), graph: true, simple: false}, + {statistic_index_id: 3, key: 'booking', label:I18n.t('statistics.bookings'), graph: false, simple: true}, + {statistic_index_id: 3, key: 'hour', label:I18n.t('statistics.hours_number'), graph: false, simple: false}, + {statistic_index_id: 4, key: 'booking', label:I18n.t('statistics.tickets_number'), graph: false, simple: false}, + {statistic_index_id: 4, key: 'hour', label:I18n.t('statistics.hours_number'), graph: false, simple: false}, + {statistic_index_id: 5, key: 'member', label:I18n.t('statistics.users'), graph: true, simple: true}, + {statistic_index_id: 6, key: 'project', label:I18n.t('statistics.projects'), graph: false, simple: true}, + {statistic_index_id: 7, key: 'revenue', label:I18n.t('statistics.revenue'), graph: false, simple: false} ]) end if StatisticSubType.count == 0 StatisticSubType.create!([ - {key: 'Stage', label:'Stage', statistic_types: StatisticIndex.find_by(es_type_key: 'event').statistic_types}, - {key: 'Atelier', label:'Atelier', statistic_types: StatisticIndex.find_by(es_type_key: 'event').statistic_types}, - - {key: 'created', label:'Création de compte', statistic_types: StatisticIndex.find_by(es_type_key: 'account').statistic_types}, - {key: 'published', label:'Publication de projet', statistic_types: StatisticIndex.find_by(es_type_key: 'project').statistic_types} + {key: 'created', label:I18n.t('statistics.account_creation'), statistic_types: StatisticIndex.find_by(es_type_key: 'account').statistic_types}, + {key: 'published', label:I18n.t('statistics.project_publication'), statistic_types: StatisticIndex.find_by(es_type_key: 'project').statistic_types} ]) end @@ -131,11 +130,11 @@ end if Training.count == 0 Training.create!([ - {name: "Formation Imprimante 3D"}, - {name: "Formation Laser / Vinyle"}, - {name: "Formation Petite fraiseuse numerique"}, - {name: "Formation Shopbot Grande Fraiseuse"}, - {name: "Formation logiciel 2D"} + {name: "Formation Imprimante 3D", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."}, + {name: "Formation Laser / Vinyle", description: "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."}, + {name: "Formation Petite fraiseuse numerique", description: "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur."}, + {name: "Formation Shopbot Grande Fraiseuse", description: "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."}, + {name: "Formation logiciel 2D", description: "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo."} ]) TrainingsPricing.all.each do |p| @@ -245,13 +244,6 @@ unless Setting.find_by(name: 'subscription_explications_alert').try(:value) setting.save end -unless Setting.find_by(name: 'event_reduced_amount_alert').try(:value) - setting = Setting.find_or_initialize_by(name: 'event_reduced_amount_alert') - setting.value = "* Tarif réduit si vous avez moins de 25 ans, que vous êtes étudiant ou demandeur d'emploi." - setting.save -end - - unless Setting.find_by(name: 'invoice_logo').try(:value) setting = Setting.find_or_initialize_by(name: 'invoice_logo') setting.value = "iVBORw0KGgoAAAANSUhEUgAAAyAAAABNCAYAAABe8gBxAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA/RpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ1dWlkOjVEMjA4OTI0OTNCRkRCMTE5MTRBODU5MEQzMTUwOEM4IiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjAzODFDRjYwMEE1RTExRTQ5NzJDRkFDOTI4MTJEOEM1IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjAzODFDRjVGMEE1RTExRTQ5NzJDRkFDOTI4MTJEOEM1IiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIElsbHVzdHJhdG9yIENTNCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ1dWlkOjI2NzQ5N2UwLTgyODEtNDg4Ny1iOGZlLTExMzA0ODhhZjRhOCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3RDc4OUVFODZFRjBFMzExQjU4NTg3NzUwQzc4MzhDMCIvPiA8ZGM6dGl0bGU+IDxyZGY6QWx0PiA8cmRmOmxpIHhtbDpsYW5nPSJ4LWRlZmF1bHQiPkxBQ0FTRU1BVEUtTE9HTy1WRUNUT1JJU0U8L3JkZjpsaT4gPC9yZGY6QWx0PiA8L2RjOnRpdGxlPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pri35wEAAEzrSURBVHja7F0HeFRF17676b33RnqvJCEEklACBAg1CSSCiihgRxQQBUERBEQpioiiIApIU0TTezZ103vvvfdkN9lk88+Jyffz8dGzd0syL899ErJ3751y5sx5z5w5QxkbGyMwMGY6GAwG0dDYqFhSXGza0dFhkZuXZ8FgMgzz8/O10cfK6JJHlyS6RCa+MoKuQXR1s9nsNlMT03o5ObkSQ0PDfGsrq1wjY+NSNVVVBpVKxY07A9HX30+pq6tTrygvN21qajIrLikxRnKim5uXqz4yMiJLoVCkJmSJgi4WuoZGR0f7NDQ0ug30DZrQPfX2dnZV0tLSFYZGRlVqamqNqioqLNyy/Al2X5/QSGub0mh7h8JQTp48ISQE/Ss83rejo0NCqiq94jZW3Wi27RDR1WFQhIX5sA79xEhzs9zYyKgSMzVdfozFkiUoFFH00SgxNjZEERXtF3ee3U0REuoQ1lDvo0pL818d+lEdmpqlxkahDhkKY8PDUAfxiY+HiVF2v5i9dbeQomIXVUa6S1hVFRtAGBg8AuX1N964zOmHDg8PixkZGWV8tG/f19O58S5cuPAyPS1tubiYGIPsdyGiCJYsc9++fQdm6em1YdGd4kTFZhNVVVVKBQUFTjm5ufNKSkrmtLa1mgwODqqhthYHYo6MREL4KQ0FZDyOPxMgIiIygAzHGm0t7czZDg5R8+fPjzY2Nq5Ffxe4djp+4sT7ZWVlTqKiokwu9ImwhIRE68FPPjmgqKjIEKR2AnlBhEOJTqe7IpKxoLS0zKmjs8MIEVsgryKTjp4nyQDIEMjSuHJG8oe+x5KUlOyWkpJq0NTQRATXINPI0CjdysoqR1tbu0NcXJzUetXV16seQUBlEUMXe7rpASaTKeE6d+7Nbdu23Z3Kc3rv3F3b9Po7P1PExITYvX2iqPOEJsjlGOpENjLeR5DB209QqV2iOtr1wlpaxeJWFulijg6pErPty4SUFEe5Kq8sFsGqrZdjpmdYI8LkOFRQaMtqbjFGxrs6KrvCaHePOCq38EQdxkUT/X1ESF6Ogf7eJayp0SSiplYqZmmeLe7kSJeY55IvrKLMRPdwc9ARIy2t4szcfHNmCt2RkZ5lN9LaajbS2KSJyqGI6iA1UQfqf+owNjZKlZVhIgLYIyQn14qIVIWYuVmOhKtLqvhs+ywRXe0eigDq6ftx48YNr9CwsC1Il3JDh1LQGKKi8XMUjaOSZ/1yS2urwmeffXYU6T1ppF9GiRmCoaEhSQd7++tvv/32vUfd89358yvTMzJeQvbl4HRtB+Gs7KwtJCh1YpQ9qoJ+nbYEpLGxUeLGrZsfDQwMmAoJCXHlnehdxF/3/kp9792dP2MK8XworyhXotHilyckJvggw9odEQ5FMPSQgU1APz5vX97/XWQ0SvX29lrkdORYpGekb/7l1ysMfX392CWeS64s8fQM0tLS6heEtsrLz9P68+6f+1H7jLcRt2TcxcUl1H/jxghBaKPOri7R0NDQxbR42kvFxcVL+vr6lGDVa1KehJ/R0w3ffWDVTGR4eFgF6VSVlpYWu7T0tI1AZhAp6VRXV89wmeMS6uHhHmZpaVUkIS7OcYLAGByUQ3PESxMEZNrpA5A3DQ31MvTrlAjI2PCwPLuzW5EqJ0cgsvFQY5nd3QtLBuqMljZzIiVtSe+tP0BxjIhoaxVIus/7S3ajz3XpJZ6lhBB5q6bM7FzVvsBgr4GQiLXDJWWuoz09alA2MLopoL8m5PURKzRi7J4+WNlRHuosMh7KyXfvCwqFOhCIfFSK29uGyL286ar0ksV0qpwsaSsLrJpaqf7wyIX9fwevY2blLBxp75hFjIxQxokDKgvl8XUgxgaZkmgMKbK7evSHyyvnDETHvUB89wMBhETU2DBG1t/3msz6NREiOtpMQZPnkZER4u69vz5Ac5un6MPkkASgOZTQCNKoRwTk42c2xJlMqeyc7M2o3DIzKVoA2kxWTrYQ/fpIAlJRWWGdkZG+Aen6adsOwmJiYqQ8GAk/ezoLUFJysgcyNkylubgMDQZASkqK/9CO138mq9+mI/r7+4kUeopTYFDQa5mZmevR/5VBOcNFRv9BP8EFfQQXmuwkKisrl3977tvlV369Urts2bKLARv9f9TT02vl53aj0WgrR0dHFbkp42Bcx8fT/PmdgFRVVcncunP7pYSEhLcaGhrMYWWDTHmaJLiT4x4MKPTeJdd/v77k1u1bX+rq6mY4OzvfWLZk6W0rK6t6Tk3mFCqVjd45NinT0w2w2iQsIjLGgU4aN34JKuUxn//7GUUI+lBscmlBeLS93bbn6g3b3t9vfSThPu+e0q53j0kv88wiONSHo13dxEBoxNzuq7/vYCQmr2b3DyhQxETHiRJVVuYZPS3UiTr8a9xSCInxn+z+foP+iOi3+kPD3xQ1M0lQfGPHMbkX/UOoMjKc0QssFjEQFmnSc+3GawPRsf6jHV06QJao4mIEVeoZDbTxPqKM14UiIkxQJP5dRRwbHVFl5uZtZKRlbOw48XWJ7Av+pxV3vnlFkIgIsg8sqqur3UAPcWu8gpMF5tWmpqbDGhoazGfUbaBf2KDbZhIBgZVukSfoHRFhkf/YENMVOED9ORESEuzP7ZAaeF9NTY0b7E/APfBkgHfz5q2bni9veTl830cfpSQlJW1Hf1aWQZMiDGpuKejJFRaYFEZGRnTv3Lnz+eaXXiw89Omhg3V1dXL82nZRUVH+3FZ+0E6FRUUrkIGvwo/t0tPTI/zV11+99vIrW3Jv3bp1rrOz0xzkCUKhuDmBgkyBPgCZQu8WQmTE+caNG6def/ONwm07tv0aGRnpBN5QDAEAGNKICFCkpEQH45P86lb7pdb7vPAtq7JqSrqB3dtLdJ761rPaZUFUw0uvJg1ExbyMhFSBKidLUCB0j5PyCgYkIgJUWVkKq7rWrfmdXcHV85f8MxAZbTil5yIZ7vvrH6taT++rdev9c3r/vLdnbJilA3UYJx6cjD5A7QHtAs8eHWSYdpw6e6HGfWlq14WfFhJswfCnRsfG+CBSzdXVSiAPra2tprR42gI8mDEwASEZRcXFqiWlpSu4tcR5v9HBYrHEAgMDfXAvPB5BQUHOm198MfzEyZMRTc3NS6SkpKgSEhI89+KCkYrKAr8qBQUHf4aISM7Fixf9Ub/yVftlZmZa1zc0uHGbZEP79Pb2qoeEhi7nN5kKDQ112PTiZtrvN25cHBsbmwXGvzCfbCaeJCOoPDL5+QUv7vv4o5TXtm0LRETEZXJPCYYATMiSyIiXkRbuCwp5u2reYnrvzT8cn+c5ff8EW1cvWh7U/MG+CFZj4yJEDJDBLkVwY4/G+MqKvDwxXFbmXeftk972yeENz/McRnqmeq33+u/r/TZlMFLTNlFlZMTHN75zgehDOJqQoiIx0t5u3fzWe1ENm7YeHu3s5OslwJaWFuHk5GRfsveFPUr/xMTGBuARjIEJCMlISEhYyWAwVHhhzIJHOjMr06e7u1sY98T/oqKyUunNt976/tDhT5OaW5qXyKAJix83f08SEWQc6n3/w4Xft+3Y/kdBYaE6v5QvjhbnA5vCefFuIPbJKckb+cWDPzw8TBz69NNd+w8eSOrs7JwLhj6/hguATgIDBAh3UUnRSkREkl55desNOp1uiLWDgAD1IYQusQcGTBs2vRKNDHjvp/3qSEOjeMPGFw83+G1KGy4sXiGkpPjwPSncqAY4fCQk5NuPfnmzdd8ne5/2e6jeRNv+Qy/XLPDKGYiNfx2RJ1EKL+LgYW/Mv2SK0nPj1if1fpuvsbu7RflVbFJSUua3trbacGtP6oN2SW5e7vL8ggJVPIAxMAEhCQwmgwgPD/PnVVweeFwbGxtto2Oi5+Pe+H+Al/fqtaver772alZaetrrUpJSQqKionxfbpgsIISnsLBw/ZtvvZl25487i3hdppaWFlFafDxPPGmTBKSkpGRhCp1uyuu2qK+vF9++Y/uvQcFBp5BMiQlSJjNxsXEiQkFtuXHnrveyPzn4yYdtbW0iWFsICA9BskaVkZZpO3LidsfJM15Pur/vz3uW1QuWxff+ce8TZLCLAQEgeJ1mH5IqyMsRHSdPn2j98MDuJ90+lF+oVLt09a32Y1//QhERVqX+u1rMez2NiNxgXEJA3foXfh3t6uLLlZC4eJo/L8jHpONjiDmkkpiYuBKPXAxMQEhCTk6OeV19vQcvDRFQMjQabSPujX/R1t4uvO+jfV99ferUPyMjIzqCmDUCwsNQ2bWPnzgRevKrk29AJjlegZ5Kd+vs7LTk1WQGYLPZEnG0uHU8JR8N9TK73t91Ly8//0VYrRLUTdhAJBGpkw4OCTn+8itbaIhcWmGtISgzNJUQkpMVbz964re+v4MsH3Vbx1dn/Oo3vpg00tTsCAY/wU+yCis6cnJE+5enT7YfOfHI8OGe3353qPFcmczMyPSjKshzJdTqqYF4HLTrQGzcxqYd7xznNzGpqKhQyMzMXMnLDcvgOIqNiw3g5dyFgQnItEZgYND4Ji9eGxQZmZneJaWlijO9P/Lz81XfefedkKjo6A9kZWUJXhrNUwWsbiHyJHL12rXzBw5+8unQ0BBPyhEUHOzP63aEiTQtLc23t7eXJzqqvr5e8r1du/6orqlZKsOhTD68tQEp4yttXV1dLnv27kk+//35rRBahiEAQGNxjMVSbnl/3wVEMP7L8zU2NEw0vPTa7taPDt6iSkvLUsAA5cfDhSGTm5wsEKXvGEn0WQ9+3Hn2/IrGV3ZEjw0OGlP48IDDfxt7jBBSUICVpr0dJ8+s4qei0RLivXr7+rR5GRoKBAQRIffMrExzPGgxMAHhMDo6OoTQ4PLldVo0MCYGBwe14+Pjl87k/khPTzd6Z+e7MZVVlZ5APqYDoG/l5eWJ6OjoQ/sP7D/CbRJSXl6uVFxc7M3r8DUgY3V1dbPjaLS53H73IINB7Pto34/V1dWQvIAYG5sehyVDPaBfkf6SPn/h+58/P3pkL4EhGHpBQoJgVVfPbz3w2dv/6U9EIBtf2X6w5+rvJ8fT3fJ7GlNEpNhMplrLxweP3J9VqvPsd+tb9nz0J1VaSo6AyAI+H28UpBPavzx1fLiklC88E+BIiIyM9Bfjg5BjpGPEIiIjcZIcDExAOI3UtLT5bW1ttvzgZYcQsMSkRH82mz0j+yItPd1yz4d7I5lMpoWEuMS0MRInAd7q6JiY/Qc/PcRVIzEhMXF5f3+/Oj9ssqbyKNTw1KlTHxYVF2+aTuTjQSIiLSXd7T7fLQprdYHptPGN6b137n7AzMweT1Hd+PL23T037nwGXnlCQMIDYU8HI4m+qe+vf9wmyMeylj0fX6NKSooRArJ6DQccsru6LDrPXdjOD+XJy88zrKioWMQPex7BOZuUlOSL7CQhPGgxMAHhIAKDAv0pVP5Q9DDQCwsLPZEhbjADyYfx3g/3BiLyoScIG82nQkKioqJOXPzppw3ceB940kLDQgP4pU3FkYynpqWuqays5NpZKX/du+f8172/PpXik82vZADOeHlx8+a3lyxZkoG1uiDN1lRibJCh1X3pil/nmXNePTdvnxSCvRKCBkQ0ui5efq3vn2CD1n0HryLyIU4IWOgspAPuvfnHjuGSUp5vOIyj0dawWCxp/uhaIYgUsc3IyMBJcjAwAeEUampqFJDB7y0myj+nUo6MjkjRaLS1M6kfcnJy1Pbs3XMXkY9Z05l8TAI2p//080/fx8bFkX74ZH5+vjGS8wX8kukJVmH6+/t1ExMTPbnxPjRxUn755ZcvUf3Fp+Op34DBwUFi7ty5Z994/Y1rWKsL4IQtgwzf23f3tn169GcheXnBrIOkBMFITfdu2rojhCImqkwI4r49VObRjk7j/iDenlfU09NDxMbGbuBVxsJH6e3QsFB/PFoxMAHhEEJCQ5b19vZq81P+f0izGRsXu6Gzq3NG9EFra6vIF8ePXRlkDFrOBPIxqczRpfjlVyfPw0FTZL7r78B/1rFYLEl+Mr6BDMXF0wK4EQp19do1/5raGg+xaSpbcNilqopqwv6P9++jUrHqF0igsTk2PKyHlIImIcgkeWxMcWxk1IQQ4KQhcEZIX3AYT/c7pGekz2lsapzDLweiAiA6Iys727u6uloBD1gMTECmCDhjIjEpKYDfjF5QOsgon5OWlj5nuvcB7HU5duL4F2VlZcskJSRnlPyBQm9pbvb45ty3u8l6R3dPDyUzI3MDrxMsPAgYcwUFBUuzs7N1yXwPIvHUsLCwnZDCeWwayhAQODSG2j788MPXNDU0cJ5MwfZKEISgr9BB+QWcBFPExYihvIL5rKpqnm1GDwoO9qNSqHwmnlQI89SOjI5aigcrBiYgU0R+QYFBZWXlIn49hCwuLnbDdO+DX678sjo2NnY37IuYjhuDn2Q8wp6EqKiojxMSE0gJxUpPT3dtbmmezU+etH/tFArsTZGBOGcy3xMTHbO4ta11Dr/Vn1Po7+8nXn7ppbfc3dxKsEbHwOAMERzt6tZh5uTxJO1sQ0ODdE5Ozjp+cxoBwFaKjo72HxkZwXKCgQnIlLwMQYFrmEND0vwYFw6xn8kpKWtra2un7a7ZouJilSu//np6Om8MfhpDHBERmQs//HCUjPMbAgMD+ZbEgoxHx0T79/b2kkdA4mIDpmNYEsgNkA/Xua5f7di+4zbW5hgYHMQYm0AEhCcHeyYnJy/q7u424Ee9BQSkpqbGs6ioyAALCQYmIM8JmLzT0tM38GtcOCgfZJgZJCUnL56uffDjjz8cHBgYMOB1+mNYibj/4oUhXlhYuPb69escPQSroaFBNr8gfzU/etIAsCrR3NzskpmZOZuM5yPyLovadQlZ9YfwQQaDMZ596lEXbA4HYgnhnpwEnEqsP0s/5tNDh/bjfR8YGJzHaGubGS/eGxIaEsCvURng+BgaGpJGZVyDJQTjkXM7boLHI44W51xbV+cizcfed1BCwSHBARv8/P6ebkZGWHiYMy0+/nVpLp+OCwQDlo9h4y4Yhcg4HRMVFR1Afx+a+FwKGY7i0N6wT4FboTtAQv64++fHa9asCVZQUOCItRoaFubZ2dU1S4ZfTyD+t72pUdHRfgsWLOB46tjc3Fy7np4ebU7L2ET4GMhOs5Oj4217e/s0NTW1FiRPsAeDij6XQJO0fHFxsXp3T49BdXW1cWdnpymUBcmdMIxrkKvnJd4gtyLCws379n24TVlZeZhP+5Wn72fD+8fGpmfKMwzyDW0RUWKosEib2+8tKirSKikt9eLnZCzg0ElMStrQP9B/WlrqkbqVgnQAhZNOPTIjVThVxgm9M2P1LiYgT01AaH5UPt/wB0qooqJiWWFRoZaVpVXDdGl7IAC/37ixHxliwtwcmAwmk6BSKcN6unppNtY2MaamJhk62jqV6urq3WA8IgVHQYajVFl5mW5+foELPZXu3dTU5IL+LER2OkQwShsaGlz+vHvX79WtW29wor6JSYkBonzqSbufeCWnJK9rbGr8TFNDk8HJZxcUFTqRMWmB/MrKymZ9efzEChsbm+ZH3bdm9Zr/TAhtbW3idXV1hjm5uc4pKSmLqqqrFiBSog1ERExMFE2uT+9ggBPdd+3c+brjbMcKfuxTNpvNQNcA2Ay8KsPoyIgsKsMgnukwntPaJdiDg8rjJ7tz0fkXHBKyCs1T8vzsGAWdBfNiQkKCk9cyr7RH+0lGO9CFVOAYJzaMUNF75cnQKah8TFTOfk48G+kdOdB/PNS93RPtzVPjFhOQxwANHr7d5PUg42cymQpxcXGrEAG5MF3aPyoqagGEBklJckfJQhiMsIhw6wIPj583+G24Ym1tXSL+mL43NzevXL1qdezAwMDxjMxMhxs3buxKS0/zR4RQmKwVEfBqwwWHYr704os3proEX1hYqFtWVraE39Maw0pTd3e3SVJS0kJfH99gTj67orzcmoz+QiSVePutt/c9jnzcP4YBqqqqTHQVzJ49u+CVLVsuI0IiSU9NdQ0JDdmYm5u7lsHoV4azYR63KgLP6uvrI5YtWXp086bN9/ixP2Gs+fj4XPDf6L8P/S7Hq3KgiZgiLy8/gGc7jOefgKlS3MxKBuGcySnJ/oKQLhwcKxGRkRseRUDU1NRaf7jwgzMnDGGkw1mdnZ2aBw8dTEZtJMvJaBBocw93j+tvvvnmW+j3KWc9A70jKyvbz4s+YbFYox9++OFqZCtmozmKpwfIYALyGERGRS1EBoAhZF7idwBJQuX13/LylgvTYbM2GNm/37yxkxv7PuBdaCCy3N3cz7380kvHkMHY9izfh/Z2d3PLRNeLf//zz7ffnf/uXEdHhxMn+wHKCEpQQV6h0me9z3feK1de5UT8b3BoyGowAKX5OPzqvgmGCAkJ9eckAYH9GYhA6nJ6BQQmXnFxcYa1tVXhVBwLiIwMrvL2jkT9HVlSWvphSEiIf3hE+NstLS3mkDL4YeMD5MRA3yDsg/ffP8SvBypCu6soKw/M0tOD0LA2AgNDcMFVJpCYmGhTW1s7DxwRgmCX5OXlrWtpbflUTVXtf4i+qKgo28LcvJ1T70MERAwRjzFOhxiBvpKTk2Xoz5oF4bMCm8Z8ItRtbJberC5DA4M+9Kc+XpYH70p8DBIS4v3J2OQ1MjLSgWSAozHZUM7GxkbXrKwsm+nQ9lHR0Tb5+fkr4bBFsjCxUQ7arvzgJwcXfv3VV+8/K/l4EKtXrUr95dLl+R7uHt9BAgMOeCvGNykjMpOz6YVNr/x25VfrXe+9d8rY2Lh1qs8GL3RqaiopZ3+gcrchGWdzdJYXFSXKysu8ikuK1TkpAxQqVZkk2ZJA7evBqeeZmZp2or4/j2TAfttr215GpLEMZOz+yRaIKmqnug8//HCHsrLyKIExswGyAeFB91+CmMacv+vAVZYfHRvjh8Y8R53HoF+QXQIhxhwNCwIHCSIFhpGRUYvwYJyZ8ooJyHMgOydbMzcvbzmnjTPYlOrq6npaQV6hBFg1h1m6yD+BgX7Tof1jYmM2I6VI2sYEULhggGtqaNIu/nhx3ipv70ROPVtDQ2P45Jdfvu3t7X3weUkIECMIo1FXV6ft2b1n7c3fbzi+v2vXLxqaGhyLV09ITLSvqqqax2mSDXsfFixY8IW4uHgrJz1R0GeIjKkEBQev5NQzW1tbhdrb28XJWGmDfSs/X7r0/YkvT+yqqKyUY3OoLRCxGHrj9dd/vXL5F7tV3qsOILLXD/ICYDAY7B3bt7/uOHt2DdbiMxRoXhlDuo3d00uMDQ2xKGJiLRRR0YrxS0yseWyYxZr4jK+J0xiDMV4Hdn8/myIi0o7KX4muclSHevQ5Y7wOqJ7EDDoXqr2jQzQ7K9uH03YJ6A9zc/MrmpqaNE6f3QG6NS4uNgAPTIwHgUOwHoH4+PhViCwocDo2HlY+vFd6X/mD+YdOS2uLNScVCTwrJzdnfVtb2+cqKirDgtr2dXV1Eunp6aTuvYH0pNra2rFnTp1eraury/FlSIg//fTgoc/Re+QjIiLel5WVfarME1AuRCRHbG1sg5ctW/r1iuUraGSF1NFocX4UCmeP0YU6CgsLN69fu+5SdXX1cnSpc5LggEyk0un+zKGhn8U5IB9ojFPAKUBGqBI8E8mBzO07d04FBgXtMzUxjbOxsYm0tbVNNjMzK1NTVZ3SUr6amtrgp4cOHfXyWnbn61Onvs/Pz1+4bu3azzZv2hxMYMxI4oGMdYIqLdMkucD9b+kVy8LFLM3zxYwM28YmPNsUISHx4Zpa1cGEJMe+P//exMzO9aJKSVIJHqc4/69qoDpQhIX7JFzmhEotcg8Ss7HOFLcwa0KKZQApGDaqg+hoV5fSYEqaVX9giM9AdCw43aQoEJI0zclITHS0B7IbzDkdMstCpMPPx/dWQWHhnOu/X1/GyT1xoLMLi4q8cnNzNZH+a8QDFQMTkMcAYqgTk5I2cpp8gGdBXV09x23+/Pra2troxKTEHZw0ssHT0N7RYZGUlOSxZs2aCEFtf0Si5nZ2dhqRtS9hIkyl5LNDnwaQQT7uN0B3f7B7b0VFhW19ff3iR/U1rIQB8QDRs7ezv7V58+bT8+fNyyEzpXJra6tEZmbmejJW+MzNzJIdHR177e3to0pLS5dykoDAxFhTW+uelZlpOXfu3IKpPk9LS2tUU1NzGPURQcZGfJABIJCImKnmF+T7ZWVn+aFxypaXl6/R1NDMNTDQTzcxMck2MjIq1tPVq1NSUhp61n53meNS8sOFH1ZeunxpzeYXNgVhDT4DucfgIEERFW1UeOv14wrbXrmCiMejTu1kCuvqdEu6zStV3v3e9c5zP3i2HTn289jwsC5FFHQBjwx4OGh1aIhA5RiQ9Vv/ndLOt74Vd7CrfwQxGkJ16BOztalW2PFq4EBY5PHmXXt/Gq6qnk8FZ800JiGxcXH+nE6YAfOPlKRko4ODQwaFSh3kfNeORxsoxMTGrEIE5Ac8WjEwAXkM6Kl0a2SQuMEmT04CljmdnJzCwMBwnTs34aeff+pCg1+Bk4amEHpWcGiIvyATkMTEJC8yjW/YcP7poUPbra2tm8mui4qy8uh7O3fu3LN3Lx3ODrnf0w6rBRAGJi4u3rls6bJfVqxY8d0cZ+dKbmy8R5OBR3NLiymnEyzAnpX58+ePG8FOjo4RN2/dOk5wMOZ0IlZZNCgk2IcTBAQ9D6yVHvLtK8q4J3CC8FFRv+sXlxTrI1KyBuQAkZ9hWVnZOg0NjRIDff1sY2OTdGNj43z9WbOqFRUVWU9aoVFUUGDsfv+DGwTGzAKSHXZvL4EIxTW1UyfeE7e3ffoNvSIihOKutyNFzU09G198NXqMxdLmyUoIpLJFdRDR0kxS/fLoNkRAnilxg9QyzxLd8H+8ar3WBLKqqhdQxMWnZVcXFhaq5OblruC00wh0tqWlZZyKisqQg719gbKyclF/f785J+chCEelxcf7b9269QcZaRk8bnmsNXjnacAE5Mlehtg4X7Laxt7OLgR+6unpNWppaiVVVVet5KTnFZ5VUlKyory8XMXIyEjgssuAQV5aWuJB1gmvsLrlOtf125UrVtK4Vaf58+YXIGP5fHx8/B7whsNKGJRDQlKyfrnX8gubN236ydTUtIWb7UyjxZNyii565oCtrV00/G5tZZ2LCFhuX1+fLScnM5iAs7KyfDo6Oo4pKSmxpma/QVYQdgu35RzaA677jAlRJPuGaOwaFhQUrACvpLCw8DCqX5Wujk6WkZFhkp2dQ4qFuXmRmppavxAfhcw8DyCDT2hY2Jq8vDzN0dFRnuQTReNQHBG+1AP7D5wUyANcYa8HgzEmv+Wl3WrfnDxFfU6HmbTXkjLFd998r+2zo3eoctzPiMzu6SGkFi/8Wf38mbdEDfSfa2OKiLbWgNqXR1+v3/hiGhrQMgRl+p0tGUejLUc6Qp3j4VeIgMxzdQ2E3xH5YNnZ2UVGRUWZc9IBC3NNTU3NvLS0dOtFCxfmYSuTNwBHFuoLobNnzxyXkZHpQPMMTyYSSP+7Y/uOU5iAPICmpibRxKREX057GSDsR0lJsXi2w+zxk5xhGdXZ2SmopLSEowQEJlJk8KmHhIZ6vfP2278JWvtXVlbqNLe0mJNxLsOEp7ll22uvfc3tem1+YdOFxMTE13t6emTUVNWKXwgI+BaRj6v6+vq93C4LMnBVs3Oyl5PhSdPR0aFbWlhUwf+R8TyKCHdYRGSkLScnM5CN5uZmm+iY6Hl+vn6xUx0verp61aWlZQSvz0KBskAZ7iuHaG9vr2l2To5pekaG/81bt8fQpNGop6ubbmVpFenu4R5hZWVVIiEuIWjDfLwPGxsbrWtra615VQYIFzQ0MJRBeuGkwDUgEGdUfpVP97+mtG/3pak+Tn7blj+6f72WMtLS6kLh4qGk7EEGIbN65Rmt3y7tokhOTY6lVywrQUTman9oxBtUKUliOmFkdIRISk7yJ2FPKjgDuue6zI2b/Ju9rV1wRETEOyRUQyQ+nuaHCQjPSQilqLjYm9NJkJ4F4ID19fH9ExOQB0BPTXXv6uqy4PTGX5js5s6dG6mmpvYfD4+bm3vkrdu3mUgJiHNyEywoqRR6iv/rO3b8JsLnJ1w/iKrqanPm0JAMGSe8gtAvXbr0V2S0cX0jnL29feXixYvPqigr12zetPk3WO7mVRvTEuJXMplMFU570kDGnZ2dg+/PT+/qOi84PCJiL6frACsA8fEJ/lMlIABDQ6OcsPBwvhwPD6yUUBDJ00KTh1Zefv6aW3dusxDhS5vj7HzLc7HnbSRjArPBcyJZAUHWgZ1PS/jExEQFcsMAe2CAkHvxhS84QT7GCaG6OiG1ZHFg90+XuUZAgECJmRiFal66MGXyMQmZlV5/9QeFvDHd7JLU1DTT0tLSBeIcDi+D1Xhtbe0UfX39hsm/Oc9xTpaRkakbHR3V4eTKIJQ9OSVlfXNz8xF1dfVhAoNn4LWzDfQ/Fc1rOA3vA4iOjvanCnG+WWAFxGWOy39lp7G0sCjT0NDI4HTaOyAdVVVVC7Ozs00Frf2RkrUiaxMhInkjXsu8rvHI60B8ceToJ7ve2/UTL8kHhLhFREQEkJFhDE1W7LkuLv+198jKyjINTWaVnPa2QPnzC/K9KysrFadOQAyzoOyCMD7+NZrFxje2o58icPbPjZs3z7z59lvFb73z9uWg4GAHWInCmOYYZRPCKirlnHykpKtLFndZFJugyshUczLsS8LFqYgqJztAsNnTqrtjY2PWIRuC40udsC8V2SUh9xMb/Vn6Pebm5rHgUOK0M6W1rc0SkRB3PIAxxucz3AT3Gb9lpUpZ2VkcP/wOjC9paelmO1vblAc9AtZW1uGcHuhg7KJnSoSEhq4TtD6ob6g3JyO+HUielpZWGuqDnJks49k52eYNDQ3unF4ZA4KNiFWuhblF/v1/n6U3axD9LY7TMg6GeHd3t1ZEVOTSqT7L1tamUFVVNR/qIEiYiOcF3QI/ZdLS0rYcPHQwbcvWV/4MCQ2xxxp92oOjipIiLtYq6HUQUlDopYiKdU+nTFjt7e3UhIQEX06vfkwmv1jg4RH54GfOTs5BZDgyRISFibDwMH88dDEwAXkAcXG0FbDJi9NnAoynJjU3jzM0NOx68LOFCxaEkrEBEryk6Rnpfl1dXQK1WxUpW10yCAj0ga2NTQynsz4JGqJjYnyQoc3x5Q/wpDnY24crKir+j+vRw909cIQE4x5kPDEhMWCqE6WCvMKIra3tvYlUyAIJ0FkQ+obICLWiomLdJwcPpr6z893zJaWlygQGxtNhZBrUYXTimjag0+muLW2tszk9L4LDRVlZOdPIyOh/so7NdnBIQISnZ4zDRG585To/f0VJaQnWSxiYgNxvQEVERgSQERsH3ndrK6vAh31mZmaWJS8nV8Bp7yvEVjc0NDjE0WgugtIHTNQHiCiok3EoHHuMTSAjM3Umy3hbW5tQfHy8nzhJaSrnz5//0APwrK2tEyTExds4PZnBWC0uLV6cnpFhMNVneS1bdg1N8IPToZ8nQrSEU1JS3ti+Y3vGr7/+umxsBp0WjTGjMe3SX8XR4jZQKZw31cApZ29nFwqH5D4IS0vLBgMDgyROr4LA3M5gMDQSExOXY1HFwARkAnn5+Sa1dXUcT/8KEz8yCHrmuc6LfdjnampqLEsrK46HYQHGN+omxG8UlD6Aw/FaWlpkyFgBEREWYaO2Lp/JMk6n0+d3dHTYcLp9IcRQRkam3MLcIu1hn5uYmLQa6BvEk7Gkzx5lS8XH06Ycauju5l6CJuNfIVHBdAFkHhsdHdU9883ZkMOfH97b29tLYGBgCA6qqqrkUtPS1pDhNAIy4OHhEfoo28FljksQGXYJOI4iIiP9wemLgQkIBkJkZOS6ERZLktPedzC69PX1ky0tLesfY/wEk5ESDTyhOTk5a2pqa2QFQhgpFAnU/hzPnzh+0quUVI/+LP32mSzjQcFB/mSQO5hILC0sonV0dAYfNZm5urqSElMMMp6ckrKhp2fqZwlu377jKJocG9nTaAMrtD0ih5S7f/11YvfePacxCcHAEBwkJCZ69vX16XI6TBsiLhQUFIptbW0fmXgAfRaJ9AfHWQIQkMrKyoW5ebkmuIcxAZnx6O7upsQnxPuRkRno32VO+9DHraw4zp6dCmnvOG34gNLq7OzUjY6JWSIgXSEycZGBPnQNzlQZr6quUigqKfEmI8QQ5NbD3SPkcfe4zJkTi8bAIKdDgSDUsLa21jkpOWnOVJ/lYG9fH+Af8H5/f/+06385OTkiPSPjvb37PjxFhlcTAwODs4DQ7bDwMFLCwsf3pZqZRykrKT9SGdjZ2ZVoampyPEvnOAFisyWCQ0LW4V7GBGTGIzkl2bW1tXU2GTnphYSERpycnCIfd4+Ojk6voYFhHBkeYlBeNBrtBQHJ8CNEcDgryn2Axh2ZqTIeF0db1tfXp81pT9rEQVZt9vb2iY+7z9jEpFJDQzOdjMkMvPyIZHMk1PCNN9646enp+WVvXy9BmWanKctISxOpaWm7zn7zzftY62Ng8Dfy8/P1Kiorl5Bxltcoe5Rwd3cPepK+cHZ2DiMjVEoM2SV0Ot2vq7sb26CYgMxshISEbiDD2ABjS1NTM3O2g0PBk+6dP39+EFkEpKioaEl2drauAHQFe+Iii9zMSHkHOQyPCPcXJWEiA0+akaFRvIGBQdvj7pOSlCTmODsHk+F9h5XLtLS0tdU1NVM+vZKK9MCe3Xv2mZuZ/w4rIdONhEhLSxHXrl87cf36dU+s+TEw+Be0+PjVw0NDspzWQbBiLS8nX+/s5JT8pHvRPWFk1A2cvW1tbbPT09JccE9jAjJj0dDQIFdQWLCGjPCrf1OTOoQ+zYnTrnPnxklJSXVyOkQFlBciNjLxCQlrBMFWJv5dqSADYJxKzEQZz83NNaiurl5MhicNSLOLi0vg00yS8+fPCxMSEuI4wYRVnb6+Pv2U5GSOGNWqKipjZ06d3mJqYnJ7uu2ZoKB/SB8J/3Tp53Nl5eXyeArEwOA/IH1GREVHbSRj8znobD1dvTgtLa3uJ91ra2ObpaysXERGBAXMGUHBwRtxb2MCMmMRExOzuKenR4+MszjGB7CtbcjT3Kenp9ekoaGRREaICpCrhIT4jf39fXzdF2w2mwkXpz0+8DxEBuVaW1vlZqKMIyW/Znh4WJoMb76wsHC/ra1NzNPca2pimqugoJhDxmQG5ApN2AGc2kelqqo6fOrrUwFz5sw5Bysh0ymNLYSs9fT2mP7008VdeArEwOA/ZGRmODQ1NbmSERYOBGTePNegp5kPVFRUhm1sbCLIWrnOzctdU19fL4t7HBOQGQcwKiKjIgPIGORgZCkqKpbMcXbOfNrB6DJnThAZ8ZZgnFXX1Mylp6bO5uf+kJOTG1SQV+jntIE6kXtctLGp0WCmyXgfIp1oMttAxgofTGQ6OjppNtY21U9zv7KyMtvOzjacDBmHUMOCwsKlBQUF2px6ppqa2ui3Z795Z4Pfhq1MJrNXkA8qfBBSklKQYWdHTm6OGp4GMTD4C4GBgRsIEs40AZtHSkqq122+W/zTfmeeq2sQGU4jcPr29PbohYWH43BQTEBmHnJyc3XLKyqWkJVlws7WNlpdXf2pXQce7h4xyFAcJsPbioxwalRUtB+fExC2nJxsGxlpUIGEFBUVOcw0Gaen0J0bGxtdyCDZIOOINIfCeRNPiwUeHqTEFEP/ovIoREVHr+Lkc6HdPty79/LZM2dnm5qa/g0hWdMhixS0FyJUauHh4evxNIiBwT9oamoSR7bJejLsEnAaaWlpJRsYGNQ/7XesraxTyMjSCRARFoEkOQH4oFRMQGYcwsLCVjMYDDmyNqA7OMwOepbvmJqZlqiqqmaQ4W2AWFI6nb6uoaGBr/dBiEtI1JGhjGAVKCs7ewEZIW78jJDQ0A2kKXcKhe042zH8Wb5iY2OTJisrW0XGZAYTdmxs7EYy0ujOdXEp/+H7C2sO7N/vpa2tnQwx2oJORMY376enr8UHgmFg8A9S6PRFXV1dxmSc2QQ6y8nRMehZHFKIrPSamZrFk3UoYUVlxdLCokId3POYgMwYgAFBT6VvJCM0BYwrZGQ1Ojs7JT3L9yAswt7ePpgMgwCWO7t7uk3oqakL+blfDA0My8jIBgYEpKKiYm5BQcGMUXT19fXSuXm5a8mQcSDJaqqqOba2trnP8j1NDc1+czOzaDImM+jjxubGeTm5ObZktKeEhAThs94n7Lcrv87f/9HHy3W0dUIHBwdZ6CIEJM31fwGMkKamJsfqmmochoWBwScICgokJSwcHFFIRw7Nnzc/4lm/6+HuHkSG8248PJrJlA0OCVmDex4TkBmDxKREh9ra2nkiJKUmNTc3DzfQN+h69oHu8beQkBAp1gwotfCI8AB+7hddXZ18Mp47uRE9JjaWZ4cfXb12dU1sXKwVt5abI6OjFnZ0dBiSdfr5bAeHewoKCs+8lOHm5v4HWQb7GHtMODQ0zJfMdhUXF2f7+PiE/nL58vKzZ87arVix4igiJ0UDAwNj6Bof/4JwmvpEGJZiTU2NGZ4KMTB4j9LSUvXSsjIvMsKvQOeqqqrSTUxMip/1u3AquqSkZBsZcxecCZKcnOIPjhyMmQXhmVrx+Ph4PwpJSf7HPbGNjXrv7tx5DA36p3Y/U6lUNoPBEENgoP9Kc3ygi4nB4UZexSXFGmamZk382C8GBgaFqP1gCYTjzBA82CGhIds3bthwUUNDg8HNeiEjT+b7Cxe+QYa7lsucOVf9fP3OOjk5ZT3L/olnRVJiUgAZBHtSlgqLiuyRjB9HMi76DDI+2tvbK4/6AhiIEBnloqfS17e2th5Bky2psUXQd65z5xai60BbR9tnGWkZdtk52UsKCgsX1tXX2/b39alMeB3HyT+sQvLbuSJAlCorK03Rr3HcfjeSG+bIyAhYHTxpFEQUpdA1/Y69xxBYBAUHew8MDCg/Ter+ZwXoHzT/yB745JNjSC89tV0COguN1VERUZHRERbnV0FAP9bV185NSEhwWLp0aSaWAvLBYrF6ke7n2bI9kkMp9PrhGUlAEDmQSM/IWE9GaAoAPM6dnZ0Lm5ubFz6PkiDD+zGpSJhMpnJUVNQKREB+5se+MTY2rlRSUqro6ekx47TnHp7X3tFhefXa1Vf27N5znpv1unHzpi9qe12YWNLS019OpqdsNjQwDA3w9z/ttcwrBhnkHHWZFxQUaBYVF3mRJeNgUCP5XlNXV7fmefqBLGI0MfYsaDSau6+vbwQnntnf3y8SR4uzWeK5JAuNzYf2k4qSCsvLyysNLqTcv2hqblYoKMi3KC8vn4P6wrm2rs62u7tbHxm8YpPtB2WFi5ekBPRNW1u7Hrffy2AwiNWrVp9fv379PtiHx4u6IyOMIi4uPkRWCnYMjGc0yogUeoo/WTob5ByNNbu09DS757EdoFyk6aoxghodG+OHCQj5GBkZGd313i5vMzOzLDQfifOiDIj8UAwNDXtnJAGJo9EWtLW1mcjIyJA6sZNxiNBUAeQmOibWf+vWV3+W4MPyKcgrDBsYGCTT6XQzWLHgNOBE7rv37h1ctmxZkI21TQ036lRZVSUbFh62d3K1Y0IuhJDxvvLI0aMrr/9+I2XxwoWn1/usv6eqwhmvfXhkxKqBwQEFGWlyZBw8+5MGNL8ByhQWEeHPKQJy+84d7xNfnvhzy5Yt+9/bufMLcbHHjxsgV7o6Ol3oSkT/TYRVBkSoRVB/a5dXlJuXlpbaVVVXWyMdZNbe3q43MDCgAO0JOgOIyeRqCbcISH1DvSK3+wjCQTQ01ActzM1htbMdmwUYMx05ubmWNbW1bqIkOWcmiQQ/2iVAbnJycta3tbd9pqKswsTSQJrTBYz/MRMTky5rKytY/eXpCvCMJCCxcbGkhabwO6DeDQ317unpaZZu890K+LGMrnNdQ5OSkl4hSwGzR0fVjh0/fu7HCz+sQSSUTfaAP/vN2U/6+vrMHgy3AjIIF+oPlx9/unjz7r2/ilevWn1u9erVV3W0tXue952wDyE5Kdn/SYbydAW0aVlZ6cqqqioVfX39tqk8q6CwUOOXK1dOKysrE7du3TpaUVFh/Pnhz99WV1MbeBYjX0FBgYWuKhsbmyr0p2D4e39/P7WxsVGpobFBHz3Xoqys3KGmpsa+qbnJAvWh4uTETMaG1AcImxSemjEweIt/Av/xGWGxRMVIioDgZ4DTqLW11SQqKnqh/8aNIVgaSAXfxAHPuLXnvPw8tby8vOVkLXMKAkZGRkRDQkL5Nv8/MtJiJSQkWsnarA19X15e7v3Z4c9OkZ2C9MeLF/3j4+N3P26vB5BCWI1DRqfZpcuXzm3avKlw/4EDnxaXFGs+zzvpdLp1ZVXl/JlKssHgR8a9Wlh4+PKpPAfS+X5+5PMzTCZDDyZI6KOcnJwtL295OTEkNGTKh3pKS0uzTUxM2hYuWJj62quv/XLi+PF3r1696nb50mWTQwcPLVq7Zu2nSkpKGRCuRHLiAgqBgYHBM3R1dQlnZWX5zGS7BBwtcXFx/lgaZtBcPdMqnJiYuBIZncr8thmUm4Al2JzcHF+k9PjSQrW0sGhFJCSMTHIAhCA6Jmbnx/v3f0XWe27eurX08i+Xf37aDYVg5E7cqxkWHnZo2/bthW+9/fZ3sXFx5s+SVSk+IR4OnJyxCSYmSV0cLc6fNYXUkT9c/HF7aWnphvtDFiAssK+vz/azw4cT9n9y4CDsJ+PoJIxkwEBfv2OVt3fMgf37P7t+7brjq1tf9UUy2k1WWyFywyIwMDB4hqjoaLempiYbslc7+RlAvvLy85bnF+TjtOCYgEw/gCcRGXMBojNwifNBQ7e1tdUmjkabx69ldHdz/5WM80DuB3i0afG0D7bt2H6roqJCgVPPhfj2K1euBJw6feoumlAkn5Xswv1SUlLwUy4tPe3NPXv3ZO144/Wb4RERc+H8msehpbVFhJ6a6sOPcb7cJiDV1dUL09JSTZ7n+zExMeZ37tw5Dv3wsGcjHSIeGhr62UtbXk779bffVjGZ5IQtS6P379i+/Y8Af//DZKWpRPXpITAwMHgGNA9t5Mf9dNzE+JkgDIZKXBxtJZYITECmijF+q2xmZqYFMjTdZjoBmRzsERERfLvc6bVsWZSOjk4K2SQEVkKKi4v9tr++I/3q1aurp3qKNpIvmbffeefsN+e+vS4mJiY5lUkF+gg87qiMYnl5eRs++vijxJe3bIm6cePGSlTOh7rKEhMSPZpbWizwZEaBVIPiSMafOdSwrb1d+NTp09+hXxUetRkcng+rVYh4WH7z7Td/b37pxag7d/5YQBZJ0NOb1UDG2SJwuJi5mRneBI6BwSPU1NYoFhQUeM/k8KtJgG2WlJzkT8ahhxj8B9LW+8bYbAoIEZo0qZDukBuVERERGX1c9piIyEgf9AOPcuLf5c6CwoJVZWVlHxsbG3fyW/nk5eXH1q1dd/6bc9+4kL2XAYz8oaEhg9Nnz9z7488/I319fb+EEBhZWdmn1oLt7e0Sd/780//WrZsH+vr6DDidxx36C12U5pbmRV+cOL4oKib69x8vXHiBQvlveUd/9xcRFsYCPtFmmVlZG1B/nJSRkXnqnOffnvv2o8amxoVP04dA9GCVpKGhYdGxE8cW3bpzK3blipXfLFmyJFSTg2fNxMRELyUrPAONrype9M/IyOi48KKxx1O2DOcvoTYYwyMGgxcICwtf2tXdpUVWxkJBIyDl5eXudDrdYt68eYVYOsgBa3iYL3SvMFlCVFlV5er/QkAuwZ0NjsLDw8Ndx744ttrSwuKh3rzW1lZhxKx9sJfhP5Mu0dvbqxkZFbUUEZAb/FjGdWvX3rr7191dbW1t9mSTkMn9Fy2tLZ6nzpz2/P3G7wWOsx3vOTs7RRsZGhUrKyu3QiYj8EJPeNeJpqYm+aLiIovY2LiV2TnZAYiE6EtIjq9YkEmyx/cJODk6xT9IPopLSpRzcnJWYhmfUArIYG9sbLSPo8XN9V7pnfA037l9545bcHDwgYeFXj1J58FVV1+/4Ow3Zxf8dvW3Slsb27seHh7/2NvZZWhqavY/z6pUZ2en0Lnvvns/LT39VTLC6qBMZqZm5dzuG2jfv+79tS0yKnLl2NgYzyZBBoMhs3Dhwq92v//BOTxiMLgNmE+QXRIgKoKjMiYBhzfHxMauxwSE8wDbBdkQQp8fPfIHmq8Y3Foc4CoBmTDQpNHkb82NSkCGGPS+7uGhoUfWJz0jYz6azG2f1bCY7t6GFHrKC6/v2HGDHzfly8nJDa1ft/7YmbNnbnEroxO8B66uri7LoOAgy8CgwI+R4dcnLy/fgsrTjiaMftRWsLqn0NLSoj04OKgK3wOjn4zTax8ErCpqaWnl+vn6Xn7ws+TkpOXIoFLnRjkECdHRMRufhoDAeS0//PjDd6gvRZ93PEAOf7iYTKZBfEL8B4j8fCAjI9Ogra2daWxklGFubp6PZKnK0MCwFY2/fiRPg4iwshDBHUNEmwonxHZ3dytVVFaYpKWleSC9ta6jo8OcjDNxQG+iuraoqKhwnYBA+/b396v09PSo8FI2IGV1V2cX3vSKwROkpqUaFhUVLZ7pe/buB7RFQmKCL9KHx5FuwrFYJOje9vZ2I5IzK/KOgNzHtLhSicmGpFAfbTSEhoVupArhE28fJCBlZWWL09LTDZydnCr5sYy+Pj63Ud+FVFVVcTV18uSBcBPyJYMIiQwyBI3ul2/4nMzVjocB9hi8unXrFwoKCv+163loeJiIjIoKwPub/ncyy87JXlNZWbnfwMCg91H3wYrWqdOnjiGD2JoTfQorC5OkAZFGrfLyci1kaKz6+59/xv+GSOIAkqFeJFsDQEDU1dXZzc3NQqh/pdH9yohIisMqJcg8WTIGZFZbSyt71qxZPAnBhPrx+hRyGC9CwkI4/AqDJ6DRaGtHR0exV/QB3dnW3mabkpIyf9WqVbG4Rcixb/gBM8IiR8arYk5OjreYKA5NeZAkDg0NSUZFRa3j1zLCitVrW1/9CCnpAV4x9kmyMRlmAxeQa26vGkG6YGNj41BfX7+bD36Wm5NjgozcBZiA/K+R293drRNHi1v6uPt+vnRpfWJi4ptkGPtQBugXkOXJC4wORAA00E+j3t5e88KiIkv00wz9XxvdLw6rWFAWMpMJAGl1cnIOwyF7GBjcR19fHyWFTt+AdfbDdKYQEUejbcAtMc37eSZUEvY5DAwMaPPa28aPAA9xckryhq6uLr4t4+LFi3NWrFhxGDJUzdTzW4B8IeOU8erWrR/LyvzvZsWwiPB16HMJLNH/C5jgE5OSAh71eV5enta169fOcDN0bdL7DxeQDDj9GH7C/7kl48JCQkx3N7dALCEYGNxHQmKic21trfNMPTD2sXaJmBiRlp62uqKyQhG3BiYgAgsIrYiGzEB4kD/cCBEWhgw+zknJyXP4uZzvvv3OSUNDw2A4y2WmAQxSiFVfunTp8WVLl2U9+HlnVyc1KSnJD3uyH01AioqKPLOysvQe/Ky3t5c4dvz4WaQndGaSgwJW00xNTUMdHR3LsIRgYHAfcbS4jTP5QOTHGqZIF/f392slJCQuwa2BCYjAorCoyLCmpmYxJiCPH+zxCfEb+bmMSkpKYx/t27dDUkqqcqblCAdjUU1dPQmRsBMP+zwzI3NuW1vbbGGcfveRBI7JZMrGxsWuefCz7y9ceKOouGjGZccDmfJc7Hlupp8Xg4HBCyCbRJpOp6/Fm88fDXAchYWHBZB9FhgGJiCkISQkZM3w8LA09jQ8GqAE09PT19bW1vJ1+iQHe4f6Xe/u3MxgMPvIOJSNHwGhV+jq2bt795tqampDD7snODQEe9KeQsZpNNrGnp7/P/SbFh9vdfevu8dkZGZW/n04td3CwiLQ18cnCksGBgb3kZSctLi3t1cfh4U/GuA0rqqq8szLz9PHrYEJiMChr78fBvoGHJryBCFASrCzs1M/PiFhMb+XdfXq1cm73nsPkRAGayaQEAgReunFl95Z4LEg52Gf19fXy2ZlZa3GMv54wOpQXX29S3pGuiP8v6amRvLoF0cvItmXm0nkbYLQMl979bVD3M7ghoGBMX7OBREcEhIgIoqjMh4H0MvDw8NSNFr8WtwamIAIHBLi452bm5vn4NCUJwOWO6OiogJAOfI7Xty8+e+d7+58CZGQkelMQvr6+mAD/mevvfrqb4+6JzIqyhPdp4c9aU9FtKmhYWG+8HtrW5smIneGYJDPJAICe4lWeXsfW7hgQSaWCAwM7qOwsFCrsrJyGT588MmAleuo6Kj/WrnGmD6Y1pZ5RGTEBrJTtyKGPoLewRWrXVhEWFSIKkSKtTS+Ube4aGl+Qb62rY1tvQCQkBuQRfj02TNXxcXEJacbyQTysWjhwm+PHjn66aPSNAL5io6JDiC77kPDQyxijOAG06OIiIiIkkWmYJUoIyNjXX1Dw2EnR8fyr09+Ne+L48duNLc0O0hJTu9U/BT0j8FkENra2mFvvvHmUTz1YWDwBrFxcauZTKY8mVn3kF0yiuwSrmyWFBISEhERFqGOEZy3tWBua2lpmZORmeG8aOGiVCw9mIAIBJBRIZ1fULCOzNAUBpNJvPrKKxvd3d3TkEIhbTcZMsjGWCMjQ8ePHbvU2NzsKUKCwTmx3KmQkJCwChGQ7wWhjzdv2nwXGefLz50/f2NoaEgDvCX8cLonR8jHokXfHP38yM7HyW9WdpZuRUXFUjLzyLNYrP5d7+1aaW1lVYPamLQXoUmM3dPTQz36xRd/9vf3W5GxORqIDXqHSUpKykJfH58gV1fXsgvnv3c/9Nmn32RnZ2+F8zmm62oIIpGEjIxM4aeHDm1RUlIaJTAwMLgOSCUfFR3lT6pdwmAQvr6+769eteoO+p1Uzwqae/rPf//9kfT09K1kbqgPDAzciAkIJiACg8jIqEWdnZ0GYFSQAQhVkpOVLVmzek2Qurr6EDfqhAymu9euX/cUIclzAkoxOiZm48svvfw9N89EmAo2+G2gGRoazjt8+PDV+oYGV0Ep98MA5An2fCzx9Dx05PMjh580SYWHR4AnTZasOkP2EQ0NjbRVK71p3NovYGtrGxIdHW1F1vtgY2NYWJi/z/r1QUA2tLW1By58//2rl3/5JfrSpUtn2Gy28uQJ5tMBExnA4OT1ypNfnlxrb2fXjKc9DAzeIDMry7apqcmVLGMd5hCk47pXLl/xh5mpWSM36uS1bNltOp2+laznwzyYm5e3trGx8aCmpuYAlqLpg2kbOB4bFxtAZorJ4eFhwsbGJpJb5APg5uYeKSoqyiTLyw/GWW1t7bys7CxbQerr2Q6zq366+NOihQsXnuwfGGALYppeMPaHhob6t2/b9uLRI0efSD7Ay5WekU6qJw1k3NnJOZibm5U93NyDyHz+RKihV0lJicZ/5F5YhNj+2rZr354962BhYXEbVqBGRgU/1TOQj8HBQUJaSir3qy9PLkXkA5/5gYHBQwQG/uPLZrNJc/zC3KepoUk3MDBo4FadLC0sk6WkpOrI2o8JK9ddXV0G4RHhi7EEYQLC98jJydEsLi72ItM4gxUQlzkuwdysl5WlZamGhkYmyQa2cEhIiK+g9bmKisoQMrL2Hvrk4EJpaek8WOoWhHAsKCOUVU5OLhOVf96bb7x59WlCqhISEx3q6upcyTzfBhmw7LlzXSK42R4ODg5pqC2qyJrMJlYElEPDQlc++JmTk3PdxR9+3LBn956V0lLSeb19vYQgJGV4lFz19PYQ+vr6f505fWaBnZ1dBZ7uMDB4h9bWVrGc3FxSzxyCUEvnOc4h3FzFRWSnx9TUNA4cVmQB5jlafHwAliJMQPge0THRqwcZg/JkxXOzx9gEYvxNdra2ydysFygVK0ursGEWeQMdlGNaevr6ltYWgczrunrVKtqvv1xxXr9u3e7hYVYbeID5lYjAYXBM5hDDa5nXYVTmeW5ubrlP+92oqCg/VC/SNiyA4a2srJxnYW6Rx802UVdXHzQ2Mo4lczIDghefkLAR9nA9bKIL8PcPvvzzJcfNL2zejsZDBRBEQVpVg5Ar1H7969es2/nD9xfWW1lZdeGpDgODt0hMSvLo6OgwJysyYzz8SliEtcBjQQS36+Y+3y2ITGcN6OyysrKlxSXFWliSMAHhSwhRhcYtTTqd7i8mSmJoytAwgRg/zdjYmOsT+4IFHqGQ0Ya0NkTKsaury4JOT3UXVDlQU1Nj7v94/9c/XbxotWjRoqPIeGwHI5JfvNlAPFB5xgwNDP48e/r07KNHjhxSUVFhPu336+rrxTMyM9aT6klDZbS3tw9FJITreY7d3d2CyDT4gWTU19d75OTkWD7qHk1NzeEP3n//4m9XfrV5ISBgm5ysbD6ksIV24VdA2VAZx8zNzW+fP/ed/SeffPKNvLy84GdlwMCYBggJCfEnMyx8wmmUYWxsVMjtujk6Osaj+aiHLGffRDipYlBQ8CosSZiA8CXQAGAi8mFWVV09j8zMQGAczXN1DeRFHZFRmIUM7HwyDTRQkqAsBV0erCwtW08cO37gxws/WGzcsGG3nJxcIRiRsH+C2+eHwPtgNQa9n2VkZPTX54cPz7t86bLP3Llzi571WTQabWFHR4cJmZMZTCNu892CedFvLnNc4mVkZNrI7CP0bJHg4CCfJ92HiMjgB+9/8NON3284oJ8rTE1N/2IymQwgtLBvh9cAowPkCl3DiHjcPX7s2Lyffry4wcHBoRxPbxgY/IHSsjLVktKSFWTaJbBqbGdrGyovJ8/1+qE5rWGWnl4SmToRHG70VPpGJpOJBWqaQJifPXpPbSwh1g2CjyZjVmh42AtoMhYmK/xq4uCyHhsb21he1FVBXoGlr68fUVdXZ0VWnCfk887MzFxTUlp6wNTEpEngiYiVVRu6vt6+fcc3MTHR7jExsZsKCguWdXd3a8IGN/CIQ75xTssMGIcgl3ApKCjUz3Odd3vVKu+fZ8+eXSAu9vxZUKKjo18C45yssQvPlhAXL7e0sEjjRX/p6uq2amhoxBeXFK8ncyUzITHRv62t7QTsH3rSvbKysqwAf/8QX1+fkJycHP24uLi1KXT6WjQOHVH/SgIZBDmCn2Sm8gX9A86HyRA1VPaSBR4L7qxateqqvZ1dMacNnDE2m4rkjAJ1mo4pisGYGWGxpl6xkVFibIRBjDG4c7r12Ch6F6eNvdFRyhiLyb06MNC7kGxxdnygi8mkwLMJEh00/3kfc7wOT3Tk/v3332vaO9rVZKRlSCsLzDPu7h4hPDEk0fw5Z45LUE5u7nIyM1EWFRe7x8bGOnh5eWU+Qj9SkG4EnUVw0kEHz2OxRrjqsGeNjCemIYS4IMc8IyDW1tZJ06EeyNjrZI2MSAwPDWk5OjpmoAFBinWGJn8JNTW1JGSc8eywvlXe3ne6urrcEQEZJgiClDVPROIk62przacDAZmEvJwca93adVFwNTY1yaSkpLhkZ2d7lpSWzGtqarJEdR53HYGhNUlInvaQPyAbYLiDcQg/kTE6oqioWInIYrzLHJd7Hh4esTra2n1TrQMyeGWoQlQZJONpqHykuJuQApcyNTX9AxEBBi/6CRTu6lWrr6GfhkjGyUq7SEHGp1B9fb0RMuILnvZLkDHLcbZjFbpOMxiM0yUlJTq5eXmuubm57mXlZY5oXBpBqMD9dQGSC9ezTCSTRGNC50w6PghJSck2bW3tPHt7+zgHe4dwZyenTERuSdswg9p/EM0RyejdEuiadueHIBmQ1tXVq52yzKoqt0o4u+RSpaT6uVFudv+ApMgs3RaOjjtlpX5UhySqlCQYWqQvEY8NDUuLmplwdKWOIiY6KuHsmI6IQTMadKQvUY4ND4uLGhnmPMmh09vXa+Tk6JSD5gVS9Bl6h6i4uHi14+zZubwaS56enqFZ2VkJSM+BA5gU+UE6V6ahsdEK/Zr5CCI0bGFhkYLuk0U6d5SD75XS0dHh6qryLL1Z9ba2tnmoX/uIaQrKdDi47f76TFxsAoMToM6EtgQvQ1VVlVpFZYU5Miitenp6zCurqgxYLJYGIiZK6BY4TAaWm8A1OGlFQruA4cdAY6hfVVW1Q0pKqhYRjlINdfVcOzu7LBNjkzJkHHKUCKOJZtIbjWP7+Uxf9PX1URoaGlTqG+oNy8rKjNH/TWrr6vRgpQ1NYKqtra0wKUJOY1j+Ep2Qp0kPMPQnMI4hJE8MNJEOaGpqtqP/Q2hDpYaGRhEihQUGBgYl6mrqHdxMjYyBgTEFkoJsLHShoU/Fdgm2SzDuw/8JMADl5cyT9j6pfQAAAABJRU5ErkJggg==" @@ -393,3 +385,15 @@ unless DatabaseProvider.count > 0 provider.save end end + +unless Setting.find_by(name: 'reminder_enable').try(:value) + setting = Setting.find_or_initialize_by(name: 'reminder_enable') + setting.value = 'true' + setting.save +end + +unless Setting.find_by(name: 'reminder_delay').try(:value) + setting = Setting.find_or_initialize_by(name: 'reminder_delay') + setting.value = '24' + setting.save +end \ No newline at end of file diff --git a/doc/controllers_brief.svg b/doc/controllers_brief.svg index b60203108..433767080 100644 --- a/doc/controllers_brief.svg +++ b/doc/controllers_brief.svg @@ -1,206 +1,306 @@ - - - + + controllers_diagram - + _diagram_info -Controllers diagram -Date: Oct 26 2015 - 13:23 -Migration version: 20151008152219 -Generated by RailRoady 1.4.0 -http://railroady.prestonlee.com +Controllers diagram +Date: Sep 15 2016 - 17:48 +Migration version: 20160915105234 +Generated by RailRoady 1.4.0 +http://railroady.prestonlee.com SessionsController - -SessionsController + +SessionsController + + +OpenAPI::V1::BookableMachinesController + +OpenAPI::V1::BookableMachinesController + + +OpenAPI::V1::ReservationsController + +OpenAPI::V1::ReservationsController + + +OpenAPI::V1::EventsController + +OpenAPI::V1::EventsController + + +OpenAPI::V1::MachinesController + +OpenAPI::V1::MachinesController + + +OpenAPI::V1::UserTrainingsController + +OpenAPI::V1::UserTrainingsController + + +OpenAPI::V1::BaseController + +OpenAPI::V1::BaseController + + +OpenAPI::V1::UsersController + +OpenAPI::V1::UsersController + + +OpenAPI::V1::TrainingsController + +OpenAPI::V1::TrainingsController + + +OpenAPI::V1::InvoicesController + +OpenAPI::V1::InvoicesController -RegistrationsController - -RegistrationsController +RegistrationsController + +RegistrationsController -API::TagsController - -API::TagsController +API::TagsController + +API::TagsController -API::StatisticsController - -API::StatisticsController +API::StatisticsController + +API::StatisticsController -API::TrainingsPricingsController - -API::TrainingsPricingsController +API::TrainingsPricingsController + +API::TrainingsPricingsController -API::PlansController - -API::PlansController +API::PlansController + +API::PlansController -API::AuthProvidersController - -API::AuthProvidersController +API::AuthProvidersController + +API::AuthProvidersController + + +API::CouponsController + +API::CouponsController + + +API::AgeRangesController + +API::AgeRangesController -API::CreditsController - -API::CreditsController +API::CreditsController + +API::CreditsController + + +API::OpenlabProjectsController + +API::OpenlabProjectsController -API::ComponentsController - -API::ComponentsController +API::ComponentsController + +API::ComponentsController -API::CustomAssetsController - -API::CustomAssetsController +API::CustomAssetsController + +API::CustomAssetsController -API::PricingController - -API::PricingController +API::PricingController + +API::PricingController + + +API::AbusesController + +API::AbusesController + + +API::PriceCategoriesController + +API::PriceCategoriesController -API::FeedsController - -API::FeedsController +API::FeedsController + +API::FeedsController -API::MembersController - -API::MembersController +API::MembersController + +API::MembersController -API::PricesController - -API::PricesController +API::PricesController + +API::PricesController + + +API::TranslationsController + +API::TranslationsController + + +API::ExportsController + +API::ExportsController -API::ReservationsController - -API::ReservationsController +API::ReservationsController + +API::ReservationsController -API::EventsController - -API::EventsController +API::EventsController + +API::EventsController -API::MachinesController - -API::MachinesController +API::MachinesController + +API::MachinesController + + +API::EventThemesController + +API::EventThemesController -API::ThemesController - -API::ThemesController +API::ThemesController + +API::ThemesController -API::CategoriesController - -API::CategoriesController +API::CategoriesController + +API::CategoriesController -API::SubscriptionsController - -API::SubscriptionsController +API::SubscriptionsController + +API::SubscriptionsController -API::StylesheetsController - -API::StylesheetsController +API::StylesheetsController + +API::StylesheetsController -API::SlotsController - -API::SlotsController +API::SlotsController + +API::SlotsController -API::AdminsController - -API::AdminsController +API::AdminsController + +API::AdminsController -API::GroupsController - -API::GroupsController +API::GroupsController + +API::GroupsController -API::AvailabilitiesController - -API::AvailabilitiesController +API::AvailabilitiesController + +API::AvailabilitiesController -API::UsersController - -API::UsersController +API::UsersController + +API::UsersController -API::ProjectsController - -API::ProjectsController +API::ProjectsController + +API::ProjectsController + + +API::WalletController + +API::WalletController -API::NotificationsController - -API::NotificationsController +API::NotificationsController + +API::NotificationsController -API::TrainingsController - -API::TrainingsController +API::TrainingsController + +API::TrainingsController -API::SettingsController - -API::SettingsController +API::SettingsController + +API::SettingsController + + +API::OpenAPIClientsController + +API::OpenAPIClientsController -API::InvoicesController - -API::InvoicesController +API::InvoicesController + +API::InvoicesController -API::LicencesController - -API::LicencesController +API::LicencesController + +API::LicencesController + + +SocialBotController + +SocialBotController -PasswordsController - -PasswordsController +PasswordsController + +PasswordsController -ApplicationController - -ApplicationController +ApplicationController + +ApplicationController -Users::OmniauthCallbacksController - -Users::OmniauthCallbacksController +Users::OmniauthCallbacksController + +Users::OmniauthCallbacksController -WebhooksController - -WebhooksController +WebhooksController + +WebhooksController -ConfirmationsController - -ConfirmationsController +ConfirmationsController + +ConfirmationsController diff --git a/doc/controllers_complete.svg b/doc/controllers_complete.svg index a8034b87e..0421faae8 100644 --- a/doc/controllers_complete.svg +++ b/doc/controllers_complete.svg @@ -1,551 +1,830 @@ - - - + + controllers_diagram - + _diagram_info -Controllers diagram -Date: Oct 26 2015 - 13:23 -Migration version: 20151008152219 -Generated by RailRoady 1.4.0 -http://railroady.prestonlee.com +Controllers diagram +Date: Sep 15 2016 - 17:48 +Migration version: 20160915105234 +Generated by RailRoady 1.4.0 +http://railroady.prestonlee.com SessionsController - -SessionsController - -new - -set_csrf_headers - -_layout + +SessionsController + +new + +set_csrf_headers + +_layout + + +OpenAPI::V1::BookableMachinesController + +OpenAPI::V1::BookableMachinesController + +index + + +_layout + + +OpenAPI::V1::ReservationsController + +OpenAPI::V1::ReservationsController + +index + + +_layout +format_type +per_page + + +OpenAPI::V1::EventsController + +OpenAPI::V1::EventsController + +index + + +_layout +per_page + + +OpenAPI::V1::MachinesController + +OpenAPI::V1::MachinesController + +index + + +_layout + + +OpenAPI::V1::UserTrainingsController + +OpenAPI::V1::UserTrainingsController + +index + + +_layout +per_page + + +OpenAPI::V1::BaseController + +OpenAPI::V1::BaseController + + +authenticate +authenticate_token +bad_request +current_api_client +not_found +render_unauthorized + +_layout +increment_calls_count + + +OpenAPI::V1::UsersController + +OpenAPI::V1::UsersController + +index + + +_layout +per_page + + +OpenAPI::V1::TrainingsController + +OpenAPI::V1::TrainingsController + +index + + +_layout + + +OpenAPI::V1::InvoicesController + +OpenAPI::V1::InvoicesController + +download +index + + +_layout +per_page -RegistrationsController - -RegistrationsController - -create - - -_layout +RegistrationsController + +RegistrationsController + +create + + +_layout -API::TagsController - -API::TagsController - -create -destroy -index -show -update - - -_layout -set_tag -tag_params +API::TagsController + +API::TagsController + +create +destroy +index +show +update + + +_layout +set_tag +tag_params -API::StatisticsController - -API::StatisticsController - -account -event -index -machine -project -subscription -training -user - - -_layout +API::StatisticsController + +API::StatisticsController + +account +event +export_account +export_event +export_global +export_machine +export_project +export_subscription +export_training +export_user +index +machine +project +scroll +subscription +training +user + + +_layout -API::TrainingsPricingsController - -API::TrainingsPricingsController - -index -trainings_pricing_params -update - - -_layout +API::TrainingsPricingsController + +API::TrainingsPricingsController + +index +trainings_pricing_params +update + + +_layout -API::PlansController - -API::PlansController - -create -destroy -index -show -update - - -_layout -plan_params +API::PlansController + +API::PlansController + +create +destroy +index +show +update + + +_layout +plan_params -API::AuthProvidersController - -API::AuthProvidersController - -active -create -destroy -index -mapping_fields -show -update - - -_layout -provider_params -set_provider +API::AuthProvidersController + +API::AuthProvidersController + +active +create +destroy +index +mapping_fields +show +update + + +_layout +provider_params +set_provider + + +API::CouponsController + +API::CouponsController + +create +destroy +index +send_to +show +update +validate + + +_layout +coupon_editable_params +coupon_params +set_coupon + + +API::AgeRangesController + +API::AgeRangesController + +create +destroy +index +show +update + + +_layout +age_range_params +set_age_range -API::CreditsController - -API::CreditsController - -create -destroy -index -update - - -_layout -credit_params -set_credit +API::CreditsController + +API::CreditsController + +create +destroy +index +update + + +_layout +credit_params +set_credit + + +API::OpenlabProjectsController + +API::OpenlabProjectsController + +index + + +_layout -API::ComponentsController - -API::ComponentsController - -create -destroy -index -show -update - - -_layout -component_params -set_component +API::ComponentsController + +API::ComponentsController + +create +destroy +index +show +update + + +_layout +component_params +set_component -API::CustomAssetsController - -API::CustomAssetsController - -create -destroy -index -show -update - - -_layout -custom_asset_params -set_custom_asset +API::CustomAssetsController + +API::CustomAssetsController + +create +destroy +index +show +update + + +_layout +custom_asset_params +set_custom_asset -API::PricingController - -API::PricingController - -index -update - - -_layout +API::PricingController + +API::PricingController + +index +update + + +_layout + + +API::AbusesController + +API::AbusesController + +create +index + + +_layout +abuse_params + + +API::PriceCategoriesController + +API::PriceCategoriesController + +create +destroy +index +show +update + + +_layout +price_category_params +set_price_category -API::FeedsController - -API::FeedsController - -twitter_timelines - - -_layout +API::FeedsController + +API::FeedsController + +twitter_timelines + + +_layout -API::MembersController - -API::MembersController - -create -destroy -export_members -export_reservations -export_subscriptions -index -last_subscribed -merge -show -update - - -_layout -set_member -user_params +API::MembersController + +API::MembersController + +create +destroy +export_members +export_reservations +export_subscriptions +index +last_subscribed +list +mapping +merge +search +show +update + + +_layout +set_member +user_params -API::PricesController - -API::PricesController - -index -price_params -update - - -_layout +API::PricesController + +API::PricesController + +compute +index +update + + +_layout +compute_price_params +coupon_params +price_params + + +API::TranslationsController + +API::TranslationsController + +set_locale +show + + +_layout + + +API::ExportsController + +API::ExportsController + +download +status + + +_layout +set_export -API::ReservationsController - -API::ReservationsController - -create -index -show -update - - -_layout -is_first_training_and_subscription -reservation_params -set_reservation +API::ReservationsController + +API::ReservationsController + +create +index +show +update + + +_layout +coupon_params +reservation_params +set_reservation -API::EventsController - -API::EventsController - -create -destroy -index -show -upcoming -update - - -_layout -event_params -set_event +API::EventsController + +API::EventsController + +create +destroy +index +show +upcoming +update + + +_layout +event_params +set_event -API::MachinesController - -API::MachinesController - -create -destroy -index -show -update - - -_layout -is_reserved -machine_params -set_machine +API::MachinesController + +API::MachinesController + +create +destroy +index +show +update + + +_layout +is_reserved +machine_params +set_machine + + +API::EventThemesController + +API::EventThemesController + +create +destroy +index +show +update + + +_layout +event_theme_params +set_event_theme -API::ThemesController - -API::ThemesController - -create -destroy -index -show -update - - -_layout -set_theme -theme_params +API::ThemesController + +API::ThemesController + +create +destroy +index +show +update + + +_layout +set_theme +theme_params -API::CategoriesController - -API::CategoriesController - -index - - -_layout +API::CategoriesController + +API::CategoriesController + +create +destroy +index +show +update + + +_layout +category_params +set_category -API::SubscriptionsController - -API::SubscriptionsController - -create -show -update - - -_layout -set_subscription -subscription_params -subscription_update_params +API::SubscriptionsController + +API::SubscriptionsController + +create +show +update + + +_layout +coupon_params +set_subscription +subscription_params +subscription_update_params +valid_card_token? -API::StylesheetsController - -API::StylesheetsController - -show - - -_layout +API::StylesheetsController + +API::StylesheetsController + +show + + +_layout -API::SlotsController - -API::SlotsController - -cancel -update - - -_layout -is_first_training_and_subscription -set_slot -slot_params +API::SlotsController + +API::SlotsController + +cancel +update + + +_layout +set_slot +slot_params -API::AdminsController - -API::AdminsController - -create -destroy -index - - -_layout -admin_params +API::AdminsController + +API::AdminsController + +create +destroy +index + + +_layout +admin_params -API::GroupsController - -API::GroupsController - -create -destroy -index -update - - -_layout -group_params +API::GroupsController + +API::GroupsController + +create +destroy +index +update + + +_layout +group_params -API::AvailabilitiesController - -API::AvailabilitiesController - -create -destroy -index -machine -reservations -show -trainings -update - - -_layout -availability_params -can_show_slot_plus_three_months -is_reserved -is_subscription_year -set_availability -verify_machine_is_reserved -verify_training_is_reserved +API::AvailabilitiesController + +API::AvailabilitiesController + +create +destroy +index +machine +public +reservations +show +trainings +update + + +_layout +availability_params +can_show_slot_plus_three_months +filter_availabilites +in_same_day +is_reserved +is_subscription_year +set_availability +verify_machine_is_reserved +verify_training_event_is_reserved -API::UsersController - -API::UsersController - -create -index - - -_layout -partner_params +API::UsersController + +API::UsersController + +create +index + + +_layout +partner_params -API::ProjectsController - -API::ProjectsController - -collaborator_valid -create -destroy -index -last_published -search -show -update - - -_layout -project_params -set_project +API::ProjectsController + +API::ProjectsController + +collaborator_valid +create +destroy +index +last_published +search +show +update + + +_layout +project_params +set_project + + +API::WalletController + +API::WalletController + +by_user +credit +transactions + + +_layout -API::NotificationsController - -API::NotificationsController - -index -show -update -update_all - - -_layout -set_notification +API::NotificationsController + +API::NotificationsController + +index +show +update +update_all + + +_layout +set_notification -API::TrainingsController - -API::TrainingsController - -create -destroy -index -show -update - - -_layout -set_training -training_params -valid_training_params +API::TrainingsController + +API::TrainingsController + +availabilities +create +destroy +index +show +update + + +_layout +set_training +training_params +valid_training_params -API::SettingsController - -API::SettingsController - -index -show -update - - -_layout -names_as_string_to_array -setting_params +API::SettingsController + +API::SettingsController + +index +show +update + + +_layout +names_as_string_to_array +setting_params + + +API::OpenAPIClientsController + +API::OpenAPIClientsController + +create +destroy +index +reset_token +update + + +_layout +client_params -API::InvoicesController - -API::InvoicesController - -create -download -index - - -_layout -avoir_params -set_invoice +API::InvoicesController + +API::InvoicesController + +create +download +index +list + + +_layout +avoir_params +set_invoice -API::LicencesController - -API::LicencesController - -create -destroy -index -show -update - - -_layout -licence_params -set_licence +API::LicencesController + +API::LicencesController + +create +destroy +index +show +update + + +_layout +licence_params +set_licence + + +SocialBotController + +SocialBotController + +share + + +_layout -PasswordsController - -PasswordsController - -create - - -_layout +PasswordsController + +PasswordsController + +create + + +_layout -ApplicationController - -ApplicationController - -index - -configure_permitted_parameters -default_url_options -permission_denied -set_csrf_cookie -verified_request? - -_layout +ApplicationController + +ApplicationController + +index + +configure_permitted_parameters +default_url_options +permission_denied +set_csrf_cookie +verified_request? + +_layout -Users::OmniauthCallbacksController - -Users::OmniauthCallbacksController - -database-fablab - - -_layout -email_exists? -generate_unique_username -username_exists? +Users::OmniauthCallbacksController + +Users::OmniauthCallbacksController + +database-fablab + + +_layout +email_exists? +generate_unique_username +username_exists? -WebhooksController - -WebhooksController - -create - - -_layout +WebhooksController + +WebhooksController + +create + + +_layout -ConfirmationsController - -ConfirmationsController - -after_confirmation_path_for - - -_layout +ConfirmationsController + +ConfirmationsController + +after_confirmation_path_for + + +_layout diff --git a/doc/diagram.mwb b/doc/diagram.mwb deleted file mode 100644 index 92ca61721..000000000 Binary files a/doc/diagram.mwb and /dev/null differ diff --git a/doc/diagram.mwb.bak b/doc/diagram.mwb.bak deleted file mode 100644 index 6377ef838..000000000 Binary files a/doc/diagram.mwb.bak and /dev/null differ diff --git a/doc/diagram.png b/doc/diagram.png index fcb0f80a9..d98ed629e 100644 Binary files a/doc/diagram.png and b/doc/diagram.png differ diff --git a/doc/diagram_eer_fablab.pdf b/doc/diagram_eer_fablab.pdf deleted file mode 100644 index 1eaba9e82..000000000 Binary files a/doc/diagram_eer_fablab.pdf and /dev/null differ diff --git a/doc/elasticsearch.md b/doc/elasticsearch.md index e15495713..32275f8be 100644 --- a/doc/elasticsearch.md +++ b/doc/elasticsearch.md @@ -408,6 +408,14 @@ http://localhost:9200/stats/_mapping?pretty }, "userId" : { "type" : "long" + }, + "ageRange" : { + "type" : "string", + "index" : "not_analyzed" + }, + "eventTheme" : { + "type" : "string", + "index" : "not_analyzed" } } } diff --git a/doc/models_brief.svg b/doc/models_brief.svg index 381ae9dac..bca030210 100644 --- a/doc/models_brief.svg +++ b/doc/models_brief.svg @@ -1,960 +1,1167 @@ - - - + + models_diagram - + _diagram_info -Models diagram -Date: Oct 26 2015 - 13:23 -Migration version: 20151008152219 -Generated by RailRoady 1.4.0 -http://railroady.prestonlee.com +Models diagram +Date: Sep 15 2016 - 17:48 +Migration version: 20160915105234 +Generated by RailRoady 1.4.0 +http://railroady.prestonlee.com Group - -Group + +Group Price - -Price + +Price Group->Price - - - -machines_prices + + + +machines_prices -TrainingsPricing - -TrainingsPricing +TrainingsPricing + +TrainingsPricing Group->TrainingsPricing - - - + + + -Plan - -Plan +Plan + +Plan Group->Plan - - - + + + -User - -User +User + +User Group->User - - - + + + InvoiceItem - -InvoiceItem + +InvoiceItem InvoiceItem->InvoiceItem - - - + + + AvailabilityTag - -AvailabilityTag + +AvailabilityTag + + +OpenAPI + +OpenAPI + + +TrainingImage + +TrainingImage + + +OpenAPI::Client + +OpenAPI::Client + + +OpenAPI::CallsCountTracing + +OpenAPI::CallsCountTracing + + +OpenAPI::Client->OpenAPI::CallsCountTracing + + + + + +OpenAPI::ParameterError + +OpenAPI::ParameterError -UserAvatar - -UserAvatar +UserAvatar + +UserAvatar -ProjectImage - -ProjectImage +ProjectImage + +ProjectImage -Tag - -Tag +Tag + +Tag -Tag->AvailabilityTag - - - +Tag->AvailabilityTag + + + -Tag->User - - - +Tag->User + + + -Availability - -Availability +Availability + +Availability -Tag->Availability - - - +Tag->Availability + + + -UserTag - -UserTag +UserTag + +UserTag -Tag->UserTag - - - +Tag->UserTag + + + -Event - -Event +Event + +Event + + +PriceCategory + +PriceCategory + + +Event->PriceCategory + + + + + +EventTheme + +EventTheme + + +Event->EventTheme + + + -EventImage - -EventImage +EventImage + +EventImage -Event->EventImage - - - - - -EventFile - -EventFile - - -Event->EventFile - - - - - -Category - -Category - - -Event->Category - - - - - -StatisticSubType - -StatisticSubType - - -StatisticType - -StatisticType - - -StatisticSubType->StatisticType - - - - - -StatisticTypeSubType - -StatisticTypeSubType - - -StatisticSubType->StatisticTypeSubType - - - - - -Project - -Project - - -Project->ProjectImage - - - - - -Project->User - - - - - -Theme - -Theme - - -Project->Theme - - - - - -ProjectCao - -ProjectCao - - -Project->ProjectCao - - - - - -ProjectStep - -ProjectStep - - -Project->ProjectStep - - - - - -ProjectUser - -ProjectUser - - -Project->ProjectUser - - - - - -Machine - -Machine - - -Project->Machine - - - - - -Component - -Component - - -Project->Component - - - - - -CustomAsset - -CustomAsset - - -CustomAssetFile - -CustomAssetFile - - -CustomAsset->CustomAssetFile - - - - - -Stats::Event - -Stats::Event - - -Stats::Project - -Stats::Project - - -Stats::User - -Stats::User - - -Stats::Subscription - -Stats::Subscription - - -Stats::Training - -Stats::Training - - -Stats::Account - -Stats::Account - - -Stats::Machine - -Stats::Machine - - -Invoice - -Invoice - - -Invoice->InvoiceItem - - - - - -Invoice->Invoice - - - -avoir - - -PlanFile - -PlanFile - - -OfferDay - -OfferDay - - -OfferDay->Invoice - - - - - -StatisticIndex - -StatisticIndex - - -StatisticType->StatisticIndex - - - - - -StatisticType->StatisticTypeSubType - - - - - -Address - -Address - - -Plan->Price - - - - - -Plan->PlanFile - - - - - -Credit - -Credit - - -Plan->Credit - - - - - -Plan->Credit - - - -training_credits - - -Plan->Credit - - - -machine_credits - - -Subscription - -Subscription - - -Plan->Subscription - - - - - -PlanImage - -PlanImage - - -Plan->PlanImage - - - - - -MachineFile - -MachineFile - - -MachinesPricing - -MachinesPricing - - -StatisticGraph - -StatisticGraph - - -OAuth2Mapping - -OAuth2Mapping - - -Licence - -Licence - - -Licence->Project - - - - - -User->Project - - - -my_projects - - -User->Invoice - - - - - -User->Credit - - - - - -Role - -Role - - -User->Role - - - - - -Notification - -Notification - - -User->Notification - - - - - -User->Subscription - - - - - -Training - -Training - - -User->Training - - - +Event->EventImage + + + -Reservation - -Reservation +Reservation + +Reservation + + +Event->Reservation + + + + + +EventPriceCategory + +EventPriceCategory + + +Event->EventPriceCategory + + + + + +EventFile + +EventFile + + +Event->EventFile + + + + + +StatisticSubType + +StatisticSubType + + +StatisticType + +StatisticType + + +StatisticSubType->StatisticType + + + + + +StatisticTypeSubType + +StatisticTypeSubType + + +StatisticSubType->StatisticTypeSubType + + + + + +Project + +Project + + +Project->ProjectImage + + + + + +Project->User + + + + + +Theme + +Theme + + +Project->Theme + + + + + +ProjectCao + +ProjectCao + + +Project->ProjectCao + + + + + +ProjectStep + +ProjectStep + + +Project->ProjectStep + + + + + +ProjectUser + +ProjectUser + + +Project->ProjectUser + + + + + +Machine + +Machine + + +Project->Machine + + + + + +Component + +Component + + +Project->Component + + + + + +CustomAsset + +CustomAsset + + +CustomAssetFile + +CustomAssetFile + + +CustomAsset->CustomAssetFile + + + + + +AgeRange + +AgeRange + + +AgeRange->Event + + + + + +Wallet + +Wallet + + +WalletTransaction + +WalletTransaction + + +Wallet->WalletTransaction + + + + + +Stats::Event + +Stats::Event + + +Stats::Project + +Stats::Project + + +Stats::User + +Stats::User + + +Stats::Subscription + +Stats::Subscription + + +Stats::Training + +Stats::Training + + +Stats::Account + +Stats::Account + + +Stats::Machine + +Stats::Machine + + +PriceCategory->EventPriceCategory + + + + + +Invoice + +Invoice + + +Invoice->InvoiceItem + + + + + +Invoice->Invoice + + + +avoir + + +PlanFile + +PlanFile + + +OfferDay + +OfferDay + + +OfferDay->Invoice + + + + + +StatisticIndex + +StatisticIndex + + +StatisticType->StatisticIndex + + + + + +StatisticType->StatisticTypeSubType + + + + + +StatisticCustomAggregation + +StatisticCustomAggregation + + +StatisticType->StatisticCustomAggregation + + + + + +Address + +Address + + +Plan->Price + + + + + +Plan->PlanFile + + + + + +Credit + +Credit + + +Plan->Credit + + + + + +Plan->Credit + + + +training_credits + + +Plan->Credit + + + +machine_credits + + +Subscription + +Subscription + + +Plan->Subscription + + + + + +PlanImage + +PlanImage + + +Plan->PlanImage + + + + + +MachineFile + +MachineFile + + +Organization + +Organization + + +Organization->Address + + + + + +StatisticGraph + +StatisticGraph + + +OAuth2Mapping + +OAuth2Mapping + + +Licence + +Licence + + +Licence->Project + + + + + +User->Project + + + +my_projects + + +User->Wallet + + + + + +User->Invoice + + + + + +User->Credit + + + + + +User->Credit + + + +training_credits + + +User->Credit + + + +machine_credits + + +Role + +Role + + +User->Role + + + + + +Notification + +Notification + + +User->Notification + + + + + +User->Subscription + + + + + +Training + +Training + + +User->Training + + + -User->Reservation - - - +User->Reservation + + + -UsersCredit - -UsersCredit +UsersCredit + +UsersCredit -User->UsersCredit - - - +User->UsersCredit + + + -User->UserTag - - - +User->UserTag + + + + + +Export + +Export + + +User->Export + + + -UserTraining - -UserTraining +UserTraining + +UserTraining -User->UserTraining - - - +User->UserTraining + + + -User->ProjectUser - - - +User->ProjectUser + + + -Profile - -Profile +Profile + +Profile -User->Profile - - - +User->Profile + + + -ProjectStepImage - -ProjectStepImage +ProjectStepImage + +ProjectStepImage -Credit->UsersCredit - - - +Credit->UsersCredit + + + -PartnerPlan - -PartnerPlan +PartnerPlan + +PartnerPlan -PartnerPlan->Price - - - +PartnerPlan->Price + + + -PartnerPlan->PlanFile - - - +PartnerPlan->PlanFile + + + -PartnerPlan->Credit - - - +PartnerPlan->Credit + + + -PartnerPlan->Credit - - - -training_credits +PartnerPlan->Credit + + + +training_credits -PartnerPlan->Credit - - - -machine_credits +PartnerPlan->Credit + + + +machine_credits -PartnerPlan->Role - - - +PartnerPlan->Role + + + -PartnerPlan->Subscription - - - +PartnerPlan->Subscription + + + -PartnerPlan->PlanImage - - - +PartnerPlan->PlanImage + + + -OAuth2Provider - -OAuth2Provider +OAuth2Provider + +OAuth2Provider -OAuth2Provider->OAuth2Mapping - - - +OAuth2Provider->OAuth2Mapping + + + -AuthProvider - -AuthProvider +AuthProvider + +AuthProvider -OAuth2Provider->AuthProvider - - - +OAuth2Provider->AuthProvider + + + -Asset - -Asset +Asset + +Asset + + +Project::OpenlabSync + +Project::OpenlabSync -DatabaseProvider - -DatabaseProvider +DatabaseProvider + +DatabaseProvider -DatabaseProvider->AuthProvider - - - +DatabaseProvider->AuthProvider + + + -Availability->AvailabilityTag - - - +Availability->AvailabilityTag + + + -Availability->Event - - - +Availability->Event + + + -Availability->Training - - - +Availability->Training + + + -Availability->Reservation - - - +Availability->Reservation + + + -Slot - -Slot +Slot + +Slot -Availability->Slot - - - +Availability->Slot + + + -MachinesAvailability - -MachinesAvailability +MachinesAvailability + +MachinesAvailability -Availability->MachinesAvailability - - - +Availability->MachinesAvailability + + + -TrainingsAvailability - -TrainingsAvailability +TrainingsAvailability + +TrainingsAvailability -Availability->TrainingsAvailability - - - +Availability->TrainingsAvailability + + + -Availability->Machine - - - +Availability->Machine + + + -Subscription->Invoice - - - +Subscription->Invoice + + + -Subscription->OfferDay - - - +Subscription->OfferDay + + + + + +Training->TrainingImage + + + -Training->TrainingsPricing - - - +Training->TrainingsPricing + + + -Training->Plan - - - +Training->Plan + + + -Training->Credit - - - +Training->Credit + + + -Training->Reservation - - - +Training->Reservation + + + -Training->UserTraining - - - +Training->UserTraining + + + -Training->TrainingsAvailability - - - +Training->TrainingsAvailability + + + -Training->Machine - - - +Training->Machine + + + -ProjectStep->ProjectStepImage - - - +ProjectStep->ProjectStepImage + + + -Reservation->Invoice - - - +Reservation->Invoice + + + -Reservation->Slot - - - +Reservation->Slot + + + + + +Ticket + +Ticket + + +Reservation->Ticket + + + -StatisticIndex->StatisticType - - - +StatisticIndex->StatisticType + + + -StatisticIndex->StatisticGraph - - - +StatisticIndex->StatisticGraph + + + -StatisticField - -StatisticField +StatisticField + +StatisticField -StatisticIndex->StatisticField - - - +StatisticIndex->StatisticField + + + -StatisticField->StatisticIndex - - - +StatisticField->StatisticIndex + + + + + +WalletTransaction->Invoice + + + + + +EventPriceCategory->Ticket + + + -Avoir - -Avoir +Avoir + +Avoir -Avoir->InvoiceItem - - - +Avoir->InvoiceItem + + + -Avoir->Invoice - - - -avoir +Avoir->Invoice + + + +avoir -NotificationType - -NotificationType +NotificationType + +NotificationType -Stylesheet - -Stylesheet +Stylesheet + +Stylesheet + + +Coupon + +Coupon + + +Coupon->Invoice + + + + + +Category + +Category + + +Category->Event + + + -MachineImage - -MachineImage +MachineImage + +MachineImage -Feed - -Feed +Feed + +Feed -Machine->Price - - - +Machine->Price + + + -Machine->Plan - - - +Machine->Plan + + + -Machine->MachineFile - - - +Machine->MachineFile + + + -Machine->Credit - - - +Machine->Credit + + + -Machine->Reservation - - - +Machine->Reservation + + + -Machine->MachinesAvailability - - - +Machine->MachinesAvailability + + + -Machine->MachineImage - - - +Machine->MachineImage + + + + + +Abuse + +Abuse -Profile->UserAvatar - - - +Profile->UserAvatar + + + -Profile->Address - - - +Profile->Address + + + + + +Profile->Organization + + + -Setting - -Setting +Setting + +Setting diff --git a/doc/models_complete.svg b/doc/models_complete.svg index 43bd49de8..dd4b1aa9c 100644 --- a/doc/models_complete.svg +++ b/doc/models_complete.svg @@ -1,1496 +1,1854 @@ - - - + + models_diagram - + _diagram_info -Models diagram -Date: Oct 26 2015 - 13:23 -Migration version: 20151008152219 -Generated by RailRoady 1.4.0 -http://railroady.prestonlee.com +Models diagram +Date: Sep 15 2016 - 17:48 +Migration version: 20160915105234 +Generated by RailRoady 1.4.0 +http://railroady.prestonlee.com Group - -Group - -id :integer -name :character varying(255) -created_at :timestamp without time zone -updated_at :timestamp without time zone -slug :character varying(255) + +Group + +id :integer +name :character varying(255) +created_at :timestamp without time zone +updated_at :timestamp without time zone +slug :character varying(255) Price - -Price - -id :integer -group_id :integer -plan_id :integer -priceable_id :integer -priceable_type :character varying -amount :integer -created_at :timestamp without time zone -updated_at :timestamp without time zone + +Price + +id :integer +group_id :integer +plan_id :integer +priceable_id :integer +priceable_type :character varying +amount :integer +created_at :timestamp without time zone +updated_at :timestamp without time zone Group->Price - - - -machines_prices + + + +machines_prices -TrainingsPricing - -TrainingsPricing - -id :integer -group_id :integer -amount :integer -created_at :timestamp without time zone -updated_at :timestamp without time zone -training_id :integer +TrainingsPricing + +TrainingsPricing + +id :integer +group_id :integer +amount :integer +created_at :timestamp without time zone +updated_at :timestamp without time zone +training_id :integer Group->TrainingsPricing - - - + + + -Plan - -Plan - -id :integer -name :character varying(255) -amount :integer -interval :character varying(255) -group_id :integer -stp_plan_id :character varying(255) -created_at :timestamp without time zone -updated_at :timestamp without time zone -training_credit_nb :integer -is_rolling :boolean -description :text -type :character varying -base_name :character varying -ui_weight :integer -interval_count :integer +Plan + +Plan + +id :integer +name :character varying(255) +amount :integer +interval :character varying(255) +group_id :integer +stp_plan_id :character varying(255) +created_at :timestamp without time zone +updated_at :timestamp without time zone +training_credit_nb :integer +is_rolling :boolean +description :text +type :character varying +base_name :character varying +ui_weight :integer +interval_count :integer Group->Plan - - - + + + -User - -User - -id :integer -username :character varying(255) -email :character varying(255) -encrypted_password :character varying(255) -reset_password_token :character varying(255) -reset_password_sent_at :timestamp without time zone -remember_created_at :timestamp without time zone -sign_in_count :integer -current_sign_in_at :timestamp without time zone -last_sign_in_at :timestamp without time zone -current_sign_in_ip :character varying(255) -last_sign_in_ip :character varying(255) -confirmation_token :character varying(255) -confirmed_at :timestamp without time zone -confirmation_sent_at :timestamp without time zone -unconfirmed_email :character varying(255) -failed_attempts :integer -unlock_token :character varying(255) -locked_at :timestamp without time zone -created_at :timestamp without time zone -updated_at :timestamp without time zone -is_allow_contact :boolean -group_id :integer -stp_customer_id :character varying(255) -slug :character varying(255) -is_active :boolean -invoicing_disabled :boolean -provider :character varying -uid :character varying -auth_token :character varying +User + +User + +id :integer +email :character varying(255) +encrypted_password :character varying(255) +reset_password_token :character varying(255) +reset_password_sent_at :timestamp without time zone +remember_created_at :timestamp without time zone +sign_in_count :integer +current_sign_in_at :timestamp without time zone +last_sign_in_at :timestamp without time zone +current_sign_in_ip :character varying(255) +last_sign_in_ip :character varying(255) +confirmation_token :character varying(255) +confirmed_at :timestamp without time zone +confirmation_sent_at :timestamp without time zone +unconfirmed_email :character varying(255) +failed_attempts :integer +unlock_token :character varying(255) +locked_at :timestamp without time zone +created_at :timestamp without time zone +updated_at :timestamp without time zone +is_allow_contact :boolean +group_id :integer +stp_customer_id :character varying(255) +username :character varying(255) +slug :character varying(255) +is_active :boolean +invoicing_disabled :boolean +provider :character varying +uid :character varying +auth_token :character varying +merged_at :timestamp without time zone +is_allow_newsletter :boolean Group->User - - - + + + InvoiceItem - -InvoiceItem - -id :integer -invoice_id :integer -stp_invoice_item_id :character varying(255) -amount :integer -created_at :timestamp without time zone -updated_at :timestamp without time zone -description :text -subscription_id :integer -invoice_item_id :integer + +InvoiceItem + +id :integer +invoice_id :integer +stp_invoice_item_id :character varying(255) +amount :integer +created_at :timestamp without time zone +updated_at :timestamp without time zone +description :text +subscription_id :integer +invoice_item_id :integer InvoiceItem->InvoiceItem - - - + + + AvailabilityTag - -AvailabilityTag - -id :integer -availability_id :integer -tag_id :integer -created_at :timestamp without time zone -updated_at :timestamp without time zone + +AvailabilityTag + +id :integer +availability_id :integer +tag_id :integer +created_at :timestamp without time zone +updated_at :timestamp without time zone + + +OpenAPI + +OpenAPI + + +TrainingImage + +TrainingImage + +id :integer +viewable_id :integer +viewable_type :character varying(255) +attachment :character varying(255) +type :character varying(255) +created_at :timestamp without time zone +updated_at :timestamp without time zone + + +OpenAPI::Client + +OpenAPI::Client + +id :integer +name :character varying +calls_count :integer +token :character varying +created_at :timestamp without time zone +updated_at :timestamp without time zone + + +OpenAPI::CallsCountTracing + +OpenAPI::CallsCountTracing + +id :integer +open_api_client_id :integer +calls_count :integer +at :timestamp without time zone +created_at :timestamp without time zone +updated_at :timestamp without time zone + + +OpenAPI::Client->OpenAPI::CallsCountTracing + + + + + +OpenAPI::ParameterError + +OpenAPI::ParameterError + + -UserAvatar - -UserAvatar - -id :integer -viewable_id :integer -viewable_type :character varying(255) -attachment :character varying(255) -type :character varying(255) -created_at :timestamp without time zone -updated_at :timestamp without time zone +UserAvatar + +UserAvatar + +id :integer +viewable_id :integer +viewable_type :character varying(255) +attachment :character varying(255) +type :character varying(255) +created_at :timestamp without time zone +updated_at :timestamp without time zone -ProjectImage - -ProjectImage - -id :integer -viewable_id :integer -viewable_type :character varying(255) -attachment :character varying(255) -type :character varying(255) -created_at :timestamp without time zone -updated_at :timestamp without time zone +ProjectImage + +ProjectImage + +id :integer +viewable_id :integer +viewable_type :character varying(255) +attachment :character varying(255) +type :character varying(255) +created_at :timestamp without time zone +updated_at :timestamp without time zone -Tag - -Tag - -id :integer -name :character varying -created_at :timestamp without time zone -updated_at :timestamp without time zone +Tag + +Tag + +id :integer +name :character varying +created_at :timestamp without time zone +updated_at :timestamp without time zone -Tag->AvailabilityTag - - - +Tag->AvailabilityTag + + + -Tag->User - - - +Tag->User + + + -Availability - -Availability - -id :integer -start_at :timestamp without time zone -end_at :timestamp without time zone -available_type :character varying(255) -created_at :timestamp without time zone -updated_at :timestamp without time zone -nb_total_places :integer +Availability + +Availability + +id :integer +start_at :timestamp without time zone +end_at :timestamp without time zone +available_type :character varying(255) +created_at :timestamp without time zone +updated_at :timestamp without time zone +nb_total_places :integer +destroying :boolean -Tag->Availability - - - +Tag->Availability + + + -UserTag - -UserTag - -id :integer -user_id :integer -tag_id :integer -created_at :timestamp without time zone -updated_at :timestamp without time zone +UserTag + +UserTag + +id :integer +user_id :integer +tag_id :integer +created_at :timestamp without time zone +updated_at :timestamp without time zone -Tag->UserTag - - - +Tag->UserTag + + + -Event - -Event - -id :integer -title :character varying(255) -description :text -created_at :timestamp without time zone -updated_at :timestamp without time zone -availability_id :integer -amount :integer -reduced_amount :integer -nb_total_places :integer -nb_free_places :integer -recurrence_id :integer +Event + +Event + +id :integer +title :character varying(255) +description :text +created_at :timestamp without time zone +updated_at :timestamp without time zone +availability_id :integer +amount :integer +nb_total_places :integer +nb_free_places :integer +recurrence_id :integer +age_range_id :integer +category_id :integer + + +PriceCategory + +PriceCategory + +id :integer +name :character varying +conditions :text +created_at :timestamp without time zone +updated_at :timestamp without time zone + + +Event->PriceCategory + + + + + +EventTheme + +EventTheme + +id :integer +name :character varying +created_at :timestamp without time zone +updated_at :timestamp without time zone +slug :character varying + + +Event->EventTheme + + + -EventImage - -EventImage - -id :integer -viewable_id :integer -viewable_type :character varying(255) -attachment :character varying(255) -type :character varying(255) -created_at :timestamp without time zone -updated_at :timestamp without time zone +EventImage + +EventImage + +id :integer +viewable_id :integer +viewable_type :character varying(255) +attachment :character varying(255) +type :character varying(255) +created_at :timestamp without time zone +updated_at :timestamp without time zone -Event->EventImage - - - - - -EventFile - -EventFile - -id :integer -viewable_id :integer -viewable_type :character varying(255) -attachment :character varying(255) -type :character varying(255) -created_at :timestamp without time zone -updated_at :timestamp without time zone - - -Event->EventFile - - - - - -Category - -Category - -id :integer -name :character varying(255) -created_at :timestamp without time zone -updated_at :timestamp without time zone - - -Event->Category - - - - - -StatisticSubType - -StatisticSubType - -id :integer -key :character varying(255) -label :character varying(255) -created_at :timestamp without time zone -updated_at :timestamp without time zone - - -StatisticType - -StatisticType - -id :integer -statistic_index_id :integer -key :character varying(255) -label :character varying(255) -graph :boolean -created_at :timestamp without time zone -updated_at :timestamp without time zone -simple :boolean - - -StatisticSubType->StatisticType - - - - - -StatisticTypeSubType - -StatisticTypeSubType - -id :integer -statistic_type_id :integer -statistic_sub_type_id :integer -created_at :timestamp without time zone -updated_at :timestamp without time zone - - -StatisticSubType->StatisticTypeSubType - - - - - -Project - -Project - -id :integer -name :character varying(255) -description :text -created_at :timestamp without time zone -updated_at :timestamp without time zone -author_id :integer -tags :text -licence_id :integer -state :character varying(255) -slug :character varying(255) -published_at :timestamp without time zone - - -Project->ProjectImage - - - - - -Project->User - - - - - -Theme - -Theme - -id :integer -name :character varying(255) - - -Project->Theme - - - - - -ProjectCao - -ProjectCao - -id :integer -viewable_id :integer -viewable_type :character varying(255) -attachment :character varying(255) -type :character varying(255) -created_at :timestamp without time zone -updated_at :timestamp without time zone - - -Project->ProjectCao - - - - - -ProjectStep - -ProjectStep - -id :integer -description :text -project_id :integer -created_at :timestamp without time zone -updated_at :timestamp without time zone -title :character varying(255) - - -Project->ProjectStep - - - - - -ProjectUser - -ProjectUser - -id :integer -project_id :integer -user_id :integer -created_at :timestamp without time zone -updated_at :timestamp without time zone -is_valid :boolean -valid_token :character varying(255) - - -Project->ProjectUser - - - - - -Machine - -Machine - -id :integer -name :character varying(255) -description :text -spec :text -created_at :timestamp without time zone -updated_at :timestamp without time zone -slug :character varying(255) - - -Project->Machine - - - - - -Component - -Component - -id :integer -name :character varying(255) - - -Project->Component - - - - - -CustomAsset - -CustomAsset - -id :integer -name :character varying -created_at :timestamp without time zone -updated_at :timestamp without time zone - - -CustomAssetFile - -CustomAssetFile - -id :integer -viewable_id :integer -viewable_type :character varying(255) -attachment :character varying(255) -type :character varying(255) -created_at :timestamp without time zone -updated_at :timestamp without time zone - - -CustomAsset->CustomAssetFile - - - - - -Stats::Event - -Stats::Event - - - - -Stats::Project - -Stats::Project - - - - -Stats::User - -Stats::User - - - - -Stats::Subscription - -Stats::Subscription - - - - -Stats::Training - -Stats::Training - - - - -Stats::Account - -Stats::Account - - - - -Stats::Machine - -Stats::Machine - - - - -Invoice - -Invoice - -id :integer -invoiced_id :integer -invoiced_type :character varying(255) -stp_invoice_id :character varying(255) -total :integer -created_at :timestamp without time zone -updated_at :timestamp without time zone -user_id :integer -reference :character varying(255) -avoir_mode :character varying(255) -avoir_date :timestamp without time zone -invoice_id :integer -type :character varying(255) -subscription_to_expire :boolean -description :text - - -Invoice->InvoiceItem - - - - - -Invoice->Invoice - - - -avoir - - -PlanFile - -PlanFile - -id :integer -viewable_id :integer -viewable_type :character varying(255) -attachment :character varying(255) -type :character varying(255) -created_at :timestamp without time zone -updated_at :timestamp without time zone - - -OfferDay - -OfferDay - -id :integer -subscription_id :integer -start_at :timestamp without time zone -end_at :timestamp without time zone -created_at :timestamp without time zone -updated_at :timestamp without time zone - - -OfferDay->Invoice - - - - - -StatisticIndex - -StatisticIndex - -id :integer -es_type_key :character varying(255) -label :character varying(255) -created_at :timestamp without time zone -updated_at :timestamp without time zone -table :boolean -ca :boolean - - -StatisticType->StatisticIndex - - - - - -StatisticType->StatisticTypeSubType - - - - - -Address - -Address - -id :integer -address :character varying(255) -street_number :character varying(255) -route :character varying(255) -locality :character varying(255) -country :character varying(255) -postal_code :character varying(255) -placeable_id :integer -placeable_type :character varying(255) -created_at :timestamp without time zone -updated_at :timestamp without time zone - - -Plan->Price - - - - - -Plan->PlanFile - - - - - -Credit - -Credit - -id :integer -creditable_id :integer -creditable_type :character varying(255) -plan_id :integer -hours :integer -created_at :timestamp without time zone -updated_at :timestamp without time zone - - -Plan->Credit - - - - - -Plan->Credit - - - -training_credits - - -Plan->Credit - - - -machine_credits - - -Subscription - -Subscription - -id :integer -plan_id :integer -user_id :integer -stp_subscription_id :character varying(255) -created_at :timestamp without time zone -updated_at :timestamp without time zone -expired_at :timestamp without time zone -canceled_at :timestamp without time zone - - -Plan->Subscription - - - - - -PlanImage - -PlanImage - -id :integer -viewable_id :integer -viewable_type :character varying(255) -attachment :character varying(255) -type :character varying(255) -created_at :timestamp without time zone -updated_at :timestamp without time zone - - -Plan->PlanImage - - - - - -MachineFile - -MachineFile - -id :integer -viewable_id :integer -viewable_type :character varying(255) -attachment :character varying(255) -type :character varying(255) -created_at :timestamp without time zone -updated_at :timestamp without time zone - - -StatisticGraph - -StatisticGraph - -id :integer -statistic_index_id :integer -chart_type :character varying(255) -limit :integer -created_at :timestamp without time zone -updated_at :timestamp without time zone - - -OAuth2Mapping - -OAuth2Mapping - -id :integer -o_auth2_provider_id :integer -local_field :character varying -api_field :character varying -created_at :timestamp without time zone -updated_at :timestamp without time zone -local_model :character varying -api_endpoint :character varying -api_data_type :character varying - - -Licence - -Licence - -id :integer -name :character varying(255) -description :text - - -Licence->Project - - - - - -User->Project - - - -my_projects - - -User->Invoice - - - - - -User->Credit - - - - - -Role - -Role - -id :integer -name :character varying(255) -resource_id :integer -resource_type :character varying(255) -created_at :timestamp without time zone -updated_at :timestamp without time zone - - -User->Role - - - - - -Notification - -Notification - -id :integer -receiver_id :integer -attached_object_id :integer -attached_object_type :character varying(255) -notification_type_id :integer -is_read :boolean -created_at :timestamp without time zone -updated_at :timestamp without time zone -receiver_type :character varying(255) -is_send :boolean -meta_data :jsonb - - -User->Notification - - - - - -User->Subscription - - - - - -Training - -Training - -id :integer -name :character varying(255) -created_at :timestamp without time zone -updated_at :timestamp without time zone -nb_total_places :integer -slug :character varying(255) -description :text - - -User->Training - - - +Event->EventImage + + + -Reservation - -Reservation - -id :integer -user_id :integer -message :text -created_at :timestamp without time zone -updated_at :timestamp without time zone -reservable_id :integer -reservable_type :character varying(255) -stp_invoice_id :character varying(255) -nb_reserve_places :integer -nb_reserve_reduced_places :integer +Reservation + +Reservation + +id :integer +user_id :integer +message :text +created_at :timestamp without time zone +updated_at :timestamp without time zone +reservable_id :integer +reservable_type :character varying(255) +stp_invoice_id :character varying(255) +nb_reserve_places :integer + + +Event->Reservation + + + + + +EventPriceCategory + +EventPriceCategory + +id :integer +event_id :integer +price_category_id :integer +amount :integer +created_at :timestamp without time zone +updated_at :timestamp without time zone + + +Event->EventPriceCategory + + + + + +EventFile + +EventFile + +id :integer +viewable_id :integer +viewable_type :character varying(255) +attachment :character varying(255) +type :character varying(255) +created_at :timestamp without time zone +updated_at :timestamp without time zone + + +Event->EventFile + + + + + +StatisticSubType + +StatisticSubType + +id :integer +key :character varying(255) +label :character varying(255) +created_at :timestamp without time zone +updated_at :timestamp without time zone + + +StatisticType + +StatisticType + +id :integer +statistic_index_id :integer +key :character varying(255) +label :character varying(255) +graph :boolean +created_at :timestamp without time zone +updated_at :timestamp without time zone +simple :boolean + + +StatisticSubType->StatisticType + + + + + +StatisticTypeSubType + +StatisticTypeSubType + +id :integer +statistic_type_id :integer +statistic_sub_type_id :integer +created_at :timestamp without time zone +updated_at :timestamp without time zone + + +StatisticSubType->StatisticTypeSubType + + + + + +Project + +Project + +id :integer +name :character varying(255) +description :text +created_at :timestamp without time zone +updated_at :timestamp without time zone +author_id :integer +tags :text +licence_id :integer +state :character varying(255) +slug :character varying(255) +published_at :timestamp without time zone + + +Project->ProjectImage + + + + + +Project->User + + + + + +Theme + +Theme + +id :integer +name :character varying(255) + + +Project->Theme + + + + + +ProjectCao + +ProjectCao + +id :integer +viewable_id :integer +viewable_type :character varying(255) +attachment :character varying(255) +type :character varying(255) +created_at :timestamp without time zone +updated_at :timestamp without time zone + + +Project->ProjectCao + + + + + +ProjectStep + +ProjectStep + +id :integer +description :text +project_id :integer +created_at :timestamp without time zone +updated_at :timestamp without time zone +title :character varying(255) +step_nb :integer + + +Project->ProjectStep + + + + + +ProjectUser + +ProjectUser + +id :integer +project_id :integer +user_id :integer +created_at :timestamp without time zone +updated_at :timestamp without time zone +is_valid :boolean +valid_token :character varying(255) + + +Project->ProjectUser + + + + + +Machine + +Machine + +id :integer +name :character varying(255) +description :text +spec :text +created_at :timestamp without time zone +updated_at :timestamp without time zone +slug :character varying(255) + + +Project->Machine + + + + + +Component + +Component + +id :integer +name :character varying(255) + + +Project->Component + + + + + +CustomAsset + +CustomAsset + +id :integer +name :character varying +created_at :timestamp without time zone +updated_at :timestamp without time zone + + +CustomAssetFile + +CustomAssetFile + +id :integer +viewable_id :integer +viewable_type :character varying(255) +attachment :character varying(255) +type :character varying(255) +created_at :timestamp without time zone +updated_at :timestamp without time zone + + +CustomAsset->CustomAssetFile + + + + + +AgeRange + +AgeRange + +id :integer +name :character varying +created_at :timestamp without time zone +updated_at :timestamp without time zone +slug :character varying + + +AgeRange->Event + + + + + +Wallet + +Wallet + +id :integer +user_id :integer +amount :integer +created_at :timestamp without time zone +updated_at :timestamp without time zone + + +WalletTransaction + +WalletTransaction + +id :integer +user_id :integer +wallet_id :integer +transactable_id :integer +transactable_type :character varying +transaction_type :character varying +amount :integer +created_at :timestamp without time zone +updated_at :timestamp without time zone + + +Wallet->WalletTransaction + + + + + +Stats::Event + +Stats::Event + + + + +Stats::Project + +Stats::Project + + + + +Stats::User + +Stats::User + + + + +Stats::Subscription + +Stats::Subscription + + + + +Stats::Training + +Stats::Training + + + + +Stats::Account + +Stats::Account + + + + +Stats::Machine + +Stats::Machine + + + + +PriceCategory->EventPriceCategory + + + + + +Invoice + +Invoice + +id :integer +invoiced_id :integer +invoiced_type :character varying(255) +stp_invoice_id :character varying(255) +total :integer +created_at :timestamp without time zone +updated_at :timestamp without time zone +user_id :integer +reference :character varying(255) +avoir_mode :character varying(255) +avoir_date :timestamp without time zone +invoice_id :integer +type :character varying(255) +subscription_to_expire :boolean +description :text +wallet_amount :integer +wallet_transaction_id :integer +coupon_id :integer + + +Invoice->InvoiceItem + + + + + +Invoice->Invoice + + + +avoir + + +PlanFile + +PlanFile + +id :integer +viewable_id :integer +viewable_type :character varying(255) +attachment :character varying(255) +type :character varying(255) +created_at :timestamp without time zone +updated_at :timestamp without time zone + + +OfferDay + +OfferDay + +id :integer +subscription_id :integer +start_at :timestamp without time zone +end_at :timestamp without time zone +created_at :timestamp without time zone +updated_at :timestamp without time zone + + +OfferDay->Invoice + + + + + +StatisticIndex + +StatisticIndex + +id :integer +es_type_key :character varying(255) +label :character varying(255) +created_at :timestamp without time zone +updated_at :timestamp without time zone +table :boolean +ca :boolean + + +StatisticType->StatisticIndex + + + + + +StatisticType->StatisticTypeSubType + + + + + +StatisticCustomAggregation + +StatisticCustomAggregation + +id :integer +query :text +statistic_type_id :integer +created_at :timestamp without time zone +updated_at :timestamp without time zone +field :character varying +es_index :character varying +es_type :character varying + + +StatisticType->StatisticCustomAggregation + + + + + +Address + +Address + +id :integer +address :character varying(255) +street_number :character varying(255) +route :character varying(255) +locality :character varying(255) +country :character varying(255) +postal_code :character varying(255) +placeable_id :integer +placeable_type :character varying(255) +created_at :timestamp without time zone +updated_at :timestamp without time zone + + +Plan->Price + + + + + +Plan->PlanFile + + + + + +Credit + +Credit + +id :integer +creditable_id :integer +creditable_type :character varying(255) +plan_id :integer +hours :integer +created_at :timestamp without time zone +updated_at :timestamp without time zone + + +Plan->Credit + + + + + +Plan->Credit + + + +training_credits + + +Plan->Credit + + + +machine_credits + + +Subscription + +Subscription + +id :integer +plan_id :integer +user_id :integer +stp_subscription_id :character varying(255) +created_at :timestamp without time zone +updated_at :timestamp without time zone +expired_at :timestamp without time zone +canceled_at :timestamp without time zone + + +Plan->Subscription + + + + + +PlanImage + +PlanImage + +id :integer +viewable_id :integer +viewable_type :character varying(255) +attachment :character varying(255) +type :character varying(255) +created_at :timestamp without time zone +updated_at :timestamp without time zone + + +Plan->PlanImage + + + + + +MachineFile + +MachineFile + +id :integer +viewable_id :integer +viewable_type :character varying(255) +attachment :character varying(255) +type :character varying(255) +created_at :timestamp without time zone +updated_at :timestamp without time zone + + +Organization + +Organization + +id :integer +name :character varying +created_at :timestamp without time zone +updated_at :timestamp without time zone +profile_id :integer + + +Organization->Address + + + + + +StatisticGraph + +StatisticGraph + +id :integer +statistic_index_id :integer +chart_type :character varying(255) +limit :integer +created_at :timestamp without time zone +updated_at :timestamp without time zone + + +OAuth2Mapping + +OAuth2Mapping + +id :integer +o_auth2_provider_id :integer +local_field :character varying +api_field :character varying +created_at :timestamp without time zone +updated_at :timestamp without time zone +local_model :character varying +api_endpoint :character varying +api_data_type :character varying +transformation :jsonb + + +Licence + +Licence + +id :integer +name :character varying(255) +description :text + + +Licence->Project + + + + + +User->Project + + + +my_projects + + +User->Wallet + + + + + +User->Invoice + + + + + +User->Credit + + + + + +User->Credit + + + +training_credits + + +User->Credit + + + +machine_credits + + +Role + +Role + +id :integer +name :character varying(255) +resource_id :integer +resource_type :character varying(255) +created_at :timestamp without time zone +updated_at :timestamp without time zone + + +User->Role + + + + + +Notification + +Notification + +id :integer +receiver_id :integer +attached_object_id :integer +attached_object_type :character varying(255) +notification_type_id :integer +is_read :boolean +created_at :timestamp without time zone +updated_at :timestamp without time zone +receiver_type :character varying +is_send :boolean +meta_data :jsonb + + +User->Notification + + + + + +User->Subscription + + + + + +Training + +Training + +id :integer +name :character varying(255) +created_at :timestamp without time zone +updated_at :timestamp without time zone +nb_total_places :integer +slug :character varying(255) +description :text +public_page :boolean + + +User->Training + + + -User->Reservation - - - +User->Reservation + + + -UsersCredit - -UsersCredit - -id :integer -user_id :integer -credit_id :integer -hours_used :integer -created_at :timestamp without time zone -updated_at :timestamp without time zone +UsersCredit + +UsersCredit + +id :integer +user_id :integer +credit_id :integer +hours_used :integer +created_at :timestamp without time zone +updated_at :timestamp without time zone -User->UsersCredit - - - +User->UsersCredit + + + -User->UserTag - - - +User->UserTag + + + + + +Export + +Export + +id :integer +category :character varying +export_type :character varying +query :character varying +created_at :timestamp without time zone +updated_at :timestamp without time zone +user_id :integer +key :character varying + + +User->Export + + + -UserTraining - -UserTraining - -id :integer -user_id :integer -created_at :timestamp without time zone -updated_at :timestamp without time zone -training_id :integer +UserTraining + +UserTraining + +id :integer +user_id :integer +created_at :timestamp without time zone +updated_at :timestamp without time zone +training_id :integer -User->UserTraining - - - +User->UserTraining + + + -User->ProjectUser - - - +User->ProjectUser + + + -Profile - -Profile - -id :integer -user_id :integer -first_name :character varying(255) -last_name :character varying(255) -gender :boolean -birthday :date -phone :character varying(255) -interest :text -software_mastered :text -created_at :timestamp without time zone -updated_at :timestamp without time zone +Profile + +Profile + +id :integer +user_id :integer +first_name :character varying(255) +last_name :character varying(255) +gender :boolean +birthday :date +phone :character varying(255) +interest :text +software_mastered :text +created_at :timestamp without time zone +updated_at :timestamp without time zone +facebook :character varying +twitter :character varying +google_plus :character varying +viadeo :character varying +linkedin :character varying +instagram :character varying +youtube :character varying +vimeo :character varying +dailymotion :character varying +github :character varying +echosciences :character varying +website :character varying +pinterest :character varying +lastfm :character varying +flickr :character varying +job :character varying -User->Profile - - - +User->Profile + + + -ProjectStepImage - -ProjectStepImage - -id :integer -viewable_id :integer -viewable_type :character varying(255) -attachment :character varying(255) -type :character varying(255) -created_at :timestamp without time zone -updated_at :timestamp without time zone +ProjectStepImage + +ProjectStepImage + +id :integer +viewable_id :integer +viewable_type :character varying(255) +attachment :character varying(255) +type :character varying(255) +created_at :timestamp without time zone +updated_at :timestamp without time zone -Credit->UsersCredit - - - +Credit->UsersCredit + + + -PartnerPlan - -PartnerPlan - -id :integer -name :character varying(255) -amount :integer -interval :character varying(255) -group_id :integer -stp_plan_id :character varying(255) -created_at :timestamp without time zone -updated_at :timestamp without time zone -training_credit_nb :integer -is_rolling :boolean -description :text -type :character varying -base_name :character varying -ui_weight :integer -interval_count :integer +PartnerPlan + +PartnerPlan + +id :integer +name :character varying(255) +amount :integer +interval :character varying(255) +group_id :integer +stp_plan_id :character varying(255) +created_at :timestamp without time zone +updated_at :timestamp without time zone +training_credit_nb :integer +is_rolling :boolean +description :text +type :character varying +base_name :character varying +ui_weight :integer +interval_count :integer -PartnerPlan->Price - - - +PartnerPlan->Price + + + -PartnerPlan->PlanFile - - - +PartnerPlan->PlanFile + + + -PartnerPlan->Credit - - - +PartnerPlan->Credit + + + -PartnerPlan->Credit - - - -training_credits +PartnerPlan->Credit + + + +training_credits -PartnerPlan->Credit - - - -machine_credits +PartnerPlan->Credit + + + +machine_credits -PartnerPlan->Role - - - +PartnerPlan->Role + + + -PartnerPlan->Subscription - - - +PartnerPlan->Subscription + + + -PartnerPlan->PlanImage - - - +PartnerPlan->PlanImage + + + -OAuth2Provider - -OAuth2Provider - -id :integer -base_url :character varying -token_endpoint :character varying -authorization_endpoint :character varying -client_id :character varying -client_secret :character varying -created_at :timestamp without time zone -updated_at :timestamp without time zone -profile_url :character varying +OAuth2Provider + +OAuth2Provider + +id :integer +base_url :character varying +token_endpoint :character varying +authorization_endpoint :character varying +client_id :character varying +client_secret :character varying +created_at :timestamp without time zone +updated_at :timestamp without time zone +profile_url :character varying -OAuth2Provider->OAuth2Mapping - - - +OAuth2Provider->OAuth2Mapping + + + -AuthProvider - -AuthProvider - -id :integer -name :character varying -status :character varying -created_at :timestamp without time zone -updated_at :timestamp without time zone -providable_type :character varying -providable_id :integer +AuthProvider + +AuthProvider + +id :integer +name :character varying +status :character varying +created_at :timestamp without time zone +updated_at :timestamp without time zone +providable_type :character varying +providable_id :integer -OAuth2Provider->AuthProvider - - - +OAuth2Provider->AuthProvider + + + -Asset - -Asset - -id :integer -viewable_id :integer -viewable_type :character varying(255) -attachment :character varying(255) -type :character varying(255) -created_at :timestamp without time zone -updated_at :timestamp without time zone +Asset + +Asset + +id :integer +viewable_id :integer +viewable_type :character varying(255) +attachment :character varying(255) +type :character varying(255) +created_at :timestamp without time zone +updated_at :timestamp without time zone + + +Project::OpenlabSync + +Project::OpenlabSync -DatabaseProvider - -DatabaseProvider - -id :integer -created_at :timestamp without time zone -updated_at :timestamp without time zone +DatabaseProvider + +DatabaseProvider + +id :integer +created_at :timestamp without time zone +updated_at :timestamp without time zone -DatabaseProvider->AuthProvider - - - +DatabaseProvider->AuthProvider + + + -Availability->AvailabilityTag - - - +Availability->AvailabilityTag + + + -Availability->Event - - - +Availability->Event + + + -Availability->Training - - - +Availability->Training + + + -Availability->Reservation - - - +Availability->Reservation + + + -Slot - -Slot - -id :integer -start_at :timestamp without time zone -end_at :timestamp without time zone -reservation_id :integer -created_at :timestamp without time zone -updated_at :timestamp without time zone -availability_id :integer -ex_start_at :timestamp without time zone -canceled_at :timestamp without time zone -ex_end_at :timestamp without time zone -offered :boolean +Slot + +Slot + +id :integer +start_at :timestamp without time zone +end_at :timestamp without time zone +reservation_id :integer +created_at :timestamp without time zone +updated_at :timestamp without time zone +availability_id :integer +ex_start_at :timestamp without time zone +ex_end_at :timestamp without time zone +canceled_at :timestamp without time zone +offered :boolean -Availability->Slot - - - +Availability->Slot + + + -MachinesAvailability - -MachinesAvailability - -id :integer -machine_id :integer -availability_id :integer +MachinesAvailability + +MachinesAvailability + +id :integer +machine_id :integer +availability_id :integer -Availability->MachinesAvailability - - - +Availability->MachinesAvailability + + + -TrainingsAvailability - -TrainingsAvailability - -id :integer -training_id :integer -availability_id :integer -created_at :timestamp without time zone -updated_at :timestamp without time zone +TrainingsAvailability + +TrainingsAvailability + +id :integer +training_id :integer +availability_id :integer +created_at :timestamp without time zone +updated_at :timestamp without time zone -Availability->TrainingsAvailability - - - +Availability->TrainingsAvailability + + + -Availability->Machine - - - +Availability->Machine + + + -Subscription->Invoice - - - +Subscription->Invoice + + + -Subscription->OfferDay - - - +Subscription->OfferDay + + + + + +Training->TrainingImage + + + -Training->TrainingsPricing - - - +Training->TrainingsPricing + + + -Training->Plan - - - +Training->Plan + + + -Training->Credit - - - +Training->Credit + + + -Training->Reservation - - - +Training->Reservation + + + -Training->UserTraining - - - +Training->UserTraining + + + -Training->TrainingsAvailability - - - +Training->TrainingsAvailability + + + -Training->Machine - - - +Training->Machine + + + -ProjectStep->ProjectStepImage - - - +ProjectStep->ProjectStepImage + + + -Reservation->Invoice - - - +Reservation->Invoice + + + -Reservation->Slot - - - +Reservation->Slot + + + + + +Ticket + +Ticket + +id :integer +reservation_id :integer +event_price_category_id :integer +booked :integer +created_at :timestamp without time zone +updated_at :timestamp without time zone + + +Reservation->Ticket + + + -StatisticIndex->StatisticType - - - +StatisticIndex->StatisticType + + + -StatisticIndex->StatisticGraph - - - +StatisticIndex->StatisticGraph + + + -StatisticField - -StatisticField - -id :integer -statistic_index_id :integer -key :character varying(255) -label :character varying(255) -created_at :timestamp without time zone -updated_at :timestamp without time zone -data_type :character varying(255) +StatisticField + +StatisticField + +id :integer +statistic_index_id :integer +key :character varying(255) +label :character varying(255) +created_at :timestamp without time zone +updated_at :timestamp without time zone +data_type :character varying(255) -StatisticIndex->StatisticField - - - +StatisticIndex->StatisticField + + + -StatisticField->StatisticIndex - - - +StatisticField->StatisticIndex + + + + + +WalletTransaction->Invoice + + + + + +EventPriceCategory->Ticket + + + -Avoir - -Avoir - -id :integer -invoiced_id :integer -invoiced_type :character varying(255) -stp_invoice_id :character varying(255) -total :integer -created_at :timestamp without time zone -updated_at :timestamp without time zone -user_id :integer -reference :character varying(255) -avoir_mode :character varying(255) -avoir_date :timestamp without time zone -invoice_id :integer -type :character varying(255) -subscription_to_expire :boolean -description :text +Avoir + +Avoir + +id :integer +invoiced_id :integer +invoiced_type :character varying(255) +stp_invoice_id :character varying(255) +total :integer +created_at :timestamp without time zone +updated_at :timestamp without time zone +user_id :integer +reference :character varying(255) +avoir_mode :character varying(255) +avoir_date :timestamp without time zone +invoice_id :integer +type :character varying(255) +subscription_to_expire :boolean +description :text +wallet_amount :integer +wallet_transaction_id :integer +coupon_id :integer -Avoir->InvoiceItem - - - +Avoir->InvoiceItem + + + -Avoir->Invoice - - - -avoir +Avoir->Invoice + + + +avoir -NotificationType - -NotificationType - - +NotificationType + +NotificationType + + -Stylesheet - -Stylesheet - -id :integer -contents :text -created_at :timestamp without time zone -updated_at :timestamp without time zone +Stylesheet + +Stylesheet + +id :integer +contents :text +created_at :timestamp without time zone +updated_at :timestamp without time zone + + +Coupon + +Coupon + +id :integer +name :character varying +code :character varying +percent_off :integer +valid_until :timestamp without time zone +max_usages :integer +active :boolean +created_at :timestamp without time zone +updated_at :timestamp without time zone +validity_per_user :character varying + + +Coupon->Invoice + + + + + +Category + +Category + +id :integer +name :character varying(255) +created_at :timestamp without time zone +updated_at :timestamp without time zone +slug :character varying + + +Category->Event + + + -MachineImage - -MachineImage - -id :integer -viewable_id :integer -viewable_type :character varying(255) -attachment :character varying(255) -type :character varying(255) -created_at :timestamp without time zone -updated_at :timestamp without time zone +MachineImage + +MachineImage + +id :integer +viewable_id :integer +viewable_type :character varying(255) +attachment :character varying(255) +type :character varying(255) +created_at :timestamp without time zone +updated_at :timestamp without time zone -Feed - -Feed - - +Feed + +Feed + + -Machine->Price - - - +Machine->Price + + + -Machine->Plan - - - +Machine->Plan + + + -Machine->MachineFile - - - +Machine->MachineFile + + + -Machine->Credit - - - +Machine->Credit + + + -Machine->Reservation - - - +Machine->Reservation + + + -Machine->MachinesAvailability - - - +Machine->MachinesAvailability + + + -Machine->MachineImage - - - +Machine->MachineImage + + + + + +Abuse + +Abuse + +id :integer +signaled_id :integer +signaled_type :character varying +first_name :character varying +last_name :character varying +email :character varying +message :text +created_at :timestamp without time zone +updated_at :timestamp without time zone -Profile->UserAvatar - - - +Profile->UserAvatar + + + -Profile->Address - - - +Profile->Address + + + + + +Profile->Organization + + + -Setting - -Setting - -id :integer -name :character varying -value :text -created_at :timestamp without time zone -updated_at :timestamp without time zone +Setting + +Setting + +id :integer +name :character varying +value :text +created_at :timestamp without time zone +updated_at :timestamp without time zone diff --git a/doc/sso_authentication.md b/doc/sso_authentication.md index 96da056d7..62e6da0ce 100644 --- a/doc/sso_authentication.md +++ b/doc/sso_authentication.md @@ -1,95 +1,105 @@ -# How to add an authentication method to the FabLab ? +# How to add an authentication method to the Fab-Manager ? -First, take a look at the [OmniAuth list of strategies](https://github.com/intridea/omniauth/wiki/List-of-Strategies) - for the Strategy or Developer Strategy you want to add to the FabLab. +First, take a look at the [OmniAuth list of strategies](https://github.com/intridea/omniauth/wiki/List-of-Strategies) for the Strategy or Developer Strategy you want to add to the Fab-Manager. For this guide, we will consider you want to add a generic *developer strategy*, like LDAP. Create the OmniAuth implementation ( **lib/omni_auth/strategies/ldap_provider.rb** ) - # first require the OmniAuth gem you added to your Gemfile (see the link above for a list of gems) - require 'omniauth-ldap' - module OmniAuth - module Strategies - # in the class name, replace Ldap with the kind of authentication you are implementing - class SsoLdapProvider < OmniAuth::Strategies::LDAP - # implement the logic here, see the gem specific documentation for more details - end - end +```ruby +# first require the OmniAuth gem you added to your Gemfile (see the link above for a list of gems) +require 'omniauth-ldap' +module OmniAuth + module Strategies + # in the class name, replace Ldap with the kind of authentication you are implementing + class SsoLdapProvider < OmniAuth::Strategies::LDAP + # implement the logic here, see the gem specific documentation for more details end + end +end +``` Create the ActiveRecord models ( **from the terminal** ) - # in the models names, replace Ldap with the kind of authentication you are implementing - # replace ldap_fields with the fields you need for implementing LDAP or whatever you are implementing - $ rails g model LdapProvider ...ldap_fields - $ rails g model LdapMapping ldap_provider:belongs_to local_field:string local_model:string ...ldap_fields +```bash +# in the models names, replace Ldap with the kind of authentication you are implementing +# replace ldap_fields with the fields you need for implementing LDAP or whatever you are implementing +rails g model LdapProvider ...ldap_fields +rails g model LdapMapping ldap_provider:belongs_to local_field:string local_model:string ...ldap_fields +``` Complete the Provider Model ( **app/model/ldap_provider.rb** ) - class LdapProvider < ActiveRecord::Base - has_one :auth_provider, as: :providable - has_many :ldap_mappings, dependent: :destroy - accepts_nested_attributes_for :ldap_mappings, allow_destroy: true - - # return here the fields you want to protect from being directly on the FabLab, typically mapped fields - def protected_fields - fields = [] - ldap_mappings.each do |mapping| - fields.push(mapping.local_model+'.'+mapping.local_field) - end - fields - end - - # return here the link the current users will have to follow to edit his profile on the SSO - def profile_url - # you can also create a profile_url field in the Database model +```ruby +class LdapProvider < ActiveRecord::Base + has_one :auth_provider, as: :providable + has_many :ldap_mappings, dependent: :destroy + accepts_nested_attributes_for :ldap_mappings, allow_destroy: true + + # return the fields you want to protect from being directly managed by the Fab-Manager, typically mapped fields + def protected_fields + fields = [] + ldap_mappings.each do |mapping| + fields.push(mapping.local_model+'.'+mapping.local_field) + end + fields + end + + # return the link, that the current user will have to follow, to edit his profile on the SSO + def profile_url + # you can also create a profile_url field in the Database model + end +end +``` +Whitelist your implementation's fields in the controller ( **app/controllers/api/auth_providers_controller.rb** ) + +```ruby +class API::AuthProvidersController < API::ApiController + ... + private + def provider_params + if params['auth_provider']['providable_type'] == DatabaseProvider.name + ... + elsif if params['auth_provider']['providable_type'] == LdapProvider.name + params.require(:auth_provider).permit(:name, :providable_type, providable_attributes: [ + # list here your LdapProvider model's fields, followed by the mappings : + ldap_mappings_attributes: [ + :id, :local_model, :local_field, ... + # add your other customs LdapMapping fields, don't forget the :_destroy symbol if + # you want your admin to be able to remove mappings + ] + ]) end end - -Whitelist your implementation fields in the controller ( **app/controllers/api/auth_providers_controller.rb** ) +end +``` - class API::AuthProvidersController < API::ApiController - ... - private - def provider_params - if params['auth_provider']['providable_type'] == DatabaseProvider.name - ... - elsif if params['auth_provider']['providable_type'] == LdapProvider.name - params.require(:auth_provider).permit(:name, :providable_type, providable_attributes: [ - # list here your LdapProvider model's fields, followed by the mappings : - ldap_mappings_attributes: [ - :id, :local_model, :local_field, ... - # add your other customs LdapMapping fields, don't forget the :_destroy symbol if - # you want your admin to be able to remove mappings - ] - ]) - end - end - end - List the fields to display in the JSON API view ( **app/views/api/auth_providers/show.json.jbuilder** ) - json.partial! 'api/auth_providers/auth_provider', auth_provider: @provider - - ... - - if @provider.providable_type == LdapProvider.name - json.providable_attributes do - json.extract! @provider.providable, :id, ... # list LdapProvider fields here - json.ldap_mappings_attributes @provider.providable.ldap_mappings do |m| - json.extract! m, :id, :local_model, :local_field, ... # list LdapMapping fields here - end - end +```ruby +json.partial! 'api/auth_providers/auth_provider', auth_provider: @provider + +... + +if @provider.providable_type == LdapProvider.name + json.providable_attributes do + json.extract! @provider.providable, :id, ... # list LdapProvider fields here + json.ldap_mappings_attributes @provider.providable.ldap_mappings do |m| + json.extract! m, :id, :local_model, :local_field, ... # list LdapMapping fields here end + end +end +``` Configure the initializer ( **config/initializers/devise.rb** ) - require_relative '../../lib/omni_auth/omni_auth' - ... - elsif active_provider.providable_type == LdapProvider.name - config.omniauth OmniAuth::Strategies::SsoLdapProvider.name.to_sym, # pass here the required parameters, see the gem documentation for details - end +```ruby +require_relative '../../lib/omni_auth/omni_auth' +... +elsif active_provider.providable_type == LdapProvider.name + config.omniauth OmniAuth::Strategies::SsoLdapProvider.name.to_sym, # pass here the required parameters, see the gem documentation for details +end +``` Finally you have to create an admin interface with AngularJS: @@ -97,34 +107,38 @@ Finally you have to create an admin interface with AngularJS: - **app/assets/templates/admin/authentifications/_ldap_mapping.html.erb** must contains html partial to configure the LdapMappings, see _oauth2_mapping.html.erb for a working example - **app/assets/javascript/controllers/admin/authentifications.coffee** +```coffeescript +## list of supported authentication methods +METHODS = { + ... + 'LdapProvider' : 'LDAP' # add the name of your ActiveRecord model class here as a hash key, associated with a human readable name as a hash value (string) +} - ## list of supported authentication methods - METHODS = { - ... - 'LdapProvider' : 'LDAP' # add the name of your ActiveRecord model class here as a hash key, associated with a human readable name as a hash value (string) - } +Application.Controllers.controller "newAuthentificationController", ... + +$scope.updateProvidable = -> + ... + if $scope.provider.providable_type == 'LdapProvider' + # you may want to do some stuff to initialize your provider here - Application.Controllers.controller "newAuthentificationController", ... - - $scope.updateProvidable = -> - ... - if $scope.provider.providable_type == 'LdapProvider' - # you may want to do some stuff to initialize your provider here - - $scope.registerProvider = -> - ... - # === LdapProvider === - else if $scope.provider.providable_type == 'LdapProvider' - # here you may want to do some data validation - # then: save the settings - AuthProvider.save auth_provider: $scope.provider, (provider) -> - # register was a success, display a message, redirect, etc. - +$scope.registerProvider = -> + ... + # === LdapProvider === + else if $scope.provider.providable_type == 'LdapProvider' + # here you may want to do some data validation + # then: save the settings + AuthProvider.save auth_provider: $scope.provider, (provider) -> + # register was a success, display a message, redirect, etc. +``` + And to include this interface into the existing one ( **app/assets/templates/admin/authentifications/edit.html.erb**) -
- ... - - -
- \ No newline at end of file +```html +
+ ... + + +
+``` + +Do not forget that you can find examples and inspiration in the OAuth 2.0 implementation. \ No newline at end of file diff --git a/doc/sso_with_github.md b/doc/sso_with_github.md new file mode 100644 index 000000000..2e723592e --- /dev/null +++ b/doc/sso_with_github.md @@ -0,0 +1,66 @@ +# How to configure Fab-manager to use a Single Sign-On authentication? + +For this guide, we will use [GitHub](https://developer.github.com/v3/oauth/) as an example authentication provider, because it uses OAuth 2.0 which is currently implemented in fab-manager, it has a standard implementation of the protocol and it is free to use for everyone. + +- First, you must have a GitHub account. This is free, so create one if you don't have any. + Visit https://github.com/join?source=login to create an account. + +- Secondly, you will need to register your fab-manager instance as an application in GitHub. + Visit https://github.com/settings/applications/new to register your instance. + - In `Application name`, we advise you to set the same name as your fab-manager's instance title. + - In `Homepage URL`, put the public URL where your fab-manager's instance is located (eg. https://example.com). + - In `Authorization callback URL`, you must specify an URL that will match this scheme: https://example.com/users/auth/oauth2-github/callback + - **example.com** is your own fab-manager's address + - **oauth2-github** match the provider's "strategy name" in the fab-manager. + It is composed of: **SSO's protocol**, _dash_, **slug of the provider's name**. + If you have a doubt about what it will be, start by creating the authentication provider in your fab-manager (see below), then the strategy's name will be shown in the providers list. + +- You'll be redirected to a page displaying two important informations: your **Client ID** and your **Client Secret**. + +- Now go to your fab-manager's instance, login as an administrator, go to `Users management` and `Authentication`. + Click `Add a new authentication provider`, and select _OAuth 2.0_ in the `Authentication type` drop-down list. + In `name`, you can set whatever you want, but you must be aware that: + 1. You will need to type this name in a terminal to activate the provider, so prefer avoiding chars that must be escaped. + 2. This name will be occasionally displayed to end users, so prefer sweet and speaking names. + 3. The slug of this name is used in the callback URL provided to the SSO server (eg. /users/auth/oauth2-**github**/callback) + +- Fulfill the form with the following parameters: + - **Common URL**: `https://github.com/login/oauth/` This is the common part in the URLs of the two following parameters. + - **Authorization endpoint**: `authorize` This URL can be found [here](https://developer.github.com/v3/oauth/). + - **Token Acquisition Endpoint**: `access_token` This URL can be found [here](https://developer.github.com/v3/oauth/). + - **Profile edition URL**: `https://github.com/settings/profile` This is the URL where you are directed when you click on `Edit profile` in your GitHub dashboard. + - **Client identifier**: Your Client ID, collected just before. + - **Client secret**: Your Client Secret, collected just before. + +- Then you will need to define the matching of the fields between the fab-manager and what the external SSO can provide. + Please note that the only mandatory field is `User.uid`. + To continue with our GitHub example, you will need to look at [this documentation page](https://developer.github.com/v3/users/#get-the-authenticated-user) to know witch field can be mapped and how, and [this one](https://developer.github.com/v3/) to know the root URL of the API. + - **Model**: `User` + - **Field**: `uid` + - **API endpoint URL**: `https://api.github.com/user` Here you can set a complete URL **OR** only an endpoint referring to the previously set **Common URL**. + - **API type**: `JSON` Only JSON API are currently supported + - **API fields**: `id` According to the GitHub API documentation, this is the name of the JSON field which uniquely identify the user. + + Once you have completed and validated the mapping's line, an information button will be available. + A click on it will show you the type of data expected from the API and, in some cases, you'll be able to configure a transformation. + For example, the `Profile.gender` field require a boolean attribute but your API may return strings like `man / woman`. + In this case, you'll be able to configure a transformation for `man` <-> `true` and `woman` <-> `false`. + + Now, you are free to map more fields, like `Profile.github` to `html_url`, or `Profile.avatar` to `avatar_url`... + +- Once you are done, your newly created authentication provider, will be marked as **Pending** in the authentication providers list. + To set it as the current active provider, you must open a terminal on the hosting server (and/or container) and run the following commands: + +```bash +# replace GitHub with the name of the provider you just created +rake fablab:switch_auth_provider[GitHub] +``` + +- As the command just prompted you, you have to re-compile the assets + - In development, `rake tmp:clear` will do the job. + - In production with Docker, `rm -rf public/assets`, followed by `docker-compose run --rm fabmanager bundle exec rake assets:precompile` +- Then restart the web-server or the container. +- Finally, to notify all existing users about the change (and send them their migration code/link), run: +```bash +rake fablab:notify_auth_changed +``` diff --git a/docker/README.md b/docker/README.md index f95543420..ad53b0587 100644 --- a/docker/README.md +++ b/docker/README.md @@ -85,13 +85,62 @@ exit mkdir -p /home/core/fabmanager/config ``` -Copy the previously customized `env` file as `/home/core/fabmanager/config/env`. +Copy the previously customized `env.example` file as `/home/core/fabmanager/config/env` ```bash mkdir -p /home/core/fabmanager/config/nginx ``` -Copy the previously customized `nginx.conf` as `/home/core/fabmanager/config/nginx/fabmanager.conf`. +Copy the previously customized `nginx_with_ssl.conf.example` as `/home/core/fabmanager/config/nginx/fabmanager.conf` +OR +Copy the previously customized `nginx.conf.example` as `/home/core/fabmanager/config/nginx/fabmanager.conf` if you do not want ssl support (not recommended !). + + +### SSL certificate with LetsEncrypt +Let's Encrypt is a new Certificate Authority that is free, automated, and open. +Let’s Encrypt certificates expire after 90 days, so automation of renewing your certificates is important. +Here is the setup for a systemd timer and service to renew the certificates and reboot the app Docker container + +```bash +mkdir -p /home/core/fabmanager/config/nginx/ssl +``` +Run `openssl dhparam -out dhparam.pem 4096` in the folder /home/core/fabmanager/config/nginx/ssl (generate dhparam.pem file) +```bash +mkdir -p /home/core/fabmanager/letsencrypt/config/ +``` +Copy the previously customized `webroot.ini.example` as `/home/core/fabmanager/letsencrypt/config/webroot.ini` +```bash +mkdir -p /home/core/fabmanager/letsencrypt/etc/webrootauth +``` + +Run `docker pull quay.io/letsencrypt/letsencrypt:latest` + +Create file (with sudo) /etc/systemd/system/letsencrypt.service with + +```bash +[Unit] +Description=letsencrypt cert update oneshot +Requires=docker.service + +[Service] +Type=oneshot +ExecStart=/usr/bin/docker run --rm --name letsencrypt -v "/home/core/fabmanager/log:/var/log/letsencrypt" -v "/home/core/fabmanager/letsencrypt/etc:/etc/letsencrypt" -v "/home/core/fabmanager/letsencrypt/config:/letsencrypt-config" quay.io/letsencrypt/letsencrypt:latest -c "/letsencrypt-config/webroot.ini" certonly +ExecStartPost=-/usr/bin/docker restart fabmanager +``` + +Create file (with sudo) /etc/systemd/system/letsencrypt.timer with +```bash +[Unit] +Description=letsencrypt oneshot timer +Requires=docker.service + +[Timer] +OnCalendar=*-*-1 06:00:00 +Persistent=true +Unit=letsencrypt.service +``` + +Then deploy your app and read the "Generate SSL certificate by Letsencrypt" section to complete the installation of the letsencrypt certificate. ### Deploy dockers containers on host @@ -131,6 +180,7 @@ docker run --rm \ --link=fabmanager-elastic:elasticsearch \ -e RAILS_ENV=production \ --env-file /home/core/fabmanager/config/env \ + -v /home/core/fabmanager/plugins:/usr/src/app/plugins \ sleede/fab-manager \ bundle exec rake db:migrate ``` @@ -144,6 +194,7 @@ docker run --rm \ --link=fabmanager-elastic:elasticsearch \ -e RAILS_ENV=production \ --env-file /home/core/fabmanager/config/env \ + -v /home/core/fabmanager/plugins:/usr/src/app/plugins \ sleede/fab-manager \ bundle exec rake db:seed ``` @@ -159,6 +210,7 @@ docker run --rm \ --link=fabmanager-elastic:elasticsearch \ -e RAILS_ENV=production \ --env-file /home/core/fabmanager/config/env \ + -v /home/core/fabmanager/plugins:/usr/src/app/plugins \ sleede/fab-manager \ bundle exec rake fablab:es_build_stats ``` @@ -174,6 +226,7 @@ docker run --rm \ -e RAILS_ENV=production \ --env-file /home/core/fabmanager/config/env \ -v /home/core/fabmanager/public/assets:/usr/src/app/public/assets \ + -v /home/core/fabmanager/plugins:/usr/src/app/plugins \ sleede/fab-manager \ bundle exec rake assets:precompile ``` @@ -183,23 +236,50 @@ docker run --rm \ ```bash docker run --restart=always -d --name=fabmanager \ - -p 80:80 \ - -p 443:443 \ --link=fabmanager-postgres:postgres \ --link=fabmanager-redis:redis \ --link=fabmanager-elastic:elasticsearch \ -e RAILS_ENV=production \ -e RACK_ENV=production \ --env-file /home/core/fabmanager/config/env \ - -v /home/core/fabmanager/config/nginx:/etc/nginx/conf.d \ -v /home/core/fabmanager/public/assets:/usr/src/app/public/assets \ -v /home/core/fabmanager/public/uploads:/usr/src/app/public/uploads \ -v /home/core/fabmanager/invoices:/usr/src/app/invoices \ + -v /home/core/fabmanager/exports:/usr/src/app/exports \ + -v /home/core/fabmanager/plugins:/usr/src/app/plugins \ -v /home/core/fabmanager/log:/var/log/supervisor \ sleede/fab-manager + +docker run --restart=always -d --name=nginx \ + -p 80:80 \ + -p 443:443 \ + --link=fabmanager:fabmanager \ + -v /home/core/fabmanager/config/nginx:/etc/nginx/conf.d \ + -v /home/core/fabmanager/letsencrypt/etc:/etc/letsencrypt \ + -v /home/core/fabmanager/log:/var/log/nginx \ + --volumes-from fabmanager:ro \ + nginx:1.9 + ``` +### Generate SSL certificate by Letsencrypt (app must be run before start letsencrypt) + +Start letsencrypt service : +```bash +sudo systemctl start letsencrypt.service +``` + +If the certificate was successfully generated then update the nginx configuration file and activate the ssl port and certificate. +Edit `/home/core/fabmanager/config/nginx/fabmanager.conf` +Remove your app and Run your app to apply changes + +Finally, if everything is ok, start letsencrypt timer to update the certificate every 1st of the month : + +```bash +sudo systemctl start letsencrypt.timer +``` + ### Dockers utils @@ -218,7 +298,7 @@ docker run --restart=always -d --name=fabmanager \ -### Docker Compose +### If you want deploy with Docker Compose #### download docker compose https://github.com/docker/compose/releases @@ -235,13 +315,17 @@ sudo chmod +x /opt/bin/docker-compose mkdir -p /home/core/fabmanager/config ``` -Copy the previously customized `env` file as `/home/core/fabmanager/config/env`. +Copy the previously customized `env` file as `/home/core/fabmanager/config/env` ```bash mkdir -p /home/core/fabmanager/config/nginx ``` -Copy the previously customized `nginx.conf` as `/home/core/fabmanager/config/nginx/fabmanager.conf`. +Copy the previously customized `nginx_with_ssl.conf.example` as `/home/core/fabmanager/config/nginx/fabmanager.conf` +Read the "SSL certificate with LetsEncrypt" section +OR +Copy the previously customized `nginx.conf.example` as `/home/core/fabmanager/config/nginx/fabmanager.conf` if you do not want ssl support (not recommended !). + #### copy docker-compose.yml to /home/core/ @@ -279,5 +363,6 @@ docker-compose pull fabmanager docker-compose stop fabmanager sudo rm -rf fabmanager/public/assets docker-compose run --rm fabmanager bundle exec rake assets:precompile -docker-compose start fabmanager +docker-compose down +docker-compose up -d ``` diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 14c86b5ed..8b5435319 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -5,37 +5,50 @@ services: environment: RAILS_ENV: production RACK_ENV: production - ports: - - "80:80" - - "443:443" env_file: - /home/core/fabmanager/config/env volumes: - - /home/core/fabmanager/config/nginx:/etc/nginx/conf.d - /home/core/fabmanager/public/assets:/usr/src/app/public/assets - /home/core/fabmanager/public/uploads:/usr/src/app/public/uploads - /home/core/fabmanager/invoices:/usr/src/app/invoices + - /home/core/fabmanager/exports:/usr/src/app/exports - /home/core/fabmanager/log:/var/log/supervisor + - /home/core/fabmanager/plugins:/usr/src/app/plugins depends_on: - - fabmanager-postgres - - fabmanager-redis - - fabmanager-elastic + - postgres + - redis + - elasticsearch restart: always - fabmanager-postgres: + postgres: image: postgres:9.4 volumes: - /home/core/fabmanager/postgresql:/var/lib/postgresql/data restart: always - fabmanager-elastic: + elasticsearch: image: elasticsearch:1.7 volumes: - /home/core/fabmanager/elasticsearch:/usr/share/elasticsearch/data restart: always - fabmanager-redis: + redis: image: redis:3.0 volumes: - /home/core/fabmanager/redis:/data restart: always + + nginx: + image: nginx:1.9 + ports: + - "80:80" + - "443:443" + volumes: + - /home/core/fabmanager/config/nginx:/etc/nginx/conf.d + - /home/core/fabmanager/letsencrypt/etc:/etc/letsencrypt + - /home/core/fabmanager/log:/var/log/nginx + volumes_from: + - fabmanager:ro + links: + - fabmanager:fabmanager + restart: always diff --git a/docker/env.example b/docker/env.example index f81ef266d..1b56819e4 100644 --- a/docker/env.example +++ b/docker/env.example @@ -16,7 +16,7 @@ DEFAULT_HOST=demo.fab-manager.com DEFAULT_PROTOCOL=http DELIVERY_METHOD=smtp -SMTP_ADDRESS=smtp.mailgun.org +SMTP_ADDRESS=smtp.sendgrid.net SMTP_PORT=587 SMTP_USER_NAME= SMTP_PASSWORD= @@ -31,6 +31,8 @@ TWITTER_CONSUMER_SECRET= TWITTER_ACCESS_TOKEN= TWITTER_ACCESS_TOKEN_SECRET= +FACEBOOK_APP_ID= + RAILS_LOCALE=fr MOMENT_LOCALE=fr SUMMERNOTE_LOCALE=fr-FR @@ -44,10 +46,17 @@ TIME_ZONE=Paris WEEK_STARTING_DAY=monday D3_DATE_FORMAT=%d/%m/%y UIB_DATE_FORMAT=dd/MM/yyyy +EXCEL_DATE_FORMAT=dd/mm/yyyy -OPENLAB_APP_SECRET=fSF9jZEWxjHyqjAzzg34jd92 -OPENLAB_APP_ID=xLn9CmryyURNNHZiDRYVRXbv - +OPENLAB_APP_SECRET= +OPENLAB_APP_ID= +OPENLAB_BASE_URI=https://openprojects.fab-manager.com NAVINUM_API_LOGIN: NAVINUM_API_PASSWORD: + +LOG_LEVEL=debug + + +ALLOWED_EXTENSIONS=pdf ai eps cad math svg stl dxf dwg obj step iges igs 3dm 3dmf doc docx png ino scad fcad skp sldprt sldasm slddrw slddrt tex latex ps +ALLOWED_MIME_TYPES=application/pdf application/postscript application/illustrator image/x-eps image/svg+xml application/sla application/dxf application/acad application/dwg application/octet-stream application/step application/iges model/iges x-world/x-3dmf application/ application/vnd.openxmlformats-officedocument.wordprocessingml.document image/png text/x-arduino text/plain application/scad application/vnd.sketchup.skp application/x-koan application/vnd-koan koan/x-skm application/vnd.koan application/x-tex application/x-latex diff --git a/docker/nginx.conf.example b/docker/nginx.conf.example index 7e0a2bafe..6cffc1e7b 100644 --- a/docker/nginx.conf.example +++ b/docker/nginx.conf.example @@ -1,5 +1,5 @@ upstream puma { - server unix:/usr/src/app/tmp/sockets/fabmanager.sock fail_timeout=0; + server fabmanager:3000; } server { diff --git a/docker/nginx_with_ssl.conf.example b/docker/nginx_with_ssl.conf.example index 105f0fcd1..4ac583b3a 100644 --- a/docker/nginx_with_ssl.conf.example +++ b/docker/nginx_with_ssl.conf.example @@ -1,5 +1,5 @@ upstream puma { - server unix:/usr/src/app/tmp/sockets/fabmanager.sock fail_timeout=0; + server fabmanager:3000; } server { @@ -7,12 +7,26 @@ server { server_name MAIN_DOMAIN; root /usr/src/app/public; ssl on; - ssl_certificate /etc/nginx/conf.d/ssl/MAIN_DOMAIN.crt; - ssl_certificate_key /etc/nginx/conf.d/ssl/MAIN_DOMAIN.deprotected.key; - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH'; + ## with your ssl certificate + #ssl_certificate /etc/nginx/conf.d/ssl/MAIN_DOMAIN.crt; + #ssl_certificate_key /etc/nginx/conf.d/ssl/MAIN_DOMAIN.deprotected.key; + ## + ## with letsencrypt certificate (free) + ssl_certificate_key /etc/letsencrypt/live/MAIN_DOMAIN/privkey.pem; + ssl_certificate /etc/letsencrypt/live/MAIN_DOMAIN/fullchain.pem; + ssl_trusted_certificate /etc/letsencrypt/live/MAIN_DOMAIN/chain.pem; + ## + ssl_protocols TLSv1.2 TLSv1.1 TLSv1; ssl_prefer_server_ciphers on; - ssl_session_cache shared:SSL:10m; + ssl_ciphers 'kEECDH+ECDSA+AES128 kEECDH+ECDSA+AES256 kEECDH+AES128 kEECDH+AES256 kEDH+AES128 kEDH+AES256 DES-CBC3-SHA +SHA !aNULL !eNULL !LOW !MD5 !EXP !DSS !PSK !SRP !kECDH !CAMELLIA !RC4 !SEED'; + ssl_session_cache shared:SSL:50m; + ssl_session_tickets off; + ssl_session_timeout 1d; + ssl_dhparam /etc/nginx/conf.d/ssl/dhparam.pem; + add_header Strict-Transport-Security max-age=15768000; + ssl_stapling on; + ssl_stapling_verify on; + location ^~ /assets/ { gzip_static on; @@ -20,6 +34,13 @@ server { add_header Cache-Control public; } + ## required by letsencrypt to generate the certificat + location /.well-known/acme-challenge { + root /etc/letsencrypt/webrootauth; + default_type "text/plain"; + } + ## + try_files $uri/index.html $uri @puma; location @puma { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/docker/supervisor.conf b/docker/supervisor.conf index 35173f0ce..fcd84d226 100644 --- a/docker/supervisor.conf +++ b/docker/supervisor.conf @@ -14,13 +14,8 @@ childlogdir=/var/log/supervisor/ ; where child log files will liv [supervisorctl] serverurl=unix:///var/run/supervisor.sock ; use a unix:// URL for a unix socket -[program:nginx] -command=nginx -g "daemon off;" -stderr_logfile=/var/log/supervisor/nginx-stderr.log -stdout_logfile=/var/log/supervisor/nginx-stdout.log - [program:app] -command=bundle exec puma -b unix:///usr/src/app/tmp/sockets/fabmanager.sock --pidfile /usr/src/app/tmp/pids/fabmanager.pid +command=bundle exec rails s puma -p 3000 -b 0.0.0.0 stderr_logfile=/var/log/supervisor/app-stderr.log stdout_logfile=/var/log/supervisor/app-stdout.log diff --git a/docker/webroot.ini.example b/docker/webroot.ini.example new file mode 100644 index 000000000..890917738 --- /dev/null +++ b/docker/webroot.ini.example @@ -0,0 +1,10 @@ +rsa-key-size = 4096 +server = https://acme-v01.api.letsencrypt.org/directory +email = REPLACE_WITH_YOUR@EMAIL.COM +text = True +agree-tos = True +agree-dev-preview = True +renew-by-default = True +authenticator = webroot +domains = MAIN_DOMAIN, ANOTHER_DOMAIN_1, ANOTHER_DOMAIN_2 +webroot-path = /etc/letsencrypt/webrootauth \ No newline at end of file diff --git a/lib/omni_auth/strategies/sso_oauth2_provider.rb b/lib/omni_auth/strategies/sso_oauth2_provider.rb index 6289da712..5c5aea1d7 100644 --- a/lib/omni_auth/strategies/sso_oauth2_provider.rb +++ b/lib/omni_auth/strategies/sso_oauth2_provider.rb @@ -54,12 +54,62 @@ module OmniAuth @parsed_info ||= Hash.new unless @parsed_info.size > 0 OmniAuth::Strategies::SsoOauth2Provider.active_provider.providable.o_auth2_mappings.each do |mapping| - @parsed_info[(mapping.local_model+'.'+mapping.local_field).to_sym] = raw_info[mapping.api_endpoint.to_sym][mapping.api_field] + + if mapping.transformation + case mapping.transformation['type'] + ## INTEGER + when 'integer' + mapping.transformation['mapping'].each do |m| + if m['from'] == raw_info[mapping.api_endpoint.to_sym][mapping.api_field] + @parsed_info[local_sym(mapping)] = m['to'] + break + end + end + # if no transformation had set any value, set the raw value + unless @parsed_info[local_sym(mapping)] + @parsed_info[local_sym(mapping)] = raw_info[mapping.api_endpoint.to_sym][mapping.api_field] + end + + ## BOOLEAN + when 'boolean' + @parsed_info[local_sym(mapping)] = !(raw_info[mapping.api_endpoint.to_sym][mapping.api_field] == mapping.transformation['false_value']) + @parsed_info[local_sym(mapping)] = (raw_info[mapping.api_endpoint.to_sym][mapping.api_field] == mapping.transformation['true_value']) + + ## DATE + when 'date' + case mapping.transformation['format'] + when 'iso8601' + @parsed_info[local_sym(mapping)] = DateTime.iso8601(raw_info[mapping.api_endpoint.to_sym][mapping.api_field]) + when 'rfc2822' + @parsed_info[local_sym(mapping)] = DateTime.rfc2822(raw_info[mapping.api_endpoint.to_sym][mapping.api_field]) + when 'rfc3339' + @parsed_info[local_sym(mapping)] = DateTime.rfc3339(raw_info[mapping.api_endpoint.to_sym][mapping.api_field]) + when 'timestamp-s' + @parsed_info[local_sym(mapping)] = DateTime.strptime(raw_info[mapping.api_endpoint.to_sym][mapping.api_field],'%s') + when 'timestamp-ms' + @parsed_info[local_sym(mapping)] = DateTime.strptime(raw_info[mapping.api_endpoint.to_sym][mapping.api_field],'%Q') + else + @parsed_info[local_sym(mapping)] = DateTime.parse(raw_info[mapping.api_endpoint.to_sym][mapping.api_field]) + end + + ## OTHER TRANSFORMATIONS (not supported) + else + @parsed_info[local_sym(mapping)] = raw_info[mapping.api_endpoint.to_sym][mapping.api_field] + end + + ## NO TRANSFORMATION + else + @parsed_info[local_sym(mapping)] = raw_info[mapping.api_endpoint.to_sym][mapping.api_field] + end end end @parsed_info end + private + def local_sym(mapping) + (mapping.local_model+'.'+mapping.local_field).to_sym + end end end end \ No newline at end of file diff --git a/lib/tasks/fablab.rake b/lib/tasks/fablab.rake index ed09f7f95..eaf947e89 100644 --- a/lib/tasks/fablab.rake +++ b/lib/tasks/fablab.rake @@ -1,42 +1,42 @@ namespace :fablab do - #desc "Get all stripe plans and create in fablab app" - #task stripe_plan: :environment do - #Stripe::Plan.all.data.each do |plan| - #unless Plan.find_by_stp_plan_id(plan.id) - #group = Group.friendly.find(plan.id.split('-').first) - #if group - #Plan.create(stp_plan_id: plan.id, name: plan.name, amount: plan.amount, interval: plan.interval, group_id: group.id, skip_create_stripe_plan: true) - #else - #puts plan.name + " n'a pas été créé. [error]" - #end - #end - #end + # desc "Get all stripe plans and create in fablab app" + # task stripe_plan: :environment do + # Stripe::Plan.all.data.each do |plan| + # unless Plan.find_by_stp_plan_id(plan.id) + # group = Group.friendly.find(plan.id.split('-').first) + # if group + # Plan.create(stp_plan_id: plan.id, name: plan.name, amount: plan.amount, interval: plan.interval, group_id: group.id, skip_create_stripe_plan: true) + # else + # puts plan.name + " n'a pas été créé. [error]" + # end + # end + # end + # + # if Plan.column_names.include? "training_credit_nb" + # Plan.all.each do |p| + # p.update_columns(training_credit_nb: (p.interval == 'month' ? 1 : 5)) + # end + # end + # end - #if Plan.column_names.include? "training_credit_nb" - #Plan.all.each do |p| - #p.update_columns(training_credit_nb: (p.interval == 'month' ? 1 : 5)) - #end - #end - #end - - desc "Regenerate the invoices" + desc 'Regenerate the invoices' task :regenerate_invoices, [:year, :month] => :environment do |task, args| year = args.year || Time.now.year month = args.month || Time.now.month start_date = Time.new(year.to_i, month.to_i, 1) end_date = start_date.next_month puts "-> Start regenerate the invoices between #{I18n.l start_date, format: :long} in #{I18n.l end_date-1.minute, format: :long}" - invoices = Invoice.only_invoice.where("created_at >= :start_date AND created_at < :end_date", {start_date: start_date, end_date: end_date}).order(created_at: :asc) + invoices = Invoice.only_invoice.where('created_at >= :start_date AND created_at < :end_date', {start_date: start_date, end_date: end_date}).order(created_at: :asc) invoices.each(&:regenerate_invoice_pdf) - puts "-> Done" + puts '-> Done' end - desc "Cancel stripe subscriptions" + desc 'Cancel stripe subscriptions' task cancel_subscriptions: :environment do - Subscription.where("expired_at >= ?", Time.now.at_beginning_of_day).each do |s| + Subscription.where('expired_at >= ?', Time.now.at_beginning_of_day).each do |s| puts "-> Start cancel subscription of #{s.user.email}" s.cancel - puts "-> Done" + puts '-> Done' end end @@ -72,19 +72,74 @@ namespace :fablab do } }';` end + es_add_event_filters end - desc "sync all/one project in elastic search index" + desc 'add event filters to statistics' + task es_add_event_filters: :environment do + es_add_event_filters + end + + def es_add_event_filters + `curl -XPUT http://#{ENV["ELASTICSEARCH_HOST"]}:9200/stats/event/_mapping -d ' + { + "properties": { + "ageRange": { + "type": "string", + "index" : "not_analyzed" + }, + "eventTheme": { + "type": "string", + "index" : "not_analyzed" + } + } + }';` + end + + desc 'sync all/one project in ElasticSearch index' task :es_build_projects_index, [:id] => :environment do |task, args| - if Project.__elasticsearch__.client.indices.exists? index: 'fablab' - Project.__elasticsearch__.client.indices.delete index: 'fablab' + client = Project.__elasticsearch__.client + # create index if not exists + unless client.indices.exists? index: Project.index_name + client.indices.create Project.index_name end - Project.__elasticsearch__.client.indices.create index: Project.index_name, body: { settings: Project.settings.to_hash, mappings: Project.mappings.to_hash } + # delete doctype if exists + if client.indices.exists_type? index: Project.index_name, type: Project.document_type + client.indices.delete_mapping index: Project.index_name, type: Project.document_type + end + # create doctype + client.indices.put_mapping index: Project.index_name, type: Project.document_type, body: Project.mappings.to_hash + + # index requested documents if args.id - IndexerWorker.perform_async(:index, id) + ProjectIndexerWorker.perform_async(:index, id) else Project.pluck(:id).each do |project_id| - IndexerWorker.perform_async(:index, project_id) + ProjectIndexerWorker.perform_async(:index, project_id) + end + end + end + + desc 'sync all/one availabilities in ElasticSearch index' + task :es_build_availabilities_index, [:id] => :environment do |task, args| + client = Availability.__elasticsearch__.client + # create index if not exists + unless client.indices.exists? index: Availability.index_name + client.indices.create Availability.index_name + end + # delete doctype if exists + if client.indices.exists_type? index: Availability.index_name, type: Availability.document_type + client.indices.delete_mapping index: Availability.index_name, type: Availability.document_type + end + # create doctype + client.indices.put_mapping index: Availability.index_name, type: Availability.document_type, body: Availability.mappings.to_hash + + # index requested documents + if args.id + AvailabilityIndexerWorker.perform_async(:index, id) + else + Availability.pluck(:id).each do |availability_id| + AvailabilityIndexerWorker.perform_async(:index, availability_id) end end end @@ -152,14 +207,17 @@ namespace :fablab do puts "\n/!\\ WARNING: Please consider the following, otherwise the authentication will be bogus:" puts "\t1) CLEAN the cache with `rake tmp:clear`" - puts "\t2) RESTART the application" - puts "\t3) NOTIFY the current users with `rake fablab:notify_auth_changed`\n\n" + puts "\t2) REBUILD the assets with `rake assets:precompile`" + puts "\t3) RESTART the application" + puts "\t4) NOTIFY the current users with `rake fablab:notify_auth_changed`\n\n" end desc 'notify users that the auth provider has changed' task notify_auth_changed: :environment do + I18n.locale = I18n.default_locale + # notify every users if the provider is not local database provider if AuthProvider.active.providable_type != DatabaseProvider.name User.all.each do |user| @@ -172,7 +230,7 @@ namespace :fablab do puts "\nUsers successfully notified\n\n" end - desc "generate fixtures from db" + desc 'generate fixtures from db' task generate_fixtures: :environment do Rails.application.eager_load! ActiveRecord::Base.descendants.reject { |c| c == ActiveRecord::SchemaMigration or c == PartnerPlan }.each do |ar_base| @@ -191,4 +249,16 @@ namespace :fablab do File.write(cassette_file, cassette) end end + + desc '(re)generate statistics in elasticsearch for the past period' + task :generate_stats, [:period] => :environment do |task, args| + unless args.period + fail 'FATAL ERROR: You must pass a number of days (=> past period) to generate statistics on' + end + + days = args.period.to_i + days.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 + end end diff --git a/lib/tasks/fablab/fix.rake b/lib/tasks/fablab/fix.rake index 59e3d95cd..f230f2d47 100644 --- a/lib/tasks/fablab/fix.rake +++ b/lib/tasks/fablab/fix.rake @@ -8,5 +8,13 @@ namespace :fablab do ' AND reservations.reservable_type = \'Event\'' ) end + + task assign_category_to_uncategorized_events: :environment do + c = Category.find_or_create_by!({name: 'No category'}) + Event.where(category: nil).each do |e| + e.category = c + e.save! + end + end end end diff --git a/lib/tasks/fablab/openlab.rake b/lib/tasks/fablab/openlab.rake index 9e87ae109..a1e78bf79 100644 --- a/lib/tasks/fablab/openlab.rake +++ b/lib/tasks/fablab/openlab.rake @@ -1,9 +1,11 @@ namespace :fablab do namespace :openlab do + desc 'bulk and export projects to openlab' task bulk_export: :environment do if Rails.application.secrets.openlab_app_secret.present? Project.find_each do |project| project.openlab_create + puts '-> Done' end else warn "Rails.application.secrets.openlab_app_secret not present. Export can't be done." diff --git a/test/fixtures/addresses.yml b/test/fixtures/addresses.yml index f1a7d1d92..54549dd21 100644 --- a/test/fixtures/addresses.yml +++ b/test/fixtures/addresses.yml @@ -2,11 +2,11 @@ address_1: id: 1 address: 14 rue Brecourt, Annecy - street_number: - route: - locality: - country: - postal_code: + street_number: + route: + locality: + country: + postal_code: placeable_id: 2 placeable_type: Profile created_at: 2016-04-04 15:06:22.166469000 Z @@ -15,11 +15,11 @@ address_1: address_2: id: 2 address: 23 cours Berriat, Grenoble - street_number: - route: - locality: - country: - postal_code: + street_number: + route: + locality: + country: + postal_code: placeable_id: 4 placeable_type: Profile created_at: 2016-04-04 15:10:42.353039000 Z @@ -28,11 +28,11 @@ address_2: address_3: id: 3 address: 36 chemin des puits, Mousson-sur-Luire - street_number: - route: - locality: - country: - postal_code: + street_number: + route: + locality: + country: + postal_code: placeable_id: 5 placeable_type: Profile created_at: 2016-04-04 15:14:08.579603000 Z @@ -41,12 +41,24 @@ address_3: address_4: id: 4 address: '' - street_number: - route: - locality: - country: - postal_code: + street_number: + route: + locality: + country: + postal_code: placeable_id: 3 placeable_type: Profile created_at: 2016-04-05 08:35:18.597812000 Z updated_at: 2016-04-05 08:35:18.597812000 Z + +address_5: + id: 5 + address: 2 Place St Laurent 38000 GRENOBLE + street_number: + route: + locality: + postal_code: + placeable_id: 1 + placeable_type: Organization + created_at: 2016-08-02 11:16:24.412236000 Z + updated_at: 2016-08-02 11:16:24.412236000 Z diff --git a/test/fixtures/age_ranges.yml b/test/fixtures/age_ranges.yml new file mode 100644 index 000000000..9c9144d3a --- /dev/null +++ b/test/fixtures/age_ranges.yml @@ -0,0 +1,13 @@ +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +youngs: + id: 1 + name: De 10 à 21 ans + +adults: + id: 2 + name: A partir de 18 ans + +seniors: + id: 3 + name: A partir de 65 ans diff --git a/test/fixtures/coupons.yml b/test/fixtures/coupons.yml new file mode 100644 index 000000000..f5eb6d362 --- /dev/null +++ b/test/fixtures/coupons.yml @@ -0,0 +1,19 @@ +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + name: Christmas deals + code: XMAS10 + percent_off: 10 + valid_until: 2015-12-31 23:59:59 + max_usages: nil + active: true + validity_per_user: once + +two: + name: Summer discounts + code: SUNNYFABLAB + percent_off: 15 + valid_until: <%= 1.month.from_now.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + max_usages: 10 + active: true + validity_per_user: always diff --git a/test/fixtures/event_price_categories.yml b/test/fixtures/event_price_categories.yml new file mode 100644 index 000000000..e99967bbe --- /dev/null +++ b/test/fixtures/event_price_categories.yml @@ -0,0 +1,13 @@ +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + id: 1 + event_id: 1 + price_category_id: 1 + amount: 1500 + +two: + id: 2 + event_id: 2 + price_category_id: 1 + amount: 1700 diff --git a/test/fixtures/event_themes.yml b/test/fixtures/event_themes.yml new file mode 100644 index 000000000..4e972ea89 --- /dev/null +++ b/test/fixtures/event_themes.yml @@ -0,0 +1,7 @@ +diy: + id: 1 + name: Do it yourself + +robot: + id: 2 + name: Robot diff --git a/test/fixtures/events.yml b/test/fixtures/events.yml index 624603910..66268f37e 100644 --- a/test/fixtures/events.yml +++ b/test/fixtures/events.yml @@ -8,10 +8,11 @@ event_2: updated_at: 2016-04-04 15:44:03.161942000 Z availability_id: 10 amount: 2000 - reduced_amount: 1500 nb_total_places: 10 nb_free_places: 10 recurrence_id: 1 + age_range_id: 2 + category_id: 2 event_3: id: 3 @@ -22,10 +23,10 @@ event_3: updated_at: 2016-04-04 15:44:04.028290000 Z availability_id: 11 amount: 2000 - reduced_amount: 1500 nb_total_places: 10 nb_free_places: 10 recurrence_id: 1 + category_id: 2 event_1: id: 1 @@ -36,7 +37,7 @@ event_1: updated_at: 2016-04-04 15:44:02.236165000 Z availability_id: 9 amount: 2000 - reduced_amount: 1500 nb_total_places: 10 nb_free_places: 10 recurrence_id: 1 + category_id: 2 diff --git a/test/fixtures/events_categories.yml b/test/fixtures/events_categories.yml deleted file mode 100644 index 3d0913d07..000000000 --- a/test/fixtures/events_categories.yml +++ /dev/null @@ -1,84 +0,0 @@ - -events_category_1: - id: 1 - event_id: 1 - category_id: 2 - created_at: 2016-04-04 15:44:02.258622000 Z - updated_at: 2016-04-04 15:44:02.258622000 Z - -events_category_2: - id: 2 - event_id: 2 - category_id: 2 - created_at: 2016-04-04 15:44:03.173253000 Z - updated_at: 2016-04-04 15:44:03.173253000 Z - -events_category_3: - id: 3 - event_id: 2 - category_id: 2 - created_at: 2016-04-04 15:44:03.177952000 Z - updated_at: 2016-04-04 15:44:03.177952000 Z - -events_category_4: - id: 4 - event_id: 3 - category_id: 2 - created_at: 2016-04-04 15:44:04.041364000 Z - updated_at: 2016-04-04 15:44:04.041364000 Z - -events_category_5: - id: 5 - event_id: 3 - category_id: 2 - created_at: 2016-04-04 15:44:04.044667000 Z - updated_at: 2016-04-04 15:44:04.044667000 Z - -events_category_6: - id: 6 - event_id: 1 - category_id: 2 - created_at: 2016-04-04 15:44:04.049543000 Z - updated_at: 2016-04-04 15:44:04.049543000 Z - -events_category_1: - id: 1 - event_id: 1 - category_id: 2 - created_at: 2016-04-04 15:44:02.258622000 Z - updated_at: 2016-04-04 15:44:02.258622000 Z - -events_category_2: - id: 2 - event_id: 2 - category_id: 2 - created_at: 2016-04-04 15:44:03.173253000 Z - updated_at: 2016-04-04 15:44:03.173253000 Z - -events_category_3: - id: 3 - event_id: 2 - category_id: 2 - created_at: 2016-04-04 15:44:03.177952000 Z - updated_at: 2016-04-04 15:44:03.177952000 Z - -events_category_4: - id: 4 - event_id: 3 - category_id: 2 - created_at: 2016-04-04 15:44:04.041364000 Z - updated_at: 2016-04-04 15:44:04.041364000 Z - -events_category_5: - id: 5 - event_id: 3 - category_id: 2 - created_at: 2016-04-04 15:44:04.044667000 Z - updated_at: 2016-04-04 15:44:04.044667000 Z - -events_category_6: - id: 6 - event_id: 1 - category_id: 2 - created_at: 2016-04-04 15:44:04.049543000 Z - updated_at: 2016-04-04 15:44:04.049543000 Z diff --git a/test/fixtures/events_event_themes.yml b/test/fixtures/events_event_themes.yml new file mode 100644 index 000000000..e0504cef6 --- /dev/null +++ b/test/fixtures/events_event_themes.yml @@ -0,0 +1,5 @@ + +events_event_themes_1: + id: 1 + event_id: 1 + event_theme_id: 1 \ No newline at end of file diff --git a/test/fixtures/exports.yml b/test/fixtures/exports.yml new file mode 100644 index 000000000..33f7bac19 --- /dev/null +++ b/test/fixtures/exports.yml @@ -0,0 +1,21 @@ +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + id: 1 + category: statistics + export_type: global + query: '{"query":{"bool":{"must":[{"range":{"date":{"gte":"2016-06-25T02:00:00+02:00","lte":"2016-07-25T23:59:59+02:00"}}}]}}}' + created_at: 2016-07-26 13:06:28.096552 + updated_at: 2016-07-26 13:06:28.096552 + user_id: 1 + key: + +two: + id: 2 + category: statistics + export_type: subscription + query: '{"query":{"bool":{"must":[{"term":{"type":"2592000"}},{"range":{"date":{"gte":"2016-06-25T02:00:00+02:00","lte":"2016-07-25T23:59:59+02:00"}}}]}},"sort":[{"date":{"order":"desc"}}],"aggs":{"total_ca":{"sum":{"field":"ca"}},"average_age":{"avg":{"field":"age"}},"total_stat":{"sum":{"field":"stat"}}}}' + created_at: 2016-07-26 14:59:02.624663 + updated_at: 2016-07-26 14:59:02.624663 + user_id: 1 + key: '2592000' diff --git a/test/fixtures/organizations.yml b/test/fixtures/organizations.yml new file mode 100644 index 000000000..beca74d5b --- /dev/null +++ b/test/fixtures/organizations.yml @@ -0,0 +1,4 @@ +casemate: + id: 1 + name: La Casemate + profile_id: 7 diff --git a/test/fixtures/price_categories.yml b/test/fixtures/price_categories.yml new file mode 100644 index 000000000..f6380d579 --- /dev/null +++ b/test/fixtures/price_categories.yml @@ -0,0 +1,11 @@ +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +youngs: + id: 1 + name: "- de 25 ans" + conditions: "Tarif réservé aux jeunes de moins de 25 ans" + +unemployed: + id: 2 + name: "chômeurs" + conditions: "Tarif préférentiel pour les demandeurs d'emploi pouvant justifier de leur situation." diff --git a/test/fixtures/reservations.yml b/test/fixtures/reservations.yml index 45c898f07..6b430e38d 100644 --- a/test/fixtures/reservations.yml +++ b/test/fixtures/reservations.yml @@ -8,5 +8,4 @@ reservation_1: reservable_id: 2 reservable_type: Training stp_invoice_id: - nb_reserve_places: - nb_reserve_reduced_places: \ No newline at end of file + nb_reserve_places: \ No newline at end of file diff --git a/test/fixtures/settings.yml b/test/fixtures/settings.yml index 856dc615f..08fdf8b07 100644 --- a/test/fixtures/settings.yml +++ b/test/fixtures/settings.yml @@ -75,14 +75,6 @@ setting_7: created_at: 2016-04-04 14:11:34.588478000 Z updated_at: 2016-04-04 14:11:34.588478000 Z -setting_8: - id: 8 - name: event_reduced_amount_alert - value: "* Tarif réduit si vous avez moins de 25 ans, que vous êtes étudiant ou demandeur - d'emploi." - created_at: 2016-04-04 14:11:34.592250000 Z - updated_at: 2016-04-04 14:11:34.592250000 Z - setting_9: id: 9 name: invoice_logo diff --git a/test/fixtures/statistic_custom_aggregations.yml b/test/fixtures/statistic_custom_aggregations.yml new file mode 100644 index 000000000..35c1e14fd --- /dev/null +++ b/test/fixtures/statistic_custom_aggregations.yml @@ -0,0 +1,8 @@ +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + query: '{"size":0, "aggregations":{"%{aggs_name}":{"sum":{"field":"hours_duration"}}}, "query":{"bool":{"must":[{"range":{"start_at":{"gte":"%{start_date}", "lte":"%{end_date}"}}}, {"match":{"available_type":"machines"}}]}}}' + statistic_type_id: 2 + field: "available_hours" + es_index: "fablab" + es_type: "availabilities" diff --git a/test/fixtures/statistic_fields.yml b/test/fixtures/statistic_fields.yml index 644d66006..76ac90c1a 100644 --- a/test/fixtures/statistic_fields.yml +++ b/test/fixtures/statistic_fields.yml @@ -88,3 +88,23 @@ statistic_field_10: created_at: 2016-04-04 14:11:33.423307000 Z updated_at: 2016-04-04 14:11:33.423307000 Z data_type: text + +statistic_field_11: + id: 11 + statistic_index_id: 4 + key: ageRange + label: Tranche d'âge + created_at: 2016-06-30 12:10:49.812226000 Z + updated_at: 2016-06-30 12:10:49.812226000 Z + data_type: text + + +statistic_field_12: + id: 12 + statistic_index_id: 4 + key: eventTheme + label: Thématique + created_at: 2016-06-30 12:10:49.814331000 Z + updated_at: 2016-06-30 12:10:49.814331000 Z + data_type: text + diff --git a/test/fixtures/tickets.yml b/test/fixtures/tickets.yml new file mode 100644 index 000000000..6fac8be98 --- /dev/null +++ b/test/fixtures/tickets.yml @@ -0,0 +1 @@ +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html diff --git a/test/fixtures/wallet_transactions.yml b/test/fixtures/wallet_transactions.yml new file mode 100644 index 000000000..012604359 --- /dev/null +++ b/test/fixtures/wallet_transactions.yml @@ -0,0 +1,7 @@ +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +transaction1: + user_id: 5 + wallet: wallet_5 + transaction_type: credit + amount: 1000 diff --git a/test/fixtures/wallets.yml b/test/fixtures/wallets.yml new file mode 100644 index 000000000..696573c94 --- /dev/null +++ b/test/fixtures/wallets.yml @@ -0,0 +1,27 @@ +wallet_2: + user_id: 2 + amount: 0 + +wallet_4: + user_id: 4 + amount: 0 + +wallet_6: + user_id: 6 + amount: 0 + +wallet_5: + user_id: 5 + amount: 1000 + +wallet_3: + user_id: 3 + amount: 0 + +wallet_1: + user_id: 1 + amount: 0 + +wallet_7: + user_id: 7 + amount: 0 diff --git a/test/integration/events_test.rb b/test/integration/events_test.rb index ca821ad45..f15c69f8d 100644 --- a/test/integration/events_test.rb +++ b/test/integration/events_test.rb @@ -17,7 +17,7 @@ class EventsTest < ActionDispatch::IntegrationTest start_time: 1.week.from_now.utc.change({hour: 16}), end_date: 1.week.from_now.utc, end_time: 1.week.from_now.utc.change({hour: 20}), - category_ids: [Category.first.id], + category_id: Category.first.id, amount: 0 } }.to_json, @@ -45,7 +45,7 @@ class EventsTest < ActionDispatch::IntegrationTest start_time: 1.week.from_now.utc.change({hour: 16}), end_date: 1.week.from_now.utc, end_time: 1.week.from_now.utc.change({hour: 20}), - category_ids: [Category.first.id], + category_id: Category.first.id, amount: 0, nb_total_places: 10 } @@ -68,8 +68,7 @@ class EventsTest < ActionDispatch::IntegrationTest reservable_id: e.id, reservable_type: 'Event', nb_reserve_places: 2, - nb_reserve_reduced_places: 0, - slot_attributes: [ + slots_attributes: [ { start_at: e.availability.start_at, end_at: e.availability.end_at, @@ -99,7 +98,7 @@ class EventsTest < ActionDispatch::IntegrationTest start_time: 1.week.from_now.utc.change({hour: 16}), end_date: 1.week.from_now.utc, end_time: 1.week.from_now.utc.change({hour: 20}), - category_ids: [Category.first.id], + category_id: Category.first.id, amount: 0, nb_total_places: 20 } @@ -114,4 +113,92 @@ class EventsTest < ActionDispatch::IntegrationTest assert_equal 20, e.nb_total_places, 'Total number of places was not updated' assert_equal 18, e.nb_free_places, 'Number of free places was not updated' end -end \ No newline at end of file + + test 'create event with custom price and reserve it with success' do + + price_category = PriceCategory.first + + # First, we create a new event + post '/api/events', + { + event: { + title: 'Electronics initiation', + description: 'A workshop about electronics and the abilities to create robots.', + start_date: 1.week.from_now.utc + 2.days, + start_time: 1.week.from_now.utc.change({hour: 18}) + 2.days, + end_date: 1.week.from_now.utc + 2.days, + end_time: 1.week.from_now.utc.change({hour: 22}) + 2.days, + category_id: Category.last.id, + amount: 20, + nb_total_places: 10, + event_price_categories_attributes: [ + { + price_category_id: price_category.id.to_s, + amount: 16.to_s + } + ] + } + }.to_json, + default_headers + + # Check response format & status + assert_equal 201, response.status, response.body + assert_equal Mime::JSON, response.content_type + + # Check the event was created correctly + event = json_response(response.body) + e = Event.where(id: event[:id]).first + assert_not_nil e, 'Event was not created in database' + + # Check the places numbers were set successfully + e = Event.where(id: event[:id]).first + assert_equal 10, e.nb_total_places, 'Total number of places was not updated' + assert_equal 10, e.nb_free_places, 'Number of free places was not updated' + + # Now, let's make a reservation on this event + post '/api/reservations', + { + reservation: { + user_id: User.find_by_username('lseguin').id, + reservable_id: e.id, + reservable_type: 'Event', + nb_reserve_places: 4, + slots_attributes: [ + { + start_at: e.availability.start_at, + end_at: e.availability.end_at, + availability_id: e.availability.id, + offered: false + } + ], + tickets_attributes: [ + { + event_price_category_id: e.event_price_categories.first.id, + booked: 4 + } + ] + } + }.to_json, + default_headers + + # Check response format & status + assert_equal 201, response.status, response.body + assert_equal Mime::JSON, response.content_type + + # Check the reservation match the required event + reservation = json_response(response.body) + r = Reservation.find(reservation[:id]) + + assert_equal e.id, r.reservable_id + assert_equal 'Event', r.reservable_type + + # Check the remaining places were updated successfully + e = Event.where(id: event[:id]).first + assert_equal 2, e.nb_free_places, 'Number of free places was not updated' + + # Check the resulting invoice generation and it has right price + assert_invoice_pdf r.invoice + assert_equal (4 * 20) + (4 * 16), r.invoice.total / 100.0 + + end +end diff --git a/test/integration/reservations/create_as_admin_test.rb b/test/integration/reservations/create_as_admin_test.rb index 4b569735b..f745433e2 100644 --- a/test/integration/reservations/create_as_admin_test.rb +++ b/test/integration/reservations/create_as_admin_test.rb @@ -220,5 +220,174 @@ module Reservations # notification assert_not_empty Notification.where(attached_object: reservation) end + + test "user without subscription reserves a machine and pay by wallet with success" do + @vlonchamp = User.find_by(username: 'vlonchamp') + machine = Machine.find(6) + availability = machine.availabilities.first + + reservations_count = Reservation.count + invoice_count = Invoice.count + invoice_items_count = InvoiceItem.count + users_credit_count = UsersCredit.count + + post reservations_path, { reservation: { + user_id: @vlonchamp.id, + reservable_id: machine.id, + reservable_type: machine.class.name, + slots_attributes: [ + { start_at: availability.start_at.to_s(:iso8601), + end_at: (availability.start_at + 1.hour).to_s(:iso8601), + availability_id: availability.id + } + ] + }}.to_json, default_headers + + # general assertions + assert_equal 201, response.status + assert_equal reservations_count + 1, Reservation.count + assert_equal invoice_count + 1, Invoice.count + assert_equal invoice_items_count + 1, InvoiceItem.count + assert_equal users_credit_count, UsersCredit.count + + # reservation assertions + reservation = Reservation.last + + assert reservation.invoice + assert reservation.stp_invoice_id.blank? + assert_equal 1, reservation.invoice.invoice_items.count + + # invoice assertions + invoice = reservation.invoice + + assert invoice.stp_invoice_id.blank? + refute invoice.total.blank? + + # invoice_items assertions + invoice_item = InvoiceItem.last + + refute invoice_item.stp_invoice_item_id + assert_equal invoice_item.amount, machine.prices.find_by(group_id: @vlonchamp.group_id, plan_id: nil).amount + + # invoice assertions + invoice = Invoice.find_by(invoiced: reservation) + assert_invoice_pdf invoice + + # notification + assert_not_empty Notification.where(attached_object: reservation) + + # wallet + assert_equal @vlonchamp.wallet.amount, 0 + assert_equal @vlonchamp.wallet.wallet_transactions.count, 2 + transaction = @vlonchamp.wallet.wallet_transactions.last + assert_equal transaction.transaction_type, 'debit' + assert_equal transaction.amount, 10 + assert_equal transaction.amount, invoice.wallet_amount / 100.0 + assert_equal transaction.id, invoice.wallet_transaction_id + end + + test "user reserves a machine and plan pay by wallet with success" do + @vlonchamp = User.find_by(username: 'vlonchamp') + machine = Machine.find(6) + availability = machine.availabilities.first + plan = Plan.find_by(group_id: @vlonchamp.group.id, type: 'Plan', base_name: 'Mensuel tarif réduit') + + reservations_count = Reservation.count + invoice_count = Invoice.count + invoice_items_count = InvoiceItem.count + users_credit_count = UsersCredit.count + wallet_transactions_count = WalletTransaction.count + + post reservations_path, { reservation: { + user_id: @vlonchamp.id, + reservable_id: machine.id, + reservable_type: machine.class.name, + plan_id: plan.id, + slots_attributes: [ + { start_at: availability.start_at.to_s(:iso8601), + end_at: (availability.start_at + 1.hour).to_s(:iso8601), + availability_id: availability.id + } + ] + }}.to_json, default_headers + + # general assertions + assert_equal 201, response.status + assert_equal reservations_count + 1, Reservation.count + assert_equal invoice_count + 1, Invoice.count + assert_equal invoice_items_count + 2, InvoiceItem.count + assert_equal users_credit_count + 1, UsersCredit.count + assert_equal wallet_transactions_count + 1, WalletTransaction.count + + # reservation assertions + reservation = Reservation.last + + assert reservation.invoice + assert reservation.stp_invoice_id.blank? + assert_equal 2, reservation.invoice.invoice_items.count + + # invoice assertions + invoice = reservation.invoice + + assert invoice.stp_invoice_id.blank? + refute invoice.total.blank? + assert_equal invoice.total, 2000 + + # invoice assertions + invoice = Invoice.find_by(invoiced: reservation) + assert_invoice_pdf invoice + + # notification + assert_not_empty Notification.where(attached_object: reservation) + + # wallet + assert_equal @vlonchamp.wallet.amount, 0 + assert_equal @vlonchamp.wallet.wallet_transactions.count, 2 + transaction = @vlonchamp.wallet.wallet_transactions.last + assert_equal transaction.transaction_type, 'debit' + assert_equal transaction.amount, 10 + assert_equal transaction.amount, invoice.wallet_amount / 100.0 + assert_equal transaction.id, invoice.wallet_transaction_id + end + + test "user without subscription and with invoicing disabled reserves a machine and pay wallet with success" do + @vlonchamp = User.find_by(username: 'vlonchamp') + @vlonchamp.update!(invoicing_disabled: true) + machine = Machine.find(6) + availability = machine.availabilities.first + + reservations_count = Reservation.count + invoice_count = Invoice.count + invoice_items_count = InvoiceItem.count + users_credit_count = UsersCredit.count + + post reservations_path, { reservation: { + user_id: @vlonchamp.id, + reservable_id: machine.id, + reservable_type: machine.class.name, + slots_attributes: [ + { start_at: availability.start_at.to_s(:iso8601), + end_at: (availability.start_at + 1.hour).to_s(:iso8601), + availability_id: availability.id + } + ] + }}.to_json, default_headers + + # general assertions + assert_equal 201, response.status + assert_equal reservations_count + 1, Reservation.count + assert_equal invoice_count, Invoice.count + assert_equal invoice_items_count, InvoiceItem.count + assert_equal users_credit_count, UsersCredit.count + + # reservation assertions + reservation = Reservation.last + + refute reservation.invoice + assert reservation.stp_invoice_id.blank? + + # notification + assert_not_empty Notification.where(attached_object: reservation) + end end end diff --git a/test/integration/reservations/create_test.rb b/test/integration/reservations/create_test.rb index 296162816..f3eb2a43e 100644 --- a/test/integration/reservations/create_test.rb +++ b/test/integration/reservations/create_test.rb @@ -5,7 +5,7 @@ module Reservations @user_with_subscription = User.with_role(:member).with_subscription.second end - test "user without subscription reserves a machine with success" do + test 'user without subscription reserves a machine with success' do login_as(@user_without_subscription, scope: :user) machine = Machine.find(6) @@ -15,8 +15,9 @@ module Reservations invoice_count = Invoice.count invoice_items_count = InvoiceItem.count users_credit_count = UsersCredit.count + subscriptions_count = Subscription.count - VCR.use_cassette("reservations_create_for_machine_without_subscription_success") do + VCR.use_cassette('reservations_create_for_machine_without_subscription_success') do post reservations_path, { reservation: { user_id: @user_without_subscription.id, reservable_id: machine.id, @@ -37,6 +38,7 @@ module Reservations assert_equal invoice_count + 1, Invoice.count assert_equal invoice_items_count + 1, InvoiceItem.count assert_equal users_credit_count, UsersCredit.count + assert_equal subscriptions_count, Subscription.count # reservation assertions reservation = Reservation.last @@ -65,7 +67,7 @@ module Reservations assert_not_empty Notification.where(attached_object: reservation) end - test "user without subscription reserves a machine with error" do + test 'user without subscription reserves a machine with error' do login_as(@user_without_subscription, scope: :user) machine = Machine.find(6) @@ -76,7 +78,7 @@ module Reservations invoice_items_count = InvoiceItem.count notifications_count = Notification.count - VCR.use_cassette("reservations_create_for_machine_without_subscription_error") do + VCR.use_cassette('reservations_create_for_machine_without_subscription_error') do post reservations_path, { reservation: { user_id: @user_without_subscription.id, reservable_id: machine.id, @@ -99,7 +101,7 @@ module Reservations assert_equal notifications_count, Notification.count end - test "user without subscription reserves a training with success" do + test 'user without subscription reserves a training with success' do login_as(@user_without_subscription, scope: :user) training = Training.first @@ -109,7 +111,7 @@ module Reservations invoice_count = Invoice.count invoice_items_count = InvoiceItem.count - VCR.use_cassette("reservations_create_for_training_without_subscription_success") do + VCR.use_cassette('reservations_create_for_training_without_subscription_success') do post reservations_path, { reservation: { user_id: @user_without_subscription.id, reservable_id: training.id, @@ -157,7 +159,7 @@ module Reservations assert_not_empty Notification.where(attached_object: reservation) end - test "user with subscription reserves a machine with success" do + test 'user with subscription reserves a machine with success' do login_as(@user_with_subscription, scope: :user) plan = @user_with_subscription.subscribed_plan @@ -169,7 +171,7 @@ module Reservations invoice_items_count = InvoiceItem.count users_credit_count = UsersCredit.count - VCR.use_cassette("reservations_create_for_machine_with_subscription_success") do + VCR.use_cassette('reservations_create_for_machine_with_subscription_success') do post reservations_path, { reservation: { user_id: @user_with_subscription.id, reservable_id: machine.id, @@ -230,7 +232,7 @@ module Reservations assert_not_empty Notification.where(attached_object: reservation) end - test "user with subscription reserves the FIRST training with success" do + test 'user with subscription reserves the FIRST training with success' do login_as(@user_with_subscription, scope: :user) plan = @user_with_subscription.subscribed_plan plan.update!(is_rolling: true) @@ -242,7 +244,7 @@ module Reservations invoice_count = Invoice.count invoice_items_count = InvoiceItem.count - VCR.use_cassette("reservations_create_for_training_with_subscription_success") do + VCR.use_cassette('reservations_create_for_training_with_subscription_success') do post reservations_path, { reservation: { user_id: @user_with_subscription.id, reservable_id: training.id, @@ -292,5 +294,265 @@ module Reservations # check that user subscription were extended assert_equal reservation.slots.first.start_at + plan.duration, @user_with_subscription.subscription.expired_at end + + test 'user reserves a machine and pay by wallet with success' do + @vlonchamp = User.find_by(username: 'vlonchamp') + login_as(@vlonchamp, scope: :user) + + machine = Machine.find(6) + availability = machine.availabilities.first + + reservations_count = Reservation.count + invoice_count = Invoice.count + invoice_items_count = InvoiceItem.count + users_credit_count = UsersCredit.count + wallet_transactions_count = WalletTransaction.count + + VCR.use_cassette('reservations_create_for_machine_and_pay_wallet_success') do + post reservations_path, { reservation: { + user_id: @vlonchamp.id, + reservable_id: machine.id, + reservable_type: machine.class.name, + card_token: stripe_card_token, + slots_attributes: [ + { start_at: availability.start_at.to_s(:iso8601), + end_at: (availability.start_at + 1.hour).to_s(:iso8601), + availability_id: availability.id + } + ] + }}.to_json, default_headers + end + + # general assertions + assert_equal 201, response.status + assert_equal reservations_count + 1, Reservation.count + assert_equal invoice_count + 1, Invoice.count + assert_equal invoice_items_count + 1, InvoiceItem.count + assert_equal users_credit_count, UsersCredit.count + assert_equal wallet_transactions_count + 1, WalletTransaction.count + + # reservation assertions + reservation = Reservation.last + + assert reservation.invoice + refute reservation.stp_invoice_id.blank? + assert_equal 1, reservation.invoice.invoice_items.count + + # invoice assertions + invoice = reservation.invoice + + refute invoice.stp_invoice_id.blank? + refute invoice.total.blank? + + # invoice_items assertions + invoice_item = InvoiceItem.last + + assert invoice_item.stp_invoice_item_id + assert_equal invoice_item.amount, machine.prices.find_by(group_id: @vlonchamp.group_id, plan_id: nil).amount + + # invoice assertions + invoice = Invoice.find_by(invoiced: reservation) + assert_invoice_pdf invoice + + # notification + assert_not_empty Notification.where(attached_object: reservation) + + # wallet + assert_equal @vlonchamp.wallet.amount, 0 + assert_equal @vlonchamp.wallet.wallet_transactions.count, 2 + transaction = @vlonchamp.wallet.wallet_transactions.last + assert_equal transaction.transaction_type, 'debit' + assert_equal transaction.amount, 10 + assert_equal transaction.amount, invoice.wallet_amount / 100.0 + end + + test 'user reserves a training and plan by wallet with success' do + @vlonchamp = User.find_by(username: 'vlonchamp') + login_as(@vlonchamp, scope: :user) + + training = Training.first + availability = training.availabilities.first + plan = Plan.find_by(group_id: @vlonchamp.group.id, type: 'Plan', base_name: 'Mensuel tarif réduit') + + reservations_count = Reservation.count + invoice_count = Invoice.count + invoice_items_count = InvoiceItem.count + wallet_transactions_count = WalletTransaction.count + + VCR.use_cassette('reservations_create_for_training_and_plan_by_pay_wallet_success') do + post reservations_path, { reservation: { + user_id: @user_without_subscription.id, + reservable_id: training.id, + reservable_type: training.class.name, + card_token: stripe_card_token, + plan_id: plan.id, + slots_attributes: [ + { + start_at: availability.start_at.to_s(:iso8601), + end_at: availability.end_at.to_s(:iso8601), + availability_id: availability.id + } + ] + }}.to_json, default_headers + end + + # general assertions + assert_equal 201, response.status + assert_equal reservations_count + 1, Reservation.count + assert_equal invoice_count + 1, Invoice.count + assert_equal invoice_items_count + 2, InvoiceItem.count + assert_equal wallet_transactions_count + 1, WalletTransaction.count + + # reservation assertions + reservation = Reservation.last + + assert reservation.invoice + refute reservation.stp_invoice_id.blank? + assert_equal 2, reservation.invoice.invoice_items.count + + # invoice assertions + invoice = reservation.invoice + + refute invoice.stp_invoice_id.blank? + refute invoice.total.blank? + assert_equal invoice.total, 2000 + + # invoice assertions + invoice = Invoice.find_by(invoiced: reservation) + assert_invoice_pdf invoice + + # notification + assert_not_empty Notification.where(attached_object: reservation) + + # wallet + assert_equal @vlonchamp.wallet.amount, 0 + assert_equal @vlonchamp.wallet.wallet_transactions.count, 2 + transaction = @vlonchamp.wallet.wallet_transactions.last + assert_equal transaction.transaction_type, 'debit' + assert_equal transaction.amount, 10 + assert_equal transaction.amount, invoice.wallet_amount / 100.0 + end + + test 'user reserves a machine and a subscription using a coupon with success' do + login_as(@user_without_subscription, scope: :user) + + machine = Machine.find(6) + plan = Plan.where(group_id: @user_without_subscription.group_id).first + availability = machine.availabilities.first + + reservations_count = Reservation.count + invoice_count = Invoice.count + invoice_items_count = InvoiceItem.count + subscriptions_count = Subscription.count + users_credit_count = UsersCredit.count + + VCR.use_cassette('reservations_machine_and_plan_using_coupon_success') do + post reservations_path, { + reservation: { + user_id: @user_without_subscription.id, + reservable_id: machine.id, + reservable_type: machine.class.name, + card_token: stripe_card_token, + slots_attributes: [ + { start_at: availability.start_at.to_s(:iso8601), + end_at: (availability.start_at + 1.hour).to_s(:iso8601), + availability_id: availability.id + } + ], + plan_id: plan.id + }, + coupon_code: 'SUNNYFABLAB' + }.to_json, default_headers + end + + # general assertions + assert_equal 201, response.status + assert_equal reservations_count + 1, Reservation.count + assert_equal invoice_count + 1, Invoice.count + assert_equal invoice_items_count + 2, InvoiceItem.count + assert_equal users_credit_count, UsersCredit.count + assert_equal subscriptions_count + 1, Subscription.count + + # reservation assertions + reservation = Reservation.last + + assert reservation.invoice + refute reservation.stp_invoice_id.blank? + assert_equal 2, reservation.invoice.invoice_items.count + + # invoice assertions + invoice = reservation.invoice + + refute invoice.stp_invoice_id.blank? + refute invoice.total.blank? + + # invoice_items assertions + ## reservation + reservation_item = invoice.invoice_items.where(subscription_id: nil).first + + assert_not_nil reservation_item + assert reservation_item.stp_invoice_item_id + assert_equal reservation_item.amount, machine.prices.find_by(group_id: @user_without_subscription.group_id, plan_id: plan.id).amount + ## subscription + subscription_item = invoice.invoice_items.where.not(subscription_id: nil).first + + assert_not_nil subscription_item + + subscription = Subscription.find(subscription_item.subscription_id) + + assert subscription_item.stp_invoice_item_id + assert_equal subscription_item.amount, plan.amount + assert_equal subscription.plan_id, plan.id + + # invoice assertions + invoice = Invoice.find_by(invoiced: reservation) + assert_invoice_pdf invoice + + VCR.use_cassette('reservations_machine_and_plan_using_coupon_retrieve_invoice_from_stripe') do + stp_invoice = Stripe::Invoice.retrieve(invoice.stp_invoice_id) + assert_equal stp_invoice.total, invoice.total + end + + # notifications + assert_not_empty Notification.where(attached_object: reservation) + assert_not_empty Notification.where(attached_object: subscription) + end + + test 'user reserves a training with an expired coupon with error' do + login_as(@user_without_subscription, scope: :user) + + training = Training.find(1) + availability = training.availabilities.first + + reservations_count = Reservation.count + invoice_count = Invoice.count + invoice_items_count = InvoiceItem.count + notifications_count = Notification.count + + VCR.use_cassette('reservations_training_with_expired_coupon_error') do + post reservations_path, { + reservation: { + user_id: @user_without_subscription.id, + reservable_id: training.id, + reservable_type: training.class.name, + card_token: stripe_card_token, + slots_attributes: [ + { start_at: availability.start_at.to_s(:iso8601), + end_at: (availability.start_at + 1.hour).to_s(:iso8601), + availability_id: availability.id + } + ], + }, + coupon_code: 'XMAS10' + }.to_json, default_headers + end + + # general assertions + assert_equal 422, response.status + assert_equal reservations_count, Reservation.count + assert_equal invoice_count, Invoice.count + assert_equal invoice_items_count, InvoiceItem.count + assert_equal notifications_count, Notification.count + end end end diff --git a/test/integration/settings_test.rb b/test/integration/settings_test.rb new file mode 100644 index 000000000..ae7aec4ff --- /dev/null +++ b/test/integration/settings_test.rb @@ -0,0 +1,43 @@ +class SettingsTest < ActionDispatch::IntegrationTest + + # Called before every test method runs. Can be used + # to set up fixture information. + def setup + @admin = User.find_by(username: 'admin') + login_as(@admin, scope: :user) + end + + # Called after every test method runs. Can be used to tear + # down fixture information. + + def teardown + # Do nothing + end + + test 'update setting value' do + put '/api/settings/fablab_name', + { + setting: { + value: 'Test Fablab' + } + } + assert_equal 200, response.status + assert_equal Mime::JSON, response.content_type + resp = json_response(response.body) + assert_equal 'fablab_name', resp[:setting][:name] + assert_equal 'Test Fablab', resp[:setting][:value] + end + + + test 'update setting with wrong name' do + put '/api/settings/does_not_exists', + { + setting: { + value: 'ERROR EXPECTED' + } + } + assert_equal 422, response.status + assert_match /Name is not included in the list/, response.body + end + +end diff --git a/test/integration/subscriptions/create_as_user_test.rb b/test/integration/subscriptions/create_as_user_test.rb index fd1882bca..f9c8d32fc 100644 --- a/test/integration/subscriptions/create_as_user_test.rb +++ b/test/integration/subscriptions/create_as_user_test.rb @@ -83,4 +83,64 @@ class Subscriptions::CreateAsUserTest < ActionDispatch::IntegrationTest assert_nil @user.subscription, "user's subscription was found" end -end \ No newline at end of file + + test 'user successfully takes a subscription with wallet' do + @vlonchamp = User.find_by(username: 'vlonchamp') + login_as(@vlonchamp, scope: :user) + plan = Plan.find_by(group_id: @vlonchamp.group.id, type: 'Plan', base_name: 'Mensuel tarif réduit') + + VCR.use_cassette("subscriptions_user_create_success_with_wallet") do + post '/api/subscriptions', + { + subscription: { + plan_id: plan.id, + user_id: @vlonchamp.id, + card_token: stripe_card_token + } + }.to_json, default_headers + end + + # Check response format & status + assert_equal 201, response.status, response.body + assert_equal Mime::JSON, response.content_type + + # Check the correct plan was subscribed + subscription = json_response(response.body) + assert_equal plan.id, subscription[:plan_id], 'subscribed plan does not match' + + # Check that the user has the correct subscription + assert_not_nil @vlonchamp.subscription, "user's subscription was not found" + assert_not_nil @vlonchamp.subscription.plan, "user's subscribed plan was not found" + assert_equal plan.id, @vlonchamp.subscription.plan_id, "user's plan does not match" + + # Check that the training credits were set correctly + assert_empty @vlonchamp.training_credits, 'training credits were not reset' + assert_equal @vlonchamp.subscription.plan.training_credit_nb, plan.training_credit_nb, 'trainings credits were not allocated' + + # Check that the user benefit from prices of his plan + printer = Machine.find_by_slug('imprimante-3d') + assert_equal 10, (printer.prices.find_by(group_id: @vlonchamp.group_id, plan_id: @vlonchamp.subscription.plan_id).amount / 100), 'machine hourly price does not match' + + # Check notifications were sent for every admins + notifications = Notification.where(notification_type_id: NotificationType.find_by_name('notify_admin_subscribed_plan'), attached_object_type: 'Subscription', attached_object_id: subscription[:id]) + assert_not_empty notifications, 'no notifications were created' + notified_users_ids = notifications.map {|n| n.receiver_id } + User.admins.each do |adm| + assert_includes notified_users_ids, adm.id, "Admin #{adm.id} was not notified" + end + + # Check generated invoice + invoice = Invoice.find_by(invoiced_type: 'Subscription', invoiced_id: subscription[:id]) + assert_invoice_pdf invoice + assert_equal plan.amount, invoice.total, 'Invoice total price does not match the bought subscription' + + # wallet + assert_equal @vlonchamp.wallet.amount, 0 + assert_equal @vlonchamp.wallet.wallet_transactions.count, 2 + transaction = @vlonchamp.wallet.wallet_transactions.last + assert_equal transaction.transaction_type, 'debit' + assert_equal transaction.amount, 10 + assert_equal transaction.amount, invoice.wallet_amount / 100.0 + assert_equal transaction.id, invoice.wallet_transaction_id + end +end diff --git a/test/integration/wallets_test.rb b/test/integration/wallets_test.rb new file mode 100644 index 000000000..bc9c89aac --- /dev/null +++ b/test/integration/wallets_test.rb @@ -0,0 +1,78 @@ +class WalletsTest < ActionDispatch::IntegrationTest + + # Called before every test method runs. Can be used + # to set up fixture information. + def setup + @vlonchamp = User.find_by(username: 'vlonchamp') + login_as(@vlonchamp, scope: :user) + end + + # Called after every test method runs. Can be used to tear + # down fixture information. + + def teardown + # Do nothing + end + + test 'get my wallet' do + get "/api/wallet/by_user/#{@vlonchamp.id}" + assert_equal 200, response.status + assert_equal Mime::JSON, response.content_type + wallet = json_response(response.body) + assert_equal @vlonchamp.wallet.user_id, wallet[:user_id] + assert_equal @vlonchamp.wallet.amount, wallet[:amount] + end + + test 'admin can get wallet by user id' do + @admin = User.find_by_username('admin') + login_as(@admin, scope: :user) + @user1 = User.first + get "/api/wallet/by_user/#{@user1.id}" + assert_equal 200, response.status + assert_equal Mime::JSON, response.content_type + wallet = json_response(response.body) + assert_equal @user1.wallet.user_id, wallet[:user_id] + assert_equal @user1.wallet.amount, wallet[:amount] + end + + test 'cant get wallet of an user if not admin' do + user5 = users(:user_4) + get "/api/wallet/by_user/#{user5.id}" + assert_equal 403, response.status + end + + test 'get all transactions of wallet' do + w = @vlonchamp.wallet + get "/api/wallet/#{w.id}/transactions" + assert_equal 200, response.status + assert_equal Mime::JSON, response.content_type + transactions = json_response(response.body) + assert_equal w.wallet_transactions.count, transactions.size + assert_equal wallet_transactions(:transaction1).id, transactions.first[:id] + end + + test 'only admin and wallet owner can show their transactions' do + user5 = users(:user_4) + get "/api/wallet/#{user5.wallet.id}/transactions" + assert_equal 403, response.status + end + + test 'admin can credit amount to a wallet' do + admin = users(:user_1) + login_as(admin, scope: :user) + w = @vlonchamp.wallet + amount = 10.5 + expected_amount = w.amount + amount + put "/api/wallet/#{w.id}/credit", + { + amount: amount + } + + assert_equal 200, response.status + assert_equal Mime::JSON, response.content_type + wallet = json_response(response.body) + w.reload + assert_equal w.amount, expected_amount + assert_equal w.amount, wallet[:amount] + end +end diff --git a/test/models/coupon_test.rb b/test/models/coupon_test.rb new file mode 100644 index 000000000..4ccdc535e --- /dev/null +++ b/test/models/coupon_test.rb @@ -0,0 +1,18 @@ +require 'test_helper' + +class CouponTest < ActiveSupport::TestCase + test 'coupon must have a valid percentage' do + c = Coupon.new({name: 'Amazing deal', code: 'DISCOUNT', percent_off: 200, validity_per_user: 'once'}) + assert c.invalid? + end + + test 'expired coupon must return the proper status' do + c = Coupon.find_by_code('XMAS10') + assert c.status == 'expired' + end + + test 'two coupons cannot have the same code' do + c = Coupon.new({name: 'Summer deals', code: 'SUNNYFABLAB', percent_off: 15, validity_per_user: 'always'}) + assert c.invalid? + end +end diff --git a/test/models/event_price_category_test.rb b/test/models/event_price_category_test.rb new file mode 100644 index 000000000..2b1733689 --- /dev/null +++ b/test/models/event_price_category_test.rb @@ -0,0 +1,9 @@ +require 'test_helper' + +class EventPriceCategoryTest < ActiveSupport::TestCase + test "event price's category cannot be empty" do + epc = EventPriceCategory.new({price_category_id: 1, event_id: 3}) + assert epc.invalid? + assert epc.errors[:amount].present? + end +end diff --git a/test/models/event_test.rb b/test/models/event_test.rb new file mode 100644 index 000000000..63685e4a4 --- /dev/null +++ b/test/models/event_test.rb @@ -0,0 +1,23 @@ +require 'test_helper' + +class EventTest < ActiveSupport::TestCase + test 'event must have a category' do + e = Event.first + assert_not_nil e.category + end + + test 'event must have a theme' do + e = Event.find(1) + assert_not_empty e.themes + end + + test 'event must have an age range' do + e = Event.find(2) + assert_not_nil e.age_range.name + end + + test 'event must not have any age range' do + e = Event.find(3) + assert_nil e.age_range + end +end diff --git a/test/models/export_test.rb b/test/models/export_test.rb new file mode 100644 index 000000000..aed660cac --- /dev/null +++ b/test/models/export_test.rb @@ -0,0 +1,16 @@ +require 'test_helper' + +class ExportTest < ActiveSupport::TestCase + test 'export must have a category' do + e = Export.new({export_type: 'global', user: User.first, query: '{"query":{"bool":{"must":[{"range":{"date":{"gte":"2016-06-25T02:00:00+02:00","lte":"2016-07-25T23:59:59+02:00"}}}]}}}'}) + assert e.invalid? + end + + test 'export generate an XLSX file' do + e = Export.create({category: 'statistics', export_type: 'global', user: User.first, query: '{"query":{"bool":{"must":[{"range":{"date":{"gte":"2016-06-25T02:00:00+02:00","lte":"2016-07-25T23:59:59+02:00"}}}]}}}'}) + e.save! + VCR.use_cassette("export_generate_an_xlsx_file") do + assert_export_xlsx e + end + end +end diff --git a/test/models/organization_test.rb b/test/models/organization_test.rb new file mode 100644 index 000000000..0dd15f376 --- /dev/null +++ b/test/models/organization_test.rb @@ -0,0 +1,14 @@ +require 'test_helper' + +class OrganizationTest < ActiveSupport::TestCase + test 'organization must have a name' do + a = Address.new({address: '14 avenue du Maréchal Tartanpion, 12345 Saint-Robert-sur-Mer'}) + o = Organization.new({address: a}) + assert o.invalid? + end + + test 'organization must have an address' do + o = Organization.new({name: 'Menuiserie G. Dubois'}) + assert o.invalid? + end +end diff --git a/test/models/price_category_test.rb b/test/models/price_category_test.rb new file mode 100644 index 000000000..374b6a916 --- /dev/null +++ b/test/models/price_category_test.rb @@ -0,0 +1,15 @@ +require 'test_helper' + +class PriceCategoryTest < ActiveSupport::TestCase + test 'price category name must be unique' do + pc = PriceCategory.new({name: '- DE 25 ANS', conditions: 'Tarif préférentiel pour les jeunes'}) + assert pc.invalid? + assert pc.errors[:name].present? + end + + test 'associated price category cannot be destroyed' do + pc = PriceCategory.find(1) + assert_not pc.safe_destroy + assert_not_empty PriceCategory.where(id: 1) + end +end diff --git a/test/models/ticket_test.rb b/test/models/ticket_test.rb new file mode 100644 index 000000000..5007c4e81 --- /dev/null +++ b/test/models/ticket_test.rb @@ -0,0 +1,9 @@ +require 'test_helper' + +class TicketTest < ActiveSupport::TestCase + test "ticket must have at least 1 seat" do + t = Ticket.new({event_price_category_id: 1, booked: -1}) + assert t.invalid? + assert t.errors[:booked].present? + end +end diff --git a/test/models/user_test.rb b/test/models/user_test.rb new file mode 100644 index 000000000..7426e4f3f --- /dev/null +++ b/test/models/user_test.rb @@ -0,0 +1,9 @@ +require 'test_helper' + +class UserTest < ActiveSupport::TestCase + test "must create a wallet after create user" do + u = User.create(username: 'user', email: 'userwallet@fabmanager.com', password: 'testpassword', password_confirmation: 'testpassword', + profile_attributes: {first_name: 'user', last_name: 'wallet', gender: true, birthday: 18.years.ago, phone: '0123456789'} ) + assert u.wallet.present? + end +end diff --git a/test/models/wallet_test.rb b/test/models/wallet_test.rb new file mode 100644 index 000000000..7eb1fbb01 --- /dev/null +++ b/test/models/wallet_test.rb @@ -0,0 +1,39 @@ +require 'test_helper' + +class WalletTest < ActiveSupport::TestCase + test "default amount must be zero" do + w = Wallet.new + assert w.amount == 0 + end + + test 'should user present' do + w = Wallet.create + assert w.errors[:user].present? + end + + test 'can credit amount' do + w = Wallet.first + expected_amount = w.amount + 5.5 + assert w.credit(5.5) + assert_equal w.amount, expected_amount + end + + test 'can debit amount' do + w = Wallet.first + w.credit(5) + expected_amount = w.amount - 5 + assert w.debit(5) + assert_equal w.amount, expected_amount + end + + test 'cant debit/credit a negative' do + w = Wallet.new + assert_not w.credit(-5) + assert_not w.debit(-5) + end + + test 'wallet amount cant < 0 after debit' do + w = Wallet.new + assert_not w.debit(5) + end +end diff --git a/test/models/wallet_transaction_test.rb b/test/models/wallet_transaction_test.rb new file mode 100644 index 000000000..f51de148a --- /dev/null +++ b/test/models/wallet_transaction_test.rb @@ -0,0 +1,15 @@ +require 'test_helper' + +class WalletTransactionTest < ActiveSupport::TestCase + test 'transaction type must be credit or debit' do + @jdupond = User.find_by(username: 'jdupond') + @jdupond_wallet = @jdupond.wallet + transaction = WalletTransaction.new amount: 5, user: @jdupond, wallet: @jdupond_wallet + transaction.transaction_type = 'credit' + assert transaction.valid? + transaction.transaction_type = 'debit' + assert transaction.valid? + transaction.transaction_type = 'other' + assert_not transaction.valid? + end +end diff --git a/test/services/wallet_service_test.rb b/test/services/wallet_service_test.rb new file mode 100644 index 000000000..15f68dace --- /dev/null +++ b/test/services/wallet_service_test.rb @@ -0,0 +1,62 @@ +require 'test_helper' + +class WalletServiceTest < ActiveSupport::TestCase + setup do + @admin = User.find_by(username: 'admin') + @jdupond = User.find_by(username: 'jdupond') + @jdupond_wallet = @jdupond.wallet + @vlonchamp = User.find_by(username: 'vlonchamp') + @vlonchamp_wallet = @vlonchamp.wallet + end + + test 'admin can credit a wallet' do + service = WalletService.new(user: @admin, wallet: @jdupond_wallet) + expected_amount = @jdupond_wallet.amount + 5 + assert service.credit(5) + assert_equal @jdupond_wallet.amount, expected_amount + end + + test 'create a credit transaction after credit amount to wallet' do + service = WalletService.new(user: @admin, wallet: @jdupond_wallet) + expected_amount = @jdupond_wallet.amount + 10 + assert_equal 0, @jdupond_wallet.wallet_transactions.count + transaction = service.credit(10) + @jdupond_wallet.reload + assert transaction + assert_equal @jdupond_wallet.amount, expected_amount + assert_equal transaction.transaction_type, 'credit' + assert_equal transaction.amount, 10 + assert_equal transaction.user, @admin + assert_equal transaction.wallet, @jdupond_wallet + end + + test 'create a debit transaction after debit amount to wallet' do + service = WalletService.new(user: @vlonchamp, wallet: @vlonchamp_wallet) + expected_amount = @vlonchamp_wallet.amount - 5 + transaction = service.debit(5, nil) + @vlonchamp_wallet.reload + assert transaction + assert_equal @vlonchamp_wallet.amount, expected_amount + assert_equal transaction.transaction_type, 'debit' + assert_equal transaction.amount, 5 + assert_equal transaction.user, @vlonchamp + assert_equal transaction.wallet, @vlonchamp_wallet + end + + test 'dont debit amount > wallet amount' do + service = WalletService.new(user: @vlonchamp, wallet: @vlonchamp_wallet) + expected_amount = @vlonchamp_wallet.amount + service.debit(100, nil) + @vlonchamp_wallet.reload + assert_equal @vlonchamp_wallet.amount, expected_amount + end + + test 'rollback debited amount if has an error when create wallet transaction' do + service = WalletService.new(wallet: @vlonchamp_wallet) + expected_amount = @vlonchamp_wallet.amount + transaction = service.debit(5, nil) + @vlonchamp_wallet.reload + assert_equal @vlonchamp_wallet.amount, expected_amount + assert_not transaction + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index ef130913f..792006eac 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -70,6 +70,25 @@ class ActiveSupport::TestCase File.delete(invoice.file) end + + + # Force the statistics export generation worker to run NOW and check the resulting file generated. + # Delete the file afterwards. + # @param export {Export} + def assert_export_xlsx(export) + assert_not_nil export, 'Export was not created' + + if export.category == 'statistics' + export_worker = StatisticsExportWorker.new + export_worker.perform(export.id) + + assert File.exist?(export.file), 'Export XLSX was not generated' + + File.delete(export.file) + else + skip('Unable to test export which is not of the category "statistics"') + end + end end class ActionDispatch::IntegrationTest diff --git a/test/vcr_cassettes/export_generate_an_xlsx_file.yml b/test/vcr_cassettes/export_generate_an_xlsx_file.yml new file mode 100644 index 000000000..dc53d3e51 --- /dev/null +++ b/test/vcr_cassettes/export_generate_an_xlsx_file.yml @@ -0,0 +1,153 @@ +--- +http_interactions: +- request: + method: get + uri: http://localhost:9200/stats/_search?scroll=30s + body: + encoding: UTF-8 + string: '{"query":{"bool":{"must":[{"range":{"date":{"gte":"2016-06-25T02:00:00+02:00","lte":"2016-07-25T23:59:59+02:00"}}}]}}}' + headers: + User-Agent: + - Faraday v0.9.1 + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json; charset=UTF-8 + Content-Length: + - '4119' + body: + encoding: ASCII-8BIT + string: !binary |- + eyJfc2Nyb2xsX2lkIjoiY1hWbGNubFVhR1Z1Um1WMFkyZzdOVHN4TWpFNk9X + eENiRkowYTE5U1UxYzNOSFZLYTFFeE56Vk9VVHN4TWpJNk9XeENiRkowYTE5 + U1UxYzNOSFZLYTFFeE56Vk9VVHN4TWpRNk9XeENiRkowYTE5U1UxYzNOSFZL + YTFFeE56Vk9VVHN4TWpNNk9XeENiRkowYTE5U1UxYzNOSFZLYTFFeE56Vk9V + VHN4TWpVNk9XeENiRkowYTE5U1UxYzNOSFZLYTFFeE56Vk9VVHN3T3c9PSIs + InRvb2siOjUsInRpbWVkX291dCI6ZmFsc2UsIl9zaGFyZHMiOnsidG90YWwi + OjUsInN1Y2Nlc3NmdWwiOjUsImZhaWxlZCI6MH0sImhpdHMiOnsidG90YWwi + OjE3LCJtYXhfc2NvcmUiOjEuMCwiaGl0cyI6W3siX2luZGV4Ijoic3RhdHMi + LCJfdHlwZSI6ImV2ZW50IiwiX2lkIjoiQVZZblM3QzdkaDZFOXVGdWxMcnki + LCJfc2NvcmUiOjEuMCwiX3NvdXJjZSI6eyJjcmVhdGVkX2F0IjoiMjAxNi0w + Ny0yNlQxMzowMToyNy4wOTcrMDA6MDAiLCJ1cGRhdGVkX2F0IjoiMjAxNi0w + Ny0yNlQxMzowMToyNy4wOTcrMDA6MDAiLCJ0eXBlIjoiYm9va2luZyIsInN1 + YlR5cGUiOiJBdGVsaWVyIiwiZGF0ZSI6IjIwMTYtMDYtMjgiLCJzdGF0Ijox + LCJ1c2VySWQiOjE5LCJnZW5kZXIiOiJtYWxlIiwiYWdlIjozMCwiZ3JvdXAi + OiJzdHVkZW50IiwicmVzZXJ2YXRpb25JZCI6MzExNSwiY2EiOjI1LjAsIm5h + bWUiOiJST0JPVCBCUk9TU0UgIiwiZXZlbnRJZCI6MTczLCJldmVudERhdGUi + OiIyMDE2LTA3LTA3IiwiYWdlUmFuZ2UiOiIiLCJldmVudFRoZW1lIjoiIn19 + LHsiX2luZGV4Ijoic3RhdHMiLCJfdHlwZSI6ImV2ZW50IiwiX2lkIjoiQVZZ + blM3REhkaDZFOXVGdWxMcnoiLCJfc2NvcmUiOjEuMCwiX3NvdXJjZSI6eyJj + cmVhdGVkX2F0IjoiMjAxNi0wNy0yNlQxMzowMToyNy4xMDkrMDA6MDAiLCJ1 + cGRhdGVkX2F0IjoiMjAxNi0wNy0yNlQxMzowMToyNy4xMDkrMDA6MDAiLCJ0 + eXBlIjoiaG91ciIsInN1YlR5cGUiOiJBdGVsaWVyIiwiZGF0ZSI6IjIwMTYt + MDYtMjgiLCJzdGF0IjozLCJ1c2VySWQiOjE5LCJnZW5kZXIiOiJtYWxlIiwi + YWdlIjozMCwiZ3JvdXAiOiJzdHVkZW50IiwicmVzZXJ2YXRpb25JZCI6MzEx + NSwiY2EiOjI1LjAsIm5hbWUiOiJST0JPVCBCUk9TU0UgIiwiZXZlbnRJZCI6 + MTczLCJldmVudERhdGUiOiIyMDE2LTA3LTA3IiwiYWdlUmFuZ2UiOiIiLCJl + dmVudFRoZW1lIjoiIn19LHsiX2luZGV4Ijoic3RhdHMiLCJfdHlwZSI6ImFj + Y291bnQiLCJfaWQiOiJBVlluUzZGRWRoNkU5dUZ1bExybyIsIl9zY29yZSI6 + MS4wLCJfc291cmNlIjp7ImNyZWF0ZWRfYXQiOiIyMDE2LTA3LTI2VDEzOjAx + OjIzLjEzOCswMDowMCIsInVwZGF0ZWRfYXQiOiIyMDE2LTA3LTI2VDEzOjAx + OjIzLjEzOCswMDowMCIsInR5cGUiOiJtZW1iZXIiLCJzdWJUeXBlIjoiY3Jl + YXRlZCIsImRhdGUiOiIyMDE2LTA3LTI1Iiwic3RhdCI6MSwidXNlcklkIjox + NjY2LCJnZW5kZXIiOiJtYWxlIiwiYWdlIjo2MywiZ3JvdXAiOiJzdGFuZGFy + ZCJ9fSx7Il9pbmRleCI6InN0YXRzIiwiX3R5cGUiOiJzdWJzY3JpcHRpb24i + LCJfaWQiOiJBVlluUzZEWWRoNkU5dUZ1bExybCIsIl9zY29yZSI6MS4wLCJf + c291cmNlIjp7ImNyZWF0ZWRfYXQiOiIyMDE2LTA3LTI2VDEzOjAxOjIzLjAz + MCswMDowMCIsInVwZGF0ZWRfYXQiOiIyMDE2LTA3LTI2VDEzOjAxOjIzLjAz + MCswMDowMCIsInR5cGUiOiI1MTg0MDAwIiwic3ViVHlwZSI6ImJpbWVuc3Vl + bC1zdGFuZGFyZC1tb250aC0yMDE2MDcyNTEyNDMyNCIsImRhdGUiOiIyMDE2 + LTA3LTI1Iiwic3RhdCI6MSwidXNlcklkIjoxNjY2LCJnZW5kZXIiOiJtYWxl + IiwiYWdlIjo2MywiZ3JvdXAiOiJzdGFuZGFyZCIsImNhIjo1MC4wLCJwbGFu + SWQiOjEwLCJzdWJzY3JpcHRpb25JZCI6MzgwLCJpbnZvaWNlSXRlbUlkIjoz + NTk3LCJncm91cE5hbWUiOiJzdGFuZGFyZCwgYXNzb2NpYXRpb24ifX0seyJf + aW5kZXgiOiJzdGF0cyIsIl90eXBlIjoidXNlciIsIl9pZCI6IkFWWW5TNkdY + ZGg2RTl1RnVsTHJwIiwiX3Njb3JlIjoxLjAsIl9zb3VyY2UiOnsiY3JlYXRl + ZF9hdCI6IjIwMTYtMDctMjZUMTM6MDE6MjMuMjIxKzAwOjAwIiwidXBkYXRl + ZF9hdCI6IjIwMTYtMDctMjZUMTM6MDE6MjMuMjIxKzAwOjAwIiwidHlwZSI6 + InJldmVudWUiLCJzdWJUeXBlIjoic3R1ZGVudCIsImRhdGUiOiIyMDE2LTA3 + LTI1Iiwic3RhdCI6MjAsInVzZXJJZCI6NSwiZ2VuZGVyIjoiZmVtYWxlIiwi + YWdlIjowLCJncm91cCI6InN0dWRlbnQifX0seyJfaW5kZXgiOiJzdGF0cyIs + Il90eXBlIjoidHJhaW5pbmciLCJfaWQiOiJBVlluUzZod2RoNkU5dUZ1bExy + dCIsIl9zY29yZSI6MS4wLCJfc291cmNlIjp7ImNyZWF0ZWRfYXQiOiIyMDE2 + LTA3LTI2VDEzOjAxOjI0Ljk3MyswMDowMCIsInVwZGF0ZWRfYXQiOiIyMDE2 + LTA3LTI2VDEzOjAxOjI0Ljk3MyswMDowMCIsInR5cGUiOiJob3VyIiwic3Vi + VHlwZSI6ImRvbG9yLXNpdC1hbWV0IiwiZGF0ZSI6IjIwMTYtMDctMTMiLCJz + dGF0IjozLCJ1c2VySWQiOjUxMCwiZ2VuZGVyIjoibWFsZSIsImFnZSI6MzMs + Imdyb3VwIjoic3RhbmRhcmQiLCJyZXNlcnZhdGlvbklkIjozMTE2LCJjYSI6 + MC4wLCJuYW1lIjoiTG9yZW0gaXBzdW0iLCJ0cmFpbmluZ0lkIjoxMCwidHJh + aW5pbmdEYXRlIjoiMjAxNi0wNy0xNSJ9fSx7Il9pbmRleCI6InN0YXRzIiwi + X3R5cGUiOiJ1c2VyIiwiX2lkIjoiQVZZblM2R3FkaDZFOXVGdWxMcnIiLCJf + c2NvcmUiOjEuMCwiX3NvdXJjZSI6eyJjcmVhdGVkX2F0IjoiMjAxNi0wNy0y + NlQxMzowMToyMy4yNDArMDA6MDAiLCJ1cGRhdGVkX2F0IjoiMjAxNi0wNy0y + NlQxMzowMToyMy4yNDArMDA6MDAiLCJ0eXBlIjoicmV2ZW51ZSIsInN1YlR5 + cGUiOiJzdGFuZGFyZCIsImRhdGUiOiIyMDE2LTA3LTI1Iiwic3RhdCI6NTAs + InVzZXJJZCI6MTY2NiwiZ2VuZGVyIjoibWFsZSIsImFnZSI6NjMsImdyb3Vw + Ijoic3RhbmRhcmQifX0seyJfaW5kZXgiOiJzdGF0cyIsIl90eXBlIjoidHJh + aW5pbmciLCJfaWQiOiJBVlluUzZoLWRoNkU5dUZ1bExydSIsIl9zY29yZSI6 + MS4wLCJfc291cmNlIjp7ImNyZWF0ZWRfYXQiOiIyMDE2LTA3LTI2VDEzOjAx + OjI0Ljk4NiswMDowMCIsInVwZGF0ZWRfYXQiOiIyMDE2LTA3LTI2VDEzOjAx + OjI0Ljk4NiswMDowMCIsInR5cGUiOiJib29raW5nIiwic3ViVHlwZSI6ImZv + cm1hdGlvbi1pbXByaW1hbnRlLTNkIiwiZGF0ZSI6IjIwMTYtMDctMTMiLCJz + dGF0IjoxLCJ1c2VySWQiOjMzMiwiZ2VuZGVyIjoiZmVtYWxlIiwiYWdlIjox + OSwiZ3JvdXAiOiJzdHVkZW50IiwicmVzZXJ2YXRpb25JZCI6MzExNywiY2Ei + OjI1LjAsIm5hbWUiOiJGb3JtYXRpb24gSW1wcmltYW50ZSAzRCIsInRyYWlu + aW5nSWQiOjEsInRyYWluaW5nRGF0ZSI6IjIwMTYtMDctMTQifX0seyJfaW5k + ZXgiOiJzdGF0cyIsIl90eXBlIjoidXNlciIsIl9pZCI6IkFWWW5TNmtLZGg2 + RTl1RnVsTHJ3IiwiX3Njb3JlIjoxLjAsIl9zb3VyY2UiOnsiY3JlYXRlZF9h + dCI6IjIwMTYtMDctMjZUMTM6MDE6MjUuMTI4KzAwOjAwIiwidXBkYXRlZF9h + dCI6IjIwMTYtMDctMjZUMTM6MDE6MjUuMTI5KzAwOjAwIiwidHlwZSI6InJl + dmVudWUiLCJzdWJUeXBlIjoic3RhbmRhcmQiLCJkYXRlIjoiMjAxNi0wNy0x + MyIsInN0YXQiOjAsInVzZXJJZCI6NTEwLCJnZW5kZXIiOiJtYWxlIiwiYWdl + IjozMywiZ3JvdXAiOiJzdGFuZGFyZCJ9fSx7Il9pbmRleCI6InN0YXRzIiwi + X3R5cGUiOiJzdWJzY3JpcHRpb24iLCJfaWQiOiJBVlluUzZEQWRoNkU5dUZ1 + bExyayIsIl9zY29yZSI6MS4wLCJfc291cmNlIjp7ImNyZWF0ZWRfYXQiOiIy + MDE2LTA3LTI2VDEzOjAxOjIzLjAwMSswMDowMCIsInVwZGF0ZWRfYXQiOiIy + MDE2LTA3LTI2VDEzOjAxOjIzLjAwMSswMDowMCIsInR5cGUiOiIyNTkyMDAw + Iiwic3ViVHlwZSI6InN0dWRlbnQtbW9udGgiLCJkYXRlIjoiMjAxNi0wNy0y + NSIsInN0YXQiOjEsInVzZXJJZCI6NjI2LCJnZW5kZXIiOiJtYWxlIiwiYWdl + IjoxOSwiZ3JvdXAiOiJzdHVkZW50IiwiY2EiOjI1LjAsInBsYW5JZCI6Mywi + c3Vic2NyaXB0aW9uSWQiOjM3OSwiaW52b2ljZUl0ZW1JZCI6MzU5NiwiZ3Jv + dXBOYW1lIjoiw6l0dWRpYW50LCAtIGRlIDI1IGFucywgZW5zZWlnbmFudCwg + ZGVtYW5kZXVyIGQnZW1wbG9pIn19XX19 + http_version: + recorded_at: Wed, 27 Jul 2016 09:25:19 GMT +- request: + method: get + uri: http://localhost:9200/_search/scroll?scroll=30s + body: + encoding: UTF-8 + string: cXVlcnlUaGVuRmV0Y2g7NTsxMjE6OWxCbFJ0a19SU1c3NHVKa1ExNzVOUTsxMjI6OWxCbFJ0a19SU1c3NHVKa1ExNzVOUTsxMjQ6OWxCbFJ0a19SU1c3NHVKa1ExNzVOUTsxMjM6OWxCbFJ0a19SU1c3NHVKa1ExNzVOUTsxMjU6OWxCbFJ0a19SU1c3NHVKa1ExNzVOUTswOw== + headers: + User-Agent: + - Faraday v0.9.1 + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json; charset=UTF-8 + Content-Length: + - '2843' + body: + encoding: UTF-8 + string: '{"_scroll_id":"cXVlcnlUaGVuRmV0Y2g7NTsxMjE6OWxCbFJ0a19SU1c3NHVKa1ExNzVOUTsxMjI6OWxCbFJ0a19SU1c3NHVKa1ExNzVOUTsxMjQ6OWxCbFJ0a19SU1c3NHVKa1ExNzVOUTsxMjM6OWxCbFJ0a19SU1c3NHVKa1ExNzVOUTsxMjU6OWxCbFJ0a19SU1c3NHVKa1ExNzVOUTswOw==","took":3,"timed_out":false,"_shards":{"total":5,"successful":5,"failed":0},"hits":{"total":17,"max_score":1.0,"hits":[{"_index":"stats","_type":"machine","_id":"AVYnS6EHdh6E9uFulLrm","_score":1.0,"_source":{"created_at":"2016-07-26T13:01:23.078+00:00","updated_at":"2016-07-26T13:01:23.078+00:00","type":"booking","subType":"petite-fraiseuse","date":"2016-07-25","stat":1,"userId":5,"gender":"female","age":0,"group":"student","reservationId":3118,"ca":20.0,"name":"Petite + Fraiseuse","machineId":5}},{"_index":"stats","_type":"training","_id":"AVYnS6iPdh6E9uFulLrv","_score":1.0,"_source":{"created_at":"2016-07-26T13:01:24.998+00:00","updated_at":"2016-07-26T13:01:24.998+00:00","type":"hour","subType":"formation-imprimante-3d","date":"2016-07-13","stat":4,"userId":332,"gender":"female","age":19,"group":"student","reservationId":3117,"ca":25.0,"name":"Formation + Imprimante 3D","trainingId":1,"trainingDate":"2016-07-14"}},{"_index":"stats","_type":"user","_id":"AVYnS7ENdh6E9uFulLr0","_score":1.0,"_source":{"created_at":"2016-07-26T13:01:27.180+00:00","updated_at":"2016-07-26T13:01:27.180+00:00","type":"revenue","subType":"student","date":"2016-06-28","stat":25,"userId":19,"gender":"male","age":30,"group":"student"}},{"_index":"stats","_type":"machine","_id":"AVYnS6EQdh6E9uFulLrn","_score":1.0,"_source":{"created_at":"2016-07-26T13:01:23.086+00:00","updated_at":"2016-07-26T13:01:23.086+00:00","type":"hour","subType":"petite-fraiseuse","date":"2016-07-25","stat":1,"userId":5,"gender":"female","age":0,"group":"student","reservationId":3118,"ca":20.0,"name":"Petite + Fraiseuse","machineId":5}},{"_index":"stats","_type":"user","_id":"AVYnS6Gfdh6E9uFulLrq","_score":1.0,"_source":{"created_at":"2016-07-26T13:01:23.230+00:00","updated_at":"2016-07-26T13:01:23.230+00:00","type":"revenue","subType":"student","date":"2016-07-25","stat":25,"userId":626,"gender":"male","age":19,"group":"student"}},{"_index":"stats","_type":"training","_id":"AVYnS6hldh6E9uFulLrs","_score":1.0,"_source":{"created_at":"2016-07-26T13:01:24.963+00:00","updated_at":"2016-07-26T13:01:24.963+00:00","type":"booking","subType":"dolor-sit-amet","date":"2016-07-13","stat":1,"userId":510,"gender":"male","age":33,"group":"standard","reservationId":3116,"ca":0.0,"name":"Lorem + ipsum","trainingId":10,"trainingDate":"2016-07-15"}},{"_index":"stats","_type":"user","_id":"AVYnS6kSdh6E9uFulLrx","_score":1.0,"_source":{"created_at":"2016-07-26T13:01:25.135+00:00","updated_at":"2016-07-26T13:01:25.135+00:00","type":"revenue","subType":"student","date":"2016-07-13","stat":25,"userId":332,"gender":"female","age":19,"group":"student"}}]}}' + http_version: + recorded_at: Wed, 27 Jul 2016 09:25:19 GMT +recorded_with: VCR 3.0.1 diff --git a/test/vcr_cassettes/reservations_create_for_machine_and_pay_wallet_success.yml b/test/vcr_cassettes/reservations_create_for_machine_and_pay_wallet_success.yml new file mode 100644 index 000000000..ea837fd19 --- /dev/null +++ b/test/vcr_cassettes/reservations_create_for_machine_and_pay_wallet_success.yml @@ -0,0 +1,1063 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.stripe.com/v1/tokens + body: + encoding: UTF-8 + string: card[number]=4242424242424242&card[exp_month]=4&card[exp_year]=2017&card[cvc]=314 + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin14","engine":"ruby","publisher":"stripe","uname":"Darwin + MBP-sleede-Nicolas.local 14.5.0 Darwin Kernel Version 14.5.0: Wed Jul 29 02:26:53 + PDT 2015; root:xnu-2782.40.9~1/RELEASE_X86_64 x86_64","hostname":"MBP-sleede-Nicolas.local"}' + Content-Length: + - '81' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Wed, 06 Apr 2016 14:02:58 GMT + Content-Type: + - application/json + Content-Length: + - '778' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_8DigzZOUKbFLP8 + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "tok_17xHFG2sOmf47Nz9pZ4CafpU", + "object": "token", + "card": { + "id": "card_17xHFG2sOmf47Nz95yErDQbL", + "object": "card", + "address_city": null, + "address_country": null, + "address_line1": null, + "address_line1_check": null, + "address_line2": null, + "address_state": null, + "address_zip": null, + "address_zip_check": null, + "brand": "Visa", + "country": "US", + "cvc_check": "unchecked", + "dynamic_last4": null, + "exp_month": 4, + "exp_year": 2017, + "fingerprint": "o52jybR7bnmNn6AT", + "funding": "credit", + "last4": "4242", + "metadata": {}, + "name": null, + "tokenization_method": null + }, + "client_ip": "86.76.5.109", + "created": 1459951378, + "livemode": false, + "type": "card", + "used": false + } + http_version: + recorded_at: Wed, 06 Apr 2016 14:02:58 GMT +- request: + method: post + uri: https://api.stripe.com/v1/invoiceitems + body: + encoding: UTF-8 + string: customer=cus_8CzNtM08NVlSGN&amount=3200¤cy=usd&description=FORM1%2B+imprimante+3D+April+11%2C+2016+14%3A00+-+03%3A00+PM + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin14","engine":"ruby","publisher":"stripe","uname":"Darwin + MBP-sleede-Nicolas.local 14.5.0 Darwin Kernel Version 14.5.0: Wed Jul 29 02:26:53 + PDT 2015; root:xnu-2782.40.9~1/RELEASE_X86_64 x86_64","hostname":"MBP-sleede-Nicolas.local"}' + Content-Length: + - '125' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Wed, 06 Apr 2016 14:02:58 GMT + Content-Type: + - application/json + Content-Length: + - '469' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_8DigTaKJ04PVMc + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "ii_17xHFG2sOmf47Nz9hhIaJZtF", + "object": "invoiceitem", + "amount": 3200, + "currency": "usd", + "customer": "cus_8CzNtM08NVlSGN", + "date": 1459951378, + "description": "FORM1+ imprimante 3D April 11, 2016 14:00 - 03:00 PM", + "discountable": true, + "invoice": null, + "livemode": false, + "metadata": {}, + "period": { + "start": 1459951378, + "end": 1459951378 + }, + "plan": null, + "proration": false, + "quantity": null, + "subscription": null + } + http_version: + recorded_at: Wed, 06 Apr 2016 14:02:58 GMT +- request: + method: post + uri: https://api.stripe.com/v1/invoiceitems + body: + encoding: UTF-8 + string: customer=cus_8CzNtM08NVlSGN&amount=-1000¤cy=usd&description=Wallet+-1000 + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin14","engine":"ruby","publisher":"stripe","uname":"Darwin + MBP-sleede-Nicolas.local 14.5.0 Darwin Kernel Version 14.5.0: Wed Jul 29 02:26:53 + PDT 2015; root:xnu-2782.40.9~1/RELEASE_X86_64 x86_64","hostname":"MBP-sleede-Nicolas.local"}' + Content-Length: + - '125' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Wed, 06 Apr 2016 14:02:58 GMT + Content-Type: + - application/json + Content-Length: + - '469' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_8DigTaKJ04PVMc + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "ii_17xHFG2sOmf47N59hh8aJSt6", + "object": "invoiceitem", + "amount": -1000, + "currency": "usd", + "customer": "cus_8CzNtM08NVlSGN", + "date": 1459951378, + "description": "Wallet -1000", + "discountable": true, + "invoice": null, + "livemode": false, + "metadata": {}, + "period": { + "start": 1459951378, + "end": 1459951378 + }, + "plan": null, + "proration": false, + "quantity": null, + "subscription": null + } + http_version: + recorded_at: Wed, 06 Apr 2016 14:02:58 GMT +- request: + method: get + uri: https://api.stripe.com/v1/customers/cus_8CzNtM08NVlSGN + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin14","engine":"ruby","publisher":"stripe","uname":"Darwin + MBP-sleede-Nicolas.local 14.5.0 Darwin Kernel Version 14.5.0: Wed Jul 29 02:26:53 + PDT 2015; root:xnu-2782.40.9~1/RELEASE_X86_64 x86_64","hostname":"MBP-sleede-Nicolas.local"}' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Wed, 06 Apr 2016 14:02:59 GMT + Content-Type: + - application/json + Content-Length: + - '3462' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_8Digc2V3aKSGrn + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "cus_8CzNtM08NVlSGN", + "object": "customer", + "account_balance": 0, + "created": 1459948888, + "currency": "usd", + "default_source": "card_17xGjJ2sOmf47Nz9UrQOP8Cl", + "delinquent": false, + "description": "Jean Dupond", + "discount": null, + "email": "jean.dupond@gmail.com", + "livemode": false, + "metadata": {}, + "shipping": null, + "sources": { + "object": "list", + "data": [ + { + "id": "card_17xGjJ2sOmf47Nz9UrQOP8Cl", + "object": "card", + "address_city": null, + "address_country": null, + "address_line1": null, + "address_line1_check": null, + "address_line2": null, + "address_state": null, + "address_zip": null, + "address_zip_check": null, + "brand": "Visa", + "country": "US", + "customer": "cus_8CzNtM08NVlSGN", + "cvc_check": "pass", + "dynamic_last4": null, + "exp_month": 4, + "exp_year": 2017, + "fingerprint": "o52jybR7bnmNn6AT", + "funding": "credit", + "last4": "4242", + "metadata": {}, + "name": null, + "tokenization_method": null + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/customers/cus_8CzNtM08NVlSGN/sources" + }, + "subscriptions": { + "object": "list", + "data": [ + { + "id": "sub_8Di9gqPLwt5IIC", + "object": "subscription", + "application_fee_percent": null, + "cancel_at_period_end": true, + "canceled_at": 1459949404, + "current_period_end": 1462541399, + "current_period_start": 1459949399, + "customer": "cus_8CzNtM08NVlSGN", + "discount": null, + "ended_at": null, + "metadata": {}, + "plan": { + "id": "mensuel-standard-month-20160404171519", + "object": "plan", + "amount": 3000, + "created": 1459782921, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "name": "Mensuel - standard, association - month", + "statement_descriptor": null, + "trial_period_days": null + }, + "quantity": 1, + "start": 1459949399, + "status": "active", + "tax_percent": null, + "trial_end": null, + "trial_start": null + }, + { + "id": "sub_8Di2VadRvr7A99", + "object": "subscription", + "application_fee_percent": null, + "cancel_at_period_end": true, + "canceled_at": 1459948972, + "current_period_end": 1462540968, + "current_period_start": 1459948968, + "customer": "cus_8CzNtM08NVlSGN", + "discount": null, + "ended_at": null, + "metadata": {}, + "plan": { + "id": "mensuel-standard-month-20160404171519", + "object": "plan", + "amount": 3000, + "created": 1459782921, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "name": "Mensuel - standard, association - month", + "statement_descriptor": null, + "trial_period_days": null + }, + "quantity": 1, + "start": 1459948968, + "status": "active", + "tax_percent": null, + "trial_end": null, + "trial_start": null + } + ], + "has_more": false, + "total_count": 2, + "url": "/v1/customers/cus_8CzNtM08NVlSGN/subscriptions" + } + } + http_version: + recorded_at: Wed, 06 Apr 2016 14:02:59 GMT +- request: + method: post + uri: https://api.stripe.com/v1/customers/cus_8CzNtM08NVlSGN/sources + body: + encoding: UTF-8 + string: card=tok_17xHFG2sOmf47Nz9pZ4CafpU + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin14","engine":"ruby","publisher":"stripe","uname":"Darwin + MBP-sleede-Nicolas.local 14.5.0 Darwin Kernel Version 14.5.0: Wed Jul 29 02:26:53 + PDT 2015; root:xnu-2782.40.9~1/RELEASE_X86_64 x86_64","hostname":"MBP-sleede-Nicolas.local"}' + Content-Length: + - '33' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Wed, 06 Apr 2016 14:03:00 GMT + Content-Type: + - application/json + Content-Length: + - '577' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_8DigAxuQClwx3A + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "card_17xHFG2sOmf47Nz95yErDQbL", + "object": "card", + "address_city": null, + "address_country": null, + "address_line1": null, + "address_line1_check": null, + "address_line2": null, + "address_state": null, + "address_zip": null, + "address_zip_check": null, + "brand": "Visa", + "country": "US", + "customer": "cus_8CzNtM08NVlSGN", + "cvc_check": "pass", + "dynamic_last4": null, + "exp_month": 4, + "exp_year": 2017, + "fingerprint": "o52jybR7bnmNn6AT", + "funding": "credit", + "last4": "4242", + "metadata": {}, + "name": null, + "tokenization_method": null + } + http_version: + recorded_at: Wed, 06 Apr 2016 14:03:00 GMT +- request: + method: post + uri: https://api.stripe.com/v1/customers/cus_8CzNtM08NVlSGN + body: + encoding: UTF-8 + string: default_source=card_17xHFG2sOmf47Nz95yErDQbL + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin14","engine":"ruby","publisher":"stripe","uname":"Darwin + MBP-sleede-Nicolas.local 14.5.0 Darwin Kernel Version 14.5.0: Wed Jul 29 02:26:53 + PDT 2015; root:xnu-2782.40.9~1/RELEASE_X86_64 x86_64","hostname":"MBP-sleede-Nicolas.local"}' + Content-Length: + - '44' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Wed, 06 Apr 2016 14:03:01 GMT + Content-Type: + - application/json + Content-Length: + - '4190' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_8Dig1Js3cBEeqQ + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "cus_8CzNtM08NVlSGN", + "object": "customer", + "account_balance": 0, + "created": 1459948888, + "currency": "usd", + "default_source": "card_17xHFG2sOmf47Nz95yErDQbL", + "delinquent": false, + "description": "Jean Dupond", + "discount": null, + "email": "jean.dupond@gmail.com", + "livemode": false, + "metadata": {}, + "shipping": null, + "sources": { + "object": "list", + "data": [ + { + "id": "card_17xHFG2sOmf47Nz95yErDQbL", + "object": "card", + "address_city": null, + "address_country": null, + "address_line1": null, + "address_line1_check": null, + "address_line2": null, + "address_state": null, + "address_zip": null, + "address_zip_check": null, + "brand": "Visa", + "country": "US", + "customer": "cus_8CzNtM08NVlSGN", + "cvc_check": "pass", + "dynamic_last4": null, + "exp_month": 4, + "exp_year": 2017, + "fingerprint": "o52jybR7bnmNn6AT", + "funding": "credit", + "last4": "4242", + "metadata": {}, + "name": null, + "tokenization_method": null + }, + { + "id": "card_17xGjJ2sOmf47Nz9UrQOP8Cl", + "object": "card", + "address_city": null, + "address_country": null, + "address_line1": null, + "address_line1_check": null, + "address_line2": null, + "address_state": null, + "address_zip": null, + "address_zip_check": null, + "brand": "Visa", + "country": "US", + "customer": "cus_8CzNtM08NVlSGN", + "cvc_check": "pass", + "dynamic_last4": null, + "exp_month": 4, + "exp_year": 2017, + "fingerprint": "o52jybR7bnmNn6AT", + "funding": "credit", + "last4": "4242", + "metadata": {}, + "name": null, + "tokenization_method": null + } + ], + "has_more": false, + "total_count": 2, + "url": "/v1/customers/cus_8CzNtM08NVlSGN/sources" + }, + "subscriptions": { + "object": "list", + "data": [ + { + "id": "sub_8Di9gqPLwt5IIC", + "object": "subscription", + "application_fee_percent": null, + "cancel_at_period_end": true, + "canceled_at": 1459949404, + "current_period_end": 1462541399, + "current_period_start": 1459949399, + "customer": "cus_8CzNtM08NVlSGN", + "discount": null, + "ended_at": null, + "metadata": {}, + "plan": { + "id": "mensuel-standard-month-20160404171519", + "object": "plan", + "amount": 3000, + "created": 1459782921, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "name": "Mensuel - standard, association - month", + "statement_descriptor": null, + "trial_period_days": null + }, + "quantity": 1, + "start": 1459949399, + "status": "active", + "tax_percent": null, + "trial_end": null, + "trial_start": null + }, + { + "id": "sub_8Di2VadRvr7A99", + "object": "subscription", + "application_fee_percent": null, + "cancel_at_period_end": true, + "canceled_at": 1459948972, + "current_period_end": 1462540968, + "current_period_start": 1459948968, + "customer": "cus_8CzNtM08NVlSGN", + "discount": null, + "ended_at": null, + "metadata": {}, + "plan": { + "id": "mensuel-standard-month-20160404171519", + "object": "plan", + "amount": 3000, + "created": 1459782921, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "name": "Mensuel - standard, association - month", + "statement_descriptor": null, + "trial_period_days": null + }, + "quantity": 1, + "start": 1459948968, + "status": "active", + "tax_percent": null, + "trial_end": null, + "trial_start": null + } + ], + "has_more": false, + "total_count": 2, + "url": "/v1/customers/cus_8CzNtM08NVlSGN/subscriptions" + } + } + http_version: + recorded_at: Wed, 06 Apr 2016 14:03:01 GMT +- request: + method: post + uri: https://api.stripe.com/v1/invoices + body: + encoding: UTF-8 + string: customer=cus_8CzNtM08NVlSGN + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin14","engine":"ruby","publisher":"stripe","uname":"Darwin + MBP-sleede-Nicolas.local 14.5.0 Darwin Kernel Version 14.5.0: Wed Jul 29 02:26:53 + PDT 2015; root:xnu-2782.40.9~1/RELEASE_X86_64 x86_64","hostname":"MBP-sleede-Nicolas.local"}' + Content-Length: + - '27' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Wed, 06 Apr 2016 14:03:02 GMT + Content-Type: + - application/json + Content-Length: + - '1426' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_8DigRXqOIStdA0 + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "in_17xHFK2sOmf47Nz9jegPFlNt", + "object": "invoice", + "amount_due": 2200, + "application_fee": null, + "attempt_count": 0, + "attempted": false, + "charge": null, + "closed": false, + "currency": "usd", + "customer": "cus_8CzNtM08NVlSGN", + "date": 1459951382, + "description": null, + "discount": null, + "ending_balance": null, + "forgiven": false, + "lines": { + "object": "list", + "data": [ + { + "id": "ii_17xHFG2sOmf47Nz9hhIaJZtF", + "object": "line_item", + "amount": 3200, + "currency": "usd", + "description": "FORM1+ imprimante 3D April 11, 2016 14:00 - 03:00 PM", + "discountable": true, + "livemode": false, + "metadata": {}, + "period": { + "start": 1459951378, + "end": 1459951378 + }, + "plan": null, + "proration": false, + "quantity": null, + "subscription": null, + "type": "invoiceitem" + }, + { + "id": "ii_17xHFG2sOmf47N59hh8aJSt6", + "object": "invoiceitem", + "amount": -1000, + "currency": "usd", + "customer": "cus_8CzNtM08NVlSGN", + "date": 1459951378, + "description": "Wallet -1000", + "discountable": true, + "invoice": null, + "livemode": false, + "metadata": {}, + "period": { + "start": 1459951378, + "end": 1459951378 + }, + "plan": null, + "proration": false, + "quantity": null, + "subscription": null + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/invoices/in_17xHFK2sOmf47Nz9jegPFlNt/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": 1459954982, + "paid": false, + "period_end": 1459951382, + "period_start": 1459948968, + "receipt_number": null, + "starting_balance": 0, + "statement_descriptor": null, + "subscription": null, + "subtotal": 2200, + "tax": null, + "tax_percent": null, + "total": 2200, + "webhooks_delivered_at": null + } + http_version: + recorded_at: Wed, 06 Apr 2016 14:03:02 GMT +- request: + method: post + uri: https://api.stripe.com/v1/invoices/in_17xHFK2sOmf47Nz9jegPFlNt/pay + body: + encoding: ASCII-8BIT + string: '' + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin14","engine":"ruby","publisher":"stripe","uname":"Darwin + MBP-sleede-Nicolas.local 14.5.0 Darwin Kernel Version 14.5.0: Wed Jul 29 02:26:53 + PDT 2015; root:xnu-2782.40.9~1/RELEASE_X86_64 x86_64","hostname":"MBP-sleede-Nicolas.local"}' + Content-Length: + - '0' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Wed, 06 Apr 2016 14:03:03 GMT + Content-Type: + - application/json + Content-Length: + - '1445' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_8DigckzVuj8MLI + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "in_17xHFK2sOmf47Nz9jegPFlNt", + "object": "invoice", + "amount_due": 2200, + "application_fee": null, + "attempt_count": 1, + "attempted": true, + "charge": "ch_17xHFL2sOmf47Nz9FCQ0BJKc", + "closed": true, + "currency": "usd", + "customer": "cus_8CzNtM08NVlSGN", + "date": 1459951382, + "description": null, + "discount": null, + "ending_balance": 0, + "forgiven": false, + "lines": { + "object": "list", + "data": [ + { + "id": "ii_17xHFG2sOmf47Nz9hhIaJZtF", + "object": "line_item", + "amount": 3200, + "currency": "usd", + "description": "FORM1+ imprimante 3D April 11, 2016 14:00 - 03:00 PM", + "discountable": true, + "livemode": false, + "metadata": {}, + "period": { + "start": 1459951378, + "end": 1459951378 + }, + "plan": null, + "proration": false, + "quantity": null, + "subscription": null, + "type": "invoiceitem" + }, + { + "id": "ii_17xHFG2sOmf47N59hh8aJSt6", + "object": "invoiceitem", + "amount": -1000, + "currency": "usd", + "customer": "cus_8CzNtM08NVlSGN", + "date": 1459951378, + "description": "Wallet -1000", + "discountable": true, + "invoice": null, + "livemode": false, + "metadata": {}, + "period": { + "start": 1459951378, + "end": 1459951378 + }, + "plan": null, + "proration": false, + "quantity": null, + "subscription": null + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/invoices/in_17xHFK2sOmf47Nz9jegPFlNt/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": null, + "paid": true, + "period_end": 1459951382, + "period_start": 1459948968, + "receipt_number": null, + "starting_balance": 0, + "statement_descriptor": null, + "subscription": null, + "subtotal": 2200, + "tax": null, + "tax_percent": null, + "total": 2200, + "webhooks_delivered_at": 1459951382 + } + http_version: + recorded_at: Wed, 06 Apr 2016 14:03:03 GMT +- request: + method: delete + uri: https://api.stripe.com/v1/customers/cus_8CzNtM08NVlSGN/sources/card_17xHFG2sOmf47Nz95yErDQbL + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin14","engine":"ruby","publisher":"stripe","uname":"Darwin + MBP-sleede-Nicolas.local 14.5.0 Darwin Kernel Version 14.5.0: Wed Jul 29 02:26:53 + PDT 2015; root:xnu-2782.40.9~1/RELEASE_X86_64 x86_64","hostname":"MBP-sleede-Nicolas.local"}' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Wed, 06 Apr 2016 14:03:04 GMT + Content-Type: + - application/json + Content-Length: + - '63' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_8Dig3VHawFrxab + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "deleted": true, + "id": "card_17xHFG2sOmf47Nz95yErDQbL" + } + http_version: + recorded_at: Wed, 06 Apr 2016 14:03:04 GMT +recorded_with: VCR 3.0.1 diff --git a/test/vcr_cassettes/reservations_create_for_machine_without_subscription_error.yml b/test/vcr_cassettes/reservations_create_for_machine_without_subscription_error.yml index 51b5dc8b1..65d6aa1af 100644 --- a/test/vcr_cassettes/reservations_create_for_machine_without_subscription_error.yml +++ b/test/vcr_cassettes/reservations_create_for_machine_without_subscription_error.yml @@ -18,9 +18,9 @@ http_interactions: Content-Type: - application/x-www-form-urlencoded X-Stripe-Client-User-Agent: - - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin14","engine":"ruby","publisher":"stripe","uname":"Darwin - MBP-sleede-Nicolas.local 14.5.0 Darwin Kernel Version 14.5.0: Wed Jul 29 02:26:53 - PDT 2015; root:xnu-2782.40.9~1/RELEASE_X86_64 x86_64","hostname":"MBP-sleede-Nicolas.local"}' + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin15","engine":"ruby","publisher":"stripe","uname":"Darwin + mbp-sleede-peng.home 15.4.0 Darwin Kernel Version 15.4.0: Fri Feb 26 22:08:05 + PST 2016; root:xnu-3248.40.184~3/RELEASE_X86_64 x86_64","hostname":"mbp-sleede-peng.home"}' Content-Length: - '81' response: @@ -31,11 +31,11 @@ http_interactions: Server: - nginx Date: - - Wed, 06 Apr 2016 14:31:05 GMT + - Mon, 11 Jul 2016 13:26:13 GMT Content-Type: - application/json Content-Length: - - '778' + - '780' Connection: - keep-alive Access-Control-Allow-Credentials: @@ -49,7 +49,7 @@ http_interactions: Cache-Control: - no-cache, no-store Request-Id: - - req_8Dj8hOci59D4xC + - req_8nflaXEiTf6O0X Stripe-Version: - '2015-10-16' Strict-Transport-Security: @@ -58,10 +58,10 @@ http_interactions: encoding: UTF-8 string: | { - "id": "tok_17xHgT2sOmf47Nz962TaLtD7", + "id": "tok_18W4QL2sOmf47Nz9GM695H9O", "object": "token", "card": { - "id": "card_17xHgT2sOmf47Nz98eSsgCfX", + "id": "card_18W4QL2sOmf47Nz9NuqbiJMr", "object": "card", "address_city": null, "address_country": null, @@ -84,20 +84,20 @@ http_interactions: "name": null, "tokenization_method": null }, - "client_ip": "86.76.5.109", - "created": 1459953065, + "client_ip": "82.122.118.54", + "created": 1468243573, "livemode": false, "type": "card", "used": false } http_version: - recorded_at: Wed, 06 Apr 2016 14:31:05 GMT + recorded_at: Mon, 11 Jul 2016 13:26:13 GMT - request: method: post uri: https://api.stripe.com/v1/invoiceitems body: encoding: UTF-8 - string: customer=cus_8Di1wjdVktv5kt&amount=3200¤cy=usd&description=FORM1%2B+imprimante+3D+April+11%2C+2016+14%3A00+-+03%3A00+PM + string: customer=cus_8Di1wjdVktv5kt&amount=3200¤cy=usd&description=FORM1%2B+imprimante+3D+July+10%2C+2016+14%3A00+-+03%3A00+PM headers: Accept: - "*/*; q=0.5, application/xml" @@ -110,11 +110,11 @@ http_interactions: Content-Type: - application/x-www-form-urlencoded X-Stripe-Client-User-Agent: - - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin14","engine":"ruby","publisher":"stripe","uname":"Darwin - MBP-sleede-Nicolas.local 14.5.0 Darwin Kernel Version 14.5.0: Wed Jul 29 02:26:53 - PDT 2015; root:xnu-2782.40.9~1/RELEASE_X86_64 x86_64","hostname":"MBP-sleede-Nicolas.local"}' + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin15","engine":"ruby","publisher":"stripe","uname":"Darwin + mbp-sleede-peng.home 15.4.0 Darwin Kernel Version 15.4.0: Fri Feb 26 22:08:05 + PST 2016; root:xnu-3248.40.184~3/RELEASE_X86_64 x86_64","hostname":"mbp-sleede-peng.home"}' Content-Length: - - '125' + - '124' response: status: code: 200 @@ -123,11 +123,11 @@ http_interactions: Server: - nginx Date: - - Wed, 06 Apr 2016 14:31:06 GMT + - Mon, 11 Jul 2016 13:26:14 GMT Content-Type: - application/json Content-Length: - - '469' + - '468' Connection: - keep-alive Access-Control-Allow-Credentials: @@ -141,7 +141,7 @@ http_interactions: Cache-Control: - no-cache, no-store Request-Id: - - req_8Dj87wp0RJ3Z71 + - req_8nflWbFKH0Wr7j Stripe-Version: - '2015-10-16' Strict-Transport-Security: @@ -150,20 +150,20 @@ http_interactions: encoding: UTF-8 string: | { - "id": "ii_17xHgU2sOmf47Nz9Pp3l4ZAA", + "id": "ii_18W4QM2sOmf47Nz9g7EdrbZV", "object": "invoiceitem", "amount": 3200, "currency": "usd", "customer": "cus_8Di1wjdVktv5kt", - "date": 1459953066, - "description": "FORM1+ imprimante 3D April 11, 2016 14:00 - 03:00 PM", + "date": 1468243574, + "description": "FORM1+ imprimante 3D July 10, 2016 14:00 - 03:00 PM", "discountable": true, "invoice": null, "livemode": false, "metadata": {}, "period": { - "start": 1459953066, - "end": 1459953066 + "start": 1468243574, + "end": 1468243574 }, "plan": null, "proration": false, @@ -171,7 +171,7 @@ http_interactions: "subscription": null } http_version: - recorded_at: Wed, 06 Apr 2016 14:31:06 GMT + recorded_at: Mon, 11 Jul 2016 13:26:14 GMT - request: method: get uri: https://api.stripe.com/v1/customers/cus_8Di1wjdVktv5kt @@ -190,9 +190,9 @@ http_interactions: Content-Type: - application/x-www-form-urlencoded X-Stripe-Client-User-Agent: - - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin14","engine":"ruby","publisher":"stripe","uname":"Darwin - MBP-sleede-Nicolas.local 14.5.0 Darwin Kernel Version 14.5.0: Wed Jul 29 02:26:53 - PDT 2015; root:xnu-2782.40.9~1/RELEASE_X86_64 x86_64","hostname":"MBP-sleede-Nicolas.local"}' + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin15","engine":"ruby","publisher":"stripe","uname":"Darwin + mbp-sleede-peng.home 15.4.0 Darwin Kernel Version 15.4.0: Fri Feb 26 22:08:05 + PST 2016; root:xnu-3248.40.184~3/RELEASE_X86_64 x86_64","hostname":"mbp-sleede-peng.home"}' response: status: code: 200 @@ -201,11 +201,11 @@ http_interactions: Server: - nginx Date: - - Wed, 06 Apr 2016 14:31:07 GMT + - Mon, 11 Jul 2016 13:26:15 GMT Content-Type: - application/json Content-Length: - - '3462' + - '1408' Connection: - keep-alive Access-Control-Allow-Credentials: @@ -219,7 +219,7 @@ http_interactions: Cache-Control: - no-cache, no-store Request-Id: - - req_8Dj8aOsnrVp2zU + - req_8nflczxuJx3Fit Stripe-Version: - '2015-10-16' Strict-Transport-Security: @@ -233,7 +233,7 @@ http_interactions: "account_balance": 0, "created": 1459948888, "currency": "usd", - "default_source": "card_17xGjJ2sOmf47Nz9UrQOP8Cl", + "default_source": "card_17z7CT2sOmf47Nz9wtWkhGor", "delinquent": false, "description": "Jean Dupond", "discount": null, @@ -245,7 +245,7 @@ http_interactions: "object": "list", "data": [ { - "id": "card_17xGjJ2sOmf47Nz9UrQOP8Cl", + "id": "card_17z7CT2sOmf47Nz9wtWkhGor", "object": "card", "address_city": null, "address_country": null, @@ -276,87 +276,20 @@ http_interactions: }, "subscriptions": { "object": "list", - "data": [ - { - "id": "sub_8Di9gqPLwt5IIC", - "object": "subscription", - "application_fee_percent": null, - "cancel_at_period_end": true, - "canceled_at": 1459949404, - "current_period_end": 1462541399, - "current_period_start": 1459949399, - "customer": "cus_8Di1wjdVktv5kt", - "discount": null, - "ended_at": null, - "metadata": {}, - "plan": { - "id": "mensuel-standard-month-20160404171519", - "object": "plan", - "amount": 3000, - "created": 1459782921, - "currency": "usd", - "interval": "month", - "interval_count": 1, - "livemode": false, - "metadata": {}, - "name": "Mensuel - standard, association - month", - "statement_descriptor": null, - "trial_period_days": null - }, - "quantity": 1, - "start": 1459949399, - "status": "active", - "tax_percent": null, - "trial_end": null, - "trial_start": null - }, - { - "id": "sub_8Di2VadRvr7A99", - "object": "subscription", - "application_fee_percent": null, - "cancel_at_period_end": true, - "canceled_at": 1459948972, - "current_period_end": 1462540968, - "current_period_start": 1459948968, - "customer": "cus_8Di1wjdVktv5kt", - "discount": null, - "ended_at": null, - "metadata": {}, - "plan": { - "id": "mensuel-standard-month-20160404171519", - "object": "plan", - "amount": 3000, - "created": 1459782921, - "currency": "usd", - "interval": "month", - "interval_count": 1, - "livemode": false, - "metadata": {}, - "name": "Mensuel - standard, association - month", - "statement_descriptor": null, - "trial_period_days": null - }, - "quantity": 1, - "start": 1459948968, - "status": "active", - "tax_percent": null, - "trial_end": null, - "trial_start": null - } - ], + "data": [], "has_more": false, - "total_count": 2, + "total_count": 0, "url": "/v1/customers/cus_8Di1wjdVktv5kt/subscriptions" } } http_version: - recorded_at: Wed, 06 Apr 2016 14:31:07 GMT + recorded_at: Mon, 11 Jul 2016 13:26:15 GMT - request: method: post uri: https://api.stripe.com/v1/customers/cus_8Di1wjdVktv5kt/sources body: encoding: UTF-8 - string: card=tok_17xHgT2sOmf47Nz962TaLtD7 + string: card=tok_18W4QL2sOmf47Nz9GM695H9O headers: Accept: - "*/*; q=0.5, application/xml" @@ -369,9 +302,9 @@ http_interactions: Content-Type: - application/x-www-form-urlencoded X-Stripe-Client-User-Agent: - - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin14","engine":"ruby","publisher":"stripe","uname":"Darwin - MBP-sleede-Nicolas.local 14.5.0 Darwin Kernel Version 14.5.0: Wed Jul 29 02:26:53 - PDT 2015; root:xnu-2782.40.9~1/RELEASE_X86_64 x86_64","hostname":"MBP-sleede-Nicolas.local"}' + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin15","engine":"ruby","publisher":"stripe","uname":"Darwin + mbp-sleede-peng.home 15.4.0 Darwin Kernel Version 15.4.0: Fri Feb 26 22:08:05 + PST 2016; root:xnu-3248.40.184~3/RELEASE_X86_64 x86_64","hostname":"mbp-sleede-peng.home"}' Content-Length: - '33' response: @@ -382,7 +315,7 @@ http_interactions: Server: - nginx Date: - - Wed, 06 Apr 2016 14:31:09 GMT + - Mon, 11 Jul 2016 13:26:16 GMT Content-Type: - application/json Content-Length: @@ -400,7 +333,7 @@ http_interactions: Cache-Control: - no-cache, no-store Request-Id: - - req_8Dj8Q0yiZoox2F + - req_8nflIAexuhWlA2 Stripe-Version: - '2015-10-16' body: @@ -415,10 +348,10 @@ http_interactions: } } http_version: - recorded_at: Wed, 06 Apr 2016 14:31:09 GMT + recorded_at: Mon, 11 Jul 2016 13:26:16 GMT - request: method: delete - uri: https://api.stripe.com/v1/invoiceitems/ii_17xHgU2sOmf47Nz9Pp3l4ZAA + uri: https://api.stripe.com/v1/invoiceitems/ii_18W4QM2sOmf47Nz9g7EdrbZV body: encoding: US-ASCII string: '' @@ -434,9 +367,9 @@ http_interactions: Content-Type: - application/x-www-form-urlencoded X-Stripe-Client-User-Agent: - - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin14","engine":"ruby","publisher":"stripe","uname":"Darwin - MBP-sleede-Nicolas.local 14.5.0 Darwin Kernel Version 14.5.0: Wed Jul 29 02:26:53 - PDT 2015; root:xnu-2782.40.9~1/RELEASE_X86_64 x86_64","hostname":"MBP-sleede-Nicolas.local"}' + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin15","engine":"ruby","publisher":"stripe","uname":"Darwin + mbp-sleede-peng.home 15.4.0 Darwin Kernel Version 15.4.0: Fri Feb 26 22:08:05 + PST 2016; root:xnu-3248.40.184~3/RELEASE_X86_64 x86_64","hostname":"mbp-sleede-peng.home"}' response: status: code: 200 @@ -445,7 +378,7 @@ http_interactions: Server: - nginx Date: - - Wed, 06 Apr 2016 14:31:09 GMT + - Mon, 11 Jul 2016 13:26:17 GMT Content-Type: - application/json Content-Length: @@ -463,7 +396,7 @@ http_interactions: Cache-Control: - no-cache, no-store Request-Id: - - req_8Dj86hW0UVRjBL + - req_8nflVBopesOpHn Stripe-Version: - '2015-10-16' Strict-Transport-Security: @@ -473,8 +406,8 @@ http_interactions: string: | { "deleted": true, - "id": "ii_17xHgU2sOmf47Nz9Pp3l4ZAA" + "id": "ii_18W4QM2sOmf47Nz9g7EdrbZV" } http_version: - recorded_at: Wed, 06 Apr 2016 14:31:09 GMT + recorded_at: Mon, 11 Jul 2016 13:26:17 GMT recorded_with: VCR 3.0.1 diff --git a/test/vcr_cassettes/reservations_create_for_training_and_plan_by_pay_wallet_success.yml b/test/vcr_cassettes/reservations_create_for_training_and_plan_by_pay_wallet_success.yml new file mode 100644 index 000000000..c4fbf5854 --- /dev/null +++ b/test/vcr_cassettes/reservations_create_for_training_and_plan_by_pay_wallet_success.yml @@ -0,0 +1,1149 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.stripe.com/v1/tokens + body: + encoding: UTF-8 + string: card[number]=4242424242424242&card[exp_month]=4&card[exp_year]=2017&card[cvc]=314 + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin15","engine":"ruby","publisher":"stripe","uname":"Darwin + mbp-sleede-peng.home 15.6.0 Darwin Kernel Version 15.6.0: Thu Jun 23 18:25:34 + PDT 2016; root:xnu-3248.60.10~1/RELEASE_X86_64 x86_64","hostname":"mbp-sleede-peng.home"}' + Content-Length: + - '81' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 21 Jul 2016 13:45:41 GMT + Content-Type: + - application/json + Content-Length: + - '778' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_8rQLLl2gl7snRL + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "tok_18ZhUf2sOmf47Nz96Yt86HJI", + "object": "token", + "card": { + "id": "card_18ZhUf2sOmf47Nz9LhkQeEmO", + "object": "card", + "address_city": null, + "address_country": null, + "address_line1": null, + "address_line1_check": null, + "address_line2": null, + "address_state": null, + "address_zip": null, + "address_zip_check": null, + "brand": "Visa", + "country": "US", + "cvc_check": "unchecked", + "dynamic_last4": null, + "exp_month": 4, + "exp_year": 2017, + "fingerprint": "o52jybR7bnmNn6AT", + "funding": "credit", + "last4": "4242", + "metadata": {}, + "name": null, + "tokenization_method": null + }, + "client_ip": "90.52.50.40", + "created": 1469108741, + "livemode": false, + "type": "card", + "used": false + } + http_version: + recorded_at: Thu, 21 Jul 2016 13:45:41 GMT +- request: + method: post + uri: https://api.stripe.com/v1/invoiceitems + body: + encoding: UTF-8 + string: customer=cus_8CzNtM08NVlSGN&amount=0¤cy=usd&description=Formation+Imprimante+3D+July+18%2C+2016+08%3A00+-+12%3A00+PM + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin15","engine":"ruby","publisher":"stripe","uname":"Darwin + mbp-sleede-peng.home 15.6.0 Darwin Kernel Version 15.6.0: Thu Jun 23 18:25:34 + PDT 2016; root:xnu-3248.60.10~1/RELEASE_X86_64 x86_64","hostname":"mbp-sleede-peng.home"}' + Content-Length: + - '122' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 21 Jul 2016 13:45:42 GMT + Content-Type: + - application/json + Content-Length: + - '468' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_8rQLLf16BTkRiQ + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "ii_18ZhUg2sOmf47Nz9vSsl5J9z", + "object": "invoiceitem", + "amount": 0, + "currency": "usd", + "customer": "cus_8CzNtM08NVlSGN", + "date": 1469108742, + "description": "Formation Imprimante 3D July 18, 2016 08:00 - 12:00 PM", + "discountable": true, + "invoice": null, + "livemode": false, + "metadata": {}, + "period": { + "start": 1469108742, + "end": 1469108742 + }, + "plan": null, + "proration": false, + "quantity": null, + "subscription": null + } + http_version: + recorded_at: Thu, 21 Jul 2016 13:45:42 GMT +- request: + method: post + uri: https://api.stripe.com/v1/invoiceitems + body: + encoding: UTF-8 + string: customer=cus_8CzNtM08NVlSGN&amount=-1000¤cy=usd&description=wallet+-10.0 + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin15","engine":"ruby","publisher":"stripe","uname":"Darwin + mbp-sleede-peng.home 15.6.0 Darwin Kernel Version 15.6.0: Thu Jun 23 18:25:34 + PDT 2016; root:xnu-3248.60.10~1/RELEASE_X86_64 x86_64","hostname":"mbp-sleede-peng.home"}' + Content-Length: + - '78' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 21 Jul 2016 13:45:43 GMT + Content-Type: + - application/json + Content-Length: + - '431' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_8rQLqGKwXBxt3a + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "ii_18ZhUh2sOmf47Nz902ykGybB", + "object": "invoiceitem", + "amount": -1000, + "currency": "usd", + "customer": "cus_8CzNtM08NVlSGN", + "date": 1469108743, + "description": "wallet -10.0", + "discountable": false, + "invoice": null, + "livemode": false, + "metadata": {}, + "period": { + "start": 1469108743, + "end": 1469108743 + }, + "plan": null, + "proration": false, + "quantity": null, + "subscription": null + } + http_version: + recorded_at: Thu, 21 Jul 2016 13:45:43 GMT +- request: + method: get + uri: https://api.stripe.com/v1/customers/cus_8CzNtM08NVlSGN + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin15","engine":"ruby","publisher":"stripe","uname":"Darwin + mbp-sleede-peng.home 15.6.0 Darwin Kernel Version 15.6.0: Thu Jun 23 18:25:34 + PDT 2016; root:xnu-3248.60.10~1/RELEASE_X86_64 x86_64","hostname":"mbp-sleede-peng.home"}' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 21 Jul 2016 13:45:44 GMT + Content-Type: + - application/json + Content-Length: + - '2561' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_8rQLPVnSOwscpK + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: ASCII-8BIT + string: !binary |- + ewogICJpZCI6ICJjdXNfOEN6TnRNMDhOVmxTR04iLAogICJvYmplY3QiOiAi + Y3VzdG9tZXIiLAogICJhY2NvdW50X2JhbGFuY2UiOiAwLAogICJjcmVhdGVk + IjogMTQ1OTc4Mjg0OSwKICAiY3VycmVuY3kiOiAidXNkIiwKICAiZGVmYXVs + dF9zb3VyY2UiOiAiY2FyZF8xOFc0R0syc09tZjQ3Tno5SzZkZlNtWmwiLAog + ICJkZWxpbnF1ZW50IjogZmFsc2UsCiAgImRlc2NyaXB0aW9uIjogIlZhbmVz + c2EgTG9uY2hhbXAiLAogICJkaXNjb3VudCI6IG51bGwsCiAgImVtYWlsIjog + InZhbmVzc2EubG9uY2hhbXBAc2ZyLmZyIiwKICAibGl2ZW1vZGUiOiBmYWxz + ZSwKICAibWV0YWRhdGEiOiB7fSwKICAic2hpcHBpbmciOiBudWxsLAogICJz + b3VyY2VzIjogewogICAgIm9iamVjdCI6ICJsaXN0IiwKICAgICJkYXRhIjog + WwogICAgICB7CiAgICAgICAgImlkIjogImNhcmRfMThXNEdLMnNPbWY0N056 + OUs2ZGZTbVpsIiwKICAgICAgICAib2JqZWN0IjogImNhcmQiLAogICAgICAg + ICJhZGRyZXNzX2NpdHkiOiBudWxsLAogICAgICAgICJhZGRyZXNzX2NvdW50 + cnkiOiBudWxsLAogICAgICAgICJhZGRyZXNzX2xpbmUxIjogbnVsbCwKICAg + ICAgICAiYWRkcmVzc19saW5lMV9jaGVjayI6IG51bGwsCiAgICAgICAgImFk + ZHJlc3NfbGluZTIiOiBudWxsLAogICAgICAgICJhZGRyZXNzX3N0YXRlIjog + bnVsbCwKICAgICAgICAiYWRkcmVzc196aXAiOiBudWxsLAogICAgICAgICJh + ZGRyZXNzX3ppcF9jaGVjayI6IG51bGwsCiAgICAgICAgImJyYW5kIjogIlZp + c2EiLAogICAgICAgICJjb3VudHJ5IjogIlVTIiwKICAgICAgICAiY3VzdG9t + ZXIiOiAiY3VzXzhDek50TTA4TlZsU0dOIiwKICAgICAgICAiY3ZjX2NoZWNr + IjogInBhc3MiLAogICAgICAgICJkeW5hbWljX2xhc3Q0IjogbnVsbCwKICAg + ICAgICAiZXhwX21vbnRoIjogNCwKICAgICAgICAiZXhwX3llYXIiOiAyMDE3 + LAogICAgICAgICJmaW5nZXJwcmludCI6ICJvNTJqeWJSN2JubU5uNkFUIiwK + ICAgICAgICAiZnVuZGluZyI6ICJjcmVkaXQiLAogICAgICAgICJsYXN0NCI6 + ICI0MjQyIiwKICAgICAgICAibWV0YWRhdGEiOiB7fSwKICAgICAgICAibmFt + ZSI6IG51bGwsCiAgICAgICAgInRva2VuaXphdGlvbl9tZXRob2QiOiBudWxs + CiAgICAgIH0KICAgIF0sCiAgICAiaGFzX21vcmUiOiBmYWxzZSwKICAgICJ0 + b3RhbF9jb3VudCI6IDEsCiAgICAidXJsIjogIi92MS9jdXN0b21lcnMvY3Vz + XzhDek50TTA4TlZsU0dOL3NvdXJjZXMiCiAgfSwKICAic3Vic2NyaXB0aW9u + cyI6IHsKICAgICJvYmplY3QiOiAibGlzdCIsCiAgICAiZGF0YSI6IFsKICAg + ICAgewogICAgICAgICJpZCI6ICJzdWJfOG5mYlRHR0lRUlF6eDEiLAogICAg + ICAgICJvYmplY3QiOiAic3Vic2NyaXB0aW9uIiwKICAgICAgICAiYXBwbGlj + YXRpb25fZmVlX3BlcmNlbnQiOiBudWxsLAogICAgICAgICJjYW5jZWxfYXRf + cGVyaW9kX2VuZCI6IHRydWUsCiAgICAgICAgImNhbmNlbGVkX2F0IjogMTQ2 + ODI0Mjk2MCwKICAgICAgICAiY3JlYXRlZCI6IDE0NjgyNDI5NTYsCiAgICAg + ICAgImN1cnJlbnRfcGVyaW9kX2VuZCI6IDE0NzA5MjEzNTYsCiAgICAgICAg + ImN1cnJlbnRfcGVyaW9kX3N0YXJ0IjogMTQ2ODI0Mjk1NiwKICAgICAgICAi + Y3VzdG9tZXIiOiAiY3VzXzhDek50TTA4TlZsU0dOIiwKICAgICAgICAiZGlz + Y291bnQiOiBudWxsLAogICAgICAgICJlbmRlZF9hdCI6IG51bGwsCiAgICAg + ICAgImxpdmVtb2RlIjogZmFsc2UsCiAgICAgICAgIm1ldGFkYXRhIjoge30s + CiAgICAgICAgInBsYW4iOiB7CiAgICAgICAgICAiaWQiOiAibWVuc3VlbC10 + YXJpZi1yZWR1aXQtc3R1ZGVudC1tb250aC0yMDE2MDQwNDE3MTgyNyIsCiAg + ICAgICAgICAib2JqZWN0IjogInBsYW4iLAogICAgICAgICAgImFtb3VudCI6 + IDIwMDAsCiAgICAgICAgICAiY3JlYXRlZCI6IDE0NTk3ODMxMDgsCiAgICAg + ICAgICAiY3VycmVuY3kiOiAidXNkIiwKICAgICAgICAgICJpbnRlcnZhbCI6 + ICJtb250aCIsCiAgICAgICAgICAiaW50ZXJ2YWxfY291bnQiOiAxLAogICAg + ICAgICAgImxpdmVtb2RlIjogZmFsc2UsCiAgICAgICAgICAibWV0YWRhdGEi + OiB7fSwKICAgICAgICAgICJuYW1lIjogIk1lbnN1ZWwgdGFyaWYgcsOpZHVp + dCAtIMOpdHVkaWFudCwgLSBkZSAyNSBhbnMsIGVuc2VpZ25hbnQsIGRlbWFu + ZGV1ciBkJ2VtcGxvaSAtIG1vbnRoIiwKICAgICAgICAgICJzdGF0ZW1lbnRf + ZGVzY3JpcHRvciI6IG51bGwsCiAgICAgICAgICAidHJpYWxfcGVyaW9kX2Rh + eXMiOiBudWxsCiAgICAgICAgfSwKICAgICAgICAicXVhbnRpdHkiOiAxLAog + ICAgICAgICJzdGFydCI6IDE0NjgyNDI5NTYsCiAgICAgICAgInN0YXR1cyI6 + ICJhY3RpdmUiLAogICAgICAgICJ0YXhfcGVyY2VudCI6IG51bGwsCiAgICAg + ICAgInRyaWFsX2VuZCI6IG51bGwsCiAgICAgICAgInRyaWFsX3N0YXJ0Ijog + bnVsbAogICAgICB9CiAgICBdLAogICAgImhhc19tb3JlIjogZmFsc2UsCiAg + ICAidG90YWxfY291bnQiOiAxLAogICAgInVybCI6ICIvdjEvY3VzdG9tZXJz + L2N1c184Q3pOdE0wOE5WbFNHTi9zdWJzY3JpcHRpb25zIgogIH0KfQo= + http_version: + recorded_at: Thu, 21 Jul 2016 13:45:44 GMT +- request: + method: get + uri: https://api.stripe.com/v1/customers/cus_8CzNtM08NVlSGN + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin15","engine":"ruby","publisher":"stripe","uname":"Darwin + mbp-sleede-peng.home 15.6.0 Darwin Kernel Version 15.6.0: Thu Jun 23 18:25:34 + PDT 2016; root:xnu-3248.60.10~1/RELEASE_X86_64 x86_64","hostname":"mbp-sleede-peng.home"}' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 21 Jul 2016 13:45:45 GMT + Content-Type: + - application/json + Content-Length: + - '2561' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_8rQLZ9w0ZwAlFB + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: ASCII-8BIT + string: !binary |- + ewogICJpZCI6ICJjdXNfOEN6TnRNMDhOVmxTR04iLAogICJvYmplY3QiOiAi + Y3VzdG9tZXIiLAogICJhY2NvdW50X2JhbGFuY2UiOiAwLAogICJjcmVhdGVk + IjogMTQ1OTc4Mjg0OSwKICAiY3VycmVuY3kiOiAidXNkIiwKICAiZGVmYXVs + dF9zb3VyY2UiOiAiY2FyZF8xOFc0R0syc09tZjQ3Tno5SzZkZlNtWmwiLAog + ICJkZWxpbnF1ZW50IjogZmFsc2UsCiAgImRlc2NyaXB0aW9uIjogIlZhbmVz + c2EgTG9uY2hhbXAiLAogICJkaXNjb3VudCI6IG51bGwsCiAgImVtYWlsIjog + InZhbmVzc2EubG9uY2hhbXBAc2ZyLmZyIiwKICAibGl2ZW1vZGUiOiBmYWxz + ZSwKICAibWV0YWRhdGEiOiB7fSwKICAic2hpcHBpbmciOiBudWxsLAogICJz + b3VyY2VzIjogewogICAgIm9iamVjdCI6ICJsaXN0IiwKICAgICJkYXRhIjog + WwogICAgICB7CiAgICAgICAgImlkIjogImNhcmRfMThXNEdLMnNPbWY0N056 + OUs2ZGZTbVpsIiwKICAgICAgICAib2JqZWN0IjogImNhcmQiLAogICAgICAg + ICJhZGRyZXNzX2NpdHkiOiBudWxsLAogICAgICAgICJhZGRyZXNzX2NvdW50 + cnkiOiBudWxsLAogICAgICAgICJhZGRyZXNzX2xpbmUxIjogbnVsbCwKICAg + ICAgICAiYWRkcmVzc19saW5lMV9jaGVjayI6IG51bGwsCiAgICAgICAgImFk + ZHJlc3NfbGluZTIiOiBudWxsLAogICAgICAgICJhZGRyZXNzX3N0YXRlIjog + bnVsbCwKICAgICAgICAiYWRkcmVzc196aXAiOiBudWxsLAogICAgICAgICJh + ZGRyZXNzX3ppcF9jaGVjayI6IG51bGwsCiAgICAgICAgImJyYW5kIjogIlZp + c2EiLAogICAgICAgICJjb3VudHJ5IjogIlVTIiwKICAgICAgICAiY3VzdG9t + ZXIiOiAiY3VzXzhDek50TTA4TlZsU0dOIiwKICAgICAgICAiY3ZjX2NoZWNr + IjogInBhc3MiLAogICAgICAgICJkeW5hbWljX2xhc3Q0IjogbnVsbCwKICAg + ICAgICAiZXhwX21vbnRoIjogNCwKICAgICAgICAiZXhwX3llYXIiOiAyMDE3 + LAogICAgICAgICJmaW5nZXJwcmludCI6ICJvNTJqeWJSN2JubU5uNkFUIiwK + ICAgICAgICAiZnVuZGluZyI6ICJjcmVkaXQiLAogICAgICAgICJsYXN0NCI6 + ICI0MjQyIiwKICAgICAgICAibWV0YWRhdGEiOiB7fSwKICAgICAgICAibmFt + ZSI6IG51bGwsCiAgICAgICAgInRva2VuaXphdGlvbl9tZXRob2QiOiBudWxs + CiAgICAgIH0KICAgIF0sCiAgICAiaGFzX21vcmUiOiBmYWxzZSwKICAgICJ0 + b3RhbF9jb3VudCI6IDEsCiAgICAidXJsIjogIi92MS9jdXN0b21lcnMvY3Vz + XzhDek50TTA4TlZsU0dOL3NvdXJjZXMiCiAgfSwKICAic3Vic2NyaXB0aW9u + cyI6IHsKICAgICJvYmplY3QiOiAibGlzdCIsCiAgICAiZGF0YSI6IFsKICAg + ICAgewogICAgICAgICJpZCI6ICJzdWJfOG5mYlRHR0lRUlF6eDEiLAogICAg + ICAgICJvYmplY3QiOiAic3Vic2NyaXB0aW9uIiwKICAgICAgICAiYXBwbGlj + YXRpb25fZmVlX3BlcmNlbnQiOiBudWxsLAogICAgICAgICJjYW5jZWxfYXRf + cGVyaW9kX2VuZCI6IHRydWUsCiAgICAgICAgImNhbmNlbGVkX2F0IjogMTQ2 + ODI0Mjk2MCwKICAgICAgICAiY3JlYXRlZCI6IDE0NjgyNDI5NTYsCiAgICAg + ICAgImN1cnJlbnRfcGVyaW9kX2VuZCI6IDE0NzA5MjEzNTYsCiAgICAgICAg + ImN1cnJlbnRfcGVyaW9kX3N0YXJ0IjogMTQ2ODI0Mjk1NiwKICAgICAgICAi + Y3VzdG9tZXIiOiAiY3VzXzhDek50TTA4TlZsU0dOIiwKICAgICAgICAiZGlz + Y291bnQiOiBudWxsLAogICAgICAgICJlbmRlZF9hdCI6IG51bGwsCiAgICAg + ICAgImxpdmVtb2RlIjogZmFsc2UsCiAgICAgICAgIm1ldGFkYXRhIjoge30s + CiAgICAgICAgInBsYW4iOiB7CiAgICAgICAgICAiaWQiOiAibWVuc3VlbC10 + YXJpZi1yZWR1aXQtc3R1ZGVudC1tb250aC0yMDE2MDQwNDE3MTgyNyIsCiAg + ICAgICAgICAib2JqZWN0IjogInBsYW4iLAogICAgICAgICAgImFtb3VudCI6 + IDIwMDAsCiAgICAgICAgICAiY3JlYXRlZCI6IDE0NTk3ODMxMDgsCiAgICAg + ICAgICAiY3VycmVuY3kiOiAidXNkIiwKICAgICAgICAgICJpbnRlcnZhbCI6 + ICJtb250aCIsCiAgICAgICAgICAiaW50ZXJ2YWxfY291bnQiOiAxLAogICAg + ICAgICAgImxpdmVtb2RlIjogZmFsc2UsCiAgICAgICAgICAibWV0YWRhdGEi + OiB7fSwKICAgICAgICAgICJuYW1lIjogIk1lbnN1ZWwgdGFyaWYgcsOpZHVp + dCAtIMOpdHVkaWFudCwgLSBkZSAyNSBhbnMsIGVuc2VpZ25hbnQsIGRlbWFu + ZGV1ciBkJ2VtcGxvaSAtIG1vbnRoIiwKICAgICAgICAgICJzdGF0ZW1lbnRf + ZGVzY3JpcHRvciI6IG51bGwsCiAgICAgICAgICAidHJpYWxfcGVyaW9kX2Rh + eXMiOiBudWxsCiAgICAgICAgfSwKICAgICAgICAicXVhbnRpdHkiOiAxLAog + ICAgICAgICJzdGFydCI6IDE0NjgyNDI5NTYsCiAgICAgICAgInN0YXR1cyI6 + ICJhY3RpdmUiLAogICAgICAgICJ0YXhfcGVyY2VudCI6IG51bGwsCiAgICAg + ICAgInRyaWFsX2VuZCI6IG51bGwsCiAgICAgICAgInRyaWFsX3N0YXJ0Ijog + bnVsbAogICAgICB9CiAgICBdLAogICAgImhhc19tb3JlIjogZmFsc2UsCiAg + ICAidG90YWxfY291bnQiOiAxLAogICAgInVybCI6ICIvdjEvY3VzdG9tZXJz + L2N1c184Q3pOdE0wOE5WbFNHTi9zdWJzY3JpcHRpb25zIgogIH0KfQo= + http_version: + recorded_at: Thu, 21 Jul 2016 13:45:45 GMT +- request: + method: post + uri: https://api.stripe.com/v1/customers/cus_8CzNtM08NVlSGN/subscriptions + body: + encoding: UTF-8 + string: plan=mensuel-tarif-reduit-student-month-20160404171827&source=tok_18ZhUf2sOmf47Nz96Yt86HJI + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin15","engine":"ruby","publisher":"stripe","uname":"Darwin + mbp-sleede-peng.home 15.6.0 Darwin Kernel Version 15.6.0: Thu Jun 23 18:25:34 + PDT 2016; root:xnu-3248.60.10~1/RELEASE_X86_64 x86_64","hostname":"mbp-sleede-peng.home"}' + Content-Length: + - '90' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 21 Jul 2016 13:45:46 GMT + Content-Type: + - application/json + Content-Length: + - '926' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_8rQL1c3JVc0vk5 + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: ASCII-8BIT + string: !binary |- + ewogICJpZCI6ICJzdWJfOHJRTGtsVVVVUWU2NmsiLAogICJvYmplY3QiOiAi + c3Vic2NyaXB0aW9uIiwKICAiYXBwbGljYXRpb25fZmVlX3BlcmNlbnQiOiBu + dWxsLAogICJjYW5jZWxfYXRfcGVyaW9kX2VuZCI6IGZhbHNlLAogICJjYW5j + ZWxlZF9hdCI6IG51bGwsCiAgImNyZWF0ZWQiOiAxNDY5MTA4NzQ1LAogICJj + dXJyZW50X3BlcmlvZF9lbmQiOiAxNDcxNzg3MTQ1LAogICJjdXJyZW50X3Bl + cmlvZF9zdGFydCI6IDE0NjkxMDg3NDUsCiAgImN1c3RvbWVyIjogImN1c184 + Q3pOdE0wOE5WbFNHTiIsCiAgImRpc2NvdW50IjogbnVsbCwKICAiZW5kZWRf + YXQiOiBudWxsLAogICJsaXZlbW9kZSI6IGZhbHNlLAogICJtZXRhZGF0YSI6 + IHt9LAogICJwbGFuIjogewogICAgImlkIjogIm1lbnN1ZWwtdGFyaWYtcmVk + dWl0LXN0dWRlbnQtbW9udGgtMjAxNjA0MDQxNzE4MjciLAogICAgIm9iamVj + dCI6ICJwbGFuIiwKICAgICJhbW91bnQiOiAyMDAwLAogICAgImNyZWF0ZWQi + OiAxNDU5NzgzMTA4LAogICAgImN1cnJlbmN5IjogInVzZCIsCiAgICAiaW50 + ZXJ2YWwiOiAibW9udGgiLAogICAgImludGVydmFsX2NvdW50IjogMSwKICAg + ICJsaXZlbW9kZSI6IGZhbHNlLAogICAgIm1ldGFkYXRhIjoge30sCiAgICAi + bmFtZSI6ICJNZW5zdWVsIHRhcmlmIHLDqWR1aXQgLSDDqXR1ZGlhbnQsIC0g + ZGUgMjUgYW5zLCBlbnNlaWduYW50LCBkZW1hbmRldXIgZCdlbXBsb2kgLSBt + b250aCIsCiAgICAic3RhdGVtZW50X2Rlc2NyaXB0b3IiOiBudWxsLAogICAg + InRyaWFsX3BlcmlvZF9kYXlzIjogbnVsbAogIH0sCiAgInF1YW50aXR5Ijog + MSwKICAic3RhcnQiOiAxNDY5MTA4NzQ1LAogICJzdGF0dXMiOiAiYWN0aXZl + IiwKICAidGF4X3BlcmNlbnQiOiBudWxsLAogICJ0cmlhbF9lbmQiOiBudWxs + LAogICJ0cmlhbF9zdGFydCI6IG51bGwKfQo= + http_version: + recorded_at: Thu, 21 Jul 2016 13:45:46 GMT +- request: + method: get + uri: https://api.stripe.com/v1/invoices?customer=cus_8CzNtM08NVlSGN&limit=1 + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin15","engine":"ruby","publisher":"stripe","uname":"Darwin + mbp-sleede-peng.home 15.6.0 Darwin Kernel Version 15.6.0: Thu Jun 23 18:25:34 + PDT 2016; root:xnu-3248.60.10~1/RELEASE_X86_64 x86_64","hostname":"mbp-sleede-peng.home"}' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 21 Jul 2016 13:45:47 GMT + Content-Type: + - application/json + Content-Length: + - '3439' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_8rQLSdX3E9CFcJ + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: ASCII-8BIT + string: !binary |- + ewogICJvYmplY3QiOiAibGlzdCIsCiAgImRhdGEiOiBbCiAgICB7CiAgICAg + ICJpZCI6ICJpbl8xOFpoVWoyc09tZjQ3Tno5cjhMS0lCTksiLAogICAgICAi + b2JqZWN0IjogImludm9pY2UiLAogICAgICAiYW1vdW50X2R1ZSI6IDEwMDAs + CiAgICAgICJhcHBsaWNhdGlvbl9mZWUiOiBudWxsLAogICAgICAiYXR0ZW1w + dF9jb3VudCI6IDEsCiAgICAgICJhdHRlbXB0ZWQiOiB0cnVlLAogICAgICAi + Y2hhcmdlIjogImNoXzE4WmhVazJzT21mNDdOejl2VVpXbWFZdSIsCiAgICAg + ICJjbG9zZWQiOiB0cnVlLAogICAgICAiY3VycmVuY3kiOiAidXNkIiwKICAg + ICAgImN1c3RvbWVyIjogImN1c184Q3pOdE0wOE5WbFNHTiIsCiAgICAgICJk + YXRlIjogMTQ2OTEwODc0NSwKICAgICAgImRlc2NyaXB0aW9uIjogbnVsbCwK + ICAgICAgImRpc2NvdW50IjogbnVsbCwKICAgICAgImVuZGluZ19iYWxhbmNl + IjogMCwKICAgICAgImZvcmdpdmVuIjogZmFsc2UsCiAgICAgICJsaW5lcyI6 + IHsKICAgICAgICAib2JqZWN0IjogImxpc3QiLAogICAgICAgICJkYXRhIjog + WwogICAgICAgICAgewogICAgICAgICAgICAiaWQiOiAiaWlfMThaaFVoMnNP + bWY0N056OTAyeWtHeWJCIiwKICAgICAgICAgICAgIm9iamVjdCI6ICJsaW5l + X2l0ZW0iLAogICAgICAgICAgICAiYW1vdW50IjogLTEwMDAsCiAgICAgICAg + ICAgICJjdXJyZW5jeSI6ICJ1c2QiLAogICAgICAgICAgICAiZGVzY3JpcHRp + b24iOiAid2FsbGV0IC0xMC4wIiwKICAgICAgICAgICAgImRpc2NvdW50YWJs + ZSI6IGZhbHNlLAogICAgICAgICAgICAibGl2ZW1vZGUiOiBmYWxzZSwKICAg + ICAgICAgICAgIm1ldGFkYXRhIjoge30sCiAgICAgICAgICAgICJwZXJpb2Qi + OiB7CiAgICAgICAgICAgICAgInN0YXJ0IjogMTQ2OTEwODc0MywKICAgICAg + ICAgICAgICAiZW5kIjogMTQ2OTEwODc0MwogICAgICAgICAgICB9LAogICAg + ICAgICAgICAicGxhbiI6IG51bGwsCiAgICAgICAgICAgICJwcm9yYXRpb24i + OiBmYWxzZSwKICAgICAgICAgICAgInF1YW50aXR5IjogbnVsbCwKICAgICAg + ICAgICAgInN1YnNjcmlwdGlvbiI6IG51bGwsCiAgICAgICAgICAgICJ0eXBl + IjogImludm9pY2VpdGVtIgogICAgICAgICAgfSwKICAgICAgICAgIHsKICAg + ICAgICAgICAgImlkIjogImlpXzE4WmhVZzJzT21mNDdOejl2U3NsNUo5eiIs + CiAgICAgICAgICAgICJvYmplY3QiOiAibGluZV9pdGVtIiwKICAgICAgICAg + ICAgImFtb3VudCI6IDAsCiAgICAgICAgICAgICJjdXJyZW5jeSI6ICJ1c2Qi + LAogICAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiRm9ybWF0aW9uIEltcHJp + bWFudGUgM0QgSnVseSAxOCwgMjAxNiAwODowMCAtIDEyOjAwIFBNIiwKICAg + ICAgICAgICAgImRpc2NvdW50YWJsZSI6IHRydWUsCiAgICAgICAgICAgICJs + aXZlbW9kZSI6IGZhbHNlLAogICAgICAgICAgICAibWV0YWRhdGEiOiB7fSwK + ICAgICAgICAgICAgInBlcmlvZCI6IHsKICAgICAgICAgICAgICAic3RhcnQi + OiAxNDY5MTA4NzQyLAogICAgICAgICAgICAgICJlbmQiOiAxNDY5MTA4NzQy + CiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJwbGFuIjogbnVsbCwKICAg + ICAgICAgICAgInByb3JhdGlvbiI6IGZhbHNlLAogICAgICAgICAgICAicXVh + bnRpdHkiOiBudWxsLAogICAgICAgICAgICAic3Vic2NyaXB0aW9uIjogbnVs + bCwKICAgICAgICAgICAgInR5cGUiOiAiaW52b2ljZWl0ZW0iCiAgICAgICAg + ICB9LAogICAgICAgICAgewogICAgICAgICAgICAiaWQiOiAic3ViXzhyUUxr + bFVVVVFlNjZrIiwKICAgICAgICAgICAgIm9iamVjdCI6ICJsaW5lX2l0ZW0i + LAogICAgICAgICAgICAiYW1vdW50IjogMjAwMCwKICAgICAgICAgICAgImN1 + cnJlbmN5IjogInVzZCIsCiAgICAgICAgICAgICJkZXNjcmlwdGlvbiI6IG51 + bGwsCiAgICAgICAgICAgICJkaXNjb3VudGFibGUiOiB0cnVlLAogICAgICAg + ICAgICAibGl2ZW1vZGUiOiBmYWxzZSwKICAgICAgICAgICAgIm1ldGFkYXRh + Ijoge30sCiAgICAgICAgICAgICJwZXJpb2QiOiB7CiAgICAgICAgICAgICAg + InN0YXJ0IjogMTQ2OTEwODc0NSwKICAgICAgICAgICAgICAiZW5kIjogMTQ3 + MTc4NzE0NQogICAgICAgICAgICB9LAogICAgICAgICAgICAicGxhbiI6IHsK + ICAgICAgICAgICAgICAiaWQiOiAibWVuc3VlbC10YXJpZi1yZWR1aXQtc3R1 + ZGVudC1tb250aC0yMDE2MDQwNDE3MTgyNyIsCiAgICAgICAgICAgICAgIm9i + amVjdCI6ICJwbGFuIiwKICAgICAgICAgICAgICAiYW1vdW50IjogMjAwMCwK + ICAgICAgICAgICAgICAiY3JlYXRlZCI6IDE0NTk3ODMxMDgsCiAgICAgICAg + ICAgICAgImN1cnJlbmN5IjogInVzZCIsCiAgICAgICAgICAgICAgImludGVy + dmFsIjogIm1vbnRoIiwKICAgICAgICAgICAgICAiaW50ZXJ2YWxfY291bnQi + OiAxLAogICAgICAgICAgICAgICJsaXZlbW9kZSI6IGZhbHNlLAogICAgICAg + ICAgICAgICJtZXRhZGF0YSI6IHt9LAogICAgICAgICAgICAgICJuYW1lIjog + Ik1lbnN1ZWwgdGFyaWYgcsOpZHVpdCAtIMOpdHVkaWFudCwgLSBkZSAyNSBh + bnMsIGVuc2VpZ25hbnQsIGRlbWFuZGV1ciBkJ2VtcGxvaSAtIG1vbnRoIiwK + ICAgICAgICAgICAgICAic3RhdGVtZW50X2Rlc2NyaXB0b3IiOiBudWxsLAog + ICAgICAgICAgICAgICJ0cmlhbF9wZXJpb2RfZGF5cyI6IG51bGwKICAgICAg + ICAgICAgfSwKICAgICAgICAgICAgInByb3JhdGlvbiI6IGZhbHNlLAogICAg + ICAgICAgICAicXVhbnRpdHkiOiAxLAogICAgICAgICAgICAic3Vic2NyaXB0 + aW9uIjogbnVsbCwKICAgICAgICAgICAgInR5cGUiOiAic3Vic2NyaXB0aW9u + IgogICAgICAgICAgfQogICAgICAgIF0sCiAgICAgICAgImhhc19tb3JlIjog + ZmFsc2UsCiAgICAgICAgInRvdGFsX2NvdW50IjogMywKICAgICAgICAidXJs + IjogIi92MS9pbnZvaWNlcy9pbl8xOFpoVWoyc09tZjQ3Tno5cjhMS0lCTksv + bGluZXMiCiAgICAgIH0sCiAgICAgICJsaXZlbW9kZSI6IGZhbHNlLAogICAg + ICAibWV0YWRhdGEiOiB7fSwKICAgICAgIm5leHRfcGF5bWVudF9hdHRlbXB0 + IjogbnVsbCwKICAgICAgInBhaWQiOiB0cnVlLAogICAgICAicGVyaW9kX2Vu + ZCI6IDE0NjkxMDg3NDUsCiAgICAgICJwZXJpb2Rfc3RhcnQiOiAxNDY4MjQy + OTU2LAogICAgICAicmVjZWlwdF9udW1iZXIiOiBudWxsLAogICAgICAic3Rh + cnRpbmdfYmFsYW5jZSI6IDAsCiAgICAgICJzdGF0ZW1lbnRfZGVzY3JpcHRv + ciI6IG51bGwsCiAgICAgICJzdWJzY3JpcHRpb24iOiAic3ViXzhyUUxrbFVV + VVFlNjZrIiwKICAgICAgInN1YnRvdGFsIjogMTAwMCwKICAgICAgInRheCI6 + IG51bGwsCiAgICAgICJ0YXhfcGVyY2VudCI6IG51bGwsCiAgICAgICJ0b3Rh + bCI6IDEwMDAsCiAgICAgICJ3ZWJob29rc19kZWxpdmVyZWRfYXQiOiAxNDY5 + MTA4NzQ2CiAgICB9CiAgXSwKICAiaGFzX21vcmUiOiB0cnVlLAogICJ1cmwi + OiAiL3YxL2ludm9pY2VzIgp9Cg== + http_version: + recorded_at: Thu, 21 Jul 2016 13:45:47 GMT +- request: + method: get + uri: https://api.stripe.com/v1/customers/cus_8CzNtM08NVlSGN + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin15","engine":"ruby","publisher":"stripe","uname":"Darwin + mbp-sleede-peng.home 15.6.0 Darwin Kernel Version 15.6.0: Thu Jun 23 18:25:34 + PDT 2016; root:xnu-3248.60.10~1/RELEASE_X86_64 x86_64","hostname":"mbp-sleede-peng.home"}' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 21 Jul 2016 13:45:48 GMT + Content-Type: + - application/json + Content-Length: + - '3698' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_8rQLtUHnQxh6hr + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: ASCII-8BIT + string: !binary |- + ewogICJpZCI6ICJjdXNfOEN6TnRNMDhOVmxTR04iLAogICJvYmplY3QiOiAi + Y3VzdG9tZXIiLAogICJhY2NvdW50X2JhbGFuY2UiOiAwLAogICJjcmVhdGVk + IjogMTQ1OTc4Mjg0OSwKICAiY3VycmVuY3kiOiAidXNkIiwKICAiZGVmYXVs + dF9zb3VyY2UiOiAiY2FyZF8xOFpoVWYyc09tZjQ3Tno5TGhrUWVFbU8iLAog + ICJkZWxpbnF1ZW50IjogZmFsc2UsCiAgImRlc2NyaXB0aW9uIjogIlZhbmVz + c2EgTG9uY2hhbXAiLAogICJkaXNjb3VudCI6IG51bGwsCiAgImVtYWlsIjog + InZhbmVzc2EubG9uY2hhbXBAc2ZyLmZyIiwKICAibGl2ZW1vZGUiOiBmYWxz + ZSwKICAibWV0YWRhdGEiOiB7fSwKICAic2hpcHBpbmciOiBudWxsLAogICJz + b3VyY2VzIjogewogICAgIm9iamVjdCI6ICJsaXN0IiwKICAgICJkYXRhIjog + WwogICAgICB7CiAgICAgICAgImlkIjogImNhcmRfMThaaFVmMnNPbWY0N056 + OUxoa1FlRW1PIiwKICAgICAgICAib2JqZWN0IjogImNhcmQiLAogICAgICAg + ICJhZGRyZXNzX2NpdHkiOiBudWxsLAogICAgICAgICJhZGRyZXNzX2NvdW50 + cnkiOiBudWxsLAogICAgICAgICJhZGRyZXNzX2xpbmUxIjogbnVsbCwKICAg + ICAgICAiYWRkcmVzc19saW5lMV9jaGVjayI6IG51bGwsCiAgICAgICAgImFk + ZHJlc3NfbGluZTIiOiBudWxsLAogICAgICAgICJhZGRyZXNzX3N0YXRlIjog + bnVsbCwKICAgICAgICAiYWRkcmVzc196aXAiOiBudWxsLAogICAgICAgICJh + ZGRyZXNzX3ppcF9jaGVjayI6IG51bGwsCiAgICAgICAgImJyYW5kIjogIlZp + c2EiLAogICAgICAgICJjb3VudHJ5IjogIlVTIiwKICAgICAgICAiY3VzdG9t + ZXIiOiAiY3VzXzhDek50TTA4TlZsU0dOIiwKICAgICAgICAiY3ZjX2NoZWNr + IjogInBhc3MiLAogICAgICAgICJkeW5hbWljX2xhc3Q0IjogbnVsbCwKICAg + ICAgICAiZXhwX21vbnRoIjogNCwKICAgICAgICAiZXhwX3llYXIiOiAyMDE3 + LAogICAgICAgICJmaW5nZXJwcmludCI6ICJvNTJqeWJSN2JubU5uNkFUIiwK + ICAgICAgICAiZnVuZGluZyI6ICJjcmVkaXQiLAogICAgICAgICJsYXN0NCI6 + ICI0MjQyIiwKICAgICAgICAibWV0YWRhdGEiOiB7fSwKICAgICAgICAibmFt + ZSI6IG51bGwsCiAgICAgICAgInRva2VuaXphdGlvbl9tZXRob2QiOiBudWxs + CiAgICAgIH0KICAgIF0sCiAgICAiaGFzX21vcmUiOiBmYWxzZSwKICAgICJ0 + b3RhbF9jb3VudCI6IDEsCiAgICAidXJsIjogIi92MS9jdXN0b21lcnMvY3Vz + XzhDek50TTA4TlZsU0dOL3NvdXJjZXMiCiAgfSwKICAic3Vic2NyaXB0aW9u + cyI6IHsKICAgICJvYmplY3QiOiAibGlzdCIsCiAgICAiZGF0YSI6IFsKICAg + ICAgewogICAgICAgICJpZCI6ICJzdWJfOHJRTGtsVVVVUWU2NmsiLAogICAg + ICAgICJvYmplY3QiOiAic3Vic2NyaXB0aW9uIiwKICAgICAgICAiYXBwbGlj + YXRpb25fZmVlX3BlcmNlbnQiOiBudWxsLAogICAgICAgICJjYW5jZWxfYXRf + cGVyaW9kX2VuZCI6IGZhbHNlLAogICAgICAgICJjYW5jZWxlZF9hdCI6IG51 + bGwsCiAgICAgICAgImNyZWF0ZWQiOiAxNDY5MTA4NzQ1LAogICAgICAgICJj + dXJyZW50X3BlcmlvZF9lbmQiOiAxNDcxNzg3MTQ1LAogICAgICAgICJjdXJy + ZW50X3BlcmlvZF9zdGFydCI6IDE0NjkxMDg3NDUsCiAgICAgICAgImN1c3Rv + bWVyIjogImN1c184Q3pOdE0wOE5WbFNHTiIsCiAgICAgICAgImRpc2NvdW50 + IjogbnVsbCwKICAgICAgICAiZW5kZWRfYXQiOiBudWxsLAogICAgICAgICJs + aXZlbW9kZSI6IGZhbHNlLAogICAgICAgICJtZXRhZGF0YSI6IHt9LAogICAg + ICAgICJwbGFuIjogewogICAgICAgICAgImlkIjogIm1lbnN1ZWwtdGFyaWYt + cmVkdWl0LXN0dWRlbnQtbW9udGgtMjAxNjA0MDQxNzE4MjciLAogICAgICAg + ICAgIm9iamVjdCI6ICJwbGFuIiwKICAgICAgICAgICJhbW91bnQiOiAyMDAw + LAogICAgICAgICAgImNyZWF0ZWQiOiAxNDU5NzgzMTA4LAogICAgICAgICAg + ImN1cnJlbmN5IjogInVzZCIsCiAgICAgICAgICAiaW50ZXJ2YWwiOiAibW9u + dGgiLAogICAgICAgICAgImludGVydmFsX2NvdW50IjogMSwKICAgICAgICAg + ICJsaXZlbW9kZSI6IGZhbHNlLAogICAgICAgICAgIm1ldGFkYXRhIjoge30s + CiAgICAgICAgICAibmFtZSI6ICJNZW5zdWVsIHRhcmlmIHLDqWR1aXQgLSDD + qXR1ZGlhbnQsIC0gZGUgMjUgYW5zLCBlbnNlaWduYW50LCBkZW1hbmRldXIg + ZCdlbXBsb2kgLSBtb250aCIsCiAgICAgICAgICAic3RhdGVtZW50X2Rlc2Ny + aXB0b3IiOiBudWxsLAogICAgICAgICAgInRyaWFsX3BlcmlvZF9kYXlzIjog + bnVsbAogICAgICAgIH0sCiAgICAgICAgInF1YW50aXR5IjogMSwKICAgICAg + ICAic3RhcnQiOiAxNDY5MTA4NzQ1LAogICAgICAgICJzdGF0dXMiOiAiYWN0 + aXZlIiwKICAgICAgICAidGF4X3BlcmNlbnQiOiBudWxsLAogICAgICAgICJ0 + cmlhbF9lbmQiOiBudWxsLAogICAgICAgICJ0cmlhbF9zdGFydCI6IG51bGwK + ICAgICAgfSwKICAgICAgewogICAgICAgICJpZCI6ICJzdWJfOG5mYlRHR0lR + UlF6eDEiLAogICAgICAgICJvYmplY3QiOiAic3Vic2NyaXB0aW9uIiwKICAg + ICAgICAiYXBwbGljYXRpb25fZmVlX3BlcmNlbnQiOiBudWxsLAogICAgICAg + ICJjYW5jZWxfYXRfcGVyaW9kX2VuZCI6IHRydWUsCiAgICAgICAgImNhbmNl + bGVkX2F0IjogMTQ2ODI0Mjk2MCwKICAgICAgICAiY3JlYXRlZCI6IDE0Njgy + NDI5NTYsCiAgICAgICAgImN1cnJlbnRfcGVyaW9kX2VuZCI6IDE0NzA5MjEz + NTYsCiAgICAgICAgImN1cnJlbnRfcGVyaW9kX3N0YXJ0IjogMTQ2ODI0Mjk1 + NiwKICAgICAgICAiY3VzdG9tZXIiOiAiY3VzXzhDek50TTA4TlZsU0dOIiwK + ICAgICAgICAiZGlzY291bnQiOiBudWxsLAogICAgICAgICJlbmRlZF9hdCI6 + IG51bGwsCiAgICAgICAgImxpdmVtb2RlIjogZmFsc2UsCiAgICAgICAgIm1l + dGFkYXRhIjoge30sCiAgICAgICAgInBsYW4iOiB7CiAgICAgICAgICAiaWQi + OiAibWVuc3VlbC10YXJpZi1yZWR1aXQtc3R1ZGVudC1tb250aC0yMDE2MDQw + NDE3MTgyNyIsCiAgICAgICAgICAib2JqZWN0IjogInBsYW4iLAogICAgICAg + ICAgImFtb3VudCI6IDIwMDAsCiAgICAgICAgICAiY3JlYXRlZCI6IDE0NTk3 + ODMxMDgsCiAgICAgICAgICAiY3VycmVuY3kiOiAidXNkIiwKICAgICAgICAg + ICJpbnRlcnZhbCI6ICJtb250aCIsCiAgICAgICAgICAiaW50ZXJ2YWxfY291 + bnQiOiAxLAogICAgICAgICAgImxpdmVtb2RlIjogZmFsc2UsCiAgICAgICAg + ICAibWV0YWRhdGEiOiB7fSwKICAgICAgICAgICJuYW1lIjogIk1lbnN1ZWwg + dGFyaWYgcsOpZHVpdCAtIMOpdHVkaWFudCwgLSBkZSAyNSBhbnMsIGVuc2Vp + Z25hbnQsIGRlbWFuZGV1ciBkJ2VtcGxvaSAtIG1vbnRoIiwKICAgICAgICAg + ICJzdGF0ZW1lbnRfZGVzY3JpcHRvciI6IG51bGwsCiAgICAgICAgICAidHJp + YWxfcGVyaW9kX2RheXMiOiBudWxsCiAgICAgICAgfSwKICAgICAgICAicXVh + bnRpdHkiOiAxLAogICAgICAgICJzdGFydCI6IDE0NjgyNDI5NTYsCiAgICAg + ICAgInN0YXR1cyI6ICJhY3RpdmUiLAogICAgICAgICJ0YXhfcGVyY2VudCI6 + IG51bGwsCiAgICAgICAgInRyaWFsX2VuZCI6IG51bGwsCiAgICAgICAgInRy + aWFsX3N0YXJ0IjogbnVsbAogICAgICB9CiAgICBdLAogICAgImhhc19tb3Jl + IjogZmFsc2UsCiAgICAidG90YWxfY291bnQiOiAyLAogICAgInVybCI6ICIv + djEvY3VzdG9tZXJzL2N1c184Q3pOdE0wOE5WbFNHTi9zdWJzY3JpcHRpb25z + IgogIH0KfQo= + http_version: + recorded_at: Thu, 21 Jul 2016 13:45:48 GMT +- request: + method: get + uri: https://api.stripe.com/v1/customers/cus_8CzNtM08NVlSGN/subscriptions/sub_8rQLklUUUQe66k + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin15","engine":"ruby","publisher":"stripe","uname":"Darwin + mbp-sleede-peng.home 15.6.0 Darwin Kernel Version 15.6.0: Thu Jun 23 18:25:34 + PDT 2016; root:xnu-3248.60.10~1/RELEASE_X86_64 x86_64","hostname":"mbp-sleede-peng.home"}' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 21 Jul 2016 13:45:48 GMT + Content-Type: + - application/json + Content-Length: + - '926' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_8rQLxhq9Ba8DPf + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: ASCII-8BIT + string: !binary |- + ewogICJpZCI6ICJzdWJfOHJRTGtsVVVVUWU2NmsiLAogICJvYmplY3QiOiAi + c3Vic2NyaXB0aW9uIiwKICAiYXBwbGljYXRpb25fZmVlX3BlcmNlbnQiOiBu + dWxsLAogICJjYW5jZWxfYXRfcGVyaW9kX2VuZCI6IGZhbHNlLAogICJjYW5j + ZWxlZF9hdCI6IG51bGwsCiAgImNyZWF0ZWQiOiAxNDY5MTA4NzQ1LAogICJj + dXJyZW50X3BlcmlvZF9lbmQiOiAxNDcxNzg3MTQ1LAogICJjdXJyZW50X3Bl + cmlvZF9zdGFydCI6IDE0NjkxMDg3NDUsCiAgImN1c3RvbWVyIjogImN1c184 + Q3pOdE0wOE5WbFNHTiIsCiAgImRpc2NvdW50IjogbnVsbCwKICAiZW5kZWRf + YXQiOiBudWxsLAogICJsaXZlbW9kZSI6IGZhbHNlLAogICJtZXRhZGF0YSI6 + IHt9LAogICJwbGFuIjogewogICAgImlkIjogIm1lbnN1ZWwtdGFyaWYtcmVk + dWl0LXN0dWRlbnQtbW9udGgtMjAxNjA0MDQxNzE4MjciLAogICAgIm9iamVj + dCI6ICJwbGFuIiwKICAgICJhbW91bnQiOiAyMDAwLAogICAgImNyZWF0ZWQi + OiAxNDU5NzgzMTA4LAogICAgImN1cnJlbmN5IjogInVzZCIsCiAgICAiaW50 + ZXJ2YWwiOiAibW9udGgiLAogICAgImludGVydmFsX2NvdW50IjogMSwKICAg + ICJsaXZlbW9kZSI6IGZhbHNlLAogICAgIm1ldGFkYXRhIjoge30sCiAgICAi + bmFtZSI6ICJNZW5zdWVsIHRhcmlmIHLDqWR1aXQgLSDDqXR1ZGlhbnQsIC0g + ZGUgMjUgYW5zLCBlbnNlaWduYW50LCBkZW1hbmRldXIgZCdlbXBsb2kgLSBt + b250aCIsCiAgICAic3RhdGVtZW50X2Rlc2NyaXB0b3IiOiBudWxsLAogICAg + InRyaWFsX3BlcmlvZF9kYXlzIjogbnVsbAogIH0sCiAgInF1YW50aXR5Ijog + MSwKICAic3RhcnQiOiAxNDY5MTA4NzQ1LAogICJzdGF0dXMiOiAiYWN0aXZl + IiwKICAidGF4X3BlcmNlbnQiOiBudWxsLAogICJ0cmlhbF9lbmQiOiBudWxs + LAogICJ0cmlhbF9zdGFydCI6IG51bGwKfQo= + http_version: + recorded_at: Thu, 21 Jul 2016 13:45:49 GMT +- request: + method: delete + uri: https://api.stripe.com/v1/customers/cus_8CzNtM08NVlSGN/subscriptions/sub_8rQLklUUUQe66k?at_period_end=true + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin15","engine":"ruby","publisher":"stripe","uname":"Darwin + mbp-sleede-peng.home 15.6.0 Darwin Kernel Version 15.6.0: Thu Jun 23 18:25:34 + PDT 2016; root:xnu-3248.60.10~1/RELEASE_X86_64 x86_64","hostname":"mbp-sleede-peng.home"}' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 21 Jul 2016 13:45:49 GMT + Content-Type: + - application/json + Content-Length: + - '931' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_8rQLffnLhxa9Y2 + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: ASCII-8BIT + string: !binary |- + ewogICJpZCI6ICJzdWJfOHJRTGtsVVVVUWU2NmsiLAogICJvYmplY3QiOiAi + c3Vic2NyaXB0aW9uIiwKICAiYXBwbGljYXRpb25fZmVlX3BlcmNlbnQiOiBu + dWxsLAogICJjYW5jZWxfYXRfcGVyaW9kX2VuZCI6IHRydWUsCiAgImNhbmNl + bGVkX2F0IjogMTQ2OTEwODc0OSwKICAiY3JlYXRlZCI6IDE0NjkxMDg3NDUs + CiAgImN1cnJlbnRfcGVyaW9kX2VuZCI6IDE0NzE3ODcxNDUsCiAgImN1cnJl + bnRfcGVyaW9kX3N0YXJ0IjogMTQ2OTEwODc0NSwKICAiY3VzdG9tZXIiOiAi + Y3VzXzhDek50TTA4TlZsU0dOIiwKICAiZGlzY291bnQiOiBudWxsLAogICJl + bmRlZF9hdCI6IG51bGwsCiAgImxpdmVtb2RlIjogZmFsc2UsCiAgIm1ldGFk + YXRhIjoge30sCiAgInBsYW4iOiB7CiAgICAiaWQiOiAibWVuc3VlbC10YXJp + Zi1yZWR1aXQtc3R1ZGVudC1tb250aC0yMDE2MDQwNDE3MTgyNyIsCiAgICAi + b2JqZWN0IjogInBsYW4iLAogICAgImFtb3VudCI6IDIwMDAsCiAgICAiY3Jl + YXRlZCI6IDE0NTk3ODMxMDgsCiAgICAiY3VycmVuY3kiOiAidXNkIiwKICAg + ICJpbnRlcnZhbCI6ICJtb250aCIsCiAgICAiaW50ZXJ2YWxfY291bnQiOiAx + LAogICAgImxpdmVtb2RlIjogZmFsc2UsCiAgICAibWV0YWRhdGEiOiB7fSwK + ICAgICJuYW1lIjogIk1lbnN1ZWwgdGFyaWYgcsOpZHVpdCAtIMOpdHVkaWFu + dCwgLSBkZSAyNSBhbnMsIGVuc2VpZ25hbnQsIGRlbWFuZGV1ciBkJ2VtcGxv + aSAtIG1vbnRoIiwKICAgICJzdGF0ZW1lbnRfZGVzY3JpcHRvciI6IG51bGws + CiAgICAidHJpYWxfcGVyaW9kX2RheXMiOiBudWxsCiAgfSwKICAicXVhbnRp + dHkiOiAxLAogICJzdGFydCI6IDE0NjkxMDg3NDUsCiAgInN0YXR1cyI6ICJh + Y3RpdmUiLAogICJ0YXhfcGVyY2VudCI6IG51bGwsCiAgInRyaWFsX2VuZCI6 + IG51bGwsCiAgInRyaWFsX3N0YXJ0IjogbnVsbAp9Cg== + http_version: + recorded_at: Thu, 21 Jul 2016 13:45:50 GMT +- request: + method: get + uri: https://api.stripe.com/v1/invoiceitems/ii_18ZhUg2sOmf47Nz9vSsl5J9z + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin15","engine":"ruby","publisher":"stripe","uname":"Darwin + mbp-sleede-peng.home 15.6.0 Darwin Kernel Version 15.6.0: Thu Jun 23 18:25:34 + PDT 2016; root:xnu-3248.60.10~1/RELEASE_X86_64 x86_64","hostname":"mbp-sleede-peng.home"}' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 21 Jul 2016 13:45:50 GMT + Content-Type: + - application/json + Content-Length: + - '493' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_8rQL4mVlBuIk4d + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "ii_18ZhUg2sOmf47Nz9vSsl5J9z", + "object": "invoiceitem", + "amount": 0, + "currency": "usd", + "customer": "cus_8CzNtM08NVlSGN", + "date": 1469108742, + "description": "Formation Imprimante 3D July 18, 2016 08:00 - 12:00 PM", + "discountable": true, + "invoice": "in_18ZhUj2sOmf47Nz9r8LKIBNK", + "livemode": false, + "metadata": {}, + "period": { + "start": 1469108742, + "end": 1469108742 + }, + "plan": null, + "proration": false, + "quantity": null, + "subscription": null + } + http_version: + recorded_at: Thu, 21 Jul 2016 13:45:50 GMT +- request: + method: get + uri: https://api.stripe.com/v1/invoiceitems/ii_18ZhUg2sOmf47Nz9vSsl5J9z + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin15","engine":"ruby","publisher":"stripe","uname":"Darwin + mbp-sleede-peng.home 15.6.0 Darwin Kernel Version 15.6.0: Thu Jun 23 18:25:34 + PDT 2016; root:xnu-3248.60.10~1/RELEASE_X86_64 x86_64","hostname":"mbp-sleede-peng.home"}' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 21 Jul 2016 13:45:51 GMT + Content-Type: + - application/json + Content-Length: + - '493' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_8rQLaiuoNH33sw + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "ii_18ZhUg2sOmf47Nz9vSsl5J9z", + "object": "invoiceitem", + "amount": 0, + "currency": "usd", + "customer": "cus_8CzNtM08NVlSGN", + "date": 1469108742, + "description": "Formation Imprimante 3D July 18, 2016 08:00 - 12:00 PM", + "discountable": true, + "invoice": "in_18ZhUj2sOmf47Nz9r8LKIBNK", + "livemode": false, + "metadata": {}, + "period": { + "start": 1469108742, + "end": 1469108742 + }, + "plan": null, + "proration": false, + "quantity": null, + "subscription": null + } + http_version: + recorded_at: Thu, 21 Jul 2016 13:45:51 GMT +recorded_with: VCR 3.0.1 diff --git a/test/vcr_cassettes/reservations_machine_and_plan_using_coupon_retrieve_invoice_from_stripe.yml b/test/vcr_cassettes/reservations_machine_and_plan_using_coupon_retrieve_invoice_from_stripe.yml new file mode 100644 index 000000000..9a9a8da5e --- /dev/null +++ b/test/vcr_cassettes/reservations_machine_and_plan_using_coupon_retrieve_invoice_from_stripe.yml @@ -0,0 +1,189 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.stripe.com/v1/invoices/in_18rNSq2sOmf47Nz91hxlGSa7 + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 4.4.0-36-generic (buildd@lcy01-01) (gcc version 5.4.0 20160609 (Ubuntu + 5.4.0-6ubuntu1~16.04.2) ) #55-Ubuntu SMP Thu Aug 11 18:01:55 UTC 2016","hostname":"sylvain-sleede-pc"}' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 08 Sep 2016 12:25:04 GMT + Content-Type: + - application/json + Content-Length: + - '3362' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_99l6aCLd0IC3Qn + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "in_18rNSq2sOmf47Nz91hxlGSa7", + "object": "invoice", + "amount_due": 3825, + "application_fee": null, + "attempt_count": 1, + "attempted": true, + "charge": "ch_18rNSq2sOmf47Nz9Z8EuuyI8", + "closed": true, + "currency": "usd", + "customer": "cus_8Di1wjdVktv5kt", + "date": 1473321652, + "description": null, + "discount": null, + "ending_balance": 0, + "forgiven": false, + "lines": { + "object": "list", + "data": [ + { + "id": "ii_18rNSp2sOmf47Nz9S0rJVP2a", + "object": "line_item", + "amount": -450, + "currency": "usd", + "description": "coupon SUNNYFABLAB", + "discountable": false, + "livemode": false, + "metadata": {}, + "period": { + "start": 1473321651, + "end": 1473321651 + }, + "plan": null, + "proration": false, + "quantity": null, + "subscription": null, + "type": "invoiceitem" + }, + { + "id": "ii_18rNSm2sOmf47Nz9R11Svoer", + "object": "line_item", + "amount": -225, + "currency": "usd", + "description": "coupon SUNNYFABLAB", + "discountable": false, + "livemode": false, + "metadata": {}, + "period": { + "start": 1473321648, + "end": 1473321648 + }, + "plan": null, + "proration": false, + "quantity": null, + "subscription": null, + "type": "invoiceitem" + }, + { + "id": "ii_18rNSm2sOmf47Nz9avgL9KyW", + "object": "line_item", + "amount": 1500, + "currency": "usd", + "description": "FORM1+ imprimante 3D September 04, 2016 14:00 - 03:00 PM", + "discountable": true, + "livemode": false, + "metadata": {}, + "period": { + "start": 1473321648, + "end": 1473321648 + }, + "plan": null, + "proration": false, + "quantity": null, + "subscription": null, + "type": "invoiceitem" + }, + { + "id": "sub_99gqb47NmX9r79", + "object": "line_item", + "amount": 3000, + "currency": "usd", + "description": null, + "discountable": true, + "livemode": false, + "metadata": {}, + "period": { + "start": 1473321652, + "end": 1475913652 + }, + "plan": { + "id": "mensuel-standard-month-20160404171519", + "object": "plan", + "amount": 3000, + "created": 1459782921, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "name": "Mensuel - standard, association - month", + "statement_descriptor": null, + "trial_period_days": null + }, + "proration": false, + "quantity": 1, + "subscription": null, + "type": "subscription" + } + ], + "has_more": false, + "total_count": 4, + "url": "/v1/invoices/in_18rNSq2sOmf47Nz91hxlGSa7/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": null, + "paid": true, + "period_end": 1473321652, + "period_start": 1473321652, + "receipt_number": null, + "starting_balance": 0, + "statement_descriptor": null, + "subscription": "sub_99gqb47NmX9r79", + "subtotal": 3825, + "tax": null, + "tax_percent": null, + "total": 3825, + "webhooks_delivered_at": 1473321652 + } + http_version: + recorded_at: Thu, 08 Sep 2016 12:25:04 GMT +recorded_with: VCR 3.0.1 diff --git a/test/vcr_cassettes/reservations_machine_and_plan_using_coupon_success.yml b/test/vcr_cassettes/reservations_machine_and_plan_using_coupon_success.yml new file mode 100644 index 000000000..8d24dd2db --- /dev/null +++ b/test/vcr_cassettes/reservations_machine_and_plan_using_coupon_success.yml @@ -0,0 +1,1334 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.stripe.com/v1/tokens + body: + encoding: UTF-8 + string: card[number]=4242424242424242&card[exp_month]=4&card[exp_year]=2017&card[cvc]=314 + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 4.4.0-36-generic (buildd@lcy01-01) (gcc version 5.4.0 20160609 (Ubuntu + 5.4.0-6ubuntu1~16.04.2) ) #55-Ubuntu SMP Thu Aug 11 18:01:55 UTC 2016","hostname":"sylvain-sleede-pc"}' + Content-Length: + - '81' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 08 Sep 2016 08:00:47 GMT + Content-Type: + - application/json + Content-Length: + - '779' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_99gqPbGrfRTnyZ + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "tok_18rNSl2sOmf47Nz9cTGhyMRc", + "object": "token", + "card": { + "id": "card_18rNSk2sOmf47Nz9h1cf7obf", + "object": "card", + "address_city": null, + "address_country": null, + "address_line1": null, + "address_line1_check": null, + "address_line2": null, + "address_state": null, + "address_zip": null, + "address_zip_check": null, + "brand": "Visa", + "country": "US", + "cvc_check": "unchecked", + "dynamic_last4": null, + "exp_month": 4, + "exp_year": 2017, + "fingerprint": "o52jybR7bnmNn6AT", + "funding": "credit", + "last4": "4242", + "metadata": {}, + "name": null, + "tokenization_method": null + }, + "client_ip": "90.52.44.103", + "created": 1473321647, + "livemode": false, + "type": "card", + "used": false + } + http_version: + recorded_at: Thu, 08 Sep 2016 08:00:47 GMT +- request: + method: post + uri: https://api.stripe.com/v1/invoiceitems + body: + encoding: UTF-8 + string: customer=cus_8Di1wjdVktv5kt&amount=1500¤cy=usd&description=FORM1%2B+imprimante+3D+September+04%2C+2016+14%3A00+-+03%3A00+PM + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 4.4.0-36-generic (buildd@lcy01-01) (gcc version 5.4.0 20160609 (Ubuntu + 5.4.0-6ubuntu1~16.04.2) ) #55-Ubuntu SMP Thu Aug 11 18:01:55 UTC 2016","hostname":"sylvain-sleede-pc"}' + Content-Length: + - '129' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 08 Sep 2016 08:00:48 GMT + Content-Type: + - application/json + Content-Length: + - '473' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_99gqddasRVKxn2 + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "ii_18rNSm2sOmf47Nz9avgL9KyW", + "object": "invoiceitem", + "amount": 1500, + "currency": "usd", + "customer": "cus_8Di1wjdVktv5kt", + "date": 1473321648, + "description": "FORM1+ imprimante 3D September 04, 2016 14:00 - 03:00 PM", + "discountable": true, + "invoice": null, + "livemode": false, + "metadata": {}, + "period": { + "start": 1473321648, + "end": 1473321648 + }, + "plan": null, + "proration": false, + "quantity": null, + "subscription": null + } + http_version: + recorded_at: Thu, 08 Sep 2016 08:00:48 GMT +- request: + method: post + uri: https://api.stripe.com/v1/invoiceitems + body: + encoding: UTF-8 + string: customer=cus_8Di1wjdVktv5kt&amount=-225¤cy=usd&description=coupon+SUNNYFABLAB + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 4.4.0-36-generic (buildd@lcy01-01) (gcc version 5.4.0 20160609 (Ubuntu + 5.4.0-6ubuntu1~16.04.2) ) #55-Ubuntu SMP Thu Aug 11 18:01:55 UTC 2016","hostname":"sylvain-sleede-pc"}' + Content-Length: + - '83' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 08 Sep 2016 08:00:48 GMT + Content-Type: + - application/json + Content-Length: + - '436' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_99gq27qOy8Dn3R + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "ii_18rNSm2sOmf47Nz9R11Svoer", + "object": "invoiceitem", + "amount": -225, + "currency": "usd", + "customer": "cus_8Di1wjdVktv5kt", + "date": 1473321648, + "description": "coupon SUNNYFABLAB", + "discountable": false, + "invoice": null, + "livemode": false, + "metadata": {}, + "period": { + "start": 1473321648, + "end": 1473321648 + }, + "plan": null, + "proration": false, + "quantity": null, + "subscription": null + } + http_version: + recorded_at: Thu, 08 Sep 2016 08:00:49 GMT +- request: + method: get + uri: https://api.stripe.com/v1/customers/cus_8Di1wjdVktv5kt + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 4.4.0-36-generic (buildd@lcy01-01) (gcc version 5.4.0 20160609 (Ubuntu + 5.4.0-6ubuntu1~16.04.2) ) #55-Ubuntu SMP Thu Aug 11 18:01:55 UTC 2016","hostname":"sylvain-sleede-pc"}' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 08 Sep 2016 08:00:49 GMT + Content-Type: + - application/json + Content-Length: + - '1408' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_99gqbfntlVYpyJ + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "cus_8Di1wjdVktv5kt", + "object": "customer", + "account_balance": 0, + "created": 1459948888, + "currency": "usd", + "default_source": "card_17z7CT2sOmf47Nz9wtWkhGor", + "delinquent": false, + "description": "Jean Dupond", + "discount": null, + "email": "jean.dupond@gmail.com", + "livemode": false, + "metadata": {}, + "shipping": null, + "sources": { + "object": "list", + "data": [ + { + "id": "card_17z7CT2sOmf47Nz9wtWkhGor", + "object": "card", + "address_city": null, + "address_country": null, + "address_line1": null, + "address_line1_check": null, + "address_line2": null, + "address_state": null, + "address_zip": null, + "address_zip_check": null, + "brand": "Visa", + "country": "US", + "customer": "cus_8Di1wjdVktv5kt", + "cvc_check": "pass", + "dynamic_last4": null, + "exp_month": 4, + "exp_year": 2017, + "fingerprint": "o52jybR7bnmNn6AT", + "funding": "credit", + "last4": "4242", + "metadata": {}, + "name": null, + "tokenization_method": null + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/customers/cus_8Di1wjdVktv5kt/sources" + }, + "subscriptions": { + "object": "list", + "data": [], + "has_more": false, + "total_count": 0, + "url": "/v1/customers/cus_8Di1wjdVktv5kt/subscriptions" + } + } + http_version: + recorded_at: Thu, 08 Sep 2016 08:00:49 GMT +- request: + method: get + uri: https://api.stripe.com/v1/customers/cus_8Di1wjdVktv5kt + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 4.4.0-36-generic (buildd@lcy01-01) (gcc version 5.4.0 20160609 (Ubuntu + 5.4.0-6ubuntu1~16.04.2) ) #55-Ubuntu SMP Thu Aug 11 18:01:55 UTC 2016","hostname":"sylvain-sleede-pc"}' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 08 Sep 2016 08:00:50 GMT + Content-Type: + - application/json + Content-Length: + - '1408' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_99gqvJz00HL9sk + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "cus_8Di1wjdVktv5kt", + "object": "customer", + "account_balance": 0, + "created": 1459948888, + "currency": "usd", + "default_source": "card_17z7CT2sOmf47Nz9wtWkhGor", + "delinquent": false, + "description": "Jean Dupond", + "discount": null, + "email": "jean.dupond@gmail.com", + "livemode": false, + "metadata": {}, + "shipping": null, + "sources": { + "object": "list", + "data": [ + { + "id": "card_17z7CT2sOmf47Nz9wtWkhGor", + "object": "card", + "address_city": null, + "address_country": null, + "address_line1": null, + "address_line1_check": null, + "address_line2": null, + "address_state": null, + "address_zip": null, + "address_zip_check": null, + "brand": "Visa", + "country": "US", + "customer": "cus_8Di1wjdVktv5kt", + "cvc_check": "pass", + "dynamic_last4": null, + "exp_month": 4, + "exp_year": 2017, + "fingerprint": "o52jybR7bnmNn6AT", + "funding": "credit", + "last4": "4242", + "metadata": {}, + "name": null, + "tokenization_method": null + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/customers/cus_8Di1wjdVktv5kt/sources" + }, + "subscriptions": { + "object": "list", + "data": [], + "has_more": false, + "total_count": 0, + "url": "/v1/customers/cus_8Di1wjdVktv5kt/subscriptions" + } + } + http_version: + recorded_at: Thu, 08 Sep 2016 08:00:50 GMT +- request: + method: post + uri: https://api.stripe.com/v1/invoiceitems + body: + encoding: UTF-8 + string: customer=cus_8Di1wjdVktv5kt&amount=-450¤cy=usd&description=coupon+SUNNYFABLAB + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 4.4.0-36-generic (buildd@lcy01-01) (gcc version 5.4.0 20160609 (Ubuntu + 5.4.0-6ubuntu1~16.04.2) ) #55-Ubuntu SMP Thu Aug 11 18:01:55 UTC 2016","hostname":"sylvain-sleede-pc"}' + Content-Length: + - '83' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 08 Sep 2016 08:00:51 GMT + Content-Type: + - application/json + Content-Length: + - '436' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_99gq9UCtuA1CdL + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "ii_18rNSp2sOmf47Nz9S0rJVP2a", + "object": "invoiceitem", + "amount": -450, + "currency": "usd", + "customer": "cus_8Di1wjdVktv5kt", + "date": 1473321651, + "description": "coupon SUNNYFABLAB", + "discountable": false, + "invoice": null, + "livemode": false, + "metadata": {}, + "period": { + "start": 1473321651, + "end": 1473321651 + }, + "plan": null, + "proration": false, + "quantity": null, + "subscription": null + } + http_version: + recorded_at: Thu, 08 Sep 2016 08:00:51 GMT +- request: + method: post + uri: https://api.stripe.com/v1/customers/cus_8Di1wjdVktv5kt/subscriptions + body: + encoding: UTF-8 + string: plan=mensuel-standard-month-20160404171519&source=tok_18rNSl2sOmf47Nz9cTGhyMRc + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 4.4.0-36-generic (buildd@lcy01-01) (gcc version 5.4.0 20160609 (Ubuntu + 5.4.0-6ubuntu1~16.04.2) ) #55-Ubuntu SMP Thu Aug 11 18:01:55 UTC 2016","hostname":"sylvain-sleede-pc"}' + Content-Length: + - '78' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 08 Sep 2016 08:00:52 GMT + Content-Type: + - application/json + Content-Length: + - '867' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_99gq6A5SZpD1qQ + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "sub_99gqb47NmX9r79", + "object": "subscription", + "application_fee_percent": null, + "cancel_at_period_end": false, + "canceled_at": null, + "created": 1473321652, + "current_period_end": 1475913652, + "current_period_start": 1473321652, + "customer": "cus_8Di1wjdVktv5kt", + "discount": null, + "ended_at": null, + "livemode": false, + "metadata": {}, + "plan": { + "id": "mensuel-standard-month-20160404171519", + "object": "plan", + "amount": 3000, + "created": 1459782921, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "name": "Mensuel - standard, association - month", + "statement_descriptor": null, + "trial_period_days": null + }, + "quantity": 1, + "start": 1473321652, + "status": "active", + "tax_percent": null, + "trial_end": null, + "trial_start": null + } + http_version: + recorded_at: Thu, 08 Sep 2016 08:00:53 GMT +- request: + method: get + uri: https://api.stripe.com/v1/invoices?customer=cus_8Di1wjdVktv5kt&limit=1 + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 4.4.0-36-generic (buildd@lcy01-01) (gcc version 5.4.0 20160609 (Ubuntu + 5.4.0-6ubuntu1~16.04.2) ) #55-Ubuntu SMP Thu Aug 11 18:01:55 UTC 2016","hostname":"sylvain-sleede-pc"}' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 08 Sep 2016 08:00:53 GMT + Content-Type: + - application/json + Content-Length: + - '3963' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_99gqm0fOzzj3EP + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "object": "list", + "data": [ + { + "id": "in_18rNSq2sOmf47Nz91hxlGSa7", + "object": "invoice", + "amount_due": 3825, + "application_fee": null, + "attempt_count": 1, + "attempted": true, + "charge": "ch_18rNSq2sOmf47Nz9Z8EuuyI8", + "closed": true, + "currency": "usd", + "customer": "cus_8Di1wjdVktv5kt", + "date": 1473321652, + "description": null, + "discount": null, + "ending_balance": 0, + "forgiven": false, + "lines": { + "object": "list", + "data": [ + { + "id": "ii_18rNSp2sOmf47Nz9S0rJVP2a", + "object": "line_item", + "amount": -450, + "currency": "usd", + "description": "coupon SUNNYFABLAB", + "discountable": false, + "livemode": false, + "metadata": {}, + "period": { + "start": 1473321651, + "end": 1473321651 + }, + "plan": null, + "proration": false, + "quantity": null, + "subscription": null, + "type": "invoiceitem" + }, + { + "id": "ii_18rNSm2sOmf47Nz9R11Svoer", + "object": "line_item", + "amount": -225, + "currency": "usd", + "description": "coupon SUNNYFABLAB", + "discountable": false, + "livemode": false, + "metadata": {}, + "period": { + "start": 1473321648, + "end": 1473321648 + }, + "plan": null, + "proration": false, + "quantity": null, + "subscription": null, + "type": "invoiceitem" + }, + { + "id": "ii_18rNSm2sOmf47Nz9avgL9KyW", + "object": "line_item", + "amount": 1500, + "currency": "usd", + "description": "FORM1+ imprimante 3D September 04, 2016 14:00 - 03:00 PM", + "discountable": true, + "livemode": false, + "metadata": {}, + "period": { + "start": 1473321648, + "end": 1473321648 + }, + "plan": null, + "proration": false, + "quantity": null, + "subscription": null, + "type": "invoiceitem" + }, + { + "id": "sub_99gqb47NmX9r79", + "object": "line_item", + "amount": 3000, + "currency": "usd", + "description": null, + "discountable": true, + "livemode": false, + "metadata": {}, + "period": { + "start": 1473321652, + "end": 1475913652 + }, + "plan": { + "id": "mensuel-standard-month-20160404171519", + "object": "plan", + "amount": 3000, + "created": 1459782921, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "name": "Mensuel - standard, association - month", + "statement_descriptor": null, + "trial_period_days": null + }, + "proration": false, + "quantity": 1, + "subscription": null, + "type": "subscription" + } + ], + "has_more": false, + "total_count": 4, + "url": "/v1/invoices/in_18rNSq2sOmf47Nz91hxlGSa7/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": null, + "paid": true, + "period_end": 1473321652, + "period_start": 1473321652, + "receipt_number": null, + "starting_balance": 0, + "statement_descriptor": null, + "subscription": "sub_99gqb47NmX9r79", + "subtotal": 3825, + "tax": null, + "tax_percent": null, + "total": 3825, + "webhooks_delivered_at": 1473321652 + } + ], + "has_more": true, + "url": "/v1/invoices" + } + http_version: + recorded_at: Thu, 08 Sep 2016 08:00:53 GMT +- request: + method: get + uri: https://api.stripe.com/v1/customers/cus_8Di1wjdVktv5kt + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 4.4.0-36-generic (buildd@lcy01-01) (gcc version 5.4.0 20160609 (Ubuntu + 5.4.0-6ubuntu1~16.04.2) ) #55-Ubuntu SMP Thu Aug 11 18:01:55 UTC 2016","hostname":"sylvain-sleede-pc"}' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 08 Sep 2016 08:00:54 GMT + Content-Type: + - application/json + Content-Length: + - '2490' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_99gqRCzmQJIDBa + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "cus_8Di1wjdVktv5kt", + "object": "customer", + "account_balance": 0, + "created": 1459948888, + "currency": "usd", + "default_source": "card_18rNSk2sOmf47Nz9h1cf7obf", + "delinquent": false, + "description": "Jean Dupond", + "discount": null, + "email": "jean.dupond@gmail.com", + "livemode": false, + "metadata": {}, + "shipping": null, + "sources": { + "object": "list", + "data": [ + { + "id": "card_18rNSk2sOmf47Nz9h1cf7obf", + "object": "card", + "address_city": null, + "address_country": null, + "address_line1": null, + "address_line1_check": null, + "address_line2": null, + "address_state": null, + "address_zip": null, + "address_zip_check": null, + "brand": "Visa", + "country": "US", + "customer": "cus_8Di1wjdVktv5kt", + "cvc_check": "pass", + "dynamic_last4": null, + "exp_month": 4, + "exp_year": 2017, + "fingerprint": "o52jybR7bnmNn6AT", + "funding": "credit", + "last4": "4242", + "metadata": {}, + "name": null, + "tokenization_method": null + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/customers/cus_8Di1wjdVktv5kt/sources" + }, + "subscriptions": { + "object": "list", + "data": [ + { + "id": "sub_99gqb47NmX9r79", + "object": "subscription", + "application_fee_percent": null, + "cancel_at_period_end": false, + "canceled_at": null, + "created": 1473321652, + "current_period_end": 1475913652, + "current_period_start": 1473321652, + "customer": "cus_8Di1wjdVktv5kt", + "discount": null, + "ended_at": null, + "livemode": false, + "metadata": {}, + "plan": { + "id": "mensuel-standard-month-20160404171519", + "object": "plan", + "amount": 3000, + "created": 1459782921, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "name": "Mensuel - standard, association - month", + "statement_descriptor": null, + "trial_period_days": null + }, + "quantity": 1, + "start": 1473321652, + "status": "active", + "tax_percent": null, + "trial_end": null, + "trial_start": null + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/customers/cus_8Di1wjdVktv5kt/subscriptions" + } + } + http_version: + recorded_at: Thu, 08 Sep 2016 08:00:54 GMT +- request: + method: get + uri: https://api.stripe.com/v1/customers/cus_8Di1wjdVktv5kt/subscriptions/sub_99gqb47NmX9r79 + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 4.4.0-36-generic (buildd@lcy01-01) (gcc version 5.4.0 20160609 (Ubuntu + 5.4.0-6ubuntu1~16.04.2) ) #55-Ubuntu SMP Thu Aug 11 18:01:55 UTC 2016","hostname":"sylvain-sleede-pc"}' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 08 Sep 2016 08:00:55 GMT + Content-Type: + - application/json + Content-Length: + - '867' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_99gqhmTVKcwbNF + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "sub_99gqb47NmX9r79", + "object": "subscription", + "application_fee_percent": null, + "cancel_at_period_end": false, + "canceled_at": null, + "created": 1473321652, + "current_period_end": 1475913652, + "current_period_start": 1473321652, + "customer": "cus_8Di1wjdVktv5kt", + "discount": null, + "ended_at": null, + "livemode": false, + "metadata": {}, + "plan": { + "id": "mensuel-standard-month-20160404171519", + "object": "plan", + "amount": 3000, + "created": 1459782921, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "name": "Mensuel - standard, association - month", + "statement_descriptor": null, + "trial_period_days": null + }, + "quantity": 1, + "start": 1473321652, + "status": "active", + "tax_percent": null, + "trial_end": null, + "trial_start": null + } + http_version: + recorded_at: Thu, 08 Sep 2016 08:00:55 GMT +- request: + method: delete + uri: https://api.stripe.com/v1/customers/cus_8Di1wjdVktv5kt/subscriptions/sub_99gqb47NmX9r79?at_period_end=true + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 4.4.0-36-generic (buildd@lcy01-01) (gcc version 5.4.0 20160609 (Ubuntu + 5.4.0-6ubuntu1~16.04.2) ) #55-Ubuntu SMP Thu Aug 11 18:01:55 UTC 2016","hostname":"sylvain-sleede-pc"}' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 08 Sep 2016 08:00:56 GMT + Content-Type: + - application/json + Content-Length: + - '872' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_99gqPGBMCEM6Ec + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "sub_99gqb47NmX9r79", + "object": "subscription", + "application_fee_percent": null, + "cancel_at_period_end": true, + "canceled_at": 1473321656, + "created": 1473321652, + "current_period_end": 1475913652, + "current_period_start": 1473321652, + "customer": "cus_8Di1wjdVktv5kt", + "discount": null, + "ended_at": null, + "livemode": false, + "metadata": {}, + "plan": { + "id": "mensuel-standard-month-20160404171519", + "object": "plan", + "amount": 3000, + "created": 1459782921, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "name": "Mensuel - standard, association - month", + "statement_descriptor": null, + "trial_period_days": null + }, + "quantity": 1, + "start": 1473321652, + "status": "active", + "tax_percent": null, + "trial_end": null, + "trial_start": null + } + http_version: + recorded_at: Thu, 08 Sep 2016 08:00:57 GMT +- request: + method: get + uri: https://api.stripe.com/v1/invoiceitems/ii_18rNSm2sOmf47Nz9avgL9KyW + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 4.4.0-36-generic (buildd@lcy01-01) (gcc version 5.4.0 20160609 (Ubuntu + 5.4.0-6ubuntu1~16.04.2) ) #55-Ubuntu SMP Thu Aug 11 18:01:55 UTC 2016","hostname":"sylvain-sleede-pc"}' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 08 Sep 2016 08:00:58 GMT + Content-Type: + - application/json + Content-Length: + - '498' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_99gqJIQqEHVCZ0 + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "ii_18rNSm2sOmf47Nz9avgL9KyW", + "object": "invoiceitem", + "amount": 1500, + "currency": "usd", + "customer": "cus_8Di1wjdVktv5kt", + "date": 1473321648, + "description": "FORM1+ imprimante 3D September 04, 2016 14:00 - 03:00 PM", + "discountable": true, + "invoice": "in_18rNSq2sOmf47Nz91hxlGSa7", + "livemode": false, + "metadata": {}, + "period": { + "start": 1473321648, + "end": 1473321648 + }, + "plan": null, + "proration": false, + "quantity": null, + "subscription": null + } + http_version: + recorded_at: Thu, 08 Sep 2016 08:00:58 GMT +- request: + method: get + uri: https://api.stripe.com/v1/invoiceitems/ii_18rNSm2sOmf47Nz9avgL9KyW + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 4.4.0-36-generic (buildd@lcy01-01) (gcc version 5.4.0 20160609 (Ubuntu + 5.4.0-6ubuntu1~16.04.2) ) #55-Ubuntu SMP Thu Aug 11 18:01:55 UTC 2016","hostname":"sylvain-sleede-pc"}' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 08 Sep 2016 08:00:58 GMT + Content-Type: + - application/json + Content-Length: + - '498' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_99gqJk9T35fI7H + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "ii_18rNSm2sOmf47Nz9avgL9KyW", + "object": "invoiceitem", + "amount": 1500, + "currency": "usd", + "customer": "cus_8Di1wjdVktv5kt", + "date": 1473321648, + "description": "FORM1+ imprimante 3D September 04, 2016 14:00 - 03:00 PM", + "discountable": true, + "invoice": "in_18rNSq2sOmf47Nz91hxlGSa7", + "livemode": false, + "metadata": {}, + "period": { + "start": 1473321648, + "end": 1473321648 + }, + "plan": null, + "proration": false, + "quantity": null, + "subscription": null + } + http_version: + recorded_at: Thu, 08 Sep 2016 08:00:58 GMT +recorded_with: VCR 3.0.1 diff --git a/test/vcr_cassettes/reservations_training_with_expired_coupon_error.yml b/test/vcr_cassettes/reservations_training_with_expired_coupon_error.yml new file mode 100644 index 000000000..6b3432d1e --- /dev/null +++ b/test/vcr_cassettes/reservations_training_with_expired_coupon_error.yml @@ -0,0 +1,997 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.stripe.com/v1/tokens + body: + encoding: UTF-8 + string: card[number]=4242424242424242&card[exp_month]=4&card[exp_year]=2017&card[cvc]=314 + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 4.4.0-36-generic (buildd@lcy01-01) (gcc version 5.4.0 20160609 (Ubuntu + 5.4.0-6ubuntu1~16.04.2) ) #55-Ubuntu SMP Thu Aug 11 18:01:55 UTC 2016","hostname":"sylvain-sleede-pc"}' + Content-Length: + - '81' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 08 Sep 2016 12:45:30 GMT + Content-Type: + - application/json + Content-Length: + - '779' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_99lRiyf8VYL75c + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "tok_18rRuI2sOmf47Nz9nl5ZXXoF", + "object": "token", + "card": { + "id": "card_18rRuI2sOmf47Nz9TSeEZ96o", + "object": "card", + "address_city": null, + "address_country": null, + "address_line1": null, + "address_line1_check": null, + "address_line2": null, + "address_state": null, + "address_zip": null, + "address_zip_check": null, + "brand": "Visa", + "country": "US", + "cvc_check": "unchecked", + "dynamic_last4": null, + "exp_month": 4, + "exp_year": 2017, + "fingerprint": "o52jybR7bnmNn6AT", + "funding": "credit", + "last4": "4242", + "metadata": {}, + "name": null, + "tokenization_method": null + }, + "client_ip": "90.52.44.103", + "created": 1473338730, + "livemode": false, + "type": "card", + "used": false + } + http_version: + recorded_at: Thu, 08 Sep 2016 12:45:30 GMT +- request: + method: post + uri: https://api.stripe.com/v1/invoiceitems + body: + encoding: UTF-8 + string: customer=cus_8Di1wjdVktv5kt&amount=5100¤cy=usd&description=Formation+Imprimante+3D+September+05%2C+2016+08%3A00+-+09%3A00+AM + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 4.4.0-36-generic (buildd@lcy01-01) (gcc version 5.4.0 20160609 (Ubuntu + 5.4.0-6ubuntu1~16.04.2) ) #55-Ubuntu SMP Thu Aug 11 18:01:55 UTC 2016","hostname":"sylvain-sleede-pc"}' + Content-Length: + - '130' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 08 Sep 2016 12:45:31 GMT + Content-Type: + - application/json + Content-Length: + - '476' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_99lRUe4QZ7kRSv + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "ii_18rRuJ2sOmf47Nz9S7f6qfEz", + "object": "invoiceitem", + "amount": 5100, + "currency": "usd", + "customer": "cus_8Di1wjdVktv5kt", + "date": 1473338731, + "description": "Formation Imprimante 3D September 05, 2016 08:00 - 09:00 AM", + "discountable": true, + "invoice": null, + "livemode": false, + "metadata": {}, + "period": { + "start": 1473338731, + "end": 1473338731 + }, + "plan": null, + "proration": false, + "quantity": null, + "subscription": null + } + http_version: + recorded_at: Thu, 08 Sep 2016 12:45:31 GMT +- request: + method: post + uri: https://api.stripe.com/v1/invoiceitems + body: + encoding: UTF-8 + string: customer=cus_8Di1wjdVktv5kt&amount=-510¤cy=usd&description=coupon+XMAS10 + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 4.4.0-36-generic (buildd@lcy01-01) (gcc version 5.4.0 20160609 (Ubuntu + 5.4.0-6ubuntu1~16.04.2) ) #55-Ubuntu SMP Thu Aug 11 18:01:55 UTC 2016","hostname":"sylvain-sleede-pc"}' + Content-Length: + - '78' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 08 Sep 2016 12:45:32 GMT + Content-Type: + - application/json + Content-Length: + - '431' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_99lR2huyf6IhVY + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "ii_18rRuK2sOmf47Nz9ye4DQ4PM", + "object": "invoiceitem", + "amount": -510, + "currency": "usd", + "customer": "cus_8Di1wjdVktv5kt", + "date": 1473338732, + "description": "coupon XMAS10", + "discountable": false, + "invoice": null, + "livemode": false, + "metadata": {}, + "period": { + "start": 1473338732, + "end": 1473338732 + }, + "plan": null, + "proration": false, + "quantity": null, + "subscription": null + } + http_version: + recorded_at: Thu, 08 Sep 2016 12:45:32 GMT +- request: + method: get + uri: https://api.stripe.com/v1/customers/cus_8Di1wjdVktv5kt + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 4.4.0-36-generic (buildd@lcy01-01) (gcc version 5.4.0 20160609 (Ubuntu + 5.4.0-6ubuntu1~16.04.2) ) #55-Ubuntu SMP Thu Aug 11 18:01:55 UTC 2016","hostname":"sylvain-sleede-pc"}' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 08 Sep 2016 12:45:33 GMT + Content-Type: + - application/json + Content-Length: + - '2495' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_99lRlUD2wtvAmA + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "cus_8Di1wjdVktv5kt", + "object": "customer", + "account_balance": 0, + "created": 1459948888, + "currency": "usd", + "default_source": "card_18rNSk2sOmf47Nz9h1cf7obf", + "delinquent": false, + "description": "Jean Dupond", + "discount": null, + "email": "jean.dupond@gmail.com", + "livemode": false, + "metadata": {}, + "shipping": null, + "sources": { + "object": "list", + "data": [ + { + "id": "card_18rNSk2sOmf47Nz9h1cf7obf", + "object": "card", + "address_city": null, + "address_country": null, + "address_line1": null, + "address_line1_check": null, + "address_line2": null, + "address_state": null, + "address_zip": null, + "address_zip_check": null, + "brand": "Visa", + "country": "US", + "customer": "cus_8Di1wjdVktv5kt", + "cvc_check": "pass", + "dynamic_last4": null, + "exp_month": 4, + "exp_year": 2017, + "fingerprint": "o52jybR7bnmNn6AT", + "funding": "credit", + "last4": "4242", + "metadata": {}, + "name": null, + "tokenization_method": null + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/customers/cus_8Di1wjdVktv5kt/sources" + }, + "subscriptions": { + "object": "list", + "data": [ + { + "id": "sub_99gqb47NmX9r79", + "object": "subscription", + "application_fee_percent": null, + "cancel_at_period_end": true, + "canceled_at": 1473321656, + "created": 1473321652, + "current_period_end": 1475913652, + "current_period_start": 1473321652, + "customer": "cus_8Di1wjdVktv5kt", + "discount": null, + "ended_at": null, + "livemode": false, + "metadata": {}, + "plan": { + "id": "mensuel-standard-month-20160404171519", + "object": "plan", + "amount": 3000, + "created": 1459782921, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "name": "Mensuel - standard, association - month", + "statement_descriptor": null, + "trial_period_days": null + }, + "quantity": 1, + "start": 1473321652, + "status": "active", + "tax_percent": null, + "trial_end": null, + "trial_start": null + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/customers/cus_8Di1wjdVktv5kt/subscriptions" + } + } + http_version: + recorded_at: Thu, 08 Sep 2016 12:45:33 GMT +- request: + method: post + uri: https://api.stripe.com/v1/customers/cus_8Di1wjdVktv5kt/sources + body: + encoding: UTF-8 + string: card=tok_18rRuI2sOmf47Nz9nl5ZXXoF + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 4.4.0-36-generic (buildd@lcy01-01) (gcc version 5.4.0 20160609 (Ubuntu + 5.4.0-6ubuntu1~16.04.2) ) #55-Ubuntu SMP Thu Aug 11 18:01:55 UTC 2016","hostname":"sylvain-sleede-pc"}' + Content-Length: + - '33' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 08 Sep 2016 12:45:34 GMT + Content-Type: + - application/json + Content-Length: + - '577' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_99lReB7yNLWrc3 + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "card_18rRuI2sOmf47Nz9TSeEZ96o", + "object": "card", + "address_city": null, + "address_country": null, + "address_line1": null, + "address_line1_check": null, + "address_line2": null, + "address_state": null, + "address_zip": null, + "address_zip_check": null, + "brand": "Visa", + "country": "US", + "customer": "cus_8Di1wjdVktv5kt", + "cvc_check": "pass", + "dynamic_last4": null, + "exp_month": 4, + "exp_year": 2017, + "fingerprint": "o52jybR7bnmNn6AT", + "funding": "credit", + "last4": "4242", + "metadata": {}, + "name": null, + "tokenization_method": null + } + http_version: + recorded_at: Thu, 08 Sep 2016 12:45:34 GMT +- request: + method: post + uri: https://api.stripe.com/v1/customers/cus_8Di1wjdVktv5kt + body: + encoding: UTF-8 + string: default_source=card_18rRuI2sOmf47Nz9TSeEZ96o + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 4.4.0-36-generic (buildd@lcy01-01) (gcc version 5.4.0 20160609 (Ubuntu + 5.4.0-6ubuntu1~16.04.2) ) #55-Ubuntu SMP Thu Aug 11 18:01:55 UTC 2016","hostname":"sylvain-sleede-pc"}' + Content-Length: + - '44' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 08 Sep 2016 12:45:35 GMT + Content-Type: + - application/json + Content-Length: + - '3223' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_99lRU3qBXW4qEB + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "cus_8Di1wjdVktv5kt", + "object": "customer", + "account_balance": 0, + "created": 1459948888, + "currency": "usd", + "default_source": "card_18rRuI2sOmf47Nz9TSeEZ96o", + "delinquent": false, + "description": "Jean Dupond", + "discount": null, + "email": "jean.dupond@gmail.com", + "livemode": false, + "metadata": {}, + "shipping": null, + "sources": { + "object": "list", + "data": [ + { + "id": "card_18rRuI2sOmf47Nz9TSeEZ96o", + "object": "card", + "address_city": null, + "address_country": null, + "address_line1": null, + "address_line1_check": null, + "address_line2": null, + "address_state": null, + "address_zip": null, + "address_zip_check": null, + "brand": "Visa", + "country": "US", + "customer": "cus_8Di1wjdVktv5kt", + "cvc_check": "pass", + "dynamic_last4": null, + "exp_month": 4, + "exp_year": 2017, + "fingerprint": "o52jybR7bnmNn6AT", + "funding": "credit", + "last4": "4242", + "metadata": {}, + "name": null, + "tokenization_method": null + }, + { + "id": "card_18rNSk2sOmf47Nz9h1cf7obf", + "object": "card", + "address_city": null, + "address_country": null, + "address_line1": null, + "address_line1_check": null, + "address_line2": null, + "address_state": null, + "address_zip": null, + "address_zip_check": null, + "brand": "Visa", + "country": "US", + "customer": "cus_8Di1wjdVktv5kt", + "cvc_check": "pass", + "dynamic_last4": null, + "exp_month": 4, + "exp_year": 2017, + "fingerprint": "o52jybR7bnmNn6AT", + "funding": "credit", + "last4": "4242", + "metadata": {}, + "name": null, + "tokenization_method": null + } + ], + "has_more": false, + "total_count": 2, + "url": "/v1/customers/cus_8Di1wjdVktv5kt/sources" + }, + "subscriptions": { + "object": "list", + "data": [ + { + "id": "sub_99gqb47NmX9r79", + "object": "subscription", + "application_fee_percent": null, + "cancel_at_period_end": true, + "canceled_at": 1473321656, + "created": 1473321652, + "current_period_end": 1475913652, + "current_period_start": 1473321652, + "customer": "cus_8Di1wjdVktv5kt", + "discount": null, + "ended_at": null, + "livemode": false, + "metadata": {}, + "plan": { + "id": "mensuel-standard-month-20160404171519", + "object": "plan", + "amount": 3000, + "created": 1459782921, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "name": "Mensuel - standard, association - month", + "statement_descriptor": null, + "trial_period_days": null + }, + "quantity": 1, + "start": 1473321652, + "status": "active", + "tax_percent": null, + "trial_end": null, + "trial_start": null + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/customers/cus_8Di1wjdVktv5kt/subscriptions" + } + } + http_version: + recorded_at: Thu, 08 Sep 2016 12:45:35 GMT +- request: + method: post + uri: https://api.stripe.com/v1/invoices + body: + encoding: UTF-8 + string: customer=cus_8Di1wjdVktv5kt + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 4.4.0-36-generic (buildd@lcy01-01) (gcc version 5.4.0 20160609 (Ubuntu + 5.4.0-6ubuntu1~16.04.2) ) #55-Ubuntu SMP Thu Aug 11 18:01:55 UTC 2016","hostname":"sylvain-sleede-pc"}' + Content-Length: + - '27' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 08 Sep 2016 12:45:36 GMT + Content-Type: + - application/json + Content-Length: + - '1925' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_99lRjTRT4OH5kv + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "in_18rRuO2sOmf47Nz9qbfxBA0D", + "object": "invoice", + "amount_due": 4590, + "application_fee": null, + "attempt_count": 0, + "attempted": false, + "charge": null, + "closed": false, + "currency": "usd", + "customer": "cus_8Di1wjdVktv5kt", + "date": 1473338736, + "description": null, + "discount": null, + "ending_balance": null, + "forgiven": false, + "lines": { + "object": "list", + "data": [ + { + "id": "ii_18rRuK2sOmf47Nz9ye4DQ4PM", + "object": "line_item", + "amount": -510, + "currency": "usd", + "description": "coupon XMAS10", + "discountable": false, + "livemode": false, + "metadata": {}, + "period": { + "start": 1473338732, + "end": 1473338732 + }, + "plan": null, + "proration": false, + "quantity": null, + "subscription": null, + "type": "invoiceitem" + }, + { + "id": "ii_18rRuJ2sOmf47Nz9S7f6qfEz", + "object": "line_item", + "amount": 5100, + "currency": "usd", + "description": "Formation Imprimante 3D September 05, 2016 08:00 - 09:00 AM", + "discountable": true, + "livemode": false, + "metadata": {}, + "period": { + "start": 1473338731, + "end": 1473338731 + }, + "plan": null, + "proration": false, + "quantity": null, + "subscription": null, + "type": "invoiceitem" + } + ], + "has_more": false, + "total_count": 2, + "url": "/v1/invoices/in_18rRuO2sOmf47Nz9qbfxBA0D/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": 1473342336, + "paid": false, + "period_end": 1473338736, + "period_start": 1473321652, + "receipt_number": null, + "starting_balance": 0, + "statement_descriptor": null, + "subscription": null, + "subtotal": 4590, + "tax": null, + "tax_percent": null, + "total": 4590, + "webhooks_delivered_at": null + } + http_version: + recorded_at: Thu, 08 Sep 2016 12:45:36 GMT +- request: + method: post + uri: https://api.stripe.com/v1/invoices/in_18rRuO2sOmf47Nz9qbfxBA0D/pay + body: + encoding: ASCII-8BIT + string: '' + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 4.4.0-36-generic (buildd@lcy01-01) (gcc version 5.4.0 20160609 (Ubuntu + 5.4.0-6ubuntu1~16.04.2) ) #55-Ubuntu SMP Thu Aug 11 18:01:55 UTC 2016","hostname":"sylvain-sleede-pc"}' + Content-Length: + - '0' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 08 Sep 2016 12:45:37 GMT + Content-Type: + - application/json + Content-Length: + - '1944' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_99lRH31egNKaff + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "in_18rRuO2sOmf47Nz9qbfxBA0D", + "object": "invoice", + "amount_due": 4590, + "application_fee": null, + "attempt_count": 1, + "attempted": true, + "charge": "ch_18rRuP2sOmf47Nz9V3NRITbR", + "closed": true, + "currency": "usd", + "customer": "cus_8Di1wjdVktv5kt", + "date": 1473338736, + "description": null, + "discount": null, + "ending_balance": 0, + "forgiven": false, + "lines": { + "object": "list", + "data": [ + { + "id": "ii_18rRuK2sOmf47Nz9ye4DQ4PM", + "object": "line_item", + "amount": -510, + "currency": "usd", + "description": "coupon XMAS10", + "discountable": false, + "livemode": false, + "metadata": {}, + "period": { + "start": 1473338732, + "end": 1473338732 + }, + "plan": null, + "proration": false, + "quantity": null, + "subscription": null, + "type": "invoiceitem" + }, + { + "id": "ii_18rRuJ2sOmf47Nz9S7f6qfEz", + "object": "line_item", + "amount": 5100, + "currency": "usd", + "description": "Formation Imprimante 3D September 05, 2016 08:00 - 09:00 AM", + "discountable": true, + "livemode": false, + "metadata": {}, + "period": { + "start": 1473338731, + "end": 1473338731 + }, + "plan": null, + "proration": false, + "quantity": null, + "subscription": null, + "type": "invoiceitem" + } + ], + "has_more": false, + "total_count": 2, + "url": "/v1/invoices/in_18rRuO2sOmf47Nz9qbfxBA0D/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": null, + "paid": true, + "period_end": 1473338736, + "period_start": 1473321652, + "receipt_number": null, + "starting_balance": 0, + "statement_descriptor": null, + "subscription": null, + "subtotal": 4590, + "tax": null, + "tax_percent": null, + "total": 4590, + "webhooks_delivered_at": 1473338736 + } + http_version: + recorded_at: Thu, 08 Sep 2016 12:45:37 GMT +- request: + method: delete + uri: https://api.stripe.com/v1/customers/cus_8Di1wjdVktv5kt/sources/card_18rRuI2sOmf47Nz9TSeEZ96o + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 4.4.0-36-generic (buildd@lcy01-01) (gcc version 5.4.0 20160609 (Ubuntu + 5.4.0-6ubuntu1~16.04.2) ) #55-Ubuntu SMP Thu Aug 11 18:01:55 UTC 2016","hostname":"sylvain-sleede-pc"}' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Thu, 08 Sep 2016 12:45:38 GMT + Content-Type: + - application/json + Content-Length: + - '63' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_99lRURlSYOVhs3 + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "deleted": true, + "id": "card_18rRuI2sOmf47Nz9TSeEZ96o" + } + http_version: + recorded_at: Thu, 08 Sep 2016 12:45:38 GMT +recorded_with: VCR 3.0.1 diff --git a/test/vcr_cassettes/subscriptions_user_create_success_with_wallet.yml b/test/vcr_cassettes/subscriptions_user_create_success_with_wallet.yml new file mode 100644 index 000000000..8407e0a69 --- /dev/null +++ b/test/vcr_cassettes/subscriptions_user_create_success_with_wallet.yml @@ -0,0 +1,821 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.stripe.com/v1/tokens + body: + encoding: UTF-8 + string: card[number]=4242424242424242&card[exp_month]=4&card[exp_year]=2017&card[cvc]=314 + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin15","engine":"ruby","publisher":"stripe","uname":"Darwin + mbp-sleede-peng.home 15.4.0 Darwin Kernel Version 15.4.0: Fri Feb 26 22:08:05 + PST 2016; root:xnu-3248.40.184~3/RELEASE_X86_64 x86_64","hostname":"mbp-sleede-peng.home"}' + Content-Length: + - '81' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Mon, 11 Jul 2016 13:15:52 GMT + Content-Type: + - application/json + Content-Length: + - '780' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_8nfbr26ZMi1nMx + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "tok_18W4GK2sOmf47Nz9I7fm3Y18", + "object": "token", + "card": { + "id": "card_18W4GK2sOmf47Nz9K6dfSmZl", + "object": "card", + "address_city": null, + "address_country": null, + "address_line1": null, + "address_line1_check": null, + "address_line2": null, + "address_state": null, + "address_zip": null, + "address_zip_check": null, + "brand": "Visa", + "country": "US", + "cvc_check": "unchecked", + "dynamic_last4": null, + "exp_month": 4, + "exp_year": 2017, + "fingerprint": "o52jybR7bnmNn6AT", + "funding": "credit", + "last4": "4242", + "metadata": {}, + "name": null, + "tokenization_method": null + }, + "client_ip": "82.122.118.54", + "created": 1468242952, + "livemode": false, + "type": "card", + "used": false + } + http_version: + recorded_at: Mon, 11 Jul 2016 13:15:52 GMT +- request: + method: get + uri: https://api.stripe.com/v1/tokens/tok_18W4GK2sOmf47Nz9I7fm3Y18 + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin15","engine":"ruby","publisher":"stripe","uname":"Darwin + mbp-sleede-peng.home 15.4.0 Darwin Kernel Version 15.4.0: Fri Feb 26 22:08:05 + PST 2016; root:xnu-3248.40.184~3/RELEASE_X86_64 x86_64","hostname":"mbp-sleede-peng.home"}' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Mon, 11 Jul 2016 13:15:53 GMT + Content-Type: + - application/json + Content-Length: + - '780' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_8nfbbQfs4YR066 + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "tok_18W4GK2sOmf47Nz9I7fm3Y18", + "object": "token", + "card": { + "id": "card_18W4GK2sOmf47Nz9K6dfSmZl", + "object": "card", + "address_city": null, + "address_country": null, + "address_line1": null, + "address_line1_check": null, + "address_line2": null, + "address_state": null, + "address_zip": null, + "address_zip_check": null, + "brand": "Visa", + "country": "US", + "cvc_check": "unchecked", + "dynamic_last4": null, + "exp_month": 4, + "exp_year": 2017, + "fingerprint": "o52jybR7bnmNn6AT", + "funding": "credit", + "last4": "4242", + "metadata": {}, + "name": null, + "tokenization_method": null + }, + "client_ip": "82.122.118.54", + "created": 1468242952, + "livemode": false, + "type": "card", + "used": false + } + http_version: + recorded_at: Mon, 11 Jul 2016 13:15:53 GMT +- request: + method: get + uri: https://api.stripe.com/v1/customers/cus_8CzNtM08NVlSGN + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin15","engine":"ruby","publisher":"stripe","uname":"Darwin + mbp-sleede-peng.home 15.4.0 Darwin Kernel Version 15.4.0: Fri Feb 26 22:08:05 + PST 2016; root:xnu-3248.40.184~3/RELEASE_X86_64 x86_64","hostname":"mbp-sleede-peng.home"}' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Mon, 11 Jul 2016 13:15:54 GMT + Content-Type: + - application/json + Content-Length: + - '655' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_8nfb0Ez5UgkL6i + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "cus_8CzNtM08NVlSGN", + "object": "customer", + "account_balance": 0, + "created": 1459782849, + "currency": null, + "default_source": null, + "delinquent": false, + "description": "Vanessa Lonchamp", + "discount": null, + "email": "vanessa.lonchamp@sfr.fr", + "livemode": false, + "metadata": {}, + "shipping": null, + "sources": { + "object": "list", + "data": [], + "has_more": false, + "total_count": 0, + "url": "/v1/customers/cus_8CzNtM08NVlSGN/sources" + }, + "subscriptions": { + "object": "list", + "data": [], + "has_more": false, + "total_count": 0, + "url": "/v1/customers/cus_8CzNtM08NVlSGN/subscriptions" + } + } + http_version: + recorded_at: Mon, 11 Jul 2016 13:15:54 GMT +- request: + method: post + uri: https://api.stripe.com/v1/invoiceitems + body: + encoding: UTF-8 + string: customer=cus_8CzNtM08NVlSGN&amount=-1000¤cy=usd&description=wallet+-10.0 + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin15","engine":"ruby","publisher":"stripe","uname":"Darwin + mbp-sleede-peng.home 15.4.0 Darwin Kernel Version 15.4.0: Fri Feb 26 22:08:05 + PST 2016; root:xnu-3248.40.184~3/RELEASE_X86_64 x86_64","hostname":"mbp-sleede-peng.home"}' + Content-Length: + - '78' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Mon, 11 Jul 2016 13:15:55 GMT + Content-Type: + - application/json + Content-Length: + - '431' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_8nfbS1DU8keH5x + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "ii_18W4GN2sOmf47Nz9K1dTfTyA", + "object": "invoiceitem", + "amount": -1000, + "currency": "usd", + "customer": "cus_8CzNtM08NVlSGN", + "date": 1468242955, + "description": "wallet -10.0", + "discountable": false, + "invoice": null, + "livemode": false, + "metadata": {}, + "period": { + "start": 1468242955, + "end": 1468242955 + }, + "plan": null, + "proration": false, + "quantity": null, + "subscription": null + } + http_version: + recorded_at: Mon, 11 Jul 2016 13:15:55 GMT +- request: + method: post + uri: https://api.stripe.com/v1/customers/cus_8CzNtM08NVlSGN/subscriptions + body: + encoding: UTF-8 + string: plan=mensuel-tarif-reduit-student-month-20160404171827&source=tok_18W4GK2sOmf47Nz9I7fm3Y18 + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin15","engine":"ruby","publisher":"stripe","uname":"Darwin + mbp-sleede-peng.home 15.4.0 Darwin Kernel Version 15.4.0: Fri Feb 26 22:08:05 + PST 2016; root:xnu-3248.40.184~3/RELEASE_X86_64 x86_64","hostname":"mbp-sleede-peng.home"}' + Content-Length: + - '90' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Mon, 11 Jul 2016 13:15:57 GMT + Content-Type: + - application/json + Content-Length: + - '926' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_8nfbYSDVSvjiep + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: ASCII-8BIT + string: !binary |- + ewogICJpZCI6ICJzdWJfOG5mYlRHR0lRUlF6eDEiLAogICJvYmplY3QiOiAi + c3Vic2NyaXB0aW9uIiwKICAiYXBwbGljYXRpb25fZmVlX3BlcmNlbnQiOiBu + dWxsLAogICJjYW5jZWxfYXRfcGVyaW9kX2VuZCI6IGZhbHNlLAogICJjYW5j + ZWxlZF9hdCI6IG51bGwsCiAgImNyZWF0ZWQiOiAxNDY4MjQyOTU2LAogICJj + dXJyZW50X3BlcmlvZF9lbmQiOiAxNDcwOTIxMzU2LAogICJjdXJyZW50X3Bl + cmlvZF9zdGFydCI6IDE0NjgyNDI5NTYsCiAgImN1c3RvbWVyIjogImN1c184 + Q3pOdE0wOE5WbFNHTiIsCiAgImRpc2NvdW50IjogbnVsbCwKICAiZW5kZWRf + YXQiOiBudWxsLAogICJsaXZlbW9kZSI6IGZhbHNlLAogICJtZXRhZGF0YSI6 + IHt9LAogICJwbGFuIjogewogICAgImlkIjogIm1lbnN1ZWwtdGFyaWYtcmVk + dWl0LXN0dWRlbnQtbW9udGgtMjAxNjA0MDQxNzE4MjciLAogICAgIm9iamVj + dCI6ICJwbGFuIiwKICAgICJhbW91bnQiOiAyMDAwLAogICAgImNyZWF0ZWQi + OiAxNDU5NzgzMTA4LAogICAgImN1cnJlbmN5IjogInVzZCIsCiAgICAiaW50 + ZXJ2YWwiOiAibW9udGgiLAogICAgImludGVydmFsX2NvdW50IjogMSwKICAg + ICJsaXZlbW9kZSI6IGZhbHNlLAogICAgIm1ldGFkYXRhIjoge30sCiAgICAi + bmFtZSI6ICJNZW5zdWVsIHRhcmlmIHLDqWR1aXQgLSDDqXR1ZGlhbnQsIC0g + ZGUgMjUgYW5zLCBlbnNlaWduYW50LCBkZW1hbmRldXIgZCdlbXBsb2kgLSBt + b250aCIsCiAgICAic3RhdGVtZW50X2Rlc2NyaXB0b3IiOiBudWxsLAogICAg + InRyaWFsX3BlcmlvZF9kYXlzIjogbnVsbAogIH0sCiAgInF1YW50aXR5Ijog + MSwKICAic3RhcnQiOiAxNDY4MjQyOTU2LAogICJzdGF0dXMiOiAiYWN0aXZl + IiwKICAidGF4X3BlcmNlbnQiOiBudWxsLAogICJ0cmlhbF9lbmQiOiBudWxs + LAogICJ0cmlhbF9zdGFydCI6IG51bGwKfQo= + http_version: + recorded_at: Mon, 11 Jul 2016 13:15:57 GMT +- request: + method: get + uri: https://api.stripe.com/v1/invoices?customer=cus_8CzNtM08NVlSGN&limit=1 + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin15","engine":"ruby","publisher":"stripe","uname":"Darwin + mbp-sleede-peng.home 15.4.0 Darwin Kernel Version 15.4.0: Fri Feb 26 22:08:05 + PST 2016; root:xnu-3248.40.184~3/RELEASE_X86_64 x86_64","hostname":"mbp-sleede-peng.home"}' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Mon, 11 Jul 2016 13:15:58 GMT + Content-Type: + - application/json + Content-Length: + - '2835' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_8nfbi8eORMWkgD + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: ASCII-8BIT + string: !binary |- + ewogICJvYmplY3QiOiAibGlzdCIsCiAgImRhdGEiOiBbCiAgICB7CiAgICAg + ICJpZCI6ICJpbl8xOFc0R08yc09tZjQ3Tno5b291RFEzaGUiLAogICAgICAi + b2JqZWN0IjogImludm9pY2UiLAogICAgICAiYW1vdW50X2R1ZSI6IDEwMDAs + CiAgICAgICJhcHBsaWNhdGlvbl9mZWUiOiBudWxsLAogICAgICAiYXR0ZW1w + dF9jb3VudCI6IDEsCiAgICAgICJhdHRlbXB0ZWQiOiB0cnVlLAogICAgICAi + Y2hhcmdlIjogImNoXzE4VzRHTzJzT21mNDdOejlnb1k2T3lEbCIsCiAgICAg + ICJjbG9zZWQiOiB0cnVlLAogICAgICAiY3VycmVuY3kiOiAidXNkIiwKICAg + ICAgImN1c3RvbWVyIjogImN1c184Q3pOdE0wOE5WbFNHTiIsCiAgICAgICJk + YXRlIjogMTQ2ODI0Mjk1NiwKICAgICAgImRlc2NyaXB0aW9uIjogbnVsbCwK + ICAgICAgImRpc2NvdW50IjogbnVsbCwKICAgICAgImVuZGluZ19iYWxhbmNl + IjogMCwKICAgICAgImZvcmdpdmVuIjogZmFsc2UsCiAgICAgICJsaW5lcyI6 + IHsKICAgICAgICAib2JqZWN0IjogImxpc3QiLAogICAgICAgICJkYXRhIjog + WwogICAgICAgICAgewogICAgICAgICAgICAiaWQiOiAiaWlfMThXNEdOMnNP + bWY0N056OUsxZFRmVHlBIiwKICAgICAgICAgICAgIm9iamVjdCI6ICJsaW5l + X2l0ZW0iLAogICAgICAgICAgICAiYW1vdW50IjogLTEwMDAsCiAgICAgICAg + ICAgICJjdXJyZW5jeSI6ICJ1c2QiLAogICAgICAgICAgICAiZGVzY3JpcHRp + b24iOiAid2FsbGV0IC0xMC4wIiwKICAgICAgICAgICAgImRpc2NvdW50YWJs + ZSI6IGZhbHNlLAogICAgICAgICAgICAibGl2ZW1vZGUiOiBmYWxzZSwKICAg + ICAgICAgICAgIm1ldGFkYXRhIjoge30sCiAgICAgICAgICAgICJwZXJpb2Qi + OiB7CiAgICAgICAgICAgICAgInN0YXJ0IjogMTQ2ODI0Mjk1NSwKICAgICAg + ICAgICAgICAiZW5kIjogMTQ2ODI0Mjk1NQogICAgICAgICAgICB9LAogICAg + ICAgICAgICAicGxhbiI6IG51bGwsCiAgICAgICAgICAgICJwcm9yYXRpb24i + OiBmYWxzZSwKICAgICAgICAgICAgInF1YW50aXR5IjogbnVsbCwKICAgICAg + ICAgICAgInN1YnNjcmlwdGlvbiI6IG51bGwsCiAgICAgICAgICAgICJ0eXBl + IjogImludm9pY2VpdGVtIgogICAgICAgICAgfSwKICAgICAgICAgIHsKICAg + ICAgICAgICAgImlkIjogInN1Yl84bmZiVEdHSVFSUXp4MSIsCiAgICAgICAg + ICAgICJvYmplY3QiOiAibGluZV9pdGVtIiwKICAgICAgICAgICAgImFtb3Vu + dCI6IDIwMDAsCiAgICAgICAgICAgICJjdXJyZW5jeSI6ICJ1c2QiLAogICAg + ICAgICAgICAiZGVzY3JpcHRpb24iOiBudWxsLAogICAgICAgICAgICAiZGlz + Y291bnRhYmxlIjogdHJ1ZSwKICAgICAgICAgICAgImxpdmVtb2RlIjogZmFs + c2UsCiAgICAgICAgICAgICJtZXRhZGF0YSI6IHt9LAogICAgICAgICAgICAi + cGVyaW9kIjogewogICAgICAgICAgICAgICJzdGFydCI6IDE0NjgyNDI5NTYs + CiAgICAgICAgICAgICAgImVuZCI6IDE0NzA5MjEzNTYKICAgICAgICAgICAg + fSwKICAgICAgICAgICAgInBsYW4iOiB7CiAgICAgICAgICAgICAgImlkIjog + Im1lbnN1ZWwtdGFyaWYtcmVkdWl0LXN0dWRlbnQtbW9udGgtMjAxNjA0MDQx + NzE4MjciLAogICAgICAgICAgICAgICJvYmplY3QiOiAicGxhbiIsCiAgICAg + ICAgICAgICAgImFtb3VudCI6IDIwMDAsCiAgICAgICAgICAgICAgImNyZWF0 + ZWQiOiAxNDU5NzgzMTA4LAogICAgICAgICAgICAgICJjdXJyZW5jeSI6ICJ1 + c2QiLAogICAgICAgICAgICAgICJpbnRlcnZhbCI6ICJtb250aCIsCiAgICAg + ICAgICAgICAgImludGVydmFsX2NvdW50IjogMSwKICAgICAgICAgICAgICAi + bGl2ZW1vZGUiOiBmYWxzZSwKICAgICAgICAgICAgICAibWV0YWRhdGEiOiB7 + fSwKICAgICAgICAgICAgICAibmFtZSI6ICJNZW5zdWVsIHRhcmlmIHLDqWR1 + aXQgLSDDqXR1ZGlhbnQsIC0gZGUgMjUgYW5zLCBlbnNlaWduYW50LCBkZW1h + bmRldXIgZCdlbXBsb2kgLSBtb250aCIsCiAgICAgICAgICAgICAgInN0YXRl + bWVudF9kZXNjcmlwdG9yIjogbnVsbCwKICAgICAgICAgICAgICAidHJpYWxf + cGVyaW9kX2RheXMiOiBudWxsCiAgICAgICAgICAgIH0sCiAgICAgICAgICAg + ICJwcm9yYXRpb24iOiBmYWxzZSwKICAgICAgICAgICAgInF1YW50aXR5Ijog + MSwKICAgICAgICAgICAgInN1YnNjcmlwdGlvbiI6IG51bGwsCiAgICAgICAg + ICAgICJ0eXBlIjogInN1YnNjcmlwdGlvbiIKICAgICAgICAgIH0KICAgICAg + ICBdLAogICAgICAgICJoYXNfbW9yZSI6IGZhbHNlLAogICAgICAgICJ0b3Rh + bF9jb3VudCI6IDIsCiAgICAgICAgInVybCI6ICIvdjEvaW52b2ljZXMvaW5f + MThXNEdPMnNPbWY0N056OW9vdURRM2hlL2xpbmVzIgogICAgICB9LAogICAg + ICAibGl2ZW1vZGUiOiBmYWxzZSwKICAgICAgIm1ldGFkYXRhIjoge30sCiAg + ICAgICJuZXh0X3BheW1lbnRfYXR0ZW1wdCI6IG51bGwsCiAgICAgICJwYWlk + IjogdHJ1ZSwKICAgICAgInBlcmlvZF9lbmQiOiAxNDY4MjQyOTU2LAogICAg + ICAicGVyaW9kX3N0YXJ0IjogMTQ2ODI0Mjk1NiwKICAgICAgInJlY2VpcHRf + bnVtYmVyIjogbnVsbCwKICAgICAgInN0YXJ0aW5nX2JhbGFuY2UiOiAwLAog + ICAgICAic3RhdGVtZW50X2Rlc2NyaXB0b3IiOiBudWxsLAogICAgICAic3Vi + c2NyaXB0aW9uIjogInN1Yl84bmZiVEdHSVFSUXp4MSIsCiAgICAgICJzdWJ0 + b3RhbCI6IDEwMDAsCiAgICAgICJ0YXgiOiBudWxsLAogICAgICAidGF4X3Bl + cmNlbnQiOiBudWxsLAogICAgICAidG90YWwiOiAxMDAwLAogICAgICAid2Vi + aG9va3NfZGVsaXZlcmVkX2F0IjogMTQ2ODI0Mjk1NgogICAgfQogIF0sCiAg + Imhhc19tb3JlIjogZmFsc2UsCiAgInVybCI6ICIvdjEvaW52b2ljZXMiCn0K + http_version: + recorded_at: Mon, 11 Jul 2016 13:15:58 GMT +- request: + method: get + uri: https://api.stripe.com/v1/customers/cus_8CzNtM08NVlSGN + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin15","engine":"ruby","publisher":"stripe","uname":"Darwin + mbp-sleede-peng.home 15.4.0 Darwin Kernel Version 15.4.0: Fri Feb 26 22:08:05 + PST 2016; root:xnu-3248.40.184~3/RELEASE_X86_64 x86_64","hostname":"mbp-sleede-peng.home"}' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Mon, 11 Jul 2016 13:15:59 GMT + Content-Type: + - application/json + Content-Length: + - '2556' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_8nfbxXXi48kJ9H + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: ASCII-8BIT + string: !binary |- + ewogICJpZCI6ICJjdXNfOEN6TnRNMDhOVmxTR04iLAogICJvYmplY3QiOiAi + Y3VzdG9tZXIiLAogICJhY2NvdW50X2JhbGFuY2UiOiAwLAogICJjcmVhdGVk + IjogMTQ1OTc4Mjg0OSwKICAiY3VycmVuY3kiOiAidXNkIiwKICAiZGVmYXVs + dF9zb3VyY2UiOiAiY2FyZF8xOFc0R0syc09tZjQ3Tno5SzZkZlNtWmwiLAog + ICJkZWxpbnF1ZW50IjogZmFsc2UsCiAgImRlc2NyaXB0aW9uIjogIlZhbmVz + c2EgTG9uY2hhbXAiLAogICJkaXNjb3VudCI6IG51bGwsCiAgImVtYWlsIjog + InZhbmVzc2EubG9uY2hhbXBAc2ZyLmZyIiwKICAibGl2ZW1vZGUiOiBmYWxz + ZSwKICAibWV0YWRhdGEiOiB7fSwKICAic2hpcHBpbmciOiBudWxsLAogICJz + b3VyY2VzIjogewogICAgIm9iamVjdCI6ICJsaXN0IiwKICAgICJkYXRhIjog + WwogICAgICB7CiAgICAgICAgImlkIjogImNhcmRfMThXNEdLMnNPbWY0N056 + OUs2ZGZTbVpsIiwKICAgICAgICAib2JqZWN0IjogImNhcmQiLAogICAgICAg + ICJhZGRyZXNzX2NpdHkiOiBudWxsLAogICAgICAgICJhZGRyZXNzX2NvdW50 + cnkiOiBudWxsLAogICAgICAgICJhZGRyZXNzX2xpbmUxIjogbnVsbCwKICAg + ICAgICAiYWRkcmVzc19saW5lMV9jaGVjayI6IG51bGwsCiAgICAgICAgImFk + ZHJlc3NfbGluZTIiOiBudWxsLAogICAgICAgICJhZGRyZXNzX3N0YXRlIjog + bnVsbCwKICAgICAgICAiYWRkcmVzc196aXAiOiBudWxsLAogICAgICAgICJh + ZGRyZXNzX3ppcF9jaGVjayI6IG51bGwsCiAgICAgICAgImJyYW5kIjogIlZp + c2EiLAogICAgICAgICJjb3VudHJ5IjogIlVTIiwKICAgICAgICAiY3VzdG9t + ZXIiOiAiY3VzXzhDek50TTA4TlZsU0dOIiwKICAgICAgICAiY3ZjX2NoZWNr + IjogInBhc3MiLAogICAgICAgICJkeW5hbWljX2xhc3Q0IjogbnVsbCwKICAg + ICAgICAiZXhwX21vbnRoIjogNCwKICAgICAgICAiZXhwX3llYXIiOiAyMDE3 + LAogICAgICAgICJmaW5nZXJwcmludCI6ICJvNTJqeWJSN2JubU5uNkFUIiwK + ICAgICAgICAiZnVuZGluZyI6ICJjcmVkaXQiLAogICAgICAgICJsYXN0NCI6 + ICI0MjQyIiwKICAgICAgICAibWV0YWRhdGEiOiB7fSwKICAgICAgICAibmFt + ZSI6IG51bGwsCiAgICAgICAgInRva2VuaXphdGlvbl9tZXRob2QiOiBudWxs + CiAgICAgIH0KICAgIF0sCiAgICAiaGFzX21vcmUiOiBmYWxzZSwKICAgICJ0 + b3RhbF9jb3VudCI6IDEsCiAgICAidXJsIjogIi92MS9jdXN0b21lcnMvY3Vz + XzhDek50TTA4TlZsU0dOL3NvdXJjZXMiCiAgfSwKICAic3Vic2NyaXB0aW9u + cyI6IHsKICAgICJvYmplY3QiOiAibGlzdCIsCiAgICAiZGF0YSI6IFsKICAg + ICAgewogICAgICAgICJpZCI6ICJzdWJfOG5mYlRHR0lRUlF6eDEiLAogICAg + ICAgICJvYmplY3QiOiAic3Vic2NyaXB0aW9uIiwKICAgICAgICAiYXBwbGlj + YXRpb25fZmVlX3BlcmNlbnQiOiBudWxsLAogICAgICAgICJjYW5jZWxfYXRf + cGVyaW9kX2VuZCI6IGZhbHNlLAogICAgICAgICJjYW5jZWxlZF9hdCI6IG51 + bGwsCiAgICAgICAgImNyZWF0ZWQiOiAxNDY4MjQyOTU2LAogICAgICAgICJj + dXJyZW50X3BlcmlvZF9lbmQiOiAxNDcwOTIxMzU2LAogICAgICAgICJjdXJy + ZW50X3BlcmlvZF9zdGFydCI6IDE0NjgyNDI5NTYsCiAgICAgICAgImN1c3Rv + bWVyIjogImN1c184Q3pOdE0wOE5WbFNHTiIsCiAgICAgICAgImRpc2NvdW50 + IjogbnVsbCwKICAgICAgICAiZW5kZWRfYXQiOiBudWxsLAogICAgICAgICJs + aXZlbW9kZSI6IGZhbHNlLAogICAgICAgICJtZXRhZGF0YSI6IHt9LAogICAg + ICAgICJwbGFuIjogewogICAgICAgICAgImlkIjogIm1lbnN1ZWwtdGFyaWYt + cmVkdWl0LXN0dWRlbnQtbW9udGgtMjAxNjA0MDQxNzE4MjciLAogICAgICAg + ICAgIm9iamVjdCI6ICJwbGFuIiwKICAgICAgICAgICJhbW91bnQiOiAyMDAw + LAogICAgICAgICAgImNyZWF0ZWQiOiAxNDU5NzgzMTA4LAogICAgICAgICAg + ImN1cnJlbmN5IjogInVzZCIsCiAgICAgICAgICAiaW50ZXJ2YWwiOiAibW9u + dGgiLAogICAgICAgICAgImludGVydmFsX2NvdW50IjogMSwKICAgICAgICAg + ICJsaXZlbW9kZSI6IGZhbHNlLAogICAgICAgICAgIm1ldGFkYXRhIjoge30s + CiAgICAgICAgICAibmFtZSI6ICJNZW5zdWVsIHRhcmlmIHLDqWR1aXQgLSDD + qXR1ZGlhbnQsIC0gZGUgMjUgYW5zLCBlbnNlaWduYW50LCBkZW1hbmRldXIg + ZCdlbXBsb2kgLSBtb250aCIsCiAgICAgICAgICAic3RhdGVtZW50X2Rlc2Ny + aXB0b3IiOiBudWxsLAogICAgICAgICAgInRyaWFsX3BlcmlvZF9kYXlzIjog + bnVsbAogICAgICAgIH0sCiAgICAgICAgInF1YW50aXR5IjogMSwKICAgICAg + ICAic3RhcnQiOiAxNDY4MjQyOTU2LAogICAgICAgICJzdGF0dXMiOiAiYWN0 + aXZlIiwKICAgICAgICAidGF4X3BlcmNlbnQiOiBudWxsLAogICAgICAgICJ0 + cmlhbF9lbmQiOiBudWxsLAogICAgICAgICJ0cmlhbF9zdGFydCI6IG51bGwK + ICAgICAgfQogICAgXSwKICAgICJoYXNfbW9yZSI6IGZhbHNlLAogICAgInRv + dGFsX2NvdW50IjogMSwKICAgICJ1cmwiOiAiL3YxL2N1c3RvbWVycy9jdXNf + OEN6TnRNMDhOVmxTR04vc3Vic2NyaXB0aW9ucyIKICB9Cn0K + http_version: + recorded_at: Mon, 11 Jul 2016 13:15:58 GMT +- request: + method: get + uri: https://api.stripe.com/v1/customers/cus_8CzNtM08NVlSGN/subscriptions/sub_8nfbTGGIQRQzx1 + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin15","engine":"ruby","publisher":"stripe","uname":"Darwin + mbp-sleede-peng.home 15.4.0 Darwin Kernel Version 15.4.0: Fri Feb 26 22:08:05 + PST 2016; root:xnu-3248.40.184~3/RELEASE_X86_64 x86_64","hostname":"mbp-sleede-peng.home"}' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Mon, 11 Jul 2016 13:15:59 GMT + Content-Type: + - application/json + Content-Length: + - '926' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_8nfbwT9nBtTlrw + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: ASCII-8BIT + string: !binary |- + ewogICJpZCI6ICJzdWJfOG5mYlRHR0lRUlF6eDEiLAogICJvYmplY3QiOiAi + c3Vic2NyaXB0aW9uIiwKICAiYXBwbGljYXRpb25fZmVlX3BlcmNlbnQiOiBu + dWxsLAogICJjYW5jZWxfYXRfcGVyaW9kX2VuZCI6IGZhbHNlLAogICJjYW5j + ZWxlZF9hdCI6IG51bGwsCiAgImNyZWF0ZWQiOiAxNDY4MjQyOTU2LAogICJj + dXJyZW50X3BlcmlvZF9lbmQiOiAxNDcwOTIxMzU2LAogICJjdXJyZW50X3Bl + cmlvZF9zdGFydCI6IDE0NjgyNDI5NTYsCiAgImN1c3RvbWVyIjogImN1c184 + Q3pOdE0wOE5WbFNHTiIsCiAgImRpc2NvdW50IjogbnVsbCwKICAiZW5kZWRf + YXQiOiBudWxsLAogICJsaXZlbW9kZSI6IGZhbHNlLAogICJtZXRhZGF0YSI6 + IHt9LAogICJwbGFuIjogewogICAgImlkIjogIm1lbnN1ZWwtdGFyaWYtcmVk + dWl0LXN0dWRlbnQtbW9udGgtMjAxNjA0MDQxNzE4MjciLAogICAgIm9iamVj + dCI6ICJwbGFuIiwKICAgICJhbW91bnQiOiAyMDAwLAogICAgImNyZWF0ZWQi + OiAxNDU5NzgzMTA4LAogICAgImN1cnJlbmN5IjogInVzZCIsCiAgICAiaW50 + ZXJ2YWwiOiAibW9udGgiLAogICAgImludGVydmFsX2NvdW50IjogMSwKICAg + ICJsaXZlbW9kZSI6IGZhbHNlLAogICAgIm1ldGFkYXRhIjoge30sCiAgICAi + bmFtZSI6ICJNZW5zdWVsIHRhcmlmIHLDqWR1aXQgLSDDqXR1ZGlhbnQsIC0g + ZGUgMjUgYW5zLCBlbnNlaWduYW50LCBkZW1hbmRldXIgZCdlbXBsb2kgLSBt + b250aCIsCiAgICAic3RhdGVtZW50X2Rlc2NyaXB0b3IiOiBudWxsLAogICAg + InRyaWFsX3BlcmlvZF9kYXlzIjogbnVsbAogIH0sCiAgInF1YW50aXR5Ijog + MSwKICAic3RhcnQiOiAxNDY4MjQyOTU2LAogICJzdGF0dXMiOiAiYWN0aXZl + IiwKICAidGF4X3BlcmNlbnQiOiBudWxsLAogICJ0cmlhbF9lbmQiOiBudWxs + LAogICJ0cmlhbF9zdGFydCI6IG51bGwKfQo= + http_version: + recorded_at: Mon, 11 Jul 2016 13:15:59 GMT +- request: + method: delete + uri: https://api.stripe.com/v1/customers/cus_8CzNtM08NVlSGN/subscriptions/sub_8nfbTGGIQRQzx1?at_period_end=true + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*; q=0.5, application/xml" + Accept-Encoding: + - gzip, deflate + User-Agent: + - Stripe/v1 RubyBindings/1.30.2 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"1.30.2","lang":"ruby","lang_version":"2.3.0 p0 (2015-12-25)","platform":"x86_64-darwin15","engine":"ruby","publisher":"stripe","uname":"Darwin + mbp-sleede-peng.home 15.4.0 Darwin Kernel Version 15.4.0: Fri Feb 26 22:08:05 + PST 2016; root:xnu-3248.40.184~3/RELEASE_X86_64 x86_64","hostname":"mbp-sleede-peng.home"}' + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Mon, 11 Jul 2016 13:16:00 GMT + Content-Type: + - application/json + Content-Length: + - '931' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_8nfbwqRs1dLP5C + Stripe-Version: + - '2015-10-16' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: ASCII-8BIT + string: !binary |- + ewogICJpZCI6ICJzdWJfOG5mYlRHR0lRUlF6eDEiLAogICJvYmplY3QiOiAi + c3Vic2NyaXB0aW9uIiwKICAiYXBwbGljYXRpb25fZmVlX3BlcmNlbnQiOiBu + dWxsLAogICJjYW5jZWxfYXRfcGVyaW9kX2VuZCI6IHRydWUsCiAgImNhbmNl + bGVkX2F0IjogMTQ2ODI0Mjk2MCwKICAiY3JlYXRlZCI6IDE0NjgyNDI5NTYs + CiAgImN1cnJlbnRfcGVyaW9kX2VuZCI6IDE0NzA5MjEzNTYsCiAgImN1cnJl + bnRfcGVyaW9kX3N0YXJ0IjogMTQ2ODI0Mjk1NiwKICAiY3VzdG9tZXIiOiAi + Y3VzXzhDek50TTA4TlZsU0dOIiwKICAiZGlzY291bnQiOiBudWxsLAogICJl + bmRlZF9hdCI6IG51bGwsCiAgImxpdmVtb2RlIjogZmFsc2UsCiAgIm1ldGFk + YXRhIjoge30sCiAgInBsYW4iOiB7CiAgICAiaWQiOiAibWVuc3VlbC10YXJp + Zi1yZWR1aXQtc3R1ZGVudC1tb250aC0yMDE2MDQwNDE3MTgyNyIsCiAgICAi + b2JqZWN0IjogInBsYW4iLAogICAgImFtb3VudCI6IDIwMDAsCiAgICAiY3Jl + YXRlZCI6IDE0NTk3ODMxMDgsCiAgICAiY3VycmVuY3kiOiAidXNkIiwKICAg + ICJpbnRlcnZhbCI6ICJtb250aCIsCiAgICAiaW50ZXJ2YWxfY291bnQiOiAx + LAogICAgImxpdmVtb2RlIjogZmFsc2UsCiAgICAibWV0YWRhdGEiOiB7fSwK + ICAgICJuYW1lIjogIk1lbnN1ZWwgdGFyaWYgcsOpZHVpdCAtIMOpdHVkaWFu + dCwgLSBkZSAyNSBhbnMsIGVuc2VpZ25hbnQsIGRlbWFuZGV1ciBkJ2VtcGxv + aSAtIG1vbnRoIiwKICAgICJzdGF0ZW1lbnRfZGVzY3JpcHRvciI6IG51bGws + CiAgICAidHJpYWxfcGVyaW9kX2RheXMiOiBudWxsCiAgfSwKICAicXVhbnRp + dHkiOiAxLAogICJzdGFydCI6IDE0NjgyNDI5NTYsCiAgInN0YXR1cyI6ICJh + Y3RpdmUiLAogICJ0YXhfcGVyY2VudCI6IG51bGwsCiAgInRyaWFsX2VuZCI6 + IG51bGwsCiAgInRyaWFsX3N0YXJ0IjogbnVsbAp9Cg== + http_version: + recorded_at: Mon, 11 Jul 2016 13:16:00 GMT +recorded_with: VCR 3.0.1 diff --git a/vendor/assets/components/angular-aside/.bower.json b/vendor/assets/components/angular-aside/.bower.json new file mode 100644 index 000000000..7d913ae77 --- /dev/null +++ b/vendor/assets/components/angular-aside/.bower.json @@ -0,0 +1,49 @@ +{ + "name": "angular-aside", + "version": "1.3.2", + "homepage": "https://github.com/dbtek/angular-aside", + "author": { + "name": "İsmail Demirbilek", + "email": "ce.demirbilek@gmail.com" + }, + "description": "Off canvas side menu to use with ui-bootstrap.", + "main": [ + "dist/js/angular-aside.js", + "dist/css/angular-aside.css" + ], + "keywords": [ + "aside", + "off", + "canvas", + "menu", + "ui", + "bootstrap" + ], + "license": "MIT", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests", + "Gruntfile.js", + "karma.conf.js", + "package.json" + ], + "dependencies": { + "angular-bootstrap": ">=0.14.0" + }, + "devDependencies": { + "angular-mocks": ">=1.4.0" + }, + "_release": "1.3.2", + "_resolution": { + "type": "version", + "tag": "1.3.2", + "commit": "6093a98f325fbb606325da92a32b74b84aee7254" + }, + "_source": "https://github.com/dbtek/angular-aside.git", + "_target": "^1.3.2", + "_originalSource": "angular-aside", + "_direct": true +} \ No newline at end of file diff --git a/vendor/assets/components/angular-aside/LICENSE b/vendor/assets/components/angular-aside/LICENSE new file mode 100644 index 000000000..cbc9d516a --- /dev/null +++ b/vendor/assets/components/angular-aside/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 İsmail Demirbilek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/assets/components/angular-aside/README.md b/vendor/assets/components/angular-aside/README.md new file mode 100644 index 000000000..9d8d409a1 --- /dev/null +++ b/vendor/assets/components/angular-aside/README.md @@ -0,0 +1,55 @@ +angular-aside ![bower version](http://img.shields.io/bower/v/angular-aside.svg) [![npm version](https://badge.fury.io/js/angular-aside.svg)](https://www.npmjs.com/package/angular-aside) +============= + +Off canvas side menu for use with ui-bootstrap 0.14+. Extends ui-bootstrap's `$uibModal` provider. + +:information_desk_person: Please use v1.2.x for ui-bootstrap versions 0.13 and below. + +###[Live Demo](http://plnkr.co/edit/G7vMSv?p=preview) + +##Install + +#### Bower: +```bash + $ bower install angular-aside +``` +Then, include css/js in html. + +#### NPM +```bash + $ npm install angular-aside +``` + +##Usage + +```js + angular.module('myApp', ['ui.bootstrap', 'ngAside']); +``` + +```js +angular.module('myApp') + .controller('MyController', function($scope, $aside) { + var asideInstance = $aside.open({ + templateUrl: 'aside.html', + controller: 'AsideCtrl', + placement: 'left', + size: 'lg' + }); + }); +``` + +Supports all configuration that `$uibModal` has. Can be used with both `template` and `templateUrl`. For more info hit **Modal** section on [angular-ui bootstrap](http://angular-ui.github.io/bootstrap) documentation. + + +##Additional Config +- `placement` - Aside placement can be `'left'`, `'right'`, `'top'`, or `'bottom'`. + + +##Credits +- [Angular UI Bootstrap](angular-ui.github.io/bootstrap/) +- [Animate.css](http://daneden.github.io/animate.css/) + + +##Author + +İsmail Demirbilek ([@dbtek](https://twitter.com/dbtek)) diff --git a/vendor/assets/components/angular-aside/bower.json b/vendor/assets/components/angular-aside/bower.json new file mode 100644 index 000000000..9188c82ad --- /dev/null +++ b/vendor/assets/components/angular-aside/bower.json @@ -0,0 +1,39 @@ +{ + "name": "angular-aside", + "version": "1.3.2", + "homepage": "https://github.com/dbtek/angular-aside", + "author": { + "name": "İsmail Demirbilek", + "email": "ce.demirbilek@gmail.com" + }, + "description": "Off canvas side menu to use with ui-bootstrap.", + "main": [ + "dist/js/angular-aside.js", + "dist/css/angular-aside.css" + ], + "keywords": [ + "aside", + "off", + "canvas", + "menu", + "ui", + "bootstrap" + ], + "license": "MIT", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests", + "Gruntfile.js", + "karma.conf.js", + "package.json" + ], + "dependencies": { + "angular-bootstrap": ">=0.14.0" + }, + "devDependencies": { + "angular-mocks": ">=1.4.0" + } +} diff --git a/vendor/assets/components/angular-aside/demo.html b/vendor/assets/components/angular-aside/demo.html new file mode 100644 index 000000000..fb750378f --- /dev/null +++ b/vendor/assets/components/angular-aside/demo.html @@ -0,0 +1,151 @@ + + +
+ + + + + + + + + + + + + +
+
+

Angular Aside

+

+ Off canvas side menu to use with ui-bootstrap. Extends ui-bootstrap's $modal provider. +

+

+ + Download + +
+ + +

+
+ +
+
+
+

Demo

+

+ + + + + + + + +

+
+
+ +
+ +
+
+
+ + + + + + diff --git a/vendor/assets/components/angular-aside/dist/css/angular-aside.css b/vendor/assets/components/angular-aside/dist/css/angular-aside.css new file mode 100644 index 000000000..65542643e --- /dev/null +++ b/vendor/assets/components/angular-aside/dist/css/angular-aside.css @@ -0,0 +1,421 @@ +/*! + * angular-aside - v1.3.2 + * https://github.com/dbtek/angular-aside + * 2015-11-17 + * Copyright (c) 2015 İsmail Demirbilek + * License: MIT + */ + +/*! +Animate.css - http://daneden.me/animate +Licensed under the MIT license - http://opensource.org/licenses/MIT + +Copyright (c) 2014 Daniel Eden +*/ + +@-webkit-keyframes fadeInLeft { + 0% { + opacity: 0; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } + + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} + +@keyframes fadeInLeft { + 0% { + opacity: 0; + -webkit-transform: translate3d(-100%, 0, 0); + -ms-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } + + 100% { + opacity: 1; + -webkit-transform: none; + -ms-transform: none; + transform: none; + } +} + +.fadeInLeft { + -webkit-animation-name: fadeInLeft; + animation-name: fadeInLeft; +} + +@-webkit-keyframes fadeInRight { + 0% { + opacity: 0; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } + + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} + +@keyframes fadeInRight { + 0% { + opacity: 0; + -webkit-transform: translate3d(100%, 0, 0); + -ms-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } + + 100% { + opacity: 1; + -webkit-transform: none; + -ms-transform: none; + transform: none; + } +} + +.fadeInRight { + -webkit-animation-name: fadeInRight; + animation-name: fadeInRight; +} + +@-webkit-keyframes fadeInTop { + 0% { + opacity: 0; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } + + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} + +@keyframes fadeInTop { + 0% { + opacity: 0; + -webkit-transform: translate3d(0, -100%, 0); + -ms-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } + + 100% { + opacity: 1; + -webkit-transform: none; + -ms-transform: none; + transform: none; + } +} + +.fadeInTop { + -webkit-animation-name: fadeInTop; + animation-name: fadeInTop; +} + +@-webkit-keyframes fadeInBottom { + 0% { + opacity: 0; + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + } + + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} + +@keyframes fadeInBottom { + 0% { + opacity: 0; + -webkit-transform: translate3d(0, 100%, 0); + -ms-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + } + + 100% { + opacity: 1; + -webkit-transform: none; + -ms-transform: none; + transform: none; + } +} + +.fadeInBottom { + -webkit-animation-name: fadeInBottom; + animation-name: fadeInBottom; +} + +@-webkit-keyframes fadeOutLeft { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } +} + +@keyframes fadeOutLeft { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } +} + +.fadeOutLeft { + -webkit-animation-name: fadeOutLeft; + animation-name: fadeOutLeft; +} + +@-webkit-keyframes fadeOutRight { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } +} + +@keyframes fadeOutRight { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } +} + +.fadeOutRight { + -webkit-animation-name: fadeOutRight; + animation-name: fadeOutRight; +} + +@-webkit-keyframes fadeOutUp { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } +} + +@keyframes fadeOutUp { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } +} + +.fadeOutUp { + -webkit-animation-name: fadeOutUp; + animation-name: fadeOutUp; +} + + +@-webkit-keyframes fadeOutDown { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + } +} + +@keyframes fadeOutDown { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + } +} + +.fadeOutDown { + -webkit-animation-name: fadeOutDown; + animation-name: fadeOutDown; +} +/* Common */ + +.ng-aside { + overflow-y: auto; + overflow-x: hidden; +} + +.ng-aside .modal-dialog { + position: absolute; + margin: 0; + padding: 0; +} + +.ng-aside.fade .modal-dialog { + -o-transition: none; + -moz-transition: none; + -ms-transition: none; + -webkit-transition: none; + transition: none; + /*CSS transforms*/ + -o-transform: none; + -moz-transform: none; + -ms-transform: none; + -webkit-transform: none; + transform: none; +} + +.ng-aside .modal-dialog .modal-content { + overflow-y: auto; + overflow-x: hidden; + border: none; + border-radius: 0; +} + +/* Horizontal */ + +.ng-aside.horizontal { + height: 100%; +} + +.ng-aside.horizontal .modal-dialog .modal-content { + height: 100%; +} + +.ng-aside.horizontal .modal-dialog { + position: absolute; + top: 0; + height: 100%; +} + +.modal-open .ng-aside.horizontal.left .modal-dialog { + animation: fadeOutLeft 250ms; + -webkit-animation: fadeOutLeft 250ms; + -moz-animation: fadeOutLeft 250ms; + -o-animation: fadeOutLeft 250ms; + -ms-animation: fadeOutLeft 250ms; +} + +.ng-aside.horizontal.left.in .modal-dialog { + animation: fadeInLeft 400ms; + -webkit-animation: fadeInLeft 400ms; + -moz-animation: fadeInLeft 400ms; + -o-animation: fadeInLeft 400ms; + -ms-animation: fadeInLeft 400ms; +} + +.ng-aside.horizontal.left .modal-dialog { + left: 0; +} + +.ng-aside.horizontal.right .modal-dialog { + animation: fadeOutRight 400ms; + -webkit-animation: fadeOutRight 400ms; + -moz-animation: fadeOutRight 400ms; + -o-animation: fadeOutRight 400ms; + -ms-animation: fadeOutRight 400ms; +} + +.ng-aside.horizontal.right.in .modal-dialog { + animation: fadeInRight 250ms; + -webkit-animation: fadeInRight 250ms; +} + +.ng-aside.horizontal.right .modal-dialog { + right: 0; +} + +/* Vertical */ + +.ng-aside.vertical { + width: 100% !important; + overflow: hidden; +} + +.ng-aside.vertical .modal-dialog { + left: 0; + right: 0; + width: 100% !important; +} + +.ng-aside.vertical .modal-dialog .modal-content { + max-height: 400px; +} + +.ng-aside.vertical.top .modal-dialog { + animation: fadeOutUp 250ms; + -webkit-animation: fadeOutUp 250ms; + -webkit-animation: fadeOutUp 250ms; + -moz-animation: fadeOutUp 250ms; + -o-animation: fadeOutUp 250ms; + -ms-animation: fadeOutUp 250ms; +} + +.ng-aside.vertical.top.in .modal-dialog { + animation: fadeInTop 250ms; + -webkit-animation: fadeInTop 250ms; + -webkit-animation: fadeInTop 250ms; + -moz-animation: fadeInTop 250ms; + -o-animation: fadeInTop 250ms; + -ms-animation: fadeInTop 250ms; +} + +.ng-aside.vertical.bottom .modal-dialog { + animation: fadeOutDown 250ms; + -webkit-animation: fadeOutDown 250ms; + -webkit-animation: fadeOutDown 250ms; + -moz-animation: fadeOutDown 250ms; + -o-animation: fadeOutDown 250ms; + -ms-animation: fadeOutDown 250ms; +} +.ng-aside.vertical.bottom.in .modal-dialog { + animation: fadeInBottom 250ms; + -webkit-animation: fadeInBottom 250ms; + -webkit-animation: fadeInBottom 250ms; + -moz-animation: fadeInBottom 250ms; + -o-animation: fadeInBottom 250ms; + -ms-animation: fadeInBottom 250ms; +} + +.ng-aside.vertical.bottom .modal-dialog { + bottom: 0; +} + +.ng-aside.vertical.top .modal-dialog { + top: 0; +} + +.ng-aside.vertical .modal-dialog .modal-content { + width: 100%; +} \ No newline at end of file diff --git a/vendor/assets/components/angular-aside/dist/css/angular-aside.min.css b/vendor/assets/components/angular-aside/dist/css/angular-aside.min.css new file mode 100644 index 000000000..d0ad03900 --- /dev/null +++ b/vendor/assets/components/angular-aside/dist/css/angular-aside.min.css @@ -0,0 +1,14 @@ +/*! + * angular-aside - v1.3.2 + * https://github.com/dbtek/angular-aside + * 2015-11-17 + * Copyright (c) 2015 İsmail Demirbilek + * License: MIT + */ + +/*! +Animate.css - http://daneden.me/animate +Licensed under the MIT license - http://opensource.org/licenses/MIT + +Copyright (c) 2014 Daniel Eden +*/@-webkit-keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}100%{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0);-ms-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}100%{opacity:1;-webkit-transform:none;-ms-transform:none;transform:none}}.fadeInLeft{-webkit-animation-name:fadeInLeft;animation-name:fadeInLeft}@-webkit-keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}100%{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%,0,0);-ms-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}100%{opacity:1;-webkit-transform:none;-ms-transform:none;transform:none}}.fadeInRight{-webkit-animation-name:fadeInRight;animation-name:fadeInRight}@-webkit-keyframes fadeInTop{0%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}100%{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInTop{0%{opacity:0;-webkit-transform:translate3d(0,-100%,0);-ms-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}100%{opacity:1;-webkit-transform:none;-ms-transform:none;transform:none}}.fadeInTop{-webkit-animation-name:fadeInTop;animation-name:fadeInTop}@-webkit-keyframes fadeInBottom{0%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}100%{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInBottom{0%{opacity:0;-webkit-transform:translate3d(0,100%,0);-ms-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}100%{opacity:1;-webkit-transform:none;-ms-transform:none;transform:none}}.fadeInBottom{-webkit-animation-name:fadeInBottom;animation-name:fadeInBottom}@-webkit-keyframes fadeOutLeft{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes fadeOutLeft{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.fadeOutLeft{-webkit-animation-name:fadeOutLeft;animation-name:fadeOutLeft}@-webkit-keyframes fadeOutRight{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}@keyframes fadeOutRight{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.fadeOutRight{-webkit-animation-name:fadeOutRight;animation-name:fadeOutRight}@-webkit-keyframes fadeOutUp{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}@keyframes fadeOutUp{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}.fadeOutUp{-webkit-animation-name:fadeOutUp;animation-name:fadeOutUp}@-webkit-keyframes fadeOutDown{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes fadeOutDown{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.fadeOutDown{-webkit-animation-name:fadeOutDown;animation-name:fadeOutDown}.ng-aside{overflow-y:auto;overflow-x:hidden}.ng-aside .modal-dialog{position:absolute;margin:0;padding:0}.ng-aside.fade .modal-dialog{-o-transition:none;-moz-transition:none;-ms-transition:none;-webkit-transition:none;transition:none;-o-transform:none;-moz-transform:none;-ms-transform:none;-webkit-transform:none;transform:none}.ng-aside .modal-dialog .modal-content{overflow-y:auto;overflow-x:hidden;border:none;border-radius:0}.ng-aside.horizontal,.ng-aside.horizontal .modal-dialog .modal-content{height:100%}.ng-aside.horizontal .modal-dialog{position:absolute;top:0;height:100%}.modal-open .ng-aside.horizontal.left .modal-dialog{animation:fadeOutLeft 250ms;-webkit-animation:fadeOutLeft 250ms;-moz-animation:fadeOutLeft 250ms;-o-animation:fadeOutLeft 250ms;-ms-animation:fadeOutLeft 250ms}.ng-aside.horizontal.left.in .modal-dialog{animation:fadeInLeft 400ms;-webkit-animation:fadeInLeft 400ms;-moz-animation:fadeInLeft 400ms;-o-animation:fadeInLeft 400ms;-ms-animation:fadeInLeft 400ms}.ng-aside.horizontal.left .modal-dialog{left:0}.ng-aside.horizontal.right .modal-dialog{animation:fadeOutRight 400ms;-webkit-animation:fadeOutRight 400ms;-moz-animation:fadeOutRight 400ms;-o-animation:fadeOutRight 400ms;-ms-animation:fadeOutRight 400ms}.ng-aside.horizontal.right.in .modal-dialog{animation:fadeInRight 250ms;-webkit-animation:fadeInRight 250ms}.ng-aside.horizontal.right .modal-dialog{right:0}.ng-aside.vertical{width:100%!important;overflow:hidden}.ng-aside.vertical .modal-dialog{left:0;right:0;width:100%!important}.ng-aside.vertical .modal-dialog .modal-content{max-height:400px}.ng-aside.vertical.top .modal-dialog{animation:fadeOutUp 250ms;-webkit-animation:fadeOutUp 250ms;-webkit-animation:fadeOutUp 250ms;-moz-animation:fadeOutUp 250ms;-o-animation:fadeOutUp 250ms;-ms-animation:fadeOutUp 250ms}.ng-aside.vertical.top.in .modal-dialog{animation:fadeInTop 250ms;-webkit-animation:fadeInTop 250ms;-webkit-animation:fadeInTop 250ms;-moz-animation:fadeInTop 250ms;-o-animation:fadeInTop 250ms;-ms-animation:fadeInTop 250ms}.ng-aside.vertical.bottom .modal-dialog{animation:fadeOutDown 250ms;-webkit-animation:fadeOutDown 250ms;-webkit-animation:fadeOutDown 250ms;-moz-animation:fadeOutDown 250ms;-o-animation:fadeOutDown 250ms;-ms-animation:fadeOutDown 250ms}.ng-aside.vertical.bottom.in .modal-dialog{animation:fadeInBottom 250ms;-webkit-animation:fadeInBottom 250ms;-webkit-animation:fadeInBottom 250ms;-moz-animation:fadeInBottom 250ms;-o-animation:fadeInBottom 250ms;-ms-animation:fadeInBottom 250ms}.ng-aside.vertical.bottom .modal-dialog{bottom:0}.ng-aside.vertical.top .modal-dialog{top:0}.ng-aside.vertical .modal-dialog .modal-content{width:100%} \ No newline at end of file diff --git a/vendor/assets/components/angular-aside/dist/js/angular-aside.js b/vendor/assets/components/angular-aside/dist/js/angular-aside.js new file mode 100644 index 000000000..55770eb77 --- /dev/null +++ b/vendor/assets/components/angular-aside/dist/js/angular-aside.js @@ -0,0 +1,60 @@ + +/*! + * angular-aside - v1.3.2 + * https://github.com/dbtek/angular-aside + * 2015-11-17 + * Copyright (c) 2015 İsmail Demirbilek + * License: MIT + */ + +(function() { + 'use strict'; + + /** + * @ngdoc overview + * @name ngAside + * @description + * Main module for aside component. + * @function + * @author İsmail Demirbilek + */ + angular.module('ngAside', ['ui.bootstrap.modal']); +})(); + +(function() { + 'use strict'; + + angular.module('ngAside') + /** + * @ngdoc service + * @name ngAside.services:$aside + * @description + * Factory to create a uibModal instance to use it as aside. It simply wraps $uibModal by overriding open() method and sets a class on modal window. + * @function + */ + .factory('$aside', ['$uibModal', function($uibModal) { + var defaults = this.defaults = { + placement: 'left' + }; + + var asideFactory = { + // override open method + open: function(config) { + var options = angular.extend({}, defaults, config); + // check placement is set correct + if(['left', 'right', 'bottom', 'top'].indexOf(options.placement) === -1) { + options.placement = defaults.placement; + } + var vertHoriz = ['left', 'right'].indexOf(options.placement) === -1 ? 'vertical' : 'horizontal'; + // set aside classes + options.windowClass = 'ng-aside ' + vertHoriz + ' ' + options.placement + (options.windowClass ? ' ' + options.windowClass : ''); + delete options.placement + return $uibModal.open(options); + } + }; + + // create $aside as extended $uibModal + var $aside = angular.extend({}, $uibModal, asideFactory); + return $aside; + }]); +})(); diff --git a/vendor/assets/components/angular-aside/dist/js/angular-aside.min.js b/vendor/assets/components/angular-aside/dist/js/angular-aside.min.js new file mode 100644 index 000000000..2b68bd060 --- /dev/null +++ b/vendor/assets/components/angular-aside/dist/js/angular-aside.min.js @@ -0,0 +1,8 @@ +/*! + * angular-aside - v1.3.2 + * https://github.com/dbtek/angular-aside + * 2015-11-17 + * Copyright (c) 2015 İsmail Demirbilek + * License: MIT + */ +!function(){"use strict";angular.module("ngAside",["ui.bootstrap.modal"])}(),function(){"use strict";angular.module("ngAside").factory("$aside",["$uibModal",function(a){var b=this.defaults={placement:"left"},c={open:function(c){var d=angular.extend({},b,c);-1===["left","right","bottom","top"].indexOf(d.placement)&&(d.placement=b.placement);var e=-1===["left","right"].indexOf(d.placement)?"vertical":"horizontal";return d.windowClass="ng-aside "+e+" "+d.placement+(d.windowClass?" "+d.windowClass:""),delete d.placement,a.open(d)}},d=angular.extend({},a,c);return d}])}(); \ No newline at end of file diff --git a/vendor/assets/components/angular-aside/index.js b/vendor/assets/components/angular-aside/index.js new file mode 100644 index 000000000..c2c62b874 --- /dev/null +++ b/vendor/assets/components/angular-aside/index.js @@ -0,0 +1,2 @@ +require('./dist/js/angular-aside'); +module.exports = 'ngAside'; \ No newline at end of file diff --git a/vendor/assets/components/angular-aside/src/scripts/app.js b/vendor/assets/components/angular-aside/src/scripts/app.js new file mode 100644 index 000000000..8825914eb --- /dev/null +++ b/vendor/assets/components/angular-aside/src/scripts/app.js @@ -0,0 +1,13 @@ +(function() { + 'use strict'; + + /** + * @ngdoc overview + * @name ngAside + * @description + * Main module for aside component. + * @function + * @author İsmail Demirbilek + */ + angular.module('ngAside', ['ui.bootstrap.modal']); +})(); diff --git a/vendor/assets/components/angular-aside/src/scripts/services/aside.js b/vendor/assets/components/angular-aside/src/scripts/services/aside.js new file mode 100644 index 000000000..456e5270b --- /dev/null +++ b/vendor/assets/components/angular-aside/src/scripts/services/aside.js @@ -0,0 +1,37 @@ +(function() { + 'use strict'; + + angular.module('ngAside') + /** + * @ngdoc service + * @name ngAside.services:$aside + * @description + * Factory to create a uibModal instance to use it as aside. It simply wraps $uibModal by overriding open() method and sets a class on modal window. + * @function + */ + .factory('$aside', function($uibModal) { + var defaults = this.defaults = { + placement: 'left' + }; + + var asideFactory = { + // override open method + open: function(config) { + var options = angular.extend({}, defaults, config); + // check placement is set correct + if(['left', 'right', 'bottom', 'top'].indexOf(options.placement) === -1) { + options.placement = defaults.placement; + } + var vertHoriz = ['left', 'right'].indexOf(options.placement) === -1 ? 'vertical' : 'horizontal'; + // set aside classes + options.windowClass = 'ng-aside ' + vertHoriz + ' ' + options.placement + (options.windowClass ? ' ' + options.windowClass : ''); + delete options.placement + return $uibModal.open(options); + } + }; + + // create $aside as extended $uibModal + var $aside = angular.extend({}, $uibModal, asideFactory); + return $aside; + }); +})(); diff --git a/vendor/assets/components/angular-aside/src/styles/animate.css b/vendor/assets/components/angular-aside/src/styles/animate.css new file mode 100644 index 000000000..41c6d05cd --- /dev/null +++ b/vendor/assets/components/angular-aside/src/styles/animate.css @@ -0,0 +1,263 @@ +/*! +Animate.css - http://daneden.me/animate +Licensed under the MIT license - http://opensource.org/licenses/MIT + +Copyright (c) 2014 Daniel Eden +*/ + +@-webkit-keyframes fadeInLeft { + 0% { + opacity: 0; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } + + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} + +@keyframes fadeInLeft { + 0% { + opacity: 0; + -webkit-transform: translate3d(-100%, 0, 0); + -ms-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } + + 100% { + opacity: 1; + -webkit-transform: none; + -ms-transform: none; + transform: none; + } +} + +.fadeInLeft { + -webkit-animation-name: fadeInLeft; + animation-name: fadeInLeft; +} + +@-webkit-keyframes fadeInRight { + 0% { + opacity: 0; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } + + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} + +@keyframes fadeInRight { + 0% { + opacity: 0; + -webkit-transform: translate3d(100%, 0, 0); + -ms-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } + + 100% { + opacity: 1; + -webkit-transform: none; + -ms-transform: none; + transform: none; + } +} + +.fadeInRight { + -webkit-animation-name: fadeInRight; + animation-name: fadeInRight; +} + +@-webkit-keyframes fadeInTop { + 0% { + opacity: 0; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } + + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} + +@keyframes fadeInTop { + 0% { + opacity: 0; + -webkit-transform: translate3d(0, -100%, 0); + -ms-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } + + 100% { + opacity: 1; + -webkit-transform: none; + -ms-transform: none; + transform: none; + } +} + +.fadeInTop { + -webkit-animation-name: fadeInTop; + animation-name: fadeInTop; +} + +@-webkit-keyframes fadeInBottom { + 0% { + opacity: 0; + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + } + + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} + +@keyframes fadeInBottom { + 0% { + opacity: 0; + -webkit-transform: translate3d(0, 100%, 0); + -ms-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + } + + 100% { + opacity: 1; + -webkit-transform: none; + -ms-transform: none; + transform: none; + } +} + +.fadeInBottom { + -webkit-animation-name: fadeInBottom; + animation-name: fadeInBottom; +} + +@-webkit-keyframes fadeOutLeft { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } +} + +@keyframes fadeOutLeft { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } +} + +.fadeOutLeft { + -webkit-animation-name: fadeOutLeft; + animation-name: fadeOutLeft; +} + +@-webkit-keyframes fadeOutRight { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } +} + +@keyframes fadeOutRight { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } +} + +.fadeOutRight { + -webkit-animation-name: fadeOutRight; + animation-name: fadeOutRight; +} + +@-webkit-keyframes fadeOutUp { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } +} + +@keyframes fadeOutUp { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } +} + +.fadeOutUp { + -webkit-animation-name: fadeOutUp; + animation-name: fadeOutUp; +} + + +@-webkit-keyframes fadeOutDown { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + } +} + +@keyframes fadeOutDown { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + } +} + +.fadeOutDown { + -webkit-animation-name: fadeOutDown; + animation-name: fadeOutDown; +} \ No newline at end of file diff --git a/vendor/assets/components/angular-aside/src/styles/aside.css b/vendor/assets/components/angular-aside/src/styles/aside.css new file mode 100644 index 000000000..5820485c3 --- /dev/null +++ b/vendor/assets/components/angular-aside/src/styles/aside.css @@ -0,0 +1,150 @@ +/* Common */ + +.ng-aside { + overflow-y: auto; + overflow-x: hidden; +} + +.ng-aside .modal-dialog { + position: absolute; + margin: 0; + padding: 0; +} + +.ng-aside.fade .modal-dialog { + -o-transition: none; + -moz-transition: none; + -ms-transition: none; + -webkit-transition: none; + transition: none; + /*CSS transforms*/ + -o-transform: none; + -moz-transform: none; + -ms-transform: none; + -webkit-transform: none; + transform: none; +} + +.ng-aside .modal-dialog .modal-content { + overflow-y: auto; + overflow-x: hidden; + border: none; + border-radius: 0; +} + +/* Horizontal */ + +.ng-aside.horizontal { + height: 100%; +} + +.ng-aside.horizontal .modal-dialog .modal-content { + height: 100%; +} + +.ng-aside.horizontal .modal-dialog { + position: absolute; + top: 0; + height: 100%; +} + +.modal-open .ng-aside.horizontal.left .modal-dialog { + animation: fadeOutLeft 250ms; + -webkit-animation: fadeOutLeft 250ms; + -moz-animation: fadeOutLeft 250ms; + -o-animation: fadeOutLeft 250ms; + -ms-animation: fadeOutLeft 250ms; +} + +.ng-aside.horizontal.left.in .modal-dialog { + animation: fadeInLeft 400ms; + -webkit-animation: fadeInLeft 400ms; + -moz-animation: fadeInLeft 400ms; + -o-animation: fadeInLeft 400ms; + -ms-animation: fadeInLeft 400ms; +} + +.ng-aside.horizontal.left .modal-dialog { + left: 0; +} + +.ng-aside.horizontal.right .modal-dialog { + animation: fadeOutRight 400ms; + -webkit-animation: fadeOutRight 400ms; + -moz-animation: fadeOutRight 400ms; + -o-animation: fadeOutRight 400ms; + -ms-animation: fadeOutRight 400ms; +} + +.ng-aside.horizontal.right.in .modal-dialog { + animation: fadeInRight 250ms; + -webkit-animation: fadeInRight 250ms; +} + +.ng-aside.horizontal.right .modal-dialog { + right: 0; +} + +/* Vertical */ + +.ng-aside.vertical { + width: 100% !important; + overflow: hidden; +} + +.ng-aside.vertical .modal-dialog { + left: 0; + right: 0; + width: 100% !important; +} + +.ng-aside.vertical .modal-dialog .modal-content { + max-height: 400px; +} + +.ng-aside.vertical.top .modal-dialog { + animation: fadeOutUp 250ms; + -webkit-animation: fadeOutUp 250ms; + -webkit-animation: fadeOutUp 250ms; + -moz-animation: fadeOutUp 250ms; + -o-animation: fadeOutUp 250ms; + -ms-animation: fadeOutUp 250ms; +} + +.ng-aside.vertical.top.in .modal-dialog { + animation: fadeInTop 250ms; + -webkit-animation: fadeInTop 250ms; + -webkit-animation: fadeInTop 250ms; + -moz-animation: fadeInTop 250ms; + -o-animation: fadeInTop 250ms; + -ms-animation: fadeInTop 250ms; +} + +.ng-aside.vertical.bottom .modal-dialog { + animation: fadeOutDown 250ms; + -webkit-animation: fadeOutDown 250ms; + -webkit-animation: fadeOutDown 250ms; + -moz-animation: fadeOutDown 250ms; + -o-animation: fadeOutDown 250ms; + -ms-animation: fadeOutDown 250ms; +} +.ng-aside.vertical.bottom.in .modal-dialog { + animation: fadeInBottom 250ms; + -webkit-animation: fadeInBottom 250ms; + -webkit-animation: fadeInBottom 250ms; + -moz-animation: fadeInBottom 250ms; + -o-animation: fadeInBottom 250ms; + -ms-animation: fadeInBottom 250ms; +} + +.ng-aside.vertical.bottom .modal-dialog { + bottom: 0; +} + +.ng-aside.vertical.top .modal-dialog { + top: 0; +} + +.ng-aside.vertical .modal-dialog .modal-content { + width: 100%; +} \ No newline at end of file diff --git a/vendor/assets/components/angular-bootstrap/.bower.json b/vendor/assets/components/angular-bootstrap/.bower.json index 7f6da6277..e4c4cb6bb 100644 --- a/vendor/assets/components/angular-bootstrap/.bower.json +++ b/vendor/assets/components/angular-bootstrap/.bower.json @@ -25,7 +25,7 @@ "tag": "0.14.3", "commit": "306d1a30b4a8e8144741bb9c0126331ac884126a" }, - "_source": "git://github.com/angular-ui/bootstrap-bower.git", - "_target": ">=0.13.1", + "_source": "https://github.com/angular-ui/bootstrap-bower.git", + "_target": "~0.14.3", "_originalSource": "angular-bootstrap" } \ No newline at end of file diff --git a/vendor/assets/components/angular-ui-calendar/.bower.json b/vendor/assets/components/angular-ui-calendar/.bower.json index 755c1f194..b01057744 100644 --- a/vendor/assets/components/angular-ui-calendar/.bower.json +++ b/vendor/assets/components/angular-ui-calendar/.bower.json @@ -1,6 +1,6 @@ { "name": "angular-ui-calendar", - "version": "0.9.0-beta.1", + "version": "1.0.1", "description": "A complete AngularJS directive for the Arshaw FullCalendar.", "author": "https://github.com/angular-ui/ui-calendar/graphs/contributors", "license": "MIT", @@ -16,21 +16,27 @@ "package.json" ], "dependencies": { - "angular": "~1.2.x", - "fullcalendar": "~2.x" + "angular": "1.3.15", + "jquery": "2.x", + "fullcalendar": "2.3.1", + "moment": "2.*" }, "devDependencies": { "angular-mocks": "~1.x", - "bootstrap-css": "2.3.1", - "jquery-ui": "~1.10.3" + "bootstrap-css": "2.3.1" }, - "_release": "0.9.0-beta.1", + "resolutions": { + "jquery": "~2.x", + "angular": "1.3.15" + }, + "_release": "1.0.1", "_resolution": { "type": "version", - "tag": "0.9.0-beta.1", - "commit": "80a32ee00a6a327ea1446451d725fdd30a4535e9" + "tag": "1.0.1", + "commit": "9c658bcf574be738bbe24258992da263eafc3095" }, - "_source": "git://github.com/angular-ui/ui-calendar.git", - "_target": "0.9.0-beta.1", - "_originalSource": "angular-ui-calendar" + "_source": "https://github.com/angular-ui/ui-calendar.git", + "_target": "1.0.1", + "_originalSource": "angular-ui-calendar", + "_direct": true } \ No newline at end of file diff --git a/vendor/assets/components/angular-ui-calendar/README.md b/vendor/assets/components/angular-ui-calendar/README.md index 916c18a5d..5e88a01e3 100644 --- a/vendor/assets/components/angular-ui-calendar/README.md +++ b/vendor/assets/components/angular-ui-calendar/README.md @@ -1,50 +1,54 @@ -# ui-calendar directive [![Build Status](https://travis-ci.org/angular-ui/ui-calendar.png?branch=master)](https://travis-ci.org/angular-ui/ui-calendar) +# ui-calendar directive [![Build Status](https://travis-ci.org/angular-ui/ui-calendar.svg?branch=master)](https://travis-ci.org/angular-ui/ui-calendar) + +[![Join the chat at https://gitter.im/angular-ui/ui-calendar](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/angular-ui/ui-calendar?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) A complete AngularJS directive for the Arshaw FullCalendar. # Requirements + - ([AngularJS](http://code.angularjs.org/1.2.1/angular.js)) - ([fullcalendar.js 2.0 and it's dependencies](http://arshaw.com/fullcalendar/download/)) - optional - ([gcal-plugin](http://arshaw.com/js/fullcalendar-1.5.3/fullcalendar/gcal.js)) -# Testing - -We use karma and grunt to ensure the quality of the code. - - npm install -g grunt-cli - npm install - bower install - grunt - # Usage -We use [bower](http://twitter.github.com/bower/) for dependency management. Add +Using [bower](http://bower.io) run: + + bower install --save angular-ui-calendar + +Alternatively you can add it to your `bower.json` like this: dependencies: { "angular-ui-calendar": "latest" } -To your `components.json` file. Then run +And then run bower install -This will copy the ui-calendar files into your `components` folder, along with its dependencies. Load the script files in your application: +This will copy the ui-calendar files into your `components` folder, along with its dependencies. Load the script and style files in your application: - - - + + + + + - - + + Add the calendar module as a dependency to your application module: - var myAppModule = angular.module('MyApp', ['ui.calendar']) + var app = angular.module('App', ['ui.calendar']) -Apply the directive to your div elements. The calendar must be supplied an array of decoumented event sources to render itself: +Apply the directive to your div elements. The calendar must be supplied an array of documented event sources to render itself:
+Define your model in a scope e.g. + + $scope.eventSources = []; + ## Options All the Arshaw Fullcalendar options can be passed through the directive. This even means function objects that are declared on the scope. @@ -73,20 +77,20 @@ All the Arshaw Fullcalendar options can be passed through the directive. This ev The ui-calendar directive plays nicely with ng-model. -An Event Sources objects needs to be created to pass into ng-model. This object will be watched for changes and update the calendar accordingly, giving the calendar some Angular Magic. +An Event Sources objects needs to be created to pass into ng-model. This object's values will be watched for changes. If a change occurs, then that specific calendar will call the appropriate fullCalendar method. The ui-calendar directive expects the eventSources object to be any type allowed in the documentation for the fullcalendar. [docs](http://arshaw.com/fullcalendar/docs/event_data/Event_Source_Object/) Note that all calendar options which are functions that are passed into the calendar are wrapped in an apply automatically. ## Accessing the calendar object -To avoid potential issues, by default the calendar object is not available in the parent scope. Access the object by declaring a calendar attribute name: +It is possible to access a specific calendar object by declaring a name for it on the uiCalendar directive. In this next line we are naming the calendar 'myCalendar'. This will be attached to the uiCalendarConfig constant object, that can be accessed via DI.
-Now the calendar object is available in the parent scope: +Now the calendar object is available in uiCalendarConfig.calendars: - $scope.myCalendar.fullCalendar + uiCalendarConfig.calendars.myCalendar This allows you to declare any number of calendar objects with distinct names. @@ -102,9 +106,13 @@ If you need to automatically re-render other event data, you can use `calendar-w returns "" + event.price; } - + // will now watch for price +### Adding new events issue + +When adding new events to the calendar they can disappear when switching months. To solve this add `stick: true` to the event object being added to the scope. + ## Watching the displayed date range of the calendar There is no mechanism to $watch the displayed date range on the calendar due to the JQuery nature of fullCalendar. If you want @@ -121,9 +129,22 @@ in a service, instead of letting fullCalendar pull them via AJAX), you can add t } }; +# Minify + + grunt minify + ## Documentation for the Calendar The calendar works alongside of all the documentation represented [here](http://arshaw.com/fullcalendar/docs) ## PR's R always Welcome Make sure that if a new feature is added, that the proper tests are created. + +# Testing + +We use karma and grunt to ensure the quality of the code. + + npm install -g grunt-cli + npm install + bower install + grunt diff --git a/vendor/assets/components/angular-ui-calendar/bower.json b/vendor/assets/components/angular-ui-calendar/bower.json index 21193eea2..292d38410 100644 --- a/vendor/assets/components/angular-ui-calendar/bower.json +++ b/vendor/assets/components/angular-ui-calendar/bower.json @@ -1,6 +1,6 @@ { "name": "angular-ui-calendar", - "version": "0.9.0-beta.1", + "version": "1.0.1", "description": "A complete AngularJS directive for the Arshaw FullCalendar.", "author": "https://github.com/angular-ui/ui-calendar/graphs/contributors", "license": "MIT", @@ -16,12 +16,17 @@ "package.json" ], "dependencies": { - "angular": "~1.2.x", - "fullcalendar": "~2.x" + "angular": "1.3.15", + "jquery": "2.x", + "fullcalendar": "2.3.1", + "moment": "2.*" }, "devDependencies": { "angular-mocks": "~1.x", - "bootstrap-css": "2.3.1", - "jquery-ui": "~1.10.3" + "bootstrap-css": "2.3.1" + }, + "resolutions": { + "jquery": "~2.x", + "angular": "1.3.15" } } diff --git a/vendor/assets/components/angular-ui-calendar/src/calendar.js b/vendor/assets/components/angular-ui-calendar/src/calendar.js index 80a02eaae..6b953f1b7 100644 --- a/vendor/assets/components/angular-ui-calendar/src/calendar.js +++ b/vendor/assets/components/angular-ui-calendar/src/calendar.js @@ -9,48 +9,56 @@ */ angular.module('ui.calendar', []) - .constant('uiCalendarConfig', {}) - .controller('uiCalendarCtrl', ['$scope', '$timeout', function($scope, $timeout){ + .constant('uiCalendarConfig', {calendars: {}}) + .controller('uiCalendarCtrl', ['$scope', + '$locale', function( + $scope, + $locale){ - var sourceSerialId = 1, - eventSerialId = 1, - sources = $scope.eventSources, + var sources = $scope.eventSources, extraEventSignature = $scope.calendarWatchEvent ? $scope.calendarWatchEvent : angular.noop, wrapFunctionWithScopeApply = function(functionToWrap){ - var wrapper; - - if (functionToWrap){ - wrapper = function(){ - // This happens outside of angular context so we need to wrap it in a timeout which has an implied apply. - // In this way the function will be safely executed on the next digest. + return function(){ + // This may happen outside of angular context, so create one if outside. + if ($scope.$root.$$phase) { + return functionToWrap.apply(this, arguments); + } else { var args = arguments; - var _this = this; - $timeout(function(){ - functionToWrap.apply(_this, args); + var self = this; + return $scope.$root.$apply(function(){ + return functionToWrap.apply(self, args); }); - }; - } - - return wrapper; + } + }; }; - this.eventsFingerprint = function(e) { + var eventSerialId = 1; + // @return {String} fingerprint of the event object and its properties + this.eventFingerprint = function(e) { if (!e._id) { e._id = eventSerialId++; } // This extracts all the information we need from the event. http://jsperf.com/angular-calendar-events-fingerprint/3 return "" + e._id + (e.id || '') + (e.title || '') + (e.url || '') + (+e.start || '') + (+e.end || '') + - (e.allDay || '') + (e.className || '') + extraEventSignature(e) || ''; + (e.allDay || '') + (e.className || '') + extraEventSignature({event: e}) || ''; }; - this.sourcesFingerprint = function(source) { - return source.__id || (source.__id = sourceSerialId++); + var sourceSerialId = 1, sourceEventsSerialId = 1; + // @return {String} fingerprint of the source object and its events array + this.sourceFingerprint = function(source) { + var fp = '' + (source.__id || (source.__id = sourceSerialId++)), + events = angular.isObject(source) && source.events; + if (events) { + fp = fp + '-' + (events.__id || (events.__id = sourceEventsSerialId++)); + } + return fp; }; + // @return {Array} all events from all sources this.allEvents = function() { - // return sources.flatten(); but we don't have flatten + // do sources.map(&:events).flatten(), but we don't have flatten var arraySources = []; for (var i = 0, srcLen = sources.length; i < srcLen; i++) { var source = sources[i]; @@ -61,7 +69,7 @@ angular.module('ui.calendar', []) // event source as object, ie extended form var extEvent = {}; for(var key in source){ - if(key !== '_uiCalId' && key !== 'events'){ + if(key !== '_id' && key !== 'events'){ extEvent[key] = source[key]; } } @@ -71,14 +79,17 @@ angular.module('ui.calendar', []) arraySources.push(source.events); } } - return Array.prototype.concat.apply([], arraySources); }; - // Track changes in array by assigning id tokens to each element and watching the scope for changes in those tokens - // arguments: - // arraySource array of function that returns array of objects to watch - // tokenFn function(object) that returns the token for a given object + // Track changes in array of objects by assigning id tokens to each element and watching the scope for changes in the tokens + // @param {Array|Function} arraySource array of objects to watch + // @param tokenFn {Function} that returns the token for a given object + // @return {Object} + // subscribe: function(scope, function(newTokens, oldTokens)) + // called when source has changed. return false to prevent individual callbacks from firing + // onAdded/Removed/Changed: + // when set to a callback, called each item where a respective change is detected this.changeWatcher = function(arraySource, tokenFn) { var self; var getTokens = function() { @@ -92,8 +103,12 @@ angular.module('ui.calendar', []) } return result; }; - // returns elements in that are in a but not in b - // subtractAsSets([4, 5, 6], [4, 5, 7]) => [6] + + // @param {Array} a + // @param {Array} b + // @return {Array} elements in that are in a but not in b + // @example + // subtractAsSets([6, 100, 4, 5], [4, 5, 7]) // [6, 100] var subtractAsSets = function(a, b) { var result = [], inB = {}, i, n; for (i = 0, n = b.length; i < n; i++) { @@ -110,6 +125,7 @@ angular.module('ui.calendar', []) // Map objects to tokens and vice-versa var map = {}; + // Compare newTokens to oldTokens and call onAdded, onRemoved, and onChanged handlers for each affected event respectively. var applyChanges = function(newTokens, oldTokens) { var i, n, el, token; var replacedTokens = {}; @@ -138,9 +154,10 @@ angular.module('ui.calendar', []) } }; return self = { - subscribe: function(scope, onChanged) { + subscribe: function(scope, onArrayChanged) { scope.$watch(getTokens, function(newTokens, oldTokens) { - if (!onChanged || onChanged(newTokens, oldTokens) !== false) { + var notify = !(onArrayChanged && onArrayChanged(newTokens, oldTokens) === false); + if (notify) { applyChanges(newTokens, oldTokens); } }, true); @@ -156,7 +173,7 @@ angular.module('ui.calendar', []) angular.extend(config, uiCalendarConfig); angular.extend(config, calendarSettings); - + angular.forEach(config, function(value,key){ if (typeof value === 'function'){ config[key] = wrapFunctionWithScopeApply(config[key]); @@ -165,26 +182,31 @@ angular.module('ui.calendar', []) return config; }; - }]) - .directive('uiCalendar', ['uiCalendarConfig', '$locale', function(uiCalendarConfig, $locale) { - // Configure to use locale names by default - var tValues = function(data) { - // convert {0: "Jan", 1: "Feb", ...} to ["Jan", "Feb", ...] - var r, k; - r = []; - for (k in data) { - r[k] = data[k]; - } - return r; - }; - var dtf = $locale.DATETIME_FORMATS; - uiCalendarConfig = angular.extend({ - monthNames: tValues(dtf.MONTH), - monthNamesShort: tValues(dtf.SHORTMONTH), - dayNames: tValues(dtf.DAY), - dayNamesShort: tValues(dtf.SHORTDAY) - }, uiCalendarConfig || {}); + this.getLocaleConfig = function(fullCalendarConfig) { + if (!fullCalendarConfig.lang || fullCalendarConfig.useNgLocale) { + // Configure to use locale names by default + var tValues = function(data) { + // convert {0: "Jan", 1: "Feb", ...} to ["Jan", "Feb", ...] + var r, k; + r = []; + for (k in data) { + r[k] = data[k]; + } + return r; + }; + var dtf = $locale.DATETIME_FORMATS; + return { + monthNames: tValues(dtf.MONTH), + monthNamesShort: tValues(dtf.SHORTMONTH), + dayNames: tValues(dtf.DAY), + dayNamesShort: tValues(dtf.SHORTDAY) + }; + } + return {}; + }; + }]) + .directive('uiCalendar', ['uiCalendarConfig', function(uiCalendarConfig) { return { restrict: 'A', scope: {eventSources:'=ngModel',calendarWatchEvent: '&'}, @@ -193,8 +215,9 @@ angular.module('ui.calendar', []) var sources = scope.eventSources, sourcesChanged = false, - eventSourcesWatcher = controller.changeWatcher(sources, controller.sourcesFingerprint), - eventsWatcher = controller.changeWatcher(controller.allEvents, controller.eventsFingerprint), + calendar, + eventSourcesWatcher = controller.changeWatcher(sources, controller.sourceFingerprint), + eventsWatcher = controller.changeWatcher(controller.allEvents, controller.eventFingerprint), options = null; function getOptions(){ @@ -203,8 +226,12 @@ angular.module('ui.calendar', []) fullCalendarConfig = controller.getFullCalendarConfig(calendarSettings, uiCalendarConfig); + var localeFullCalendarConfig = controller.getLocaleConfig(fullCalendarConfig); + angular.extend(localeFullCalendarConfig, fullCalendarConfig); options = { eventSources: sources }; - angular.extend(options, fullCalendarConfig); + angular.extend(options, localeFullCalendarConfig); + //remove calendars from options + options.calendars = null; var options2 = {}; for(var o in options){ @@ -216,51 +243,60 @@ angular.module('ui.calendar', []) } scope.destroy = function(){ - if(scope.calendar && scope.calendar.fullCalendar){ - scope.calendar.fullCalendar('destroy'); + if(calendar && calendar.fullCalendar){ + calendar.fullCalendar('destroy'); } if(attrs.calendar) { - scope.calendar = scope.$parent[attrs.calendar] = $(elm).html(''); + calendar = uiCalendarConfig.calendars[attrs.calendar] = $(elm).html(''); } else { - scope.calendar = $(elm).html(''); + calendar = $(elm).html(''); } }; scope.init = function(){ - scope.calendar.fullCalendar(options); + calendar.fullCalendar(options); + if(attrs.calendar) { + uiCalendarConfig.calendars[attrs.calendar] = calendar; + } }; eventSourcesWatcher.onAdded = function(source) { - scope.calendar.fullCalendar('addEventSource', source); - sourcesChanged = true; + calendar.fullCalendar('addEventSource', source); + sourcesChanged = true; }; eventSourcesWatcher.onRemoved = function(source) { - scope.calendar.fullCalendar('removeEventSource', source); + calendar.fullCalendar('removeEventSource', source); + sourcesChanged = true; + }; + + eventSourcesWatcher.onChanged = function(source) { + calendar.fullCalendar('refetchEvents'); sourcesChanged = true; }; eventsWatcher.onAdded = function(event) { - scope.calendar.fullCalendar('renderEvent', event); + calendar.fullCalendar('renderEvent', event, (event.stick ? true : false)); }; eventsWatcher.onRemoved = function(event) { - scope.calendar.fullCalendar('removeEvents', function(e) { - return e._id === event._id; - }); + calendar.fullCalendar('removeEvents', event._id); }; eventsWatcher.onChanged = function(event) { - event._start = $.fullCalendar.moment(event.start); - event._end = $.fullCalendar.moment(event.end); - scope.calendar.fullCalendar('updateEvent', event); + var clientEvents = calendar.fullCalendar('clientEvents', event._id); + for (var i = 0; i < clientEvents.length; i++) { + var clientEvent = clientEvents[i]; + clientEvent = angular.extend(clientEvent, event); + calendar.fullCalendar('updateEvent', clientEvent); + } }; eventSourcesWatcher.subscribe(scope); - eventsWatcher.subscribe(scope, function(newTokens, oldTokens) { + eventsWatcher.subscribe(scope, function() { if (sourcesChanged === true) { sourcesChanged = false; - // prevent incremental updates in this case + // return false to prevent onAdded/Removed/Changed handlers from firing in this case return false; } }); @@ -271,4 +307,4 @@ angular.module('ui.calendar', []) }); } }; -}]); \ No newline at end of file +}]); diff --git a/vendor/assets/components/jasny-bootstrap/Gruntfile.js b/vendor/assets/components/jasny-bootstrap/Gruntfile.js index df551fecf..264d1f480 100644 --- a/vendor/assets/components/jasny-bootstrap/Gruntfile.js +++ b/vendor/assets/components/jasny-bootstrap/Gruntfile.js @@ -234,7 +234,7 @@ module.exports = function (grunt) { options: { inject: 'js/tests/unit/phantom.js' }, - files: 'js/tests/index.html' + files: 'js/tests/index.html.erb' }, connect: { @@ -315,7 +315,7 @@ module.exports = function (grunt) { options: { build: process.env.TRAVIS_JOB_ID, concurrency: 10, - urls: ['http://127.0.0.1:3000/js/tests/index.html'], + urls: ['http://127.0.0.1:3000/js/tests/index.html.erb'], browsers: grunt.file.readYAML('test-infra/sauce_browsers.yml') } } diff --git a/vendor/assets/components/ngUpload/.bower.json b/vendor/assets/components/ngUpload/.bower.json index 85c076a0b..e0571b768 100644 --- a/vendor/assets/components/ngUpload/.bower.json +++ b/vendor/assets/components/ngUpload/.bower.json @@ -1,6 +1,6 @@ { "name": "ngUpload", - "version": "0.5.17", + "version": "0.5.18", "main": "ng-upload.js", "ignore": [ "node_modules", @@ -20,13 +20,13 @@ "angular": ">=1.0.4" }, "homepage": "https://github.com/twilson63/ngUpload", - "_release": "0.5.17", + "_release": "0.5.18", "_resolution": { "type": "version", - "tag": "v0.5.17", - "commit": "df9f3edfdbcd1ca6d3f365ff85e32de229df3af1" + "tag": "v0.5.18", + "commit": "da7fe2bb94eb6adb2cd26ab4f6f979aa020baf9c" }, - "_source": "git://github.com/twilson63/ngUpload.git", + "_source": "https://github.com/twilson63/ngUpload.git", "_target": ">=0.5.11", "_originalSource": "ngUpload" } \ No newline at end of file diff --git a/vendor/assets/components/ngUpload/bower.json b/vendor/assets/components/ngUpload/bower.json index 433d85f9c..77d66321d 100644 --- a/vendor/assets/components/ngUpload/bower.json +++ b/vendor/assets/components/ngUpload/bower.json @@ -1,6 +1,6 @@ { "name": "ngUpload", - "version": "0.5.17", + "version": "0.5.18", "main": "ng-upload.js", "ignore": [ "node_modules", diff --git a/vendor/assets/components/ngUpload/ng-upload.js b/vendor/assets/components/ngUpload/ng-upload.js index 931980e81..29d5e8d35 100644 --- a/vendor/assets/components/ngUpload/ng-upload.js +++ b/vendor/assets/components/ngUpload/ng-upload.js @@ -146,6 +146,9 @@ angular.module('ngUpload', []) } // perform check before submit file if (options.beforeSubmit && options.beforeSubmit(scope, {}) === false) { + if(!scope.$$phase){ + scope.$apply(); + } $event.preventDefault(); return false; } diff --git a/vendor/assets/components/ngUpload/ng-upload.min.js b/vendor/assets/components/ngUpload/ng-upload.min.js index 4131d1c81..92fc2a5c0 100644 --- a/vendor/assets/components/ngUpload/ng-upload.min.js +++ b/vendor/assets/components/ngUpload/ng-upload.min.js @@ -1 +1 @@ -angular.module("ngUpload",[]).directive("uploadSubmit",["$parse",function(){function n(t,e){t=angular.element(t);var a=t.parent();return e=e.toLowerCase(),a&&a[0].tagName.toLowerCase()===e?a:a?n(a,e):null}return{restrict:"AC",link:function(t,e){e.bind("click",function(t){if(t&&(t.preventDefault(),t.stopPropagation()),!e.attr("disabled")){var a=n(e,"form");a.triggerHandler("submit"),a[0].submit()}})}}}]).directive("ngUpload",["$log","$parse","$document",function(n,t,e){function a(n){var t,a=e.find("head");return angular.forEach(a.find("meta"),function(e){e.getAttribute("name")===n&&(t=e)}),angular.element(t)}var r=1;return{restrict:"AC",link:function(e,o,i){function l(n){e.$isUploading=n}function u(){s.unbind("load"),e.$$phase?l(!1):e.$apply(function(){l(!1)});try{var t,a=(s[0].contentDocument||s[0].contentWindow.document).body;try{t=angular.fromJson(a.innerText||a.textContent),e.$$phase?p(e,{content:t}):e.$apply(function(){p(e,{content:t})})}catch(r){t=a.innerHTML;var o="ng-upload: Response is not valid JSON";n.warn(o),f&&(e.$$phase?f(e,{error:o}):e.$apply(function(){f(e,{error:o})}))}}catch(o){n.warn("ng-upload: Server error"),f&&(e.$$phase?f(e,{error:o}):e.$apply(function(){f(e,{error:o})}))}}r++;var d={},p=i.ngUpload?t(i.ngUpload):null,f=i.errorCatcher?t(i.errorCatcher):null,c=i.ngUploadLoading?t(i.ngUploadLoading):null;i.hasOwnProperty("uploadOptionsConvertHidden")&&(d.convertHidden="false"!=i.uploadOptionsConvertHidden),i.hasOwnProperty("uploadOptionsEnableRailsCsrf")&&(d.enableRailsCsrf="false"!=i.uploadOptionsEnableRailsCsrf),i.hasOwnProperty("uploadOptionsBeforeSubmit")&&(d.beforeSubmit=t(i.uploadOptionsBeforeSubmit)),o.attr({target:"upload-iframe-"+r,method:"post",enctype:"multipart/form-data",encoding:"multipart/form-data"});var s=angular.element('