diff --git a/.eslintrc b/.eslintrc index 4006a27f2..545c29d9c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -7,7 +7,8 @@ "Application": true, "angular": true, "Fablab": true, - "moment": true, - } + "moment": true + }, + "plugins": ["lint-erb"] } diff --git a/.ruby-version b/.ruby-version index a9eea9a34..d7edb5686 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -ruby-2.3.8 +ruby-2.6.5 diff --git a/CHANGELOG.md b/CHANGELOG.md index e5d7f5ba4..35d46025e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,50 @@ # Changelog Fab-manager +- Ability to configure most of the settings from the admin's UI +- Ability to lock some settings from the environment +- Improved display of the icons alerting about an outdated version +- Improved mime-type checking (back & front) +- Updated CarrierWave to 2.1.0 +- Updated redis to v6, with alpine image +- Updated Sidekiq to 6.0.7 +- Fix a bug: managers do not see the name of the user who reserved a slot +- Fix a bug: OpenAPI documentation is not available +- [TODO DEPLOY] `rails fablab:setup:env_to_db` +- [TODO DEPLOY] `\curl -sSL https://raw.githubusercontent.com/sleede/fab-manager/master/scripts/redis-upgrade.sh | bash` + +## v4.4.6 2020 June 01 + +- Fix a security issue: updated kaminari from 1.2.0 to 1.2.1 to fix [CVE-2020-11082](https://nvd.nist.gov/vuln/detail/CVE-2020-11082) + +## v4.4.5 2020 May 27 + +- Fix a security issue: updated rails to 5.2.4.2 to fix [CVE-2020-8162](https://nvd.nist.gov/vuln/detail/CVE-2020-8162), [CVE-2020-8165](https://nvd.nist.gov/vuln/detail/CVE-2020-8165) and [CVE-2020-8166](https://nvd.nist.gov/vuln/detail/CVE-2020-8166) + +## v4.4.4 2020 May 25 + +- Fix a security issue: updated puma from 3.12.4 to 3.12.6 to fix [CVE-2020-11077](https://nvd.nist.gov/vuln/detail/CVE-2020-11077) and [CVE-2020-11076](https://nvd.nist.gov/vuln/detail/CVE-2020-11076) + +## v4.4.3 2020 May 25 + +- Fix a bug: recurrent availabilities do not keep the custom duration +- [TODO DEPLOY] `rails fablab:fix:availabilities_duration` + +## v4.4.2 2020 May 19 + +- Upgraded to ruby 2.6.5 - Prevent admins from leaving their dedicated group - Faraday was downgraded from 1.0 to 0.17 for better compatibility with elasticsearch-ruby 5 (#205 #196) -- Added an option to allow usage in production without HTTPS +- Added [an option](doc/environment.md#ALLOW_INSECURE_HTTP) to allow usage in production without HTTPS +- Now using node.js instead of therubyracer for building javascript assets +- Removed dependency to has_secure_token to fix warnings about already initialized constant - Fix a bug: when an admin logs on the subscription page, his view is broken - Fix a bug: admin's members list shows the same members multiple times +- Fix a bug: when a new account is created through the sign-up modal, the role is not reported in the StatisticProfile (#196) +- Fix a bug: openAPI clients interface has a bugged behavior when creating/editing a client +- Fix a security issue: updated actionpack-page_caching from 1.1.0 to 1.2.2 to fix [CVE-2020-8159](https://nvd.nist.gov/vuln/detail/CVE-2020-8159) +- [TODO DEPLOY] `rails fablab:fix:role_in_statistic_profile` +- [TODO DEPLOY] `rails fablab:es:generate_stats[2019-06-13]` (run after the command above!) +- [TODO DEPLOY] -> (only dev) `rvm use && bundle install` ## v4.4.1 2020 May 12 diff --git a/Dockerfile b/Dockerfile index 04e5c8428..b93014ac6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:2.3.8-alpine +FROM ruby:2.6.5-alpine MAINTAINER peng@sleede.com # Install upgrade system packages @@ -28,6 +28,8 @@ RUN apk update && apk upgrade && \ git \ patch +RUN gem install bundler + # Throw error if Gemfile has been modified since Gemfile.lock RUN bundle config --global frozen 1 diff --git a/Gemfile b/Gemfile index a88aec5c8..178b0f077 100644 --- a/Gemfile +++ b/Gemfile @@ -12,14 +12,12 @@ gem 'rails', '~> 5.2.4' # Used by rails 5.2 to reduce the app boot time by over 50% gem 'bootsnap' # Use Puma as web server -gem 'puma', '3.12.4' +gem 'puma', '3.12.6' # Use SCSS for stylesheets gem 'sass-rails', '~> 5.0', '>= 5.0.6' # Use Uglifier as compressor for JavaScript assets gem 'uglifier', '>= 4.1.20' -# See https://github.com/sstephenson/execjs#readme for more supported runtimes -gem 'therubyracer', '= 0.12.0', platforms: :ruby # Use jquery as the JavaScript library gem 'jquery-rails' @@ -98,11 +96,10 @@ gem 'friendly_id', '~> 5.1.0' gem 'aasm' # Background job processing -gem 'redis-namespace' -gem 'sidekiq', '>= 3.4.2' -gem 'sinatra', require: false +gem 'sidekiq', '>= 6.0.7' # Recurring jobs for Sidekiq gem 'sidekiq-cron' +gem 'sidekiq-unique-jobs', '~> 6.0.22' gem 'stripe', '5.1.1' @@ -123,7 +120,7 @@ gem 'pundit' gem 'oj' -gem 'actionpack-page_caching', '1.2.1' +gem 'actionpack-page_caching', '1.2.2' gem 'rails-observers' gem 'chroma' @@ -134,7 +131,6 @@ gem 'openlab_ruby' gem 'api-pagination' gem 'apipie-rails' -gem 'has_secure_token' # XLS files generation gem 'caxlsx' diff --git a/Gemfile.lock b/Gemfile.lock index 9726ec897..60413c811 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -22,46 +22,46 @@ GEM Ascii85 (1.0.3) aasm (5.0.8) concurrent-ruby (~> 1.0) - actioncable (5.2.4.2) - actionpack (= 5.2.4.2) + actioncable (5.2.4.3) + actionpack (= 5.2.4.3) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailer (5.2.4.2) - actionpack (= 5.2.4.2) - actionview (= 5.2.4.2) - activejob (= 5.2.4.2) + actionmailer (5.2.4.3) + actionpack (= 5.2.4.3) + actionview (= 5.2.4.3) + activejob (= 5.2.4.3) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.2.4.2) - actionview (= 5.2.4.2) - activesupport (= 5.2.4.2) + actionpack (5.2.4.3) + actionview (= 5.2.4.3) + activesupport (= 5.2.4.3) rack (~> 2.0, >= 2.0.8) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionpack-page_caching (1.2.1) + actionpack-page_caching (1.2.2) actionpack (>= 5.0.0) - actionview (5.2.4.2) - activesupport (= 5.2.4.2) + actionview (5.2.4.3) + activesupport (= 5.2.4.3) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.3) active_record_query_trace (1.7) - activejob (5.2.4.2) - activesupport (= 5.2.4.2) + activejob (5.2.4.3) + activesupport (= 5.2.4.3) globalid (>= 0.3.6) - activemodel (5.2.4.2) - activesupport (= 5.2.4.2) - activerecord (5.2.4.2) - activemodel (= 5.2.4.2) - activesupport (= 5.2.4.2) + activemodel (5.2.4.3) + activesupport (= 5.2.4.3) + activerecord (5.2.4.3) + activemodel (= 5.2.4.3) + activesupport (= 5.2.4.3) arel (>= 9.0) - activestorage (5.2.4.2) - actionpack (= 5.2.4.2) - activerecord (= 5.2.4.2) + activestorage (5.2.4.3) + actionpack (= 5.2.4.3) + activerecord (= 5.2.4.3) marcel (~> 0.3.1) - activesupport (5.2.4.2) + activesupport (5.2.4.3) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) @@ -91,11 +91,13 @@ GEM sassc (>= 2.0.0) builder (3.2.4) camertron-eprun (1.1.1) - carrierwave (0.10.0) - activemodel (>= 3.2.0) - activesupport (>= 3.2.0) - json (>= 1.7) - mime-types (>= 1.16) + carrierwave (2.1.0) + activemodel (>= 5.0.0) + activesupport (>= 5.0.0) + addressable (~> 2.6) + image_processing (~> 1.1) + mimemagic (>= 0.3.0) + mini_mime (>= 0.1.3) caxlsx (3.0.1) htmlentities (~> 4.3, >= 4.3.4) mimemagic (~> 0.3) @@ -114,7 +116,7 @@ GEM sass-rails (< 5.1) sprockets (< 4.0) concurrent-ruby (1.1.6) - connection_pool (2.2.2) + connection_pool (2.2.3) coveralls (0.8.23) json (>= 1.8, < 3) simplecov (~> 0.16.1) @@ -179,8 +181,6 @@ GEM raabro (~> 1.1) globalid (0.4.2) activesupport (>= 4.2.0) - has_secure_token (1.0.0) - activerecord (>= 3.0) hashdiff (1.0.1) hashery (2.1.2) hashie (4.1.0) @@ -194,6 +194,9 @@ GEM ice_cube (~> 0.16) ice_cube (0.16.3) ice_nine (0.11.2) + image_processing (1.11.0) + mini_magick (>= 4.9.5, < 5) + ruby-vips (>= 2.0.17, < 3) jaro_winkler (1.5.4) jbuilder (2.10.0) activesupport (>= 5.0.0) @@ -205,19 +208,18 @@ GEM thor (>= 0.14, < 2.0) json (1.8.6) jwt (2.2.1) - kaminari (1.2.0) + kaminari (1.2.1) activesupport (>= 4.1.0) - kaminari-actionview (= 1.2.0) - kaminari-activerecord (= 1.2.0) - kaminari-core (= 1.2.0) - kaminari-actionview (1.2.0) + kaminari-actionview (= 1.2.1) + kaminari-activerecord (= 1.2.1) + kaminari-core (= 1.2.1) + kaminari-actionview (1.2.1) actionview - kaminari-core (= 1.2.0) - kaminari-activerecord (1.2.0) + kaminari-core (= 1.2.1) + kaminari-activerecord (1.2.1) activerecord - kaminari-core (= 1.2.0) - kaminari-core (1.2.0) - libv8 (3.16.14.19) + kaminari-core (= 1.2.1) + kaminari-core (1.2.1) listen (3.0.8) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) @@ -233,12 +235,12 @@ GEM method_source (1.0.0) mime-types (3.3.1) mime-types-data (~> 3.2015) - mime-types-data (3.2019.1009) - mimemagic (0.3.4) + mime-types-data (3.2020.0512) + mimemagic (0.3.5) mini_magick (4.10.1) mini_mime (1.0.2) mini_portile2 (2.4.0) - minitest (5.14.0) + minitest (5.14.1) minitest-reporters (1.4.2) ansi builder @@ -248,8 +250,6 @@ GEM multi_json (1.14.1) multi_xml (0.6.0) multipart-post (2.1.1) - mustermann (1.1.1) - ruby2_keywords (~> 0.0.1) nio4r (2.5.2) nokogiri (1.10.9) mini_portile2 (~> 2.4.0) @@ -294,7 +294,7 @@ GEM prawn-table (0.2.2) prawn (>= 1.3.0, < 3.0.0) public_suffix (4.0.3) - puma (3.12.4) + puma (3.12.6) pundit (2.1.0) activesupport (>= 3.0.0) raabro (1.1.6) @@ -304,18 +304,18 @@ GEM rack-test (1.1.0) rack (>= 1.0, < 3) railroady (1.5.3) - rails (5.2.4.2) - actioncable (= 5.2.4.2) - actionmailer (= 5.2.4.2) - actionpack (= 5.2.4.2) - actionview (= 5.2.4.2) - activejob (= 5.2.4.2) - activemodel (= 5.2.4.2) - activerecord (= 5.2.4.2) - activestorage (= 5.2.4.2) - activesupport (= 5.2.4.2) + rails (5.2.4.3) + actioncable (= 5.2.4.3) + actionmailer (= 5.2.4.3) + actionpack (= 5.2.4.3) + actionview (= 5.2.4.3) + activejob (= 5.2.4.3) + activemodel (= 5.2.4.3) + activerecord (= 5.2.4.3) + activestorage (= 5.2.4.3) + activesupport (= 5.2.4.3) bundler (>= 1.3.0) - railties (= 5.2.4.2) + railties (= 5.2.4.3) sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) @@ -329,9 +329,9 @@ GEM rails_stdout_logging rails_serve_static_assets (0.0.5) rails_stdout_logging (0.0.5) - railties (5.2.4.2) - actionpack (= 5.2.4.2) - activesupport (= 5.2.4.2) + railties (5.2.4.3) + actionpack (= 5.2.4.3) + activesupport (= 5.2.4.3) method_source rake (>= 0.8.7) thor (>= 0.19.0, < 2.0) @@ -345,10 +345,7 @@ GEM recurrence (1.3.0) activesupport i18n - redis (4.1.3) - redis-namespace (1.6.0) - redis (>= 3.0.4) - ref (2.0.0) + redis (4.1.4) repost (0.3.2) responders (2.4.1) actionpack (>= 4.2.0, < 6.0) @@ -364,7 +361,8 @@ GEM unicode-display_width (~> 1.4.0) ruby-progressbar (1.10.1) ruby-rc4 (0.1.5) - ruby2_keywords (0.0.2) + ruby-vips (2.0.17) + ffi (~> 1.9) rubyzip (1.3.0) safe_yaml (1.0.5) sass (3.4.25) @@ -383,24 +381,23 @@ GEM activerecord (>= 4) activesupport (>= 4) sha3 (1.0.1) - sidekiq (5.2.7) - connection_pool (~> 2.2, >= 2.2.2) - rack (>= 1.5.0) - rack-protection (>= 1.5.0) - redis (>= 3.3.5, < 5) + sidekiq (6.0.7) + connection_pool (>= 2.2.2) + rack (~> 2.0) + rack-protection (>= 2.0.0) + redis (>= 4.1.0) sidekiq-cron (1.1.0) fugit (~> 1.1) sidekiq (>= 4.2.1) + sidekiq-unique-jobs (6.0.22) + concurrent-ruby (~> 1.0, >= 1.0.5) + sidekiq (>= 4.0, < 7.0) + thor (~> 0) simplecov (0.16.1) docile (~> 1.1) json (>= 1.8, < 3) simplecov-html (~> 0.10.0) simplecov-html (0.10.2) - sinatra (2.0.8.1) - mustermann (~> 1.0) - rack (~> 2.0) - rack-protection (= 2.0.8.1) - tilt (~> 2.0) spring (2.0.2) activesupport (>= 4.2) spring-watcher-listen (2.0.1) @@ -419,10 +416,7 @@ GEM ffi term-ansicolor (1.7.1) tins (~> 1.0) - therubyracer (0.12.0) - libv8 (~> 3.16.14.0) - ref - thor (1.0.1) + thor (0.20.3) thread_safe (0.3.6) tilt (2.0.10) tins (1.24.1) @@ -454,16 +448,16 @@ GEM addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - websocket-driver (0.7.1) + websocket-driver (0.7.2) websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.4) + websocket-extensions (0.1.5) PLATFORMS ruby DEPENDENCIES aasm - actionpack-page_caching (= 1.2.1) + actionpack-page_caching (= 1.2.2) active_record_query_trace api-pagination apipie-rails @@ -489,7 +483,6 @@ DEPENDENCIES foreman forgery friendly_id (~> 5.1.0) - has_secure_token icalendar jbuilder (~> 2.5) jbuilder_cache_multi @@ -509,7 +502,7 @@ DEPENDENCIES pg prawn prawn-table - puma (= 3.12.4) + puma (= 3.12.6) pundit railroady rails (~> 5.2.4) @@ -517,7 +510,6 @@ DEPENDENCIES rails_12factor rb-readline recurrence - redis-namespace repost responders (~> 2.0) rolify @@ -527,18 +519,17 @@ DEPENDENCIES sdoc (~> 0.4.0) seed_dump sha3 - sidekiq (>= 3.4.2) + sidekiq (>= 6.0.7) sidekiq-cron - sinatra + sidekiq-unique-jobs (~> 6.0.22) spring spring-watcher-listen (~> 2.0.0) stripe (= 5.1.1) sys-filesystem - therubyracer (= 0.12.0) uglifier (>= 4.1.20) vcr (= 3.0.1) web-console (>= 3.3.0) webmock BUNDLED WITH - 1.17.3 + 2.1.4 diff --git a/README.md b/README.md index 01a4d8e53..41918d48e 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,9 @@ Fab-manager is the Fab Lab management solution. It provides a comprehensive, web Fab-manager is a Ruby on Rails / AngularJS web application that runs on the following software: - Ubuntu LTS 14.04+ / Debian 8+ -- Ruby 2.3 -- Redis 2.8.4+ -- Sidekiq 3.3.4+ +- Ruby 2.6 +- Redis 6 +- Sidekiq 6 - Elasticsearch 5.6 - PostgreSQL 9.6 @@ -71,9 +71,8 @@ The deal is fair, you share your projects and as reward you benefits from projec If you want to try it, you can visit [this Fab-manager](https://fablab.lacasemate.fr/#!/projects) and see projects from different Fab-managers. To start using this awesome feature, there are a few steps: -- send a mail to **contact@fab-manager.com** asking for your Open Projects client's credentials and giving them the name of your Fab-manager, they will give you an `OPENLAB_APP_ID` and an `OPENLAB_APP_SECRET` -- fill in the value of the keys in your environment file -- start your Fab-manager app +- send a mail to **contact@fab-manager.com** asking for your Open Projects client's credentials and giving them the name and the URL of your Fab-manager, they will give you an `App ID` and a `secret` +- fill in the value of the keys in Admin > Projects > Settings > Projects sharing - export your projects to open-projects (if you already have projects created on your Fab-manager, unless you can skip that part) executing this command: `bundle exec rake fablab:openlab:bulk_export` **IMPORTANT: please run your server in production mode.** @@ -99,9 +98,9 @@ You can see an example on the [repo of navinum gamification plugin](https://gith ## 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. +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). +For an example of how to use configure an SSO in Fab-manager, please read [sso_with_github.md](doc/sso_with_github.md). ## Known issues @@ -111,7 +110,7 @@ Before reporting an issue, please check if your issue is not listed in the [know ## Related Documentation -- [Ruby 2.3.0](http://ruby-doc.org/core-2.3.0/) +- [Ruby 2.6.5](http://ruby-doc.org/core-2.6.5/) - [Ruby on Rails](http://api.rubyonrails.org) - [AngularJS](https://docs.angularjs.org/api) - [Angular-Bootstrap](http://angular-ui.github.io/bootstrap/) diff --git a/app/assets/javascripts/app.js b/app/assets/javascripts/app.js index 0b08ad6cf..cf0751f6f 100644 --- a/app/assets/javascripts/app.js +++ b/app/assets/javascripts/app.js @@ -17,7 +17,7 @@ Application.Directives = angular.module('application.directives', []); angular.module('application', ['ngCookies', 'ngResource', 'ngSanitize', 'ui.router', 'ui.bootstrap', 'ngUpload', 'duScroll', 'application.filters', 'application.services', 'application.directives', 'frapontillo.bootstrap-switch', 'application.constants', 'application.controllers', 'application.router', - 'ui.select', 'ui.calendar', 'angularMoment', 'Devise', 'DeviseModal', 'angular-growl', 'xeditable', + 'ui.select', 'ui.calendar', 'angularMoment', 'Devise', 'angular-growl', 'xeditable', 'checklist-model', 'unsavedChanges', 'angular-loading-bar', 'ngTouch', 'angular-google-analytics', 'angularUtils.directives.dirDisqus', 'summernote', 'elasticsearch', 'angular-medium-editor', 'naif.base64', 'minicolors', 'pascalprecht.translate', 'ngFitText', 'ngAside', 'ngCapsLock', 'vcRecaptcha', 'ui.codemirror', @@ -28,10 +28,10 @@ angular.module('application', ['ngCookies', 'ngResource', 'ngSanitize', 'ui.rout // first we check the user acceptance const cookiesConsent = document.cookie.replace(/(?:(?:^|.*;\s*)fab-manager-cookies-consent\s*=\s*([^;]*).*$)|^.*$/, '$1'); if (cookiesConsent === 'accept') { - AnalyticsProvider.setAccount(Fablab.gaId); + AnalyticsProvider.setAccount(Fablab.trackingId); // track all routes (or not) AnalyticsProvider.trackPages(true); - AnalyticsProvider.setDomainName(Fablab.defaultHost); + AnalyticsProvider.setDomainName(Fablab.baseHostUrl); AnalyticsProvider.useAnalytics(true); AnalyticsProvider.setPageEvent('$stateChangeSuccess'); } else { @@ -81,25 +81,6 @@ angular.module('application', ['ngCookies', 'ngResource', 'ngSanitize', 'ui.rout $state.prevParams = fromParams; }); - // Global config: if true, the whole 'Plans & Subscriptions' feature will be disabled in the application - $rootScope.fablabWithoutPlans = Fablab.withoutPlans; - // Global config: it true, the whole 'Spaces' features will be disabled in the application - $rootScope.fablabWithoutSpaces = Fablab.withoutSpaces; - // Global config: if true, all payments will be disabled in the application for the members (only admins will be able to proceed reservations) - $rootScope.fablabWithoutOnlinePayment = Fablab.withoutOnlinePayment; - // Global config: if true, no invoices will be generated - $rootScope.fablabWithoutInvoices = Fablab.withoutInvoices; - // Global config: if true, the phone number is required to create an account - $rootScope.phoneRequired = Fablab.phoneRequired; - // Global config: if true, the events are shown in the admin calendar - $rootScope.eventsInCalendar = Fablab.eventsInCalendar; - // Global config: machine/space slot duration - $rootScope.slotDuration = Fablab.slotDuration; - // Global config: if true, user must confirm his email to sign in - $rootScope.userConfirmationNeededToSignIn = Fablab.userConfirmationNeededToSignIn; - // Global config: if true, wallet will be disable - $rootScope.fablabWithoutWallet = Fablab.fablabWithoutWallet; - // Global function to allow the user to navigate to the previous screen (ie. $state). // If no previous $state were recorded, navigate to the home page $rootScope.backPrevLocation = function (event) { diff --git a/app/assets/javascripts/controllers/admin/calendar.js.erb b/app/assets/javascripts/controllers/admin/calendar.js.erb index 287ae138b..eb3350244 100644 --- a/app/assets/javascripts/controllers/admin/calendar.js.erb +++ b/app/assets/javascripts/controllers/admin/calendar.js.erb @@ -18,8 +18,8 @@ * Controller used in the calendar management page */ -Application.Controllers.controller('AdminCalendarController', ['$scope', '$state', '$uibModal', 'moment', 'AuthService', 'Availability', 'Slot', 'Setting', 'Export', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', 'machinesPromise', 'plansPromise', 'groupsPromise', '_t', 'uiCalendarConfig', 'CalendarConfig', 'Member', 'uiTourService', - function ($scope, $state, $uibModal, moment, AuthService, Availability, Slot, Setting, Export, growl, dialogs, bookingWindowStart, bookingWindowEnd, machinesPromise, plansPromise, groupsPromise, _t, uiCalendarConfig, CalendarConfig, Member, uiTourService) { +Application.Controllers.controller('AdminCalendarController', ['$scope', '$state', '$uibModal', 'moment', 'AuthService', 'Availability', 'Slot', 'Setting', 'Export', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', 'machinesPromise', 'plansPromise', 'groupsPromise', 'settingsPromise', '_t', 'uiCalendarConfig', 'CalendarConfig', 'Member', 'uiTourService', + function ($scope, $state, $uibModal, moment, AuthService, Availability, Slot, Setting, Export, growl, dialogs, bookingWindowStart, bookingWindowEnd, machinesPromise, plansPromise, groupsPromise, settingsPromise, _t, uiCalendarConfig, CalendarConfig, Member, uiTourService) { /* PRIVATE STATIC CONSTANTS */ // The calendar is divided in slots of 30 minutes @@ -29,7 +29,7 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state const BOOKING_SNAP = '00:30:00'; // We do not allow the creation of slots that are not a multiple of 60 minutes - const SLOT_MULTIPLE = Fablab.slotDuration; + const SLOT_MULTIPLE = parseInt(settingsPromise.slot_duration, 10); /* PUBLIC SCOPE */ @@ -42,6 +42,9 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state // corresponding fullCalendar item in the DOM $scope.availabilityDom = null; + // Should we show the scheduled events in the calendar? + $scope.eventsInCalendar = (settingsPromise.events_in_calendar === 'true'); + // bind the availabilities slots with full-Calendar events $scope.eventSources = []; $scope.eventSources.push({ @@ -349,7 +352,7 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state } }); // if the user has never seen the tour, show him now - if (Fablab.featureTourDisplay !== 'manual' && $scope.currentUser.profile.tours.indexOf('calendar') < 0) { + if (settingsPromise.feature_tour_display !== 'manual' && $scope.currentUser.profile.tours.indexOf('calendar') < 0) { uitour.start(); } } @@ -429,7 +432,8 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state spacesPromise: ['Space', function (Space) { return Space.query().$promise; }], tagsPromise: ['Tag', function (Tag) { return Tag.query().$promise; }], plansPromise: ['Plan', function (Plan) { return Plan.query().$promise; }], - groupsPromise: ['Group', function (Group) { return Group.query().$promise; }] + groupsPromise: ['Group', function (Group) { return Group.query().$promise; }], + slotDurationPromise: ['Setting', function (Setting) { return Setting.get({ name: 'slot_duration' }).$promise; }] } }); // when the modal is closed, we send the slot to the server for saving modalInstance.result.then( @@ -531,8 +535,8 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state /** * Controller used in the slot creation modal window */ -Application.Controllers.controller('CreateEventModalController', ['$scope', '$uibModalInstance', '$sce', 'moment', 'start', 'end', 'slots', 'machinesPromise', 'Availability', 'trainingsPromise', 'spacesPromise', 'tagsPromise', 'plansPromise', 'groupsPromise', 'growl', '_t', - function ($scope, $uibModalInstance, $sce, moment, start, end, slots, machinesPromise, Availability, trainingsPromise, spacesPromise, tagsPromise, plansPromise, groupsPromise, growl, _t) { +Application.Controllers.controller('CreateEventModalController', ['$scope', '$uibModalInstance', '$sce', 'moment', 'start', 'end', 'slots', 'machinesPromise', 'Availability', 'trainingsPromise', 'spacesPromise', 'tagsPromise', 'plansPromise', 'groupsPromise', 'slotDurationPromise', 'growl', '_t', + function ($scope, $uibModalInstance, $sce, moment, start, end, slots, machinesPromise, Availability, trainingsPromise, spacesPromise, tagsPromise, plansPromise, groupsPromise, slotDurationPromise, growl, _t) { // $uibModal parameter $scope.start = start; @@ -595,7 +599,7 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui period: 'week', nb_periods: 1, end_date: undefined, // recurrence end - slot_duration: Fablab.slotDuration + slot_duration: parseInt(slotDurationPromise.setting.value, 10) }; // recurrent slots diff --git a/app/assets/javascripts/controllers/admin/events.js.erb b/app/assets/javascripts/controllers/admin/events.js.erb index 7417f6d6d..b57151086 100644 --- a/app/assets/javascripts/controllers/admin/events.js.erb +++ b/app/assets/javascripts/controllers/admin/events.js.erb @@ -153,8 +153,8 @@ class EventsController { /** * Controller used in the events listing page (admin view) */ -Application.Controllers.controller('AdminEventsController', ['$scope', '$state', 'dialogs', '$uibModal', 'growl', 'AuthService', 'Event', 'Category', 'EventTheme', 'AgeRange', 'PriceCategory', 'eventsPromise', 'categoriesPromise', 'themesPromise', 'ageRangesPromise', 'priceCategoriesPromise', '_t', 'Member', 'uiTourService', - function ($scope, $state, dialogs, $uibModal, growl, AuthService, Event, Category, EventTheme, AgeRange, PriceCategory, eventsPromise, categoriesPromise, themesPromise, ageRangesPromise, priceCategoriesPromise, _t, Member, uiTourService) { +Application.Controllers.controller('AdminEventsController', ['$scope', '$state', 'dialogs', '$uibModal', 'growl', 'AuthService', 'Event', 'Category', 'EventTheme', 'AgeRange', 'PriceCategory', 'eventsPromise', 'categoriesPromise', 'themesPromise', 'ageRangesPromise', 'priceCategoriesPromise', '_t', 'Member', 'uiTourService', 'settingsPromise', + function ($scope, $state, dialogs, $uibModal, growl, AuthService, Event, Category, EventTheme, AgeRange, PriceCategory, eventsPromise, categoriesPromise, themesPromise, ageRangesPromise, priceCategoriesPromise, _t, Member, uiTourService, settingsPromise) { /* PUBLIC SCOPE */ // By default, the pagination mode is activated to limit the page size @@ -465,7 +465,7 @@ Application.Controllers.controller('AdminEventsController', ['$scope', '$state', } }); // if the user has never seen the tour, show him now - if (Fablab.featureTourDisplay !== 'manual' && $scope.currentUser.profile.tours.indexOf('events') < 0) { + if (settingsPromise.feature_tour_display !== 'manual' && $scope.currentUser.profile.tours.indexOf('events') < 0) { uitour.start(); } } diff --git a/app/assets/javascripts/controllers/admin/invoices.js.erb b/app/assets/javascripts/controllers/admin/invoices.js.erb index a2c7236df..d1e4edda4 100644 --- a/app/assets/javascripts/controllers/admin/invoices.js.erb +++ b/app/assets/javascripts/controllers/admin/invoices.js.erb @@ -17,18 +17,21 @@ /** * Controller used in the admin invoices listing page */ -Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'Invoice', 'AccountingPeriod', 'AuthService', 'invoices', 'closedPeriods', '$uibModal', 'growl', '$filter', 'Setting', 'settings', '_t', 'Member', 'uiTourService', - function ($scope, $state, Invoice, AccountingPeriod, AuthService, invoices, closedPeriods, $uibModal, growl, $filter, Setting, settings, _t, Member, uiTourService) { +Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'Invoice', 'AccountingPeriod', 'AuthService', 'invoices', 'closedPeriods', '$uibModal', 'growl', '$filter', 'Setting', 'settings', 'stripeSecretKey', '_t', 'Member', 'uiTourService', 'Payment', 'onlinePaymentStatus', + function ($scope, $state, Invoice, AccountingPeriod, AuthService, invoices, closedPeriods, $uibModal, growl, $filter, Setting, settings, stripeSecretKey, _t, Member, uiTourService, Payment, onlinePaymentStatus) { /* PRIVATE STATIC CONSTANTS */ // number of invoices loaded each time we click on 'load more...' const INVOICES_PER_PAGE = 20; + // fake stripe secret key + const STRIPE_SK_HIDDEN = 'sk_test_hidden-hidden-hidden-hid'; + /* PUBLIC SCOPE */ // default active tab $scope.tabs = { - active: Fablab.withoutInvoices ? 1 : 0 + active: settings.invoicing_module === 'true' ? 0 : 1 }; // List of all invoices @@ -50,6 +53,14 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I // Default invoices ordering/sorting $scope.orderInvoice = '-reference'; + // Invoice PDF filename settings (and example) + $scope.file = { + prefix: settings.invoice_prefix, + nextId: 40, + date: moment().format('DDMMYYYY'), + templateUrl: 'editPrefix.html' + } + // Invoices parameters $scope.invoice = { logo: null, @@ -169,6 +180,15 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I } }; + // all settings + $scope.allSettings = settings; + + // is the stripe private set? + $scope.stripeSecretKey = (stripeSecretKey.isPresent ? STRIPE_SK_HIDDEN : ''); + + // has any online payment been already made? + $scope.onlinePaymentStatus = onlinePaymentStatus.status; + // Placeholding date for the invoice creation $scope.today = moment(); @@ -465,6 +485,38 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I }); }; + /** + * Open a modal dialog allowing the user to edit the prefix of the invoice file name + */ + $scope.openEditPrefix = function () { + const modalInstance = $uibModal.open({ + animation: true, + templateUrl: $scope.file.templateUrl, + size: 'lg', + resolve: { + model () { return $scope.file.prefix;} + }, + controller: ['$scope', '$uibModalInstance', 'model', function ($scope, $uibModalInstance, model) { + $scope.model = model; + $scope.ok = function () { $uibModalInstance.close($scope.model); }; + $scope.cancel = function () { $uibModalInstance.dismiss('cancel'); }; + }] + }); + + return modalInstance.result.then(function (model) { + Setting.update({ name: 'invoice_prefix' }, { value: model }, function (data) { + $scope.file.prefix = model; + return growl.success(_t('app.admin.invoices.prefix_successfully_saved')); + } + , function (error) { + if (error.status === 304) return; + + growl.error(_t('app.admin.invoices.an_error_occurred_while_saving_the_prefix')); + console.error(error); + }); + }); + }; + /** * Callback to save the value of the text zone when editing is done */ @@ -569,7 +621,7 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I { settings: Object.values($scope.settings) }, function () { growl.success(_t('app.admin.invoices.codes_customization_success')); }, function (error) { - growl.error('unexpected_error_occurred'); + growl.error('app.admin.invoices.unexpected_error_occurred'); console.error(error); } ); @@ -584,6 +636,42 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I return `${invoice.operator.first_name} ${invoice.operator.last_name}`; } + /** + * Open a modal dialog which ask for the stripe keys + * @param onlinePaymentModule {{name: String, value: String}} setting that defines the next status of the online payment module + * @return {boolean} false if the keys were not provided + */ + $scope.requireStripeKeys = function(onlinePaymentModule) { + // if the online payment is about to be disabled, accept the change without any further question + if (onlinePaymentModule.value === false) return true; + + // otherwise, open a modal to ask for the stripe keys + const modalInstance = $uibModal.open({ + templateUrl: 'stripeKeys.html', + controller: 'StripeKeysModalController', + resolve: { + stripeKeys: ['Setting', function (Setting) { return Setting.query({ names: "['stripe_public_key', 'stripe_secret_key']" }).$promise; }] + } + }); + + modalInstance.result.then(function (success) { + if (success) { + Setting.get({ name: 'stripe_public_key' }, function (res) { + $scope.allSettings.stripe_public_key = res.setting.value; + }) + Setting.isPresent({ name: 'stripe_secret_key' }, function (res) { + $scope.stripeSecretKey = (res.isPresent ? STRIPE_SK_HIDDEN : ''); + }) + Payment.onlinePaymentStatus(function (res) { + $scope.onlinePaymentStatus = res.status; + }); + } + }) + + // return the promise + return modalInstance.result; + } + /** * Setup the feature-tour for the admin/invoices page. * This is intended as a contextual help (when pressing F1) @@ -612,7 +700,7 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I orphan: true }); } - if (!Fablab.withoutInvoices && $scope.invoices.length > 0) { + if (settings.invoicing_module === 'true' && $scope.invoices.length > 0) { uitour.createStep({ selector: '.invoices-management .invoices-list', stepId: 'list', @@ -671,10 +759,19 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I content: _t('app.admin.tour.invoices.export.content'), placement: 'bottom' }); + uitour.createStep({ + selector: '.invoices-management .payment-settings', + stepId: 'payment', + order: 8, + title: _t('app.admin.tour.invoices.payment.title'), + content: _t('app.admin.tour.invoices.payment.content'), + placement: 'bottom', + popupClass: 'shift-left-50' + }); uitour.createStep({ selector: '.heading .close-accounting-periods-button', stepId: 'periods', - order: 8, + order: 9, title: _t('app.admin.tour.invoices.periods.title'), content: _t('app.admin.tour.invoices.periods.content'), placement: 'bottom', @@ -684,7 +781,7 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I uitour.createStep({ selector: 'body', stepId: 'conclusion', - order: 9, + order: 10, title: _t('app.admin.tour.conclusion.title'), content: _t('app.admin.tour.conclusion.content'), placement: 'bottom', @@ -701,6 +798,9 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I if (nextStep.stepId === 'codes' || nextStep.stepId === 'export') { $scope.tabs.active = 2; } + if (nextStep.stepId === 'payment') { + $scope.tabs.active = 3; + } }); // on tour end, save the status in database uitour.on('ended', function () { @@ -711,7 +811,7 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I } }); // if the user has never seen the tour, show him now - if (Fablab.featureTourDisplay !== 'manual' && $scope.currentUser.profile.tours.indexOf('invoices') < 0) { + if (settings.feature_tour_display !== 'manual' && $scope.currentUser.profile.tours.indexOf('invoices') < 0) { uitour.start(); } } @@ -1209,3 +1309,131 @@ Application.Controllers.controller('AccountingExportModalController', ['$scope', // !!! MUST BE CALLED AT THE END of the controller return initialize(); }]); + + + +/** + * Controller used in the modal window allowing an admin to close an accounting period + */ +Application.Controllers.controller('StripeKeysModalController', ['$scope', '$uibModalInstance', '$http', '$httpParamSerializerJQLike', 'stripeKeys', 'Setting', 'growl', '_t', + function ($scope, $uibModalInstance, $http, $httpParamSerializerJQLike, stripeKeys, Setting, growl, _t) { + /* PUBLIC SCOPE */ + + // public key + $scope.publicKey = stripeKeys.stripe_public_key; + + // test status of the public key + $scope.publicKeyStatus = undefined; + + // secret key + $scope.secretKey = stripeKeys.stripe_secret_key; + + // test status of the secret key + $scope.secretKeyStatus = undefined; + + /** + * Trigger the test of the secret key and set the result in $scope.secretKeyStatus + */ + $scope.testSecretKey = function () { + if (!$scope.secretKey.match(/^sk_/)) { + $scope.secretKeyStatus = false; + return; + } + $http({ + method: 'GET', + url: 'https://api.stripe.com/v1/charges', + headers: { + Authorization: `Bearer ${$scope.secretKey}`, + } + }).then(function () { + $scope.secretKeyStatus = true; + }, function (err) { + if (err.status === 401) $scope.secretKeyStatus = false; + }); + } + + + /** + * Trigger the test of the secret key and set the result in $scope.secretKeyStatus + */ + $scope.testPublicKey = function () { + if (!$scope.publicKey.match(/^pk_/)) { + $scope.publicKeyStatus = false; + return; + } + const today = new Date(); + $http({ + method: 'POST', + url: 'https://api.stripe.com/v1/tokens', + headers: { + Authorization: `Bearer ${$scope.publicKey}`, + 'Content-Type': 'application/x-www-form-urlencoded' + }, + data: $httpParamSerializerJQLike({ + 'card[number]': 4242424242424242, + 'card[cvc]': 123, + 'card[exp_month]': 12, + 'card[exp_year]': today.getFullYear().toString().substring(2), + }) + }).then(function () { + $scope.publicKeyStatus = true; + }, function (err) { + if (err.status === 401) $scope.publicKeyStatus = false; + }); + } + + /** + * Validate the keys + */ + $scope.ok = function () { + if ($scope.secretKeyStatus && $scope.publicKeyStatus) { + Setting.bulkUpdate( + { settings: [ + { + name: 'stripe_public_key', + value: $scope.publicKey + }, + { + name: 'stripe_secret_key', + value: $scope.secretKey + } + ] }, + function () { + growl.success(_t('app.admin.invoices.payment.stripe_keys_saved')); + $uibModalInstance.close(true); + }, + function (error) { + growl.error('app.admin.invoices.payment.error_saving_stripe_keys'); + console.error(error); + } + ); + } else { + growl.error(_t('app.admin.invoices.payment.error_check_keys')) + } + }; + + /** + * Just dismiss the modal window + */ + $scope.cancel = function () { + $uibModalInstance.dismiss('cancel'); + }; + + /* PRIVATE SCOPE */ + + /** + * Kind of constructor: these actions will be realized first when the controller is loaded + */ + const initialize = function () { + if (stripeKeys.stripe_public_key) { + $scope.testPublicKey(); + } + if (stripeKeys.stripe_secret_key) { + $scope.testSecretKey(); + } + }; + + // !!! MUST BE CALLED AT THE END of the controller! + return initialize(); + } +]); diff --git a/app/assets/javascripts/controllers/admin/members.js.erb b/app/assets/javascripts/controllers/admin/members.js.erb index 906bf8396..30c3a96b0 100644 --- a/app/assets/javascripts/controllers/admin/members.js.erb +++ b/app/assets/javascripts/controllers/admin/members.js.erb @@ -126,8 +126,8 @@ class MembersController { /** * Controller used in the members/groups management page */ -Application.Controllers.controller('AdminMembersController', ['$scope', '$sce', '$uibModal', 'membersPromise', 'adminsPromise', 'partnersPromise', 'managersPromise', 'growl', 'Admin', 'AuthService', 'dialogs', '_t', 'Member', 'Export', 'User', 'uiTourService', - function ($scope, $sce, $uibModal, membersPromise, adminsPromise, partnersPromise, managersPromise, growl, Admin, AuthService, dialogs, _t, Member, Export, User, uiTourService) { +Application.Controllers.controller('AdminMembersController', ['$scope', '$sce', '$uibModal', 'membersPromise', 'adminsPromise', 'partnersPromise', 'managersPromise', 'growl', 'Admin', 'AuthService', 'dialogs', '_t', 'Member', 'Export', 'User', 'uiTourService', 'settingsPromise', + function ($scope, $sce, $uibModal, membersPromise, adminsPromise, partnersPromise, managersPromise, growl, Admin, AuthService, dialogs, _t, Member, Export, User, uiTourService, settingsPromise) { /* PRIVATE STATIC CONSTANTS */ // number of users loaded each time we click on 'load more...' @@ -567,7 +567,7 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce', } }); // if the user has never seen the tour, show him now - if (Fablab.featureTourDisplay !== 'manual' && $scope.currentUser.profile.tours.indexOf('members') < 0) { + if (settingsPromise.feature_tour_display !== 'manual' && $scope.currentUser.profile.tours.indexOf('members') < 0) { uitour.start(); } } @@ -641,8 +641,8 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce', /** * 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', 'walletPromise', 'transactionsPromise', 'activeProviderPromise', 'Wallet', - function ($scope, $state, $stateParams, Member, Training, dialogs, growl, Group, Subscription, CSRF, memberPromise, tagsPromise, $uibModal, Plan, $filter, _t, walletPromise, transactionsPromise, activeProviderPromise, Wallet) { +Application.Controllers.controller('EditMemberController', ['$scope', '$state', '$stateParams', 'Member', 'Training', 'dialogs', 'growl', 'Group', 'Subscription', 'CSRF', 'memberPromise', 'tagsPromise', '$uibModal', 'Plan', '$filter', '_t', 'walletPromise', 'transactionsPromise', 'activeProviderPromise', 'Wallet', 'phoneRequiredPromise', + function ($scope, $state, $stateParams, Member, Training, dialogs, growl, Group, Subscription, CSRF, memberPromise, tagsPromise, $uibModal, Plan, $filter, _t, walletPromise, transactionsPromise, activeProviderPromise, Wallet, phoneRequiredPromise) { /* PUBLIC SCOPE */ // API URL where the form will be posted @@ -660,6 +660,9 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state', // Should the password be modified? $scope.password = { change: false }; + // is the phone number required in _member_form? + $scope.phoneRequired = (phoneRequiredPromise.setting.value === 'true'); + // the user subscription if (($scope.user.subscribed_plan != null) && ($scope.user.subscription != null)) { $scope.subscription = $scope.user.subscription; @@ -948,8 +951,8 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state', /** * Controller used in the member's creation page (admin view) */ -Application.Controllers.controller('NewMemberController', ['$scope', '$state', '$stateParams', 'Member', 'Training', 'Group', 'CSRF', - function ($scope, $state, $stateParams, Member, Training, Group, CSRF) { +Application.Controllers.controller('NewMemberController', ['$scope', '$state', '$stateParams', 'Member', 'Training', 'Group', 'CSRF', 'phoneRequiredPromise', + function ($scope, $state, $stateParams, Member, Training, Group, CSRF, phoneRequiredPromise) { CSRF.setMetaTags(); /* PUBLIC SCOPE */ @@ -963,6 +966,9 @@ Application.Controllers.controller('NewMemberController', ['$scope', '$state', ' // Should the password be set manually or generated? $scope.password = { change: false }; + // is the phone number required in _member_form? + $scope.phoneRequired = (phoneRequiredPromise.setting.value === 'true'); + // Default member's profile parameters $scope.user = { plan_interval: '', diff --git a/app/assets/javascripts/controllers/admin/open_api_clients.js b/app/assets/javascripts/controllers/admin/open_api_clients.js index 0b71944dc..e165ee0d6 100644 --- a/app/assets/javascripts/controllers/admin/open_api_clients.js +++ b/app/assets/javascripts/controllers/admin/open_api_clients.js @@ -10,8 +10,8 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -Application.Controllers.controller('OpenAPIClientsController', ['$scope', 'clientsPromise', 'growl', 'OpenAPIClient', 'dialogs', '_t', 'Member', 'uiTourService', - function ($scope, clientsPromise, growl, OpenAPIClient, dialogs, _t, Member, uiTourService) { +Application.Controllers.controller('OpenAPIClientsController', ['$scope', 'clientsPromise', 'settingsPromise', 'growl', 'OpenAPIClient', 'dialogs', '_t', 'Member', 'uiTourService', + function ($scope, clientsPromise, settingsPromise, growl, OpenAPIClient, dialogs, _t, Member, uiTourService) { /* PUBLIC SCOPE */ // clients list @@ -20,11 +20,18 @@ Application.Controllers.controller('OpenAPIClientsController', ['$scope', 'clien $scope.clientFormVisible = false; $scope.client = {}; - $scope.toggleForm = () => $scope.clientFormVisible = !$scope.clientFormVisible; + /** + * Show the name edition form for a new client + */ + $scope.createClient = function () { + $scope.clientFormVisible = true; + $scope.client = {}; + }; - // Change the order criterion to the one provided - // @param orderBy {string} ordering criterion - // + /** + * Change the order criterion to the one provided + * @param orderBy {string} ordering criterion + */ $scope.setOrder = function (orderBy) { if ($scope.order === orderBy) { return $scope.order = `-${orderBy}`; @@ -33,6 +40,14 @@ Application.Controllers.controller('OpenAPIClientsController', ['$scope', 'clien } }; + /** + * Reset the name ot its original value and close the edition form + */ + $scope.cancelEdit = function () { + $scope.client.name = $scope.clientOriginalName; + $scope.clientFormVisible = false; + }; + $scope.saveClient = function (client) { if (client.id != null) { OpenAPIClient.update({ id: client.id }, { open_api_client: client }, function (clientResp) { @@ -47,13 +62,13 @@ Application.Controllers.controller('OpenAPIClientsController', ['$scope', 'clien } $scope.clientFormVisible = false; - $scope.clientForm.$setPristine(); - return $scope.client = {}; + $scope.client = {}; }; $scope.editClient = function (client) { $scope.clientFormVisible = true; - return $scope.client = client; + $scope.client = client; + $scope.clientOriginalName = client.name; }; $scope.deleteClient = index => @@ -134,7 +149,7 @@ Application.Controllers.controller('OpenAPIClientsController', ['$scope', 'clien } }); // if the user has never seen the tour, and if the display behavior is not configured to manual triggering only, show the tour now - if (Fablab.featureTourDisplay !== 'manual' && $scope.currentUser.profile.tours.indexOf('open-api') < 0) { + if (settingsPromise.feature_tour_display !== 'manual' && $scope.currentUser.profile.tours.indexOf('open-api') < 0) { uitour.start(); } }; diff --git a/app/assets/javascripts/controllers/admin/pricing.js.erb b/app/assets/javascripts/controllers/admin/pricing.js.erb index 7d501e4a0..c9dfebbb5 100644 --- a/app/assets/javascripts/controllers/admin/pricing.js.erb +++ b/app/assets/javascripts/controllers/admin/pricing.js.erb @@ -18,8 +18,8 @@ /** * Controller used in the prices edition page */ -Application.Controllers.controller('EditPricingController', ['$scope', '$state', '$uibModal', '$filter', 'TrainingsPricing', 'Credit', 'Pricing', 'Plan', 'Coupon', 'plans', 'groups', 'growl', 'machinesPricesPromise', 'Price', 'dialogs', 'trainingsPricingsPromise', 'trainingsPromise', 'machineCreditsPromise', 'machinesPromise', 'trainingCreditsPromise', 'couponsPromise', 'spacesPromise', 'spacesPricesPromise', 'spacesCreditsPromise', '_t', 'Member', 'uiTourService', - function ($scope, $state, $uibModal, $filter, TrainingsPricing, Credit, Pricing, Plan, Coupon, plans, groups, growl, machinesPricesPromise, Price, dialogs, trainingsPricingsPromise, trainingsPromise, machineCreditsPromise, machinesPromise, trainingCreditsPromise, couponsPromise, spacesPromise, spacesPricesPromise, spacesCreditsPromise, _t, Member, uiTourService) { +Application.Controllers.controller('EditPricingController', ['$scope', '$state', '$uibModal', '$filter', 'TrainingsPricing', 'Credit', 'Pricing', 'Plan', 'Coupon', 'plans', 'groups', 'growl', 'machinesPricesPromise', 'Price', 'dialogs', 'trainingsPricingsPromise', 'trainingsPromise', 'machineCreditsPromise', 'machinesPromise', 'trainingCreditsPromise', 'couponsPromise', 'spacesPromise', 'spacesPricesPromise', 'spacesCreditsPromise', 'settingsPromise', '_t', 'Member', 'uiTourService', + function ($scope, $state, $uibModal, $filter, TrainingsPricing, Credit, Pricing, Plan, Coupon, plans, groups, growl, machinesPricesPromise, Price, dialogs, trainingsPricingsPromise, trainingsPromise, machineCreditsPromise, machinesPromise, trainingCreditsPromise, couponsPromise, spacesPromise, spacesPricesPromise, spacesCreditsPromise, settingsPromise, _t, Member, uiTourService) { /* PUBLIC SCOPE */ // List of machines prices (not considering any plan) @@ -76,6 +76,9 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', // Default: we show only enabled plans $scope.planFiltering = 'enabled'; + // Default duration for the slots + $scope.slotDuration = parseInt(settingsPromise.slot_duration, 10); + // Available options for filtering plans by status $scope.filterDisabled = [ 'enabled', @@ -620,7 +623,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', /** * Return the exemple price based on the configuration of the default slot duration. * @param type {string} 'hourly_rate' | * - * @returns {number} price for Fablab.slotDuration minutes. + * @returns {number} price for "SLOT_DURATION" minutes. */ $scope.examplePrice = function(type) { const hourlyRate = 10; @@ -629,7 +632,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', return $filter('currency')(hourlyRate); } - const price = (hourlyRate / 60) * Fablab.slotDuration; + const price = (hourlyRate / 60) * $scope.slotDuration; return $filter('currency')(price); } @@ -673,7 +676,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', content: _t('app.admin.tour.pricing.machines.content'), placement: 'bottom' }); - if (!Fablab.withoutSpaces) { + if ($scope.modules.spaces) { uitour.createStep({ selector: '.plans-pricing .spaces-tab', stepId: 'spaces', @@ -727,7 +730,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', } }); // if the user has never seen the tour, show him now - if (Fablab.featureTourDisplay !== 'manual' && $scope.currentUser.profile.tours.indexOf('pricing') < 0) { + if (settingsPromise.feature_tour_display !== 'manual' && $scope.currentUser.profile.tours.indexOf('pricing') < 0) { uitour.start(); } } diff --git a/app/assets/javascripts/controllers/admin/project_elements.js b/app/assets/javascripts/controllers/admin/projects.js similarity index 70% rename from app/assets/javascripts/controllers/admin/project_elements.js rename to app/assets/javascripts/controllers/admin/projects.js index 3ab047d1d..fd75f957d 100644 --- a/app/assets/javascripts/controllers/admin/project_elements.js +++ b/app/assets/javascripts/controllers/admin/projects.js @@ -12,8 +12,8 @@ */ 'use strict'; -Application.Controllers.controller('ProjectElementsController', ['$scope', '$state', 'Component', 'Licence', 'Theme', 'componentsPromise', 'licencesPromise', 'themesPromise', '_t', 'Member', 'uiTourService', - function ($scope, $state, Component, Licence, Theme, componentsPromise, licencesPromise, themesPromise, _t, Member, uiTourService) { +Application.Controllers.controller('AdminProjectsController', ['$scope', '$state', 'Component', 'Licence', 'Theme', 'componentsPromise', 'licencesPromise', 'themesPromise', '_t', 'Member', 'uiTourService', 'settingsPromise', 'growl', + function ($scope, $state, Component, Licence, Theme, componentsPromise, licencesPromise, themesPromise, _t, Member, uiTourService, settingsPromise, growl) { // Materials list (plastic, wood ...) $scope.components = componentsPromise; @@ -23,6 +23,12 @@ Application.Controllers.controller('ProjectElementsController', ['$scope', '$sta // Themes list (cooking, sport ...) $scope.themes = themesPromise; + // Application settings + $scope.allSettings = settingsPromise; + + // default tab: materials + $scope.tabs = { active: 0 }; + /** * Saves a new component / Update an existing material to the server (form validation callback) * @param data {Object} component name @@ -156,18 +162,61 @@ Application.Controllers.controller('ProjectElementsController', ['$scope', '$sta }; /** - * Setup the feature-tour for the admin/project_elements page. + * When a file is sent to the server to test it against its MIME type, + * handle the result of the test. + */ + $scope.onTestFileComplete = function (res) { + if (res) { + growl.success(_t('app.admin.projects.settings.file_is_TYPE', { TYPE: res.type })); + } + }; + + /** + * 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 = function (v) { + if (v) { + return 'fileinput-exists'; + } else { + return 'fileinput-new'; + } + }; + + /** + * Remove the initial dot from the given extension, if any + * @param extension {String} + * @returns {String} + */ + $scope.removeInitialDot = function (extension) { + if (extension.substr(0, 1) === '.') return $scope.lower(extension.substr(1)); + + return $scope.lower(extension); + }; + + /** + * Return the lowercase version of the provided string + * @param text {String} + * @returns {string} + */ + $scope.lower = function (text) { + return text.toLowerCase(); + }; + + /** + * Setup the feature-tour for the admin/projects page. * This is intended as a contextual help (when pressing F1) */ $scope.setupProjectElementsTour = function () { // get the tour defined by the ui-tour directive - const uitour = uiTourService.getTourByName('project-elements'); + const uitour = uiTourService.getTourByName('projects'); uitour.createStep({ selector: 'body', stepId: 'welcome', order: 0, - title: _t('app.admin.tour.project_elements.welcome.title'), - content: _t('app.admin.tour.project_elements.welcome.content'), + title: _t('app.admin.tour.projects.welcome.title'), + content: _t('app.admin.tour.projects.welcome.content'), placement: 'bottom', orphan: true }); @@ -175,30 +224,43 @@ Application.Controllers.controller('ProjectElementsController', ['$scope', '$sta selector: '.heading .abuses-button', stepId: 'abuses', order: 1, - title: _t('app.admin.tour.project_elements.abuses.title'), - content: _t('app.admin.tour.project_elements.abuses.content'), + title: _t('app.admin.tour.projects.abuses.title'), + content: _t('app.admin.tour.projects.abuses.content'), placement: 'bottom', popupClass: 'shift-left-40' }); + uitour.createStep({ + selector: '.projects .settings-tab', + stepId: 'settings', + order: 2, + title: _t('app.admin.tour.projects.settings.title'), + content: _t('app.admin.tour.projects.settings.content'), + placement: 'bottom', + popupClass: 'shift-left-50' + }); uitour.createStep({ selector: 'body', stepId: 'conclusion', - order: 2, + order: 3, title: _t('app.admin.tour.conclusion.title'), content: _t('app.admin.tour.conclusion.content'), placement: 'bottom', orphan: true }); + // on step change, change the active tab if needed + uitour.on('stepChanged', function (nextStep) { + if (nextStep.stepId === 'settings') { $scope.tabs.active = 3; } + }); // on tour end, save the status in database uitour.on('ended', function () { - if (uitour.getStatus() === uitour.Status.ON && $scope.currentUser.profile.tours.indexOf('project-elements') < 0) { - Member.completeTour({ id: $scope.currentUser.id }, { tour: 'project-elements' }, function (res) { + if (uitour.getStatus() === uitour.Status.ON && $scope.currentUser.profile.tours.indexOf('projects') < 0) { + Member.completeTour({ id: $scope.currentUser.id }, { tour: 'projects' }, function (res) { $scope.currentUser.profile.tours = res.tours; }); } }); // if the user has never seen the tour, show him now - if (Fablab.featureTourDisplay !== 'manual' && $scope.currentUser.profile.tours.indexOf('project-elements') < 0) { + if (settingsPromise.feature_tour_display !== 'manual' && $scope.currentUser.profile.tours.indexOf('projects') < 0) { uitour.start(); } }; diff --git a/app/assets/javascripts/controllers/admin/settings.js.erb b/app/assets/javascripts/controllers/admin/settings.js.erb index e48d2c2e6..4ad80dfe2 100644 --- a/app/assets/javascripts/controllers/admin/settings.js.erb +++ b/app/assets/javascripts/controllers/admin/settings.js.erb @@ -54,9 +54,10 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope' // full history of privacy policy drafts $scope.privacyDraftsHistory = []; + // all settings as retrieved from database + $scope.allSettings = settingsPromise; + // various configurable settings - $scope.twitterSetting = { name: 'twitter_name', value: settingsPromise.twitter_name }; - $scope.linkName = { name: 'link_name', value: settingsPromise.link_name }; $scope.aboutTitleSetting = { name: 'about_title', value: settingsPromise.about_title }; $scope.aboutBodySetting = { name: 'about_body', value: settingsPromise.about_body }; $scope.privacyDpoSetting = { name: 'privacy_dpo', value: settingsPromise.privacy_dpo }; @@ -74,9 +75,7 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope' $scope.windowEnd = { name: 'booking_window_end', value: settingsPromise.booking_window_end }; $scope.mainColorSetting = { name: 'main_color', value: settingsPromise.main_color }; $scope.secondColorSetting = { name: 'secondary_color', value: settingsPromise.secondary_color }; - $scope.fablabName = { name: 'fablab_name', value: settingsPromise.fablab_name }; $scope.nameGenre = { name: 'name_genre', value: settingsPromise.name_genre }; - $scope.machinesSortBy = { name: 'machines_sort_by', value: settingsPromise.machines_sort_by }; $scope.cguFile = cguFile.custom_asset; $scope.cgvFile = cgvFile.custom_asset; $scope.customLogo = logoFile.custom_asset; @@ -84,56 +83,6 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope' $scope.customFavicon = faviconFile.custom_asset; $scope.profileImage = profileImageFile.custom_asset; - $scope.enableMove = { - name: 'booking_move_enable', - value: (settingsPromise.booking_move_enable === 'true') - }; - - $scope.moveDelay = { - name: 'booking_move_delay', - value: parseInt(settingsPromise.booking_move_delay, 10) - }; - - $scope.enableCancel = { - name: 'booking_cancel_enable', - value: (settingsPromise.booking_cancel_enable === 'true') - }; - - $scope.cancelDelay = { - name: 'booking_cancel_delay', - value: parseInt(settingsPromise.booking_cancel_delay, 10) - }; - - $scope.enableReminder = { - name: 'reminder_enable', - value: (settingsPromise.reminder_enable === 'true') - }; - - $scope.reminderDelay = { - name: 'reminder_delay', - value: parseInt(settingsPromise.reminder_delay, 10) - }; - - $scope.visibilityYearly = { - name: 'visibility_yearly', - value: parseInt(settingsPromise.visibility_yearly, 10) - }; - - $scope.visibilityOthers = { - name: 'visibility_others', - value: parseInt(settingsPromise.visibility_others, 10) - }; - - $scope.displayNameEnable = { - name: 'display_name_enable', - value: (settingsPromise.display_name_enable === 'true') - }; - - $scope.fabAnalytics = { - name: 'fab_analytics', - value: (settingsPromise.fab_analytics === 'true') - }; - // By default, we display the currently published privacy policy $scope.privacyPolicy = { version: null, @@ -381,10 +330,18 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope' placement: 'bottom', orphan: true }); + uitour.createStep({ + selector: '.admin-settings .general-page-tab', + stepId: 'general', + order: 1, + title: _t('app.admin.tour.settings.general.title'), + content: _t('app.admin.tour.settings.general.content'), + placement: 'bottom', + }); uitour.createStep({ selector: '.admin-settings .home-page-content h4', stepId: 'home', - order: 1, + order: 2, title: _t('app.admin.tour.settings.home.title'), content: _t('app.admin.tour.settings.home.content'), placement: 'bottom' @@ -392,7 +349,7 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope' uitour.createStep({ selector: '.admin-settings .home-page-content .note-toolbar .note-insert div', stepId: 'components', - order: 2, + order: 3, title: _t('app.admin.tour.settings.components.title'), content: _t('app.admin.tour.settings.components.content'), placement: 'bottom' @@ -400,7 +357,7 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope' uitour.createStep({ selector: '.admin-settings .home-page-content .note-toolbar .btn-codeview', stepId: 'codeview', - order: 3, + order: 4, title: _t('app.admin.tour.settings.codeview.title'), content: _t('app.admin.tour.settings.codeview.content'), placement: 'bottom' @@ -408,7 +365,7 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope' uitour.createStep({ selector: '.admin-settings .reset-button', stepId: 'reset', - order: 4, + order: 5, title: _t('app.admin.tour.settings.reset.title'), content: _t('app.admin.tour.settings.reset.content'), placement: 'left' @@ -416,7 +373,7 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope' uitour.createStep({ selector: '.admin-settings .home-page-style', stepId: 'css', - order: 5, + order: 6, title: _t('app.admin.tour.settings.css.title'), content: _t('app.admin.tour.settings.css.content'), placement: 'top' @@ -424,7 +381,7 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope' uitour.createStep({ selector: '.admin-settings .about-page-tab', stepId: 'about', - order: 6, + order: 7, title: _t('app.admin.tour.settings.about.title'), content: _t('app.admin.tour.settings.about.content'), placement: 'bottom' @@ -432,7 +389,7 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope' uitour.createStep({ selector: '.admin-settings .privacy-page-tab', stepId: 'privacy', - order: 7, + order: 8, title: _t('app.admin.tour.settings.privacy.title'), content: _t('app.admin.tour.settings.privacy.content'), placement: 'bottom' @@ -440,15 +397,24 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope' uitour.createStep({ selector: '.admin-settings .history-select', stepId: 'draft', - order: 8, + order: 9, title: _t('app.admin.tour.settings.draft.title'), content: _t('app.admin.tour.settings.draft.content'), placement: 'bottom' }); + uitour.createStep({ + selector: '.admin-settings .reservations-page-tab', + stepId: 'reservations', + order: 10, + title: _t('app.admin.tour.settings.reservations.title'), + content: _t('app.admin.tour.settings.reservations.content'), + placement: 'bottom', + popupClass: 'shift-left-50' + }); uitour.createStep({ selector: 'body', stepId: 'conclusion', - order: 9, + order: 11, title: _t('app.admin.tour.conclusion.title'), content: _t('app.admin.tour.conclusion.content'), placement: 'bottom', @@ -456,9 +422,11 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope' }); // on step change, change the active tab if needed uitour.on('stepChanged', function (nextStep) { + if (nextStep.stepId === 'general') { $scope.tabs.active = 0; } if (nextStep.stepId === 'home' || nextStep.stepId === 'css') { $scope.tabs.active = 1; } if (nextStep.stepId === 'about') { $scope.tabs.active = 2; } if (nextStep.stepId === 'privacy' || nextStep.stepId === 'draft') { $scope.tabs.active = 3; } + if (nextStep.stepId === 'reservations') { $scope.tabs.active = 4; } }); // on tour end, save the status in database uitour.on('ended', function () { @@ -469,7 +437,7 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope' } }); // if the user has never seen the tour, show him now - if (Fablab.featureTourDisplay !== 'manual' && $scope.currentUser.profile.tours.indexOf('settings') < 0) { + if ($scope.allSettings.feature_tour_display !== 'manual' && $scope.currentUser.profile.tours.indexOf('settings') < 0) { uitour.start(); } } @@ -524,6 +492,19 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope' $scope.$watch('advancedSettings.open', function (newValue) { if (newValue) $scope.codeMirrorEditor.refresh(); }) + + // use the tours list, based on the selected value + $scope.$watch('allSettings.feature_tour_display', function (newValue, oldValue, scope) { + if (newValue === oldValue) return; + + if (newValue === 'session') { + $scope.currentUser.profile.tours = Fablab.sessionTours; + } else if (newValue === 'once') { + Member.get({ id: $scope.currentUser.id }, function (user) { + $scope.currentUser.profile.tours = user.profile.tours; + }); + } + }); }; // init the controller (call at the end !) diff --git a/app/assets/javascripts/controllers/admin/statistics.js.erb b/app/assets/javascripts/controllers/admin/statistics.js.erb index 85e650247..5f7f55cf0 100644 --- a/app/assets/javascripts/controllers/admin/statistics.js.erb +++ b/app/assets/javascripts/controllers/admin/statistics.js.erb @@ -15,8 +15,8 @@ */ 'use strict'; -Application.Controllers.controller('StatisticsController', ['$scope', '$state', '$rootScope', '$uibModal', 'es', 'Member', '_t', 'membersPromise', 'statisticsPromise', 'uiTourService', - function ($scope, $state, $rootScope, $uibModal, es, Member, _t, membersPromise, statisticsPromise, uiTourService) { +Application.Controllers.controller('StatisticsController', ['$scope', '$state', '$rootScope', '$uibModal', 'es', 'Member', '_t', 'membersPromise', 'statisticsPromise', 'uiTourService', 'settingsPromise', + function ($scope, $state, $rootScope, $uibModal, es, Member, _t, membersPromise, statisticsPromise, uiTourService, settingsPromise) { /* PRIVATE STATIC CONSTANTS */ // search window size @@ -179,9 +179,9 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state', */ $scope.hiddenTab = function (tab) { if (tab.table) { - if ((tab.es_type_key === 'subscription') && $rootScope.fablabWithoutPlans) { + if ((tab.es_type_key === 'subscription') && !$rootScope.modules.plans) { return true; - } else return (tab.es_type_key === 'space') && $rootScope.fablabWithoutSpaces; + } else return (tab.es_type_key === 'space') && !$rootScope.modules.spaces; } else { return true; } @@ -388,7 +388,7 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state', } }); // if the user has never seen the tour, show him now - if (Fablab.featureTourDisplay !== 'manual' && $scope.currentUser.profile.tours.indexOf('statistics') < 0) { + if (settingsPromise.feature_tour_display !== 'manual' && $scope.currentUser.profile.tours.indexOf('statistics') < 0) { uitour.start(); } } diff --git a/app/assets/javascripts/controllers/admin/trainings.js.erb b/app/assets/javascripts/controllers/admin/trainings.js.erb index b0d90551e..5bc65349d 100644 --- a/app/assets/javascripts/controllers/admin/trainings.js.erb +++ b/app/assets/javascripts/controllers/admin/trainings.js.erb @@ -150,8 +150,8 @@ Application.Controllers.controller('EditTrainingController', [ '$scope', '$state /** * 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', 'Member', 'uiTourService', - function ($scope, $state, $uibModal, Training, trainingsPromise, machinesPromise, _t, growl, dialogs, Member, uiTourService) { +Application.Controllers.controller('TrainingsAdminController', ['$scope', '$state', '$uibModal', 'Training', 'trainingsPromise', 'machinesPromise', '_t', 'growl', 'dialogs', 'Member', 'uiTourService', 'settingsPromise', + function ($scope, $state, $uibModal, Training, trainingsPromise, machinesPromise, _t, growl, dialogs, Member, uiTourService, settingsPromise) { // list of trainings $scope.trainings = trainingsPromise; @@ -398,7 +398,7 @@ Application.Controllers.controller('TrainingsAdminController', ['$scope', '$stat } }); // if the user has never seen the tour, show him now - if (Fablab.featureTourDisplay !== 'manual' && $scope.currentUser.profile.tours.indexOf('trainings') < 0) { + if (settingsPromise.feature_tour_display !== 'manual' && $scope.currentUser.profile.tours.indexOf('trainings') < 0) { uitour.start(); } } diff --git a/app/assets/javascripts/controllers/application.js.erb b/app/assets/javascripts/controllers/application.js.erb index ea478fb65..206e74a09 100644 --- a/app/assets/javascripts/controllers/application.js.erb +++ b/app/assets/javascripts/controllers/application.js.erb @@ -91,7 +91,10 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco return $uibModal.open({ templateUrl: '<%= asset_path "shared/signupModal.html" %>', size: 'md', - controller: ['$scope', '$uibModalInstance', 'Group', 'CustomAsset', 'growl', '_t', function ($scope, $uibModalInstance, Group, CustomAsset, growl, _t) { + resolve: { + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['phone_required', 'recaptcha_site_key', 'confirmation_required']" }).$promise; }] + }, + controller: ['$scope', '$uibModalInstance', 'Group', 'CustomAsset', 'settingsPromise', 'growl', '_t', function ($scope, $uibModalInstance, Group, CustomAsset, settingsPromise, growl, _t) { // default parameters for the date picker in the account creation modal $scope.datePicker = { format: Fablab.uibDateFormat, @@ -101,8 +104,11 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco } }; + // is the phone number required to sign-up? + $scope.phoneRequired = (settingsPromise.phone_required === 'true'); + // reCaptcha v2 site key (or undefined) - $scope.recaptchaSiteKey = Fablab.recaptchaSiteKey; + $scope.recaptchaSiteKey = settingsPromise.recaptcha_site_key; // callback to open the date picker (account creation modal) $scope.openDatePicker = function ($event) { @@ -147,7 +153,7 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco return Auth.register($scope.user).then(function (user) { if (user.id) { // creation successful - $uibModalInstance.close(user); + $uibModalInstance.close({ user, settings: settingsPromise }); } else { // the user was not saved in database, something wrong occurred growl.error(_t('app.public.common.unexpected_error_occurred')); @@ -168,13 +174,13 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco }); }; }] - }).result['finally'](null).then(function (user) { + }).result['finally'](null).then(function (res) { // when the account was created successfully, set the session to the newly created account - if(Fablab.userConfirmationNeededToSignIn) { + if(res.settings.confirmation_required) { Auth._currentUser = null; growl.info(_t('app.public.common.you_will_receive_confirmation_instructions_by_email_detailed')); } else { - $scope.setCurrentUser(user); + $scope.setCurrentUser(res.user); } }); <% end %> @@ -403,8 +409,15 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco return $uibModal.open({ templateUrl: '<%= asset_path "shared/deviseModal.html" %>', size: 'sm', - controller: ['$scope', '$uibModalInstance', '_t', function ($scope, $uibModalInstance, _t) { + resolve: { + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['confirmation_required']" }).$promise; }] + }, + controller: ['$scope', '$uibModalInstance', '_t', 'settingsPromise', function ($scope, $uibModalInstance, _t, settingsPromise) { const user = ($scope.user = {}); + + // email confirmation required before user sign-in? + $scope.confirmationRequired = settingsPromise.confirmation_required; + $scope.login = function () { Auth.login(user).then(function (user) { // Authentication succeeded ... @@ -435,7 +448,7 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco return $uibModalInstance.dismiss('confirmationNew'); }; - return $scope.openResetPassword = function (e) { + $scope.openResetPassword = function (e) { e.preventDefault(); return $uibModalInstance.dismiss('resetPassword'); }; @@ -450,9 +463,9 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco return $state.go(toState, toParams); } }, function (reason) { - // authentication did not ended successfully + // authentication did not end successfully if (reason === 'signup') { - // open signup modal + // open sign-up modal $scope.signup(); } else if (reason === 'resetPassword') { // open the 'reset password' modal diff --git a/app/assets/javascripts/controllers/cookies.js b/app/assets/javascripts/controllers/cookies.js index 228cbefd5..300818dba 100644 --- a/app/assets/javascripts/controllers/cookies.js +++ b/app/assets/javascripts/controllers/cookies.js @@ -32,7 +32,7 @@ Application.Controllers.controller('CookiesController', ['$scope', '$cookies', ' m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); - ga('create', Fablab.gaId, 'auto'); + ga('create', Fablab.trackingId, 'auto'); ga('send', 'pageview'); /* eslint-enable */ }; @@ -50,8 +50,8 @@ Application.Controllers.controller('CookiesController', ['$scope', '$cookies', ' $scope.learnMoreUrl = '#!/privacy-policy'; } }); - // if the GA_ID environment variable was not set, only functional cookies will be set, so user consent is not required - if (!Fablab.gaId) $scope.cookiesState = 'ignore'; + // if the tracking ID was not set in the settings, only functional cookies will be set, so user consent is not required + if (!Fablab.trackingId) $scope.cookiesState = 'ignore'; }; const readCookie = function () { diff --git a/app/assets/javascripts/controllers/events.js.erb b/app/assets/javascripts/controllers/events.js.erb index 40768ee4b..42c140a05 100644 --- a/app/assets/javascripts/controllers/events.js.erb +++ b/app/assets/javascripts/controllers/events.js.erb @@ -13,8 +13,8 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -Application.Controllers.controller('EventsController', ['$scope', '$state', 'Event', 'categoriesPromise', 'themesPromise', 'ageRangesPromise', - function ($scope, $state, Event, categoriesPromise, themesPromise, ageRangesPromise) { +Application.Controllers.controller('EventsController', ['$scope', '$state', 'Event', 'categoriesPromise', 'themesPromise', 'ageRangesPromise', 'settingsPromise', + function ($scope, $state, Event, categoriesPromise, themesPromise, ageRangesPromise, settingsPromise) { /* PUBLIC SCOPE */ // The events displayed on the page @@ -305,7 +305,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' const amountToPay = helpers.getAmountToPay($scope.reserve.amountTotal, wallet.amount); if ((AuthService.isAuthorized(['member']) && amountToPay > 0) || (AuthService.isAuthorized('manager') && $scope.ctrl.member.id === $rootScope.currentUser.id && amountToPay > 0)) { - if ($rootScope.fablabWithoutOnlinePayment) { + if (settingsPromise.online_payment_module !== 'true') { growl.error(_t('app.public.events_show.online_payment_disabled')); } else { return payByStripe(reservation); @@ -698,10 +698,11 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' }, cartItems () { return mkRequestParams(reservation, $scope.coupon.applied); - } + }, + stripeKey: ['Setting', function (Setting) { return Setting.get({ name: 'stripe_public_key' }).$promise; }] }, - controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'cgv', 'Auth', 'Reservation', 'growl', 'wallet', 'helpers', '$filter', 'coupon', 'cartItems', - function ($scope, $uibModalInstance, $state, reservation, price, cgv, Auth, Reservation, growl, wallet, helpers, $filter, coupon, cartItems) { + controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'cgv', 'Auth', 'Reservation', 'growl', 'wallet', 'helpers', '$filter', 'coupon', 'cartItems', 'stripeKey', + function ($scope, $uibModalInstance, $state, reservation, price, cgv, Auth, Reservation, growl, wallet, helpers, $filter, coupon, cartItems, stripeKey) { // User's wallet amount $scope.walletAmount = wallet.amount; @@ -720,8 +721,11 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' // Used in wallet info template to interpolate some translations $scope.numberFilter = $filter('number'); + // stripe publishable key + $scope.stripeKey = stripeKey.setting.value; + // Callback to handle the post-payment and reservation - return $scope.onPaymentSuccess = function (reservation) { + $scope.onPaymentSuccess = function (reservation) { $uibModalInstance.close(reservation); }; } @@ -859,6 +863,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' controller: 'ReserveSlotSameTimeController', resolve: { sameTimeReservations: function() { return sameTimeReservations; }, + bookOverlappingSlotsPromise: ['Setting', function (Setting) { return Setting.get({ name: 'book_overlapping_slots' }).$promise; }] } }); modalInstance.result.then(callback); diff --git a/app/assets/javascripts/controllers/home.js b/app/assets/javascripts/controllers/home.js index ca681fc40..dcf534273 100644 --- a/app/assets/javascripts/controllers/home.js +++ b/app/assets/javascripts/controllers/home.js @@ -1,7 +1,7 @@ 'use strict'; -Application.Controllers.controller('HomeController', ['$scope', '$stateParams', '$translatePartialLoader', 'AuthService', 'settingsPromise', 'Member', 'uiTourService', '_t', 'Help', - function ($scope, $stateParams, $translatePartialLoader, AuthService, settingsPromise, Member, uiTourService, _t, Help) { +Application.Controllers.controller('HomeController', ['$scope', '$stateParams', '$translatePartialLoader', 'AuthService', 'settingsPromise', 'Member', 'uiTourService', '_t', + function ($scope, $stateParams, $translatePartialLoader, AuthService, settingsPromise, Member, uiTourService, _t) { /* PUBLIC SCOPE */ // Home page HTML content @@ -140,7 +140,7 @@ Application.Controllers.controller('HomeController', ['$scope', '$stateParams', content: _t('app.public.tour.welcome.machines.content'), placement: 'right' }); - if (!Fablab.withoutSpaces) { + if ($scope.modules.spaces) { uitour.createStep({ selector: '.nav-primary li.reserve-space-link', stepId: 'spaces', @@ -303,7 +303,7 @@ Application.Controllers.controller('HomeController', ['$scope', '$stateParams', } }); // if the user has never seen the tour, show him now - if (Fablab.featureTourDisplay !== 'manual' && $scope.currentUser.profile.tours.indexOf('welcome') < 0) { + if (settingsPromise.feature_tour_display !== 'manual' && $scope.currentUser.profile.tours.indexOf('welcome') < 0) { uitour.start(); } }; diff --git a/app/assets/javascripts/controllers/machines.js.erb b/app/assets/javascripts/controllers/machines.js.erb index 3f807dfaa..831d891c1 100644 --- a/app/assets/javascripts/controllers/machines.js.erb +++ b/app/assets/javascripts/controllers/machines.js.erb @@ -180,8 +180,8 @@ const _reserveMachine = function (machine, e) { /** * Controller used in the public listing page, allowing everyone to see the list of machines */ -Application.Controllers.controller('MachinesController', ['$scope', '$state', '_t', 'AuthService', 'Machine', '$uibModal', 'machinesPromise', 'Member', 'uiTourService', - function ($scope, $state, _t, AuthService, Machine, $uibModal, machinesPromise, Member, uiTourService) { +Application.Controllers.controller('MachinesController', ['$scope', '$state', '_t', 'AuthService', 'Machine', '$uibModal', 'machinesPromise', 'settingsPromise', 'Member', 'uiTourService', + function ($scope, $state, _t, AuthService, Machine, $uibModal, machinesPromise, settingsPromise, Member, uiTourService) { /* PUBLIC SCOPE */ // Retrieve the list of machines @@ -281,7 +281,7 @@ Application.Controllers.controller('MachinesController', ['$scope', '$state', '_ } }); // if the user has never seen the tour, show him now - if (Fablab.featureTourDisplay !== 'manual' && $scope.currentUser.profile.tours.indexOf('machines') < 0) { + if (settingsPromise.feature_tour_display !== 'manual' && $scope.currentUser.profile.tours.indexOf('machines') < 0) { uitour.start(); } } diff --git a/app/assets/javascripts/controllers/main_nav.js b/app/assets/javascripts/controllers/main_nav.js index 88ee97899..73f2e3806 100644 --- a/app/assets/javascripts/controllers/main_nav.js +++ b/app/assets/javascripts/controllers/main_nav.js @@ -58,7 +58,7 @@ Application.Controllers.controller('MainNavController', ['$scope', function ($sc ]; - if (!Fablab.withoutPlans) { + if ($scope.modules.plans) { $scope.navLinks.push({ state: 'app.public.plans', linkText: 'app.public.common.subscriptions', @@ -67,7 +67,7 @@ Application.Controllers.controller('MainNavController', ['$scope', function ($sc }); } - if (!Fablab.withoutSpaces) { + if ($scope.modules.spaces) { $scope.navLinks.splice(4, 0, { state: 'app.public.spaces_list', linkText: 'app.public.common.reserve_a_space', @@ -138,8 +138,8 @@ Application.Controllers.controller('MainNavController', ['$scope', function ($sc authorizedRoles: ['admin'] }, { - state: 'app.admin.project_elements', - linkText: 'app.public.common.manage_the_projects_elements', + state: 'app.admin.projects', + linkText: 'app.public.common.projects', linkIcon: 'tasks', authorizedRoles: ['admin'] }, @@ -153,7 +153,7 @@ Application.Controllers.controller('MainNavController', ['$scope', function ($sc $scope.adminNavLinks = adminNavLinks; - if (!Fablab.withoutSpaces) { + if ($scope.modules.spaces) { return $scope.adminNavLinks.splice(3, 0, { state: 'app.public.spaces_list', linkText: 'app.public.common.manage_the_spaces', diff --git a/app/assets/javascripts/controllers/members.js b/app/assets/javascripts/controllers/members.js index 92a0232e6..2b23102ee 100644 --- a/app/assets/javascripts/controllers/members.js +++ b/app/assets/javascripts/controllers/members.js @@ -72,8 +72,8 @@ Application.Controllers.controller('MembersController', ['$scope', 'Member', 'me /** * Controller used when editing the current user's profile (in dashboard) */ -Application.Controllers.controller('EditProfileController', ['$scope', '$rootScope', '$state', '$window', '$sce', '$cookies', '$injector', 'Member', 'Auth', 'Session', 'activeProviderPromise', 'growl', 'dialogs', 'CSRF', 'memberPromise', 'groups', '_t', - function ($scope, $rootScope, $state, $window, $sce, $cookies, $injector, Member, Auth, Session, activeProviderPromise, growl, dialogs, CSRF, memberPromise, groups, _t) { +Application.Controllers.controller('EditProfileController', ['$scope', '$rootScope', '$state', '$window', '$sce', '$cookies', '$injector', 'Member', 'Auth', 'Session', 'activeProviderPromise', 'phoneRequiredPromise', 'growl', 'dialogs', 'CSRF', 'memberPromise', 'groups', '_t', + function ($scope, $rootScope, $state, $window, $sce, $cookies, $injector, Member, Auth, Session, activeProviderPromise, phoneRequiredPromise, growl, dialogs, CSRF, memberPromise, groups, _t) { /* PUBLIC SCOPE */ // API URL where the form will be posted @@ -110,6 +110,9 @@ Application.Controllers.controller('EditProfileController', ['$scope', '$rootSco // Should the passord be modified? $scope.password = { change: false }; + // is the phone number required in _member_form? + $scope.phoneRequired = (phoneRequiredPromise.setting.value === 'true'); + // Angular-Bootstrap datepicker configuration for birthday $scope.datePicker = { format: Fablab.uibDateFormat, diff --git a/app/assets/javascripts/controllers/plans.js.erb b/app/assets/javascripts/controllers/plans.js.erb index 19fbc30cf..2033d2ad4 100644 --- a/app/assets/javascripts/controllers/plans.js.erb +++ b/app/assets/javascripts/controllers/plans.js.erb @@ -12,8 +12,8 @@ */ 'use strict'; -Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScope', '$state', '$uibModal', 'Auth', 'AuthService', 'dialogs', 'growl', 'plansPromise', 'groupsPromise', 'Subscription', 'Member', 'subscriptionExplicationsPromise', '_t', 'Wallet', 'helpers', - function ($scope, $rootScope, $state, $uibModal, Auth, AuthService, dialogs, growl, plansPromise, groupsPromise, Subscription, Member, subscriptionExplicationsPromise, _t, Wallet, helpers) { +Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScope', '$state', '$uibModal', 'Auth', 'AuthService', 'dialogs', 'growl', 'plansPromise', 'groupsPromise', 'Subscription', 'Member', 'subscriptionExplicationsPromise', '_t', 'Wallet', 'helpers', 'settingsPromise', + function ($scope, $rootScope, $state, $uibModal, Auth, AuthService, dialogs, growl, plansPromise, groupsPromise, Subscription, Member, subscriptionExplicationsPromise, _t, Wallet, helpers, settingsPromise) { /* PUBLIC SCOPE */ // list of groups @@ -92,7 +92,7 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop const amountToPay = helpers.getAmountToPay($scope.cart.total, wallet.amount); if ((AuthService.isAuthorized('member') && amountToPay > 0) || (AuthService.isAuthorized('manager') && $scope.ctrl.member.id === $rootScope.currentUser.id && amountToPay > 0)) { - if ($rootScope.fablabWithoutOnlinePayment) { + if (settingsPromise.online_payment_module !== 'true') { growl.error(_t('app.public.plans.online_payment_disabled')); } else { return payByStripe(); @@ -244,10 +244,11 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop wallet () { return Wallet.getWalletByUser({ user_id: $scope.ctrl.member.id }).$promise; }, - coupon () { return $scope.coupon.applied; } + coupon () { return $scope.coupon.applied; }, + stripeKey: ['Setting', function (Setting) { return Setting.get({ name: 'stripe_public_key' }).$promise; }] }, - controller: ['$scope', '$uibModalInstance', '$state', 'selectedPlan', 'member', 'price', 'Subscription', 'CustomAsset', 'wallet', 'helpers', '$filter', 'coupon', - function ($scope, $uibModalInstance, $state, selectedPlan, member, price, Subscription, CustomAsset, wallet, helpers, $filter, coupon) { + controller: ['$scope', '$uibModalInstance', '$state', 'selectedPlan', 'member', 'price', 'Subscription', 'CustomAsset', 'wallet', 'helpers', '$filter', 'coupon', 'stripeKey', + function ($scope, $uibModalInstance, $state, selectedPlan, member, price, Subscription, CustomAsset, wallet, helpers, $filter, coupon, stripeKey) { // User's wallet amount $scope.walletAmount = wallet.amount; @@ -268,6 +269,9 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop } }; + // stripe publishable key + $scope.stripeKey = stripeKey.setting.value; + // retrieve the CGV CustomAsset.get({ name: 'cgv-file' }, function (cgv) { $scope.cgv = cgv.custom_asset; }); diff --git a/app/assets/javascripts/controllers/profile.js.erb b/app/assets/javascripts/controllers/profile.js.erb index 0ae5e79ce..df7f6ea78 100644 --- a/app/assets/javascripts/controllers/profile.js.erb +++ b/app/assets/javascripts/controllers/profile.js.erb @@ -13,8 +13,8 @@ 'use strict'; -Application.Controllers.controller('CompleteProfileController', ['$scope', '$rootScope', '$state', '$window', '_t', 'growl', 'CSRF', 'Auth', 'Member', 'settingsPromise', 'activeProviderPromise', 'groupsPromise', 'cguFile', 'memberPromise', 'Session', 'dialogs', 'AuthProvider', - function ($scope, $rootScope, $state, $window, _t, growl, CSRF, Auth, Member, settingsPromise, activeProviderPromise, groupsPromise, cguFile, memberPromise, Session, dialogs, AuthProvider) { +Application.Controllers.controller('CompleteProfileController', ['$scope', '$rootScope', '$state', '$window', '_t', 'growl', 'CSRF', 'Auth', 'Member', 'settingsPromise', 'activeProviderPromise', 'groupsPromise', 'cguFile', 'memberPromise', 'Session', 'dialogs', 'AuthProvider', 'phoneRequiredPromise', + function ($scope, $rootScope, $state, $window, _t, growl, CSRF, Auth, Member, settingsPromise, activeProviderPromise, groupsPromise, cguFile, memberPromise, Session, dialogs, AuthProvider, phoneRequiredPromise) { /* PUBLIC SCOPE */ // API URL where the form will be posted @@ -47,6 +47,9 @@ Application.Controllers.controller('CompleteProfileController', ['$scope', '$roo // CGU $scope.cgu = cguFile.custom_asset; + // is the phone number required in _member_form? + $scope.phoneRequired = (phoneRequiredPromise.setting.value === 'true'); + // Angular-Bootstrap datepicker configuration for birthday $scope.datePicker = { format: Fablab.uibDateFormat, diff --git a/app/assets/javascripts/controllers/projects.js.erb b/app/assets/javascripts/controllers/projects.js.erb index a8c2f5481..bd0b2c3ff 100644 --- a/app/assets/javascripts/controllers/projects.js.erb +++ b/app/assets/javascripts/controllers/projects.js.erb @@ -87,7 +87,7 @@ class ProjectsController { $scope.totalSteps = $scope.project.project_steps_attributes.length; // List of extensions allowed for CAD attachements upload - $scope.allowedExtensions = allowedExtensions; + $scope.allowedExtensions = allowedExtensions.setting.value.split(' '); /** * For use with ngUpload (https://github.com/twilson63/ngUpload). @@ -266,8 +266,8 @@ class ProjectsController { /** * Controller used on projects listing page */ -Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'Project', 'machinesPromise', 'themesPromise', 'componentsPromise', 'paginationService', 'OpenlabProject', '$window', 'growl', '_t', '$location', '$timeout', - function ($scope, $state, Project, machinesPromise, themesPromise, componentsPromise, paginationService, OpenlabProject, $window, growl, _t, $location, $timeout) { +Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'Project', 'machinesPromise', 'themesPromise', 'componentsPromise', 'paginationService', 'OpenlabProject', '$window', 'growl', '_t', '$location', '$timeout', 'settingsPromise', 'openLabActive', + function ($scope, $state, Project, machinesPromise, themesPromise, componentsPromise, paginationService, OpenlabProject, $window, growl, _t, $location, $timeout, settingsPromise, openLabActive) { /* PRIVATE STATIC CONSTANTS */ // Number of projects added to the page when the user clicks on 'load more projects' @@ -277,11 +277,11 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P /* PUBLIC SCOPE */ // Fab-manager's instance ID in the openLab network - $scope.openlabAppId = Fablab.openlabAppId; + $scope.openlabAppId = settingsPromise.openlab_app_id // Is openLab enabled on the instance? $scope.openlab = { - projectsActive: Fablab.openlabProjectsActive, + projectsActive: openLabActive.isPresent, searchOverWholeNetwork: false }; @@ -390,32 +390,32 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P if ($location.$$search.whole_network === 'f') { $scope.openlab.searchOverWholeNetwork = false; } else { - $scope.openlab.searchOverWholeNetwork = ($scope.openlab.projectsActive && Fablab.openlabDefault) || false; + $scope.openlab.searchOverWholeNetwork = ($scope.openlab.projectsActive && settingsPromise.openlab_default === 'true') || false; } return $scope.triggerSearch(); }; /** - * function to update url query param, little hack to turn off reloadOnSearch and re-enable it after setting the params + * function to update url query param, little hack to turn off reloadOnSearch and re-enable it after we set the params. * params example: 'q' , 'presse-purée' */ - var updateUrlParam = function (name, value) { + const updateUrlParam = function (name, value) { $state.current.reloadOnSearch = false; $location.search(name, value); return $timeout(function () { $state.current.reloadOnSearch = undefined; }); }; - var loadMoreCallback = function (projectsPromise) { + const loadMoreCallback = function (projectsPromise) { $scope.projects = $scope.projects.concat(projectsPromise.projects); return updateUrlParam('page', $scope.projectsPagination.currentPage); }; - var loadMoreOpenlabCallback = function (projectsPromise) { + const loadMoreOpenlabCallback = function (projectsPromise) { $scope.projects = $scope.projects.concat(normalizeProjectsAttrs(projectsPromise.projects)); return updateUrlParam('page', $scope.projectsPagination.currentPage); }; - var normalizeProjectsAttrs = function (projects) { + const normalizeProjectsAttrs = function (projects) { return projects.map(function (project) { project.project_image = project.image_url; return project; @@ -501,14 +501,14 @@ Application.Controllers.controller('EditProjectController', ['$rootScope', '$sco /** * Controller used in the public project's details page */ -Application.Controllers.controller('ShowProjectController', ['$scope', '$state', 'projectPromise', '$location', '$uibModal', 'dialogs', '_t', - function ($scope, $state, projectPromise, $location, $uibModal, dialogs, _t) { +Application.Controllers.controller('ShowProjectController', ['$scope', '$state', 'projectPromise', 'shortnamePromise', '$location', '$uibModal', 'dialogs', '_t', + function ($scope, $state, projectPromise, shortnamePromise, $location, $uibModal, dialogs, _t) { /* PUBLIC SCOPE */ // Store the project's details $scope.project = projectPromise; $scope.projectUrl = $location.absUrl(); - $scope.disqusShortname = Fablab.disqusShortname; + $scope.disqusShortname = shortnamePromise.setting.value; /** * Test if the provided user has the edition rights on the current project diff --git a/app/assets/javascripts/controllers/spaces.js.erb b/app/assets/javascripts/controllers/spaces.js.erb index 861c62fb0..c59e5c9c1 100644 --- a/app/assets/javascripts/controllers/spaces.js.erb +++ b/app/assets/javascripts/controllers/spaces.js.erb @@ -98,8 +98,8 @@ class SpacesController { /** * Controller used in the public listing page, allowing everyone to see the list of spaces */ -Application.Controllers.controller('SpacesController', ['$scope', '$state', 'spacesPromise', 'AuthService', '_t', 'Member', 'uiTourService', - function ($scope, $state, spacesPromise, AuthService, _t, Member, uiTourService) { +Application.Controllers.controller('SpacesController', ['$scope', '$state', 'spacesPromise', 'AuthService', '_t', 'Member', 'uiTourService', 'settingsPromise', + function ($scope, $state, spacesPromise, AuthService, _t, Member, uiTourService, settingsPromise) { /* PUBLIC SCOPE */ // Retrieve the list of spaces @@ -193,7 +193,7 @@ Application.Controllers.controller('SpacesController', ['$scope', '$state', 'spa } }); // if the user has never seen the tour, show him now - if (Fablab.featureTourDisplay !== 'manual' && $scope.currentUser.profile.tours.indexOf('spaces') < 0) { + if (settingsPromise.feature_tour_display !== 'manual' && $scope.currentUser.profile.tours.indexOf('spaces') < 0) { uitour.start(); } } diff --git a/app/assets/javascripts/directives/cart.js.erb b/app/assets/javascripts/directives/cart.js.erb index 35fb70539..f3085bb84 100644 --- a/app/assets/javascripts/directives/cart.js.erb +++ b/app/assets/javascripts/directives/cart.js.erb @@ -361,7 +361,8 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', size: 'md', controller: 'ReserveSlotSameTimeController', resolve: { - sameTimeReservations: function() { return sameTimeReservations; } + sameTimeReservations: function() { return sameTimeReservations; }, + bookOverlappingSlotsPromise: ['Setting', function (Setting) { return Setting.get({ name: 'book_overlapping_slots' }).$promise; }] } }); modalInstance.result.then(function(res) { @@ -631,10 +632,11 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', }, cartItems () { return mkRequestParams(reservation, $scope.coupon.applied); - } + }, + stripeKey: ['Setting', function (Setting) { return Setting.get({ name: 'stripe_public_key' }).$promise; }] }, - controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'cgv', 'Auth', 'Reservation', 'wallet', 'helpers', '$filter', 'coupon', 'cartItems', - function ($scope, $uibModalInstance, $state, reservation, price, cgv, Auth, Reservation, wallet, helpers, $filter, coupon, cartItems) { + controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'cgv', 'Auth', 'Reservation', 'wallet', 'helpers', '$filter', 'coupon', 'cartItems', 'stripeKey', + function ($scope, $uibModalInstance, $state, reservation, price, cgv, Auth, Reservation, wallet, helpers, $filter, coupon, cartItems, stripeKey) { // user wallet amount $scope.walletAmount = wallet.amount; @@ -653,6 +655,9 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', // Used in wallet info template to interpolate some translations $scope.numberFilter = $filter('number'); + // stripe publishable key + $scope.stripeKey = stripeKey.setting.value; + /** * Callback to handle the post-payment and reservation */ @@ -758,7 +763,7 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', const amountToPay = helpers.getAmountToPay($scope.amountTotal, wallet.amount); if ((AuthService.isAuthorized(['member']) && amountToPay > 0) || (AuthService.isAuthorized('manager') && $scope.user.id === $rootScope.currentUser.id && amountToPay > 0)) { - if ($rootScope.fablabWithoutOnlinePayment) { + if ($scope.settings.online_payment_module !== 'true') { growl.error(_t('app.shared.cart.online_payment_disabled')); } else { return payByStripe(reservation); @@ -783,10 +788,10 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', /** * Controller of the modal showing the reservations the same date at the same time */ -Application.Controllers.controller('ReserveSlotSameTimeController', ['$scope', '$uibModalInstance', 'AuthService', 'sameTimeReservations', - function ($scope, $uibModalInstance, AuthService, sameTimeReservations) { +Application.Controllers.controller('ReserveSlotSameTimeController', ['$scope', '$uibModalInstance', 'AuthService', 'sameTimeReservations', 'bookOverlappingSlotsPromise', + function ($scope, $uibModalInstance, AuthService, sameTimeReservations, bookOverlappingSlotsPromise) { $scope.sameTimeReservations = sameTimeReservations; - $scope.bookSlotAtSameTime = Fablab.bookSlotAtSameTime; + $scope.bookSlotAtSameTime = (bookOverlappingSlotsPromise.setting.value === 'true'); $scope.isAuthorized = AuthService.isAuthorized; /** * Confirmation callback diff --git a/app/assets/javascripts/directives/events.js.erb b/app/assets/javascripts/directives/home/events.js.erb similarity index 100% rename from app/assets/javascripts/directives/events.js.erb rename to app/assets/javascripts/directives/home/events.js.erb diff --git a/app/assets/javascripts/directives/news.js.erb b/app/assets/javascripts/directives/home/news.js.erb similarity index 100% rename from app/assets/javascripts/directives/news.js.erb rename to app/assets/javascripts/directives/home/news.js.erb diff --git a/app/assets/javascripts/directives/projects.js.erb b/app/assets/javascripts/directives/home/projects.js.erb similarity index 100% rename from app/assets/javascripts/directives/projects.js.erb rename to app/assets/javascripts/directives/home/projects.js.erb diff --git a/app/assets/javascripts/directives/twitter.js.erb b/app/assets/javascripts/directives/home/twitter.js.erb similarity index 100% rename from app/assets/javascripts/directives/twitter.js.erb rename to app/assets/javascripts/directives/home/twitter.js.erb diff --git a/app/assets/javascripts/directives/settings/boolean-setting.js.erb b/app/assets/javascripts/directives/settings/boolean-setting.js.erb new file mode 100644 index 000000000..942d9d8ab --- /dev/null +++ b/app/assets/javascripts/directives/settings/boolean-setting.js.erb @@ -0,0 +1,89 @@ +Application.Directives.directive('booleanSetting', ['Setting', 'growl', '_t', + function (Setting, growl, _t) { + return ({ + restrict: 'E', + scope: { + name: '@', + label: '@', + settings: '=', + yesLabel: '@', + noLabel: '@', + classes: '@', + onBeforeSave: '=' + }, + templateUrl: '<%= asset_path "admin/settings/boolean.html" %>', + link ($scope, element, attributes) { + // The setting + $scope.setting = { + name: $scope.name, + value: ($scope.settings[$scope.name] === 'true') + }; + + // default values for the switch labels + $scope.yesLabel = $scope.yesLabel || 'app.admin.settings.enabled'; + $scope.noLabel = $scope.noLabel || 'app.admin.settings.disabled'; + + /** + * Callback to save the setting value to the database + * @param setting {{value:*, name:string}} note that the value will be stringified + */ + $scope.save = function (setting) { + if (typeof $scope.onBeforeSave === 'function') { + const res = $scope.onBeforeSave(setting); + if (res && _.isFunction(res.then)) { + // res is a promise, wait for it before proceed + res.then(function (success) { + if (success) saveValue(setting); + else resetValue(); + }, function () { + resetValue(); + }); + } else { + if (res) saveValue(setting); + else resetValue(); + } + } else { + saveValue(setting); + } + }; + + /* PRIVATE SCOPE */ + + /** + * Save the setting's new value in DB + * @param setting + */ + const saveValue = function (setting) { + const value = setting.value.toString(); + + Setting.update( + { name: setting.name }, + { value }, + function () { + growl.success(_t('app.admin.settings.customization_of_SETTING_successfully_saved', { SETTING: _t(`app.admin.settings.${setting.name}`) })); + $scope.settings[$scope.name] = value; + }, + function (error) { + if (error.status === 304) return; + + if (error.status === 423) { + growl.error(_t('app.admin.settings.error_SETTING_locked', { SETTING: _t(`app.admin.settings.${setting.name}`) })); + return; + } + + growl.error(_t('app.admin.settings.an_error_occurred_saving_the_setting')); + console.log(error); + } + ); + } + + /** + * Reset the value of the setting to its original state (when the component loads) + */ + const resetValue = function () { + $scope.setting.value = $scope.settings[$scope.name] === 'true'; + } + } + }); + } +]); diff --git a/app/assets/javascripts/directives/settings/number-setting.js.erb b/app/assets/javascripts/directives/settings/number-setting.js.erb new file mode 100644 index 000000000..0c5940bce --- /dev/null +++ b/app/assets/javascripts/directives/settings/number-setting.js.erb @@ -0,0 +1,58 @@ +Application.Directives.directive('numberSetting', ['Setting', 'growl', '_t', + function (Setting, growl, _t) { + return ({ + restrict: 'E', + scope: { + name: '@', + label: '@', + settings: '=', + classes: '@', + faIcon: '@', + helperText: '@', + min: '@', + required: '<' + }, + templateUrl: '<%= asset_path "admin/settings/number.html" %>', + link ($scope, element, attributes) { + // The setting + $scope.setting = { + name: $scope.name, + value: parseInt($scope.settings[$scope.name], 10) + }; + + /** + * Callback to save the setting value to the database + * @param setting {{value:*, name:string}} note that the value will be stringified + */ + $scope.save = function (setting) { + let value; + if (typeof setting.value === 'number') { + value = setting.value.toString(); + } else { + ({ value } = setting); + } + + Setting.update( + { name: setting.name }, + { value }, + function () { + growl.success(_t('app.admin.settings.customization_of_SETTING_successfully_saved', { SETTING: _t(`app.admin.settings.${setting.name}`) })); + $scope.settings[$scope.name] = value; + }, + function (error) { + if (error.status === 304) return; + + if (error.status === 423) { + growl.error(_t('app.admin.settings.error_SETTING_locked', { SETTING: _t(`app.admin.settings.${setting.name}`) })); + return; + } + + growl.error(_t('app.admin.settings.an_error_occurred_saving_the_setting')); + console.log(error); + } + ); + }; + } + }); + } +]); diff --git a/app/assets/javascripts/directives/settings/select-multiple-setting.js.erb b/app/assets/javascripts/directives/settings/select-multiple-setting.js.erb new file mode 100644 index 000000000..b28b0a89f --- /dev/null +++ b/app/assets/javascripts/directives/settings/select-multiple-setting.js.erb @@ -0,0 +1,103 @@ +Application.Directives.directive('selectMultipleSetting', ['Setting', 'growl', '_t', '$uibModal', + function (Setting, growl, _t, $uibModal) { + return ({ + restrict: 'E', + scope: { + name: '@', + label: '@', + settings: '=', + classes: '@', + required: '<', + titleNew: '@', + descriptionNew: '@', + beforeAdd: '=' + }, + templateUrl: '<%= asset_path "admin/settings/select-multiple.html" %>', + link ($scope, element, attributes) { + // The setting + $scope.setting = { + name: $scope.name, + value: $scope.settings[$scope.name] + }; + + // the options + $scope.options = $scope.settings[$scope.name].split(' '); + + // the selected options + $scope.selection = []; + + /** + * Remove the items in the selection from the options and update setting.value + */ + $scope.removeItem = function() { + const options = $scope.options.filter(function (opt) { + return $scope.selection.indexOf(opt) < 0; + }) + $scope.options = options; + $scope.setting.value = options.join(' '); + growl.success(_t('app.admin.settings.COUNT_items_removed', { COUNT: $scope.selection.length })); + $scope.selection = []; + } + + /** + * Open a modal dialog asking for the value of a new item to add + */ + $scope.addItem = function() { + $uibModal.open({ + templateUrl: 'newSelectOption.html', + resolve: { + titleNew: function () { return $scope.titleNew; }, + descriptionNew: function () { return $scope.descriptionNew; } + }, + controller: function ($scope, $uibModalInstance, titleNew, descriptionNew) { + $scope.value = undefined; + $scope.titleNew = titleNew; + $scope.descriptionNew = descriptionNew; + $scope.ok = function () { + $uibModalInstance.close($scope.value); + }; + $scope.dismiss = function () { + $uibModalInstance.dismiss('cancel'); + }; + } + }).result['finally'](null).then(function(val) { + const options = Array.from($scope.options); + if (typeof $scope.beforeAdd === 'function') { val = $scope.beforeAdd(val); } + options.push(val); + $scope.options = options; + $scope.setting.value = options.join(' '); + growl.success(_t('app.admin.settings.item_added')); + }); + } + + /** + * Callback to save the setting value to the database + * @param setting {{value:*, name:string}} note that the value will be stringified + */ + $scope.save = function (setting) { + let { value } = setting; + + Setting.update( + { name: setting.name }, + { value }, + function () { + growl.success(_t('app.admin.settings.customization_of_SETTING_successfully_saved', { SETTING: _t(`app.admin.settings.${setting.name}`) })); + $scope.settings[$scope.name] = value; + }, + function (error) { + if (error.status === 304) return; + + if (error.status === 423) { + growl.error(_t('app.admin.settings.error_SETTING_locked', { SETTING: _t(`app.admin.settings.${setting.name}`) })); + return; + } + + growl.error(_t('app.admin.settings.an_error_occurred_saving_the_setting')); + console.log(error); + } + ); + }; + } + }); + } +]); diff --git a/app/assets/javascripts/directives/settings/select-setting.js.erb b/app/assets/javascripts/directives/settings/select-setting.js.erb new file mode 100644 index 000000000..2c0d2ba91 --- /dev/null +++ b/app/assets/javascripts/directives/settings/select-setting.js.erb @@ -0,0 +1,55 @@ +Application.Directives.directive('selectSetting', ['Setting', 'growl', '_t', + function (Setting, growl, _t) { + return ({ + restrict: 'E', + scope: { + name: '@', + label: '@', + settings: '=', + classes: '@', + required: '<', + option1: '<', + option2: '<', + option3: '<', + option4: '<', + option5: '<' + }, + templateUrl: '<%= asset_path "admin/settings/select.html" %>', + link ($scope, element, attributes) { + // The setting + $scope.setting = { + name: $scope.name, + value: $scope.settings[$scope.name] + }; + + /** + * Callback to save the setting value to the database + * @param setting {{value:*, name:string}} note that the value will be stringified + */ + $scope.save = function (setting) { + let { value } = setting; + + Setting.update( + { name: setting.name }, + { value }, + function () { + growl.success(_t('app.admin.settings.customization_of_SETTING_successfully_saved', { SETTING: _t(`app.admin.settings.${setting.name}`) })); + $scope.settings[$scope.name] = value; + }, + function (error) { + if (error.status === 304) return; + + if (error.status === 423) { + growl.error(_t('app.admin.settings.error_SETTING_locked', { SETTING: _t(`app.admin.settings.${setting.name}`) })); + return; + } + + growl.error(_t('app.admin.settings.an_error_occurred_saving_the_setting')); + console.log(error); + } + ); + }; + } + }); + } +]); diff --git a/app/assets/javascripts/directives/settings/text-setting.js.erb b/app/assets/javascripts/directives/settings/text-setting.js.erb new file mode 100644 index 000000000..2631863f9 --- /dev/null +++ b/app/assets/javascripts/directives/settings/text-setting.js.erb @@ -0,0 +1,66 @@ +Application.Directives.directive('textSetting', ['Setting', 'growl', '_t', + function (Setting, growl, _t) { + return ({ + restrict: 'E', + scope: { + name: '@', + label: '@', + settings: '=', + classes: '@', + faIcon: '@', + placeholder: '@', + required: '<', + type: '@', + maxLength: '@', + minLength: '@', + readOnly: '<' + }, + templateUrl: '<%= asset_path "admin/settings/text.html" %>', + link ($scope, element, attributes) { + // if type is not specified, use text as default + if (typeof $scope.type === 'undefined') { + $scope.type = 'text'; + } + // The setting + $scope.setting = { + name: $scope.name, + value: $scope.settings[$scope.name] + }; + + $scope.$watch(`settings.${$scope.name}`, function (newValue, oldValue, scope) { + if (newValue !== oldValue) { + $scope.setting.value = newValue; + } + }); + + /** + * Callback to save the setting value to the database + * @param setting {{value:*, name:string}} note that the value will be stringified + */ + $scope.save = function (setting) { + let { value } = setting; + + Setting.update( + { name: setting.name }, + { value }, + function () { + growl.success(_t('app.admin.settings.customization_of_SETTING_successfully_saved', { SETTING: _t(`app.admin.settings.${setting.name}`) })); + $scope.settings[$scope.name] = value; + }, + function (error) { + if (error.status === 304) return; + + if (error.status === 423) { + growl.error(_t('app.admin.settings.error_SETTING_locked', { SETTING: _t(`app.admin.settings.${setting.name}`) })); + return; + } + + growl.error(_t('app.admin.settings.an_error_occurred_saving_the_setting')); + console.log(error); + } + ); + }; + } + }); + } +]); diff --git a/app/assets/javascripts/directives/stripe-form.js.erb b/app/assets/javascripts/directives/stripe-form.js.erb index 4f125e4b4..5aa8c7ee4 100644 --- a/app/assets/javascripts/directives/stripe-form.js.erb +++ b/app/assets/javascripts/directives/stripe-form.js.erb @@ -12,10 +12,11 @@ Application.Directives.directive('stripeForm', ['Payment', 'growl', '_t', restrict: 'A', scope: { cartItems: '=', - onPaymentSuccess: '=' + onPaymentSuccess: '=', + stripeKey: '@' }, link: function($scope, element, attributes) { - const stripe = Stripe('<%= Rails.application.secrets.stripe_publishable_key %>'); + const stripe = Stripe($scope.stripeKey); const elements = stripe.elements(); const style = { diff --git a/app/assets/javascripts/router.js.erb b/app/assets/javascripts/router.js.erb index b4d800d98..57b93692a 100644 --- a/app/assets/javascripts/router.js.erb +++ b/app/assets/javascripts/router.js.erb @@ -16,7 +16,7 @@ angular.module('application.router', ['ui.router']) // abstract root parents states // these states controls the access rights to the various routes inherited from them - return $stateProvider + $stateProvider .state('app', { abstract: true, views: { @@ -36,14 +36,21 @@ angular.module('application.router', ['ui.router']) resolve: { logoFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'logo-file' }).$promise; }], logoBlackFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'logo-black-file' }).$promise; }], - sharedTranslations: ['Translations', function (Translations) { return Translations.query(['app.shared', 'app.public.common']).$promise; }] + sharedTranslations: ['Translations', function (Translations) { return Translations.query(['app.shared', 'app.public.common']).$promise; }], + modulesPromise: ['Setting', function (Setting) { return Setting.query({ names: "['spaces_module', 'plans_module', 'invoicing_module', 'wallet_module']" }).$promise; }] }, - onEnter: ['$rootScope', 'logoFile', 'logoBlackFile', 'CSRF', function ($rootScope, logoFile, logoBlackFile, CSRF) { + onEnter: ['$rootScope', 'logoFile', 'logoBlackFile', 'modulesPromise', 'CSRF', function ($rootScope, logoFile, logoBlackFile, modulesPromise, CSRF) { // Retrieve Anti-CSRF tokens from cookies CSRF.setMetaTags(); // Application logo $rootScope.logo = logoFile.custom_asset; $rootScope.logoBlack = logoBlackFile.custom_asset; + $rootScope.modules = { + spaces: (modulesPromise.spaces_module === 'true'), + plans: (modulesPromise.plans_module === 'true'), + invoicing: (modulesPromise.invoicing_module === 'true'), + wallet: (modulesPromise.wallet_module === 'true'), + }; }] }) .state('app.public', { @@ -98,7 +105,7 @@ angular.module('application.router', ['ui.router']) } }, resolve: { - settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['home_content', 'home_blogpost']" }).$promise; }] + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['home_content', 'home_blogpost', 'spaces_module', 'feature_tour_display']" }).$promise; }] } }) .state('app.public.privacy', { @@ -126,6 +133,7 @@ angular.module('application.router', ['ui.router']) groupsPromise: ['Group', function (Group) { return Group.query().$promise; }], cguFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'cgu-file' }).$promise; }], memberPromise: ['Member', 'currentUser', function (Member, currentUser) { return Member.get({ id: currentUser.id }).$promise; }], + phoneRequiredPromise: ['Setting', function (Setting) { return Setting.get({ name: 'phone_required' }).$promise; }] } }) @@ -157,6 +165,7 @@ angular.module('application.router', ['ui.router']) resolve: { groups: ['Group', function (Group) { return Group.query().$promise; }], activeProviderPromise: ['AuthProvider', function (AuthProvider) { return AuthProvider.active().$promise; }], + phoneRequiredPromise: ['Setting', function (Setting) { return Setting.get({ name: 'phone_required' }).$promise; }] } }) .state('app.logged.dashboard.projects', { @@ -197,6 +206,7 @@ angular.module('application.router', ['ui.router']) }) .state('app.logged.dashboard.wallet', { url: '/wallet', + abstract: !Fablab.walletModule, views: { 'main@': { templateUrl: '<%= asset_path "dashboard/wallet.html" %>', @@ -247,7 +257,9 @@ angular.module('application.router', ['ui.router']) resolve: { themesPromise: ['Theme', function (Theme) { return Theme.query().$promise; }], componentsPromise: ['Component', function (Component) { return Component.query().$promise; }], - machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }] + machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }], + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['openlab_app_id', 'openlab_default']" }).$promise; }], + openLabActive: ['Setting', function (Setting) { return Setting.isPresent({ name: 'openlab_app_secret' }).$promise; }], } }) .state('app.logged.projects_new', { @@ -259,7 +271,7 @@ angular.module('application.router', ['ui.router']) } }, resolve: { - allowedExtensions: ['Project', function (Project) { return Project.allowedExtensions().$promise; }] + allowedExtensions: ['Setting', function (Setting) { return Setting.get({ name: 'allowed_cad_extensions' }).$promise; }] } }) .state('app.public.projects_show', { @@ -271,7 +283,8 @@ angular.module('application.router', ['ui.router']) } }, resolve: { - projectPromise: ['$stateParams', 'Project', function ($stateParams, Project) { return Project.get({ id: $stateParams.id }).$promise; }] + projectPromise: ['$stateParams', 'Project', function ($stateParams, Project) { return Project.get({ id: $stateParams.id }).$promise; }], + shortnamePromise: ['Setting', function (Setting) { return Setting.get({ name: 'disqus_shortname' }).$promise; }] } }) .state('app.logged.projects_edit', { @@ -284,7 +297,7 @@ angular.module('application.router', ['ui.router']) }, resolve: { projectPromise: ['$stateParams', 'Project', function ($stateParams, Project) { return Project.get({ id: $stateParams.id }).$promise; }], - allowedExtensions: ['Project', function (Project) { return Project.allowedExtensions().$promise; }] + allowedExtensions: ['Setting', function (Setting) { return Setting.get({ name: 'allowed_cad_extensions' }).$promise; }] } }) @@ -298,7 +311,8 @@ angular.module('application.router', ['ui.router']) } }, resolve: { - machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }] + machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }], + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['feature_tour_display']" }).$promise; }] } }) .state('app.admin.machines_new', { @@ -336,14 +350,9 @@ angular.module('application.router', ['ui.router']) machinePromise: ['Machine', '$stateParams', function (Machine, $stateParams) { return Machine.get({ id: $stateParams.id }).$promise; }], settingsPromise: ['Setting', function (Setting) { return Setting.query({ - names: `['machine_explications_alert', \ - 'booking_window_start', \ - 'booking_window_end', \ - 'booking_move_enable', \ - 'booking_move_delay', \ - 'booking_cancel_enable', \ - 'booking_cancel_delay', \ - 'subscription_explications_alert']` + names: `['machine_explications_alert', 'booking_window_start', 'booking_window_end', 'booking_move_enable', \ + 'booking_move_delay', 'booking_cancel_enable', 'booking_cancel_delay', 'subscription_explications_alert', \ + 'online_payment_module']` }).$promise; }] } @@ -364,7 +373,7 @@ angular.module('application.router', ['ui.router']) // spaces .state('app.public.spaces_list', { url: '/spaces', - abstract: Fablab.withoutSpaces, + abstract: !Fablab.spacesModule, views: { 'main@': { templateUrl: '<%= asset_path "spaces/index.html" %>', @@ -372,12 +381,13 @@ angular.module('application.router', ['ui.router']) } }, resolve: { - spacesPromise: ['Space', function (Space) { return Space.query().$promise; }] + spacesPromise: ['Space', function (Space) { return Space.query().$promise; }], + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['feature_tour_display']" }).$promise; }] } }) .state('app.admin.space_new', { url: '/spaces/new', - abstract: Fablab.withoutSpaces, + abstract: !Fablab.spacesModule, views: { 'main@': { templateUrl: '<%= asset_path "spaces/new.html" %>', @@ -387,7 +397,7 @@ angular.module('application.router', ['ui.router']) }) .state('app.public.space_show', { url: '/spaces/:id', - abstract: Fablab.withoutSpaces, + abstract: !Fablab.spacesModule, views: { 'main@': { templateUrl: '<%= asset_path "spaces/show.html" %>', @@ -400,7 +410,7 @@ angular.module('application.router', ['ui.router']) }) .state('app.admin.space_edit', { url: '/spaces/:id/edit', - abstract: Fablab.withoutSpaces, + abstract: !Fablab.spacesModule, views: { 'main@': { templateUrl: '<%= asset_path "spaces/edit.html" %>', @@ -413,7 +423,7 @@ angular.module('application.router', ['ui.router']) }) .state('app.logged.space_reserve', { url: '/spaces/:id/reserve', - abstract: Fablab.withoutSpaces, + abstract: !Fablab.spacesModule, views: { 'main@': { templateUrl: '<%= asset_path "spaces/reserve.html" %>', @@ -427,14 +437,9 @@ angular.module('application.router', ['ui.router']) groupsPromise: ['Group', function (Group) { return Group.query().$promise; }], settingsPromise: ['Setting', function (Setting) { return Setting.query({ - names: `['booking_window_start', \ - 'booking_window_end', \ - 'booking_move_enable', \ - 'booking_move_delay', \ - 'booking_cancel_enable', \ - 'booking_cancel_delay', \ - 'subscription_explications_alert', \ - 'space_explications_alert']` }).$promise; + names: `['booking_window_start', 'booking_window_end', 'booking_move_enable', 'booking_move_delay', \ + 'booking_cancel_enable', 'booking_cancel_delay', 'subscription_explications_alert', \ + 'space_explications_alert', 'online_payment_module']` }).$promise; }] } }) @@ -482,15 +487,9 @@ angular.module('application.router', ['ui.router']) }], settingsPromise: ['Setting', function (Setting) { return Setting.query({ - names: `['booking_window_start', \ - 'booking_window_end', \ - 'booking_move_enable', \ - 'booking_move_delay', \ - 'booking_cancel_enable', \ - 'booking_cancel_delay', \ - 'subscription_explications_alert', \ - 'training_explications_alert', \ - 'training_information_message']` }).$promise; + names: `['booking_window_start', 'booking_window_end', 'booking_move_enable', 'booking_move_delay', \ + 'booking_cancel_enable', 'booking_cancel_delay', 'subscription_explications_alert', \ + 'training_explications_alert', 'training_information_message', 'online_payment_module']` }).$promise; }] } }) @@ -508,7 +507,7 @@ angular.module('application.router', ['ui.router']) // pricing .state('app.public.plans', { url: '/plans', - abstract: Fablab.withoutPlans, + abstract: !Fablab.plansModule, views: { 'main@': { templateUrl: '<%= asset_path "plans/index.html.erb" %>', @@ -518,7 +517,8 @@ angular.module('application.router', ['ui.router']) resolve: { subscriptionExplicationsPromise: ['Setting', function (Setting) { return Setting.get({ name: 'subscription_explications_alert' }).$promise; }], plansPromise: ['Plan', function (Plan) { return Plan.query().$promise; }], - groupsPromise: ['Group', function (Group) { return Group.query().$promise; }] + groupsPromise: ['Group', function (Group) { return Group.query().$promise; }], + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['online_payment_module']" }).$promise; }] } }) @@ -534,7 +534,8 @@ angular.module('application.router', ['ui.router']) resolve: { categoriesPromise: ['Category', function (Category) { return Category.query().$promise; }], themesPromise: ['EventTheme', function (EventTheme) { return EventTheme.query().$promise; }], - ageRangesPromise: ['AgeRange', function (AgeRange) { return AgeRange.query().$promise; }] + ageRangesPromise: ['AgeRange', function (AgeRange) { return AgeRange.query().$promise; }], + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['online_payment_module']" }).$promise; }] } }) .state('app.public.events_show', { @@ -586,7 +587,8 @@ angular.module('application.router', ['ui.router']) bookingWindowEnd: ['Setting', function (Setting) { return Setting.get({ name: 'booking_window_end' }).$promise; }], machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }], plansPromise: ['Plan', function (Plan) { return Plan.query().$promise; }], - groupsPromise: ['Group', function (Group) { return Group.query().$promise; }] + groupsPromise: ['Group', function (Group) { return Group.query().$promise; }], + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['slot_duration', 'events_in_calendar', 'feature_tour_display']" }).$promise; }] } }) .state('app.admin.calendar.icalendar', { @@ -602,19 +604,23 @@ angular.module('application.router', ['ui.router']) } }) - // project's elements - .state('app.admin.project_elements', { - url: '/admin/project_elements', + // project's settings + .state('app.admin.projects', { + url: '/admin/projects', views: { 'main@': { - templateUrl: '<%= asset_path "admin/project_elements/index.html.erb" %>', - controller: 'ProjectElementsController' + templateUrl: '<%= asset_path "admin/projects/index.html.erb" %>', + controller: 'AdminProjectsController' } }, resolve: { componentsPromise: ['Component', function (Component) { return Component.query().$promise; }], licencesPromise: ['Licence', function (Licence) { return Licence.query().$promise; }], - themesPromise: ['Theme', function (Theme) { return Theme.query().$promise; }] + themesPromise: ['Theme', function (Theme) { return Theme.query().$promise; }], + settingsPromise: ['Setting', function (Setting) { + return Setting.query({ names: "['feature_tour_display', 'disqus_shortname', 'allowed_cad_extensions', \ + 'allowed_cad_mime_types', 'openlab_app_id', 'openlab_app_secret', 'openlab_default']" }).$promise; + }] } }) .state('app.admin.manage_abuses', { @@ -641,7 +647,8 @@ angular.module('application.router', ['ui.router']) }, resolve: { trainingsPromise: ['Training', function (Training) { return Training.query().$promise; }], - machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }] + machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }], + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['feature_tour_display']" }).$promise; }] } }) .state('app.admin.trainings_new', { @@ -683,7 +690,8 @@ angular.module('application.router', ['ui.router']) categoriesPromise: ['Category', function (Category) { return Category.query().$promise; }], themesPromise: ['EventTheme', function (EventTheme) { return EventTheme.query().$promise; }], ageRangesPromise: ['AgeRange', function (AgeRange) { return AgeRange.query().$promise; }], - priceCategoriesPromise: ['PriceCategory', function (PriceCategory) { return PriceCategory.query().$promise; }] + priceCategoriesPromise: ['PriceCategory', function (PriceCategory) { return PriceCategory.query().$promise; }], + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['feature_tour_display']" }).$promise; }] } }) .state('app.admin.events_new', { @@ -752,7 +760,8 @@ angular.module('application.router', ['ui.router']) couponsPromise: ['Coupon', function (Coupon) { return Coupon.query({ page: 1, filter: 'all' }).$promise; }], spacesPromise: ['Space', function (Space) { return Space.query().$promise; }], spacesPricesPromise: ['Price', function (Price) { return Price.query({ priceable_type: 'Space', plan_id: 'null' }).$promise; }], - spacesCreditsPromise: ['Credit', function (Credit) { return Credit.query({ creditable_type: 'Space' }).$promise; }] + spacesCreditsPromise: ['Credit', function (Credit) { return Credit.query({ creditable_type: 'Space' }).$promise; }], + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['feature_tour_display', 'slot_duration']" }).$promise; }] } }) @@ -827,12 +836,15 @@ angular.module('application.router', ['ui.router']) return Setting.query({ names: `['invoice_legals', 'invoice_text', 'invoice_VAT-rate', 'invoice_VAT-active', 'invoice_order-nb', 'invoice_code-value', \ 'invoice_code-active', 'invoice_reference', 'invoice_logo', 'accounting_journal_code', 'accounting_card_client_code', \ - 'accounting_card_client_label', 'accounting_wallet_client_code', 'accounting_wallet_client_label', \ + 'accounting_card_client_label', 'accounting_wallet_client_code', 'accounting_wallet_client_label', 'invoicing_module', \ 'accounting_other_client_code', 'accounting_other_client_label', 'accounting_wallet_code', 'accounting_wallet_label', \ 'accounting_VAT_code', 'accounting_VAT_label', 'accounting_subscription_code', 'accounting_subscription_label', \ 'accounting_Machine_code', 'accounting_Machine_label', 'accounting_Training_code', 'accounting_Training_label', \ - 'accounting_Event_code', 'accounting_Event_label', 'accounting_Space_code', 'accounting_Space_label']` }).$promise; + 'accounting_Event_code', 'accounting_Event_label', 'accounting_Space_code', 'accounting_Space_label', \ + 'feature_tour_display', 'online_payment_module', 'stripe_public_key', 'stripe_currency', 'invoice_prefix']` }).$promise; }], + stripeSecretKey: ['Setting', function (Setting) { return Setting.isPresent({ name: 'stripe_secret_key' }).$promise; }], + onlinePaymentStatus: ['Payment', function (Payment) { return Payment.onlinePaymentStatus().$promise; }], invoices: [ 'Invoice', function (Invoice) { return Invoice.list({ query: { number: '', customer: '', date: null, order_by: '-reference', page: 1, size: 20 } @@ -870,7 +882,8 @@ angular.module('application.router', ['ui.router']) managersPromise: ['User', function (User) { return User.query({ role: 'manager' }).$promise; }], groupsPromise: ['Group', function (Group) { return Group.query().$promise; }], tagsPromise: ['Tag', function (Tag) { return Tag.query().$promise; }], - authProvidersPromise: ['AuthProvider', function (AuthProvider) { return AuthProvider.query().$promise; }] + authProvidersPromise: ['AuthProvider', function (AuthProvider) { return AuthProvider.query().$promise; }], + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['feature_tour_display']" }).$promise; }] } }) .state('app.admin.members_new', { @@ -880,6 +893,9 @@ angular.module('application.router', ['ui.router']) templateUrl: '<%= asset_path "admin/members/new.html" %>', controller: 'NewMemberController' } + }, + resolve: { + phoneRequiredPromise: ['Setting', function (Setting) { return Setting.get({ name: 'phone_required' }).$promise; }] } }) .state('app.admin.members_import', { @@ -919,7 +935,8 @@ angular.module('application.router', ['ui.router']) activeProviderPromise: ['AuthProvider', function (AuthProvider) { return AuthProvider.active().$promise; }], walletPromise: ['Wallet', '$stateParams', function (Wallet, $stateParams) { return Wallet.getWalletByUser({ user_id: $stateParams.id }).$promise; }], transactionsPromise: ['Wallet', 'walletPromise', function (Wallet, walletPromise) { return Wallet.transactions({ id: walletPromise.id }).$promise; }], - tagsPromise: ['Tag', function (Tag) { return Tag.query().$promise; }] + tagsPromise: ['Tag', function (Tag) { return Tag.query().$promise; }], + phoneRequiredPromise: ['Setting', function (Setting) { return Setting.get({ name: 'phone_required' }).$promise; }] } }) .state('app.admin.admins_new', { @@ -984,7 +1001,8 @@ angular.module('application.router', ['ui.router']) }, resolve: { membersPromise: ['Member', function (Member) { return Member.mapping().$promise; }], - statisticsPromise: ['Statistics', function (Statistics) { return Statistics.query().$promise; }] + statisticsPromise: ['Statistics', function (Statistics) { return Statistics.query().$promise; }], + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['feature_tour_display']" }).$promise; }] } }) .state('app.admin.stats_graphs', { @@ -1009,17 +1027,17 @@ angular.module('application.router', ['ui.router']) resolve: { settingsPromise: ['Setting', function (Setting) { return Setting.query({ - names: `['twitter_name', 'about_title', 'about_body', \ - 'privacy_body', 'privacy_dpo', 'about_contacts', \ - 'home_blogpost', 'machine_explications_alert', 'training_explications_alert', \ + names: `['twitter_name', 'about_title', 'about_body', 'tracking_id', 'facebook_app_id', 'email_from', \ + 'privacy_body', 'privacy_dpo', 'about_contacts', 'book_overlapping_slots', 'invoicing_module', \ + 'home_blogpost', 'machine_explications_alert', 'training_explications_alert', 'slot_duration', \ 'training_information_message', 'subscription_explications_alert', 'event_explications_alert', \ - 'space_explications_alert', 'booking_window_start', 'booking_window_end', \ - 'booking_move_enable', 'booking_move_delay', 'booking_cancel_enable', \ - 'booking_cancel_delay', 'main_color', 'secondary_color', \ - 'fablab_name', 'name_genre', 'reminder_enable', \ - 'reminder_delay', 'visibility_yearly', 'visibility_others', \ + 'space_explications_alert', 'booking_window_start', 'booking_window_end', 'events_in_calendar', \ + 'booking_move_enable', 'booking_move_delay', 'booking_cancel_enable', 'feature_tour_display', \ + 'booking_cancel_delay', 'main_color', 'secondary_color', 'spaces_module', 'twitter_analytics', \ + 'fablab_name', 'name_genre', 'reminder_enable', 'plans_module', 'confirmation_required', \ + 'reminder_delay', 'visibility_yearly', 'visibility_others', 'wallet_module', \ 'display_name_enable', 'machines_sort_by', 'fab_analytics', \ - 'link_name', 'home_content', 'home_css']` }).$promise; + 'link_name', 'home_content', 'home_css', 'phone_required']` }).$promise; }], privacyDraftsPromise: ['Setting', function (Setting) { return Setting.get({ name: 'privacy_draft', history: true }).$promise; }], cguFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'cgu-file' }).$promise; }], @@ -1039,7 +1057,8 @@ angular.module('application.router', ['ui.router']) } }, resolve: { - clientsPromise: ['OpenAPIClient', function (OpenAPIClient) { return OpenAPIClient.query().$promise; }] + clientsPromise: ['OpenAPIClient', function (OpenAPIClient) { return OpenAPIClient.query().$promise; }], + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['feature_tour_display']" }).$promise; }] } }); } diff --git a/app/assets/javascripts/services/help.js.erb b/app/assets/javascripts/services/help.js.erb index 1397422eb..15be24121 100644 --- a/app/assets/javascripts/services/help.js.erb +++ b/app/assets/javascripts/services/help.js.erb @@ -12,7 +12,7 @@ Application.Services.factory('Help', ['$rootScope', '$uibModal', '$state', 'Auth 'app.admin.invoices': 'invoices', 'app.admin.pricing': 'pricing', 'app.admin.events': 'events', - 'app.admin.project_elements': 'project-elements', + 'app.admin.projects': 'projects', 'app.admin.statistics': 'statistics', 'app.admin.settings': 'settings', 'app.admin.open_api_clients': 'open-api' diff --git a/app/assets/javascripts/services/member.js b/app/assets/javascripts/services/member.js index c920fb263..25f16eaf2 100644 --- a/app/assets/javascripts/services/member.js +++ b/app/assets/javascripts/services/member.js @@ -1,6 +1,6 @@ 'use strict'; -Application.Services.factory('Member', ['$resource', '$q', function ($resource, $q) { +Application.Services.factory('Member', ['$resource', 'Setting', function ($resource, Setting) { return $resource('/api/members/:id', { id: '@id' }, { update: { @@ -37,11 +37,13 @@ Application.Services.factory('Member', ['$resource', '$q', function ($resource, params: { id: '@id' }, interceptor: { response: function (response) { - if (Fablab.featureTourDisplay === 'session') { - Fablab.sessionTours.push(response.data.tours[0]); - return { tours: Fablab.sessionTours }; - } - return response.data; + return Setting.query({ names: "['feature_tour_display']" }).$promise.then((settings) => { + if (settings.feature_tour_display === 'session') { + Fablab.sessionTours.push(response.data.tours[0]); + return { tours: Fablab.sessionTours }; + } + return response.data; + }); } } }, diff --git a/app/assets/javascripts/services/payment.js b/app/assets/javascripts/services/payment.js index c3fd1883d..69c80d865 100644 --- a/app/assets/javascripts/services/payment.js +++ b/app/assets/javascripts/services/payment.js @@ -7,6 +7,10 @@ Application.Services.factory('Payment', ['$resource', function ($resource) { method: 'POST', url: '/api/payments/confirm_payment', isArray: false + }, + onlinePaymentStatus: { + method: 'GET', + url: '/api/payments/online_payment_status' } } ); diff --git a/app/assets/javascripts/services/project.js b/app/assets/javascripts/services/project.js index 564562f9f..2b14b36c8 100644 --- a/app/assets/javascripts/services/project.js +++ b/app/assets/javascripts/services/project.js @@ -12,11 +12,6 @@ Application.Services.factory('Project', ['$resource', function ($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/setting.js b/app/assets/javascripts/services/setting.js index f97ef9af6..db240533a 100644 --- a/app/assets/javascripts/services/setting.js +++ b/app/assets/javascripts/services/setting.js @@ -20,6 +20,11 @@ Application.Services.factory('Setting', ['$resource', function ($resource) { url: '/api/settings/reset/:name', params: { name: '@name' }, method: 'PUT' + }, + isPresent: { + url: '/api/settings/is_present/:name', + params: { name: '@name' }, + method: 'GET' } } ); diff --git a/app/assets/stylesheets/modules/invoice.scss b/app/assets/stylesheets/modules/invoice.scss index 9832248d5..a1c5a5a44 100644 --- a/app/assets/stylesheets/modules/invoice.scss +++ b/app/assets/stylesheets/modules/invoice.scss @@ -8,6 +8,28 @@ color: red; } +.invoice-file { + text-align: center; + line-height: 4em; + margin-top: 2em; + + .fa-file-pdf-o { + font-size: 4em; + vertical-align: middle; + } + + .filename { + font-size: 1.1em; + vertical-align: middle; + margin-left: 1em; + + .prefix:hover { + background-color: $yellow; + overflow-x: hidden; + } + } +} + .invoice-placeholder { width: 80%; max-width: 800px; diff --git a/app/assets/stylesheets/modules/settings.scss b/app/assets/stylesheets/modules/settings.scss index 7414033a5..3e5889803 100644 --- a/app/assets/stylesheets/modules/settings.scss +++ b/app/assets/stylesheets/modules/settings.scss @@ -67,4 +67,10 @@ } } } + + .section-separator { + background: radial-gradient(#dddddd, transparent); + height: 1px; + margin: 24px 33% 12px 33%; + } } diff --git a/app/assets/templates/admin/calendar/calendar.html.erb b/app/assets/templates/admin/calendar/calendar.html.erb index 60e293c4c..0d9b85507 100644 --- a/app/assets/templates/admin/calendar/calendar.html.erb +++ b/app/assets/templates/admin/calendar/calendar.html.erb @@ -38,7 +38,7 @@
{{ 'app.admin.calendar.trainings' }}
{{ 'app.admin.calendar.machines' }}
- {{ 'app.admin.calendar.spaces' }} + {{ 'app.admin.calendar.spaces' }} {{ 'app.admin.calendar.events' }}
diff --git a/app/assets/templates/admin/calendar/eventModal.html.erb b/app/assets/templates/admin/calendar/eventModal.html.erb index a28c21253..a47859435 100644 --- a/app/assets/templates/admin/calendar/eventModal.html.erb +++ b/app/assets/templates/admin/calendar/eventModal.html.erb @@ -18,7 +18,7 @@ {{ 'app.admin.calendar.machine' }} -
+