diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 0000000..e94f814 --- /dev/null +++ b/.browserslistrc @@ -0,0 +1 @@ +defaults diff --git a/.foreman b/.foreman new file mode 100644 index 0000000..6900384 --- /dev/null +++ b/.foreman @@ -0,0 +1 @@ +procfile: Procfile.dev \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..5168571 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +# See https://git-scm.com/docs/gitattributes for more about git attribute files. + +# Mark the database schema as having been generated. +db/schema.rb linguist-generated + +# Mark the yarn lockfile as having been generated. +yarn.lock linguist-generated + +# Mark any vendored files as having been vendored. +vendor/* linguist-vendored diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0f080e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile '~/.gitignore_global' + +# Ignore bundler config. +/.bundle + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/ +!/tmp/pids/.keep + +# Ignore uploaded files in development. +/storage/* +!/storage/.keep + +/public/assets +.byebug_history + +# Ignore master key for decrypting credentials and more. +/config/master.key +/config/credentials.yml.enc + +/public/packs +/public/packs-test +/node_modules +/yarn-error.log +yarn-debug.log* +.yarn-integrity + +config/settings.local.yml +config/settings/*.local.yml +config/environments/*.local.yml + +/app/assets/builds/* +!/app/assets/builds/.keep diff --git a/.ruby-gemset b/.ruby-gemset new file mode 100644 index 0000000..7fd1903 --- /dev/null +++ b/.ruby-gemset @@ -0,0 +1 @@ +domando \ No newline at end of file diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..e76033b --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +ruby-3.0.3 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b68a635 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,44 @@ +# Dockerfile.rails +FROM ruby:3.0.3-slim AS rails-toolbox +MAINTAINER Marco Spasiano + +ARG USER_ID +ARG GROUP_ID +ENV INSTALL_PATH /opt/app + +RUN addgroup --gid $GROUP_ID user +RUN adduser --disabled-password --gecos '' --uid $USER_ID --gid $GROUP_ID user + +# add repositories and install dependencies +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends wget gnupg ;\ + wget --quiet -O - https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - ;\ + echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list ;\ + wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - ;\ + echo "deb http://apt.postgresql.org/pub/repos/apt bullseye-pgdg main" > /etc/apt/sources.list.d/pgdg.list ;\ + apt-get update ;\ + apt-get install -y --no-install-recommends \ + apt-utils \ + build-essential \ + imagemagick \ + postgresql-client \ + libpq-dev \ + nodejs \ + yarn \ + libvips-dev ;\ + rm -rf /var/lib/apt/lists/* + +RUN mkdir -p $INSTALL_PATH + +WORKDIR $INSTALL_PATH +COPY . . + +# Install app +RUN rm -rf node_modules vendor ;\ + gem install bundler ;\ + bundle install ;\ + yarn install ;\ + chown -R user:user /opt/app + +USER $USER_ID diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..e8e8c91 --- /dev/null +++ b/Gemfile @@ -0,0 +1,69 @@ +source 'https://rubygems.org' +git_source(:github) { |repo| "https://github.com/#{repo}.git" } + +ruby '3.0.3' + +# Bundle edge Rails instead: gem 'rails', github: 'rails/rails', branch: 'main' +gem 'rails', '~> 7.0.0' +# Use postgresql as the database for Active Record +gem 'pg', '1.3.0' # '~> 1.1' +# Use Puma as the app server +gem 'puma', '~> 5.0' +# Use SCSS for stylesheets +gem 'sass-rails', '~> 6.0' +# Use Redis adapter to run Action Cable in production +gem 'redis', '~> 4.0' +# Use Active Model has_secure_password +# gem 'bcrypt', '~> 3.1.7' + +gem 'jsbundling-rails' +gem "cssbundling-rails" + +gem 'sprockets-rails' + +# Use Active Storage variant +gem 'image_processing', '~> 1.2' + +# Reduces boot times through caching; required in config/boot.rb +gem 'bootsnap', '~> 1.9.3', require: false + +group :development, :test do + # Call 'byebug' anywhere in the code to stop execution and get a debugger console + gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] +end + +group :development do + # Access an interactive console on exception pages or by calling 'console' anywhere in the code. + gem 'web-console', '>= 4.1.0' + # Display performance information such as SQL time and flame graphs for each request in your browser. + # Can be configured to work on production as well see: https://github.com/MiniProfiler/rack-mini-profiler/blob/master/README.md + gem 'rack-mini-profiler', '~> 2.0' + gem 'letter_opener' +end + +group :test do + # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] + gem "capybara" + gem "selenium-webdriver" + gem "webdrivers" + gem 'database_cleaner' +end + +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] + +gem 'turbo-rails', '~> 1.0.0' +gem 'stimulus-rails' +gem 'config' +gem 'active_storage_validations' +gem 'devise', '~> 4.8.0' +gem 'rack-cas' +gem 'devise_cas_authenticatable', '~> 2.0' +gem 'acts-as-taggable-on', '~> 9.0.0' +gem 'cancancan' +gem 'hamlit' +gem 'hamlit-rails' +gem 'high_voltage' +gem 'pagy' +gem 'route_translator' +gem 'pg_search', '~> 2.3', '>= 2.3.6' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..76addeb --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,363 @@ +GEM + remote: https://rubygems.org/ + specs: + actioncable (7.0.1) + actionpack (= 7.0.1) + activesupport (= 7.0.1) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + actionmailbox (7.0.1) + actionpack (= 7.0.1) + activejob (= 7.0.1) + activerecord (= 7.0.1) + activestorage (= 7.0.1) + activesupport (= 7.0.1) + mail (>= 2.7.1) + net-imap + net-pop + net-smtp + actionmailer (7.0.1) + actionpack (= 7.0.1) + actionview (= 7.0.1) + activejob (= 7.0.1) + activesupport (= 7.0.1) + mail (~> 2.5, >= 2.5.4) + net-imap + net-pop + net-smtp + rails-dom-testing (~> 2.0) + actionpack (7.0.1) + actionview (= 7.0.1) + activesupport (= 7.0.1) + rack (~> 2.0, >= 2.2.0) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actiontext (7.0.1) + actionpack (= 7.0.1) + activerecord (= 7.0.1) + activestorage (= 7.0.1) + activesupport (= 7.0.1) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (7.0.1) + activesupport (= 7.0.1) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.1, >= 1.2.0) + active_storage_validations (0.9.6) + activejob (>= 5.2.0) + activemodel (>= 5.2.0) + activestorage (>= 5.2.0) + activesupport (>= 5.2.0) + activejob (7.0.1) + activesupport (= 7.0.1) + globalid (>= 0.3.6) + activemodel (7.0.1) + activesupport (= 7.0.1) + activerecord (7.0.1) + activemodel (= 7.0.1) + activesupport (= 7.0.1) + activestorage (7.0.1) + actionpack (= 7.0.1) + activejob (= 7.0.1) + activerecord (= 7.0.1) + activesupport (= 7.0.1) + marcel (~> 1.0) + mini_mime (>= 1.1.0) + activesupport (7.0.1) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + acts-as-taggable-on (9.0.1) + activerecord (>= 6.0, < 7.1) + addressable (2.8.0) + public_suffix (>= 2.0.2, < 5.0) + bcrypt (3.1.16) + bindex (0.8.1) + bootsnap (1.9.4) + msgpack (~> 1.0) + builder (3.2.4) + byebug (11.1.3) + cancancan (3.3.0) + capybara (3.36.0) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.8) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) + childprocess (4.1.0) + concurrent-ruby (1.1.9) + config (3.1.1) + deep_merge (~> 1.2, >= 1.2.1) + dry-validation (~> 1.0, >= 1.0.0) + crass (1.0.6) + cssbundling-rails (1.0.0) + railties (>= 6.0.0) + database_cleaner (2.0.1) + database_cleaner-active_record (~> 2.0.0) + database_cleaner-active_record (2.0.1) + activerecord (>= 5.a) + database_cleaner-core (~> 2.0.0) + database_cleaner-core (2.0.1) + deep_merge (1.2.2) + devise (4.8.1) + bcrypt (~> 3.0) + orm_adapter (~> 0.1) + railties (>= 4.1.0) + responders + warden (~> 1.2.3) + devise_cas_authenticatable (2.0.1) + devise (>= 4.0.0) + rack-cas + digest (3.1.0) + dry-configurable (0.14.0) + concurrent-ruby (~> 1.0) + dry-core (~> 0.6) + dry-container (0.9.0) + concurrent-ruby (~> 1.0) + dry-configurable (~> 0.13, >= 0.13.0) + dry-core (0.7.1) + concurrent-ruby (~> 1.0) + dry-inflector (0.2.1) + dry-initializer (3.1.1) + dry-logic (1.2.0) + concurrent-ruby (~> 1.0) + dry-core (~> 0.5, >= 0.5) + dry-schema (1.8.0) + concurrent-ruby (~> 1.0) + dry-configurable (~> 0.13, >= 0.13.0) + dry-core (~> 0.5, >= 0.5) + dry-initializer (~> 3.0) + dry-logic (~> 1.0) + dry-types (~> 1.5) + dry-types (1.5.1) + concurrent-ruby (~> 1.0) + dry-container (~> 0.3) + dry-core (~> 0.5, >= 0.5) + dry-inflector (~> 0.1, >= 0.1.2) + dry-logic (~> 1.0, >= 1.0.2) + dry-validation (1.7.0) + concurrent-ruby (~> 1.0) + dry-container (~> 0.7, >= 0.7.1) + dry-core (~> 0.5, >= 0.5) + dry-initializer (~> 3.0) + dry-schema (~> 1.8, >= 1.8.0) + erubi (1.10.0) + ffi (1.15.5-x64-mingw32) + globalid (1.0.0) + activesupport (>= 5.0) + hamlit (2.16.0) + temple (>= 0.8.2) + thor + tilt + hamlit-rails (0.2.3) + actionpack (>= 4.0.1) + activesupport (>= 4.0.1) + hamlit (>= 1.2.0) + railties (>= 4.0.1) + high_voltage (3.1.2) + i18n (1.9.1) + concurrent-ruby (~> 1.0) + image_processing (1.12.1) + mini_magick (>= 4.9.5, < 5) + ruby-vips (>= 2.0.17, < 3) + io-wait (0.2.1) + jsbundling-rails (1.0.0) + railties (>= 6.0.0) + launchy (2.5.0) + addressable (~> 2.7) + letter_opener (1.7.0) + launchy (~> 2.2) + loofah (2.13.0) + crass (~> 1.0.2) + nokogiri (>= 1.5.9) + mail (2.7.1) + mini_mime (>= 0.1.1) + marcel (1.0.2) + matrix (0.4.2) + method_source (1.0.0) + mini_magick (4.11.0) + mini_mime (1.1.2) + minitest (5.15.0) + msgpack (1.4.4) + net-imap (0.2.3) + digest + net-protocol + strscan + net-pop (0.1.1) + digest + net-protocol + timeout + net-protocol (0.1.2) + io-wait + timeout + net-smtp (0.3.1) + digest + net-protocol + timeout + nio4r (2.5.8) + nokogiri (1.13.1-x64-mingw32) + racc (~> 1.4) + orm_adapter (0.5.0) + pagy (5.10.1) + activesupport + pg (1.3.0-x64-mingw32) + pg_search (2.3.6) + activerecord (>= 5.2) + activesupport (>= 5.2) + public_suffix (4.0.6) + puma (5.6.1) + nio4r (~> 2.0) + racc (1.6.0) + rack (2.2.3) + rack-cas (0.16.1) + addressable (~> 2.3) + nokogiri (~> 1.5) + rack (>= 1.3) + rack-mini-profiler (2.3.3) + rack (>= 1.2.0) + rack-test (1.1.0) + rack (>= 1.0, < 3) + rails (7.0.1) + actioncable (= 7.0.1) + actionmailbox (= 7.0.1) + actionmailer (= 7.0.1) + actionpack (= 7.0.1) + actiontext (= 7.0.1) + actionview (= 7.0.1) + activejob (= 7.0.1) + activemodel (= 7.0.1) + activerecord (= 7.0.1) + activestorage (= 7.0.1) + activesupport (= 7.0.1) + bundler (>= 1.15.0) + railties (= 7.0.1) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + nokogiri (>= 1.6) + rails-html-sanitizer (1.4.2) + loofah (~> 2.3) + railties (7.0.1) + actionpack (= 7.0.1) + activesupport (= 7.0.1) + method_source + rake (>= 12.2) + thor (~> 1.0) + zeitwerk (~> 2.5) + rake (13.0.6) + redis (4.6.0) + regexp_parser (2.2.0) + responders (3.0.1) + actionpack (>= 5.0) + railties (>= 5.0) + rexml (3.2.5) + route_translator (12.1.0) + actionpack (>= 5.2, < 7.1) + activesupport (>= 5.2, < 7.1) + addressable (~> 2.7) + ruby-vips (2.1.4) + ffi (~> 1.12) + rubyzip (2.3.2) + sass-rails (6.0.0) + sassc-rails (~> 2.1, >= 2.1.1) + sassc (2.4.0-x64-mingw32) + ffi (~> 1.9) + sassc-rails (2.1.2) + railties (>= 4.0.0) + sassc (>= 2.0) + sprockets (> 3.0) + sprockets-rails + tilt + selenium-webdriver (4.1.0) + childprocess (>= 0.5, < 5.0) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2) + sprockets (4.0.2) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-rails (3.4.2) + actionpack (>= 5.2) + activesupport (>= 5.2) + sprockets (>= 3.0.0) + stimulus-rails (1.0.2) + railties (>= 6.0.0) + strscan (3.0.1) + temple (0.8.2) + thor (1.2.1) + tilt (2.0.10) + timeout (0.2.0) + turbo-rails (1.0.1) + actionpack (>= 6.0.0) + railties (>= 6.0.0) + tzinfo (2.0.4) + concurrent-ruby (~> 1.0) + tzinfo-data (1.2021.5) + tzinfo (>= 1.0.0) + warden (1.2.9) + rack (>= 2.0.9) + web-console (4.2.0) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) + bindex (>= 0.4.0) + railties (>= 6.0.0) + webdrivers (5.0.0) + nokogiri (~> 1.6) + rubyzip (>= 1.3.0) + selenium-webdriver (~> 4.0) + websocket-driver (0.7.5) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + xpath (3.2.0) + nokogiri (~> 1.8) + zeitwerk (2.5.4) + +PLATFORMS + x64-mingw32 + +DEPENDENCIES + active_storage_validations + acts-as-taggable-on (~> 9.0.0) + bootsnap (~> 1.9.3) + byebug + cancancan + capybara + config + cssbundling-rails + database_cleaner + devise (~> 4.8.0) + devise_cas_authenticatable (~> 2.0) + hamlit + hamlit-rails + high_voltage + image_processing (~> 1.2) + jsbundling-rails + letter_opener + pagy + pg (= 1.3.0) + pg_search (~> 2.3, >= 2.3.6) + puma (~> 5.0) + rack-cas + rack-mini-profiler (~> 2.0) + rails (~> 7.0.0) + redis (~> 4.0) + route_translator + sass-rails (~> 6.0) + selenium-webdriver + sprockets-rails + stimulus-rails + turbo-rails (~> 1.0.0) + tzinfo-data + web-console (>= 4.1.0) + webdrivers + +RUBY VERSION + ruby 3.0.3p157 + +BUNDLED WITH + 2.3.0 diff --git a/Procfile.dev b/Procfile.dev new file mode 100644 index 0000000..1459669 --- /dev/null +++ b/Procfile.dev @@ -0,0 +1,3 @@ +web: rails server -p 3000 +js: yarn build:js --watch +css: yarn build:css --watch diff --git a/README.md b/README.md new file mode 100644 index 0000000..641fac4 --- /dev/null +++ b/README.md @@ -0,0 +1,119 @@ +[![Coverage Status](https://coveralls.io/repos/github/isprambiente/domando/badge.svg?branch=main)](https://coveralls.io/github/isprambiente/domando?branch=main) +[![Inline docs](http://inch-ci.org/github/remote-exec/command-designer.png)](http://inch-ci.org/github/isprambiente/domando) +[![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://rubydoc.info/github/isprambiente/domando/main) +[![Maintainability](https://api.codeclimate.com/v1/badges/be06f3229dd434cdd732/maintainability)](https://codeclimate.com/github/isprambiente/domando/maintainability) + +# Domande frequenti (FAQ) + +Il programma, interamente sviluppato all'interno del settore Sviluppo di AGP-INF, nasce da una specifica necessità di [I.S.P.R.A.](http://www.isprambiente.gov.it) nel gestire le domande frequenti (FAQ) per i dipendenti ISPRA. + +Il programma è stato appositamente sviluppato su piattaforma web per consentire l'accesso alle risorse interne tramite l'utilizzo di un comune browser web. + +All'interno dell'applicazione è possibile gestire la pubblicazione di eventi e le partecipazioni nelle varie date. + +## Licenza +Il codice sorgente del sito progetto è rilasciato sotto licenza MIT License (codice SPDX: MIT). La licenza è visibile nel file [LICENSE](https://opensource.org/licenses/MIT) + +## Repository +Questo repository contiene il codice sorgente del programma. + +Il sito è sviluppato in linguaggio Ruby 2.7, framework Rails 6.0 e webpacker StimulusJS. + +### Specifiche tecniche progetto +* [Ruby 3.0.x](https://www.ruby-lang.org) +* [RVM](https://rvm.io/) +* [Ruby on Rais 7.0](https://rubyonrails.org/) +* [NodeJS](https://nodejs.org/) +* [Yarn](https://yarnpkg.com/) +* [Webpack StimulusJS](https://stimulusjs.org/) +* [Postgresql](https://www.postgresql.org/) +* HTML5 + CSS3 +* no jQuery +* [Server CAS](https://rubycas.github.io/) - autenticazione SingleSignOn +* [Openssl](https://www.openssl.org/) - + +\* In alternativa al server CAS e' necessario sviluppare altri sistemi di autenticazione come ldap + +### Requisiti tecnici per ambiente server +* Sistema operativo: Linux +* Gestore pacchetto ruby: RVM +* Linguaggio di programmazione: Ruby 3.0 +* Framework: Rais 7.0 +* Webpacker: StimulusJS +* Database: PostgreSQL >= 14 +* NodeJS: JavaScript runtime >= v13.10 +* Package Manager: Yarn >= 1.22 +* Deploy applicazione: Accesso ssh per deploy applicazione via Capistrano +* Webserver: Nginx + Puma +* Autenticazione utenti: CAS Server + +### Requisiti minimi per i client +* Mozilla Firefox 53, Chrome 58, Microsoft Edge, Internet Explorer 11, Safari 9.0 o altro browser compatibile con HTML 5, CSS 3; +* Internet Explorer 11 non supportato; +* Javascript abilitato; +* Cookie abilitati; +* Supporto ai certificati SSL; +* Risoluzione schermo 1024x768. + +### Configurazione consigliata per i client +* Mozilla Firefox >= 53, Chrome >= 58, Microsoft Edge, Safari 9.0 o altro browser compatibile con HTML 5 e CSS 3; +* Javascript abilitato; +* Cookie abilitati; +* Supporto ai certificati SSL; + +## Installazione ambiente +Installare ruby 3.0.2, consigliato [RVM](https://rvm.io/). + +## Installazione applicazione + +### In sviluppo + +1. Clonare il progetto in sviluppo + + ``` + git clone https://github.com/isprambiente/domando.git + ``` + +2. Da una shell posizionarsi sulla root del progetto ed eseguire + + ``` + gem install bundle + bundle install + yarn install + ``` + +3. Creare il file `config/settings.local.yml` partendo da `config/settings.yml` per sovrascrivere i parametri di default. Il file è incluso nel `.gitignore` pertanto sarà necessario ricopiarlo manualmente sul server nel path `shared/config/settings.local.yml` + +### Demo con docker / docker compose +1. Clonare il progetto in sviluppo + + ``` + git clone https://github.com/isprambiente/domando.git + ``` + +2. Configurare il DNS o modificare il proprio file hosts per risolvere il nome cas-mock-server sull'indirizzo del server docker. + + Nel seguente esempio il docker viene eseguito localmente e viene modificato il file `/etc/hosts` del computer locale. + + ``` + 127.0.0.1 localhost cas-mock-server + ``` + la modifica è necessaria per raggiungere con un nome condiviso il server CAS + +3. Eseguire la build del docker tramite compose + + ``` + sudo docker-compose up --build -d + ``` + +4. Per accedere utilizzare le seguenti credenziali: + * user - password # per utente standard + * editor - password # per utente editor + * admin - password # per utente admin + +### Partecipa! +Puoi collaborare allo sviluppo dell'applicazione e della documentazione tramite [github](https://github.com/isprambiente/domando). + +Tramite [Github discussions](https://github.com/isprambiente/domando/discussions) è possibile richiedere e offrire aiuto. + +Se riscontrate errori e bug potete segnalarli nella paggina delle [Issues](https://github.com/isprambiente/domando/issues) diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..9a5ea73 --- /dev/null +++ b/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative "config/application" + +Rails.application.load_tasks diff --git a/app/assets/builds/.keep b/app/assets/builds/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js new file mode 100644 index 0000000..0f15987 --- /dev/null +++ b/app/assets/config/manifest.js @@ -0,0 +1,3 @@ +//= link_tree ../images +//= link_tree ../builds +//= link application.css diff --git a/app/assets/images/.keep b/app/assets/images/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/assets/images/favicon.ico b/app/assets/images/favicon.ico new file mode 100644 index 0000000..db2701c Binary files /dev/null and b/app/assets/images/favicon.ico differ diff --git a/app/assets/images/icon.png b/app/assets/images/icon.png new file mode 100644 index 0000000..3705531 Binary files /dev/null and b/app/assets/images/icon.png differ diff --git a/app/assets/images/logo.jpg b/app/assets/images/logo.jpg new file mode 100644 index 0000000..57f9fd7 Binary files /dev/null and b/app/assets/images/logo.jpg differ diff --git a/app/assets/stylesheets/actiontext.css b/app/assets/stylesheets/actiontext.css new file mode 100644 index 0000000..bd9e8fd --- /dev/null +++ b/app/assets/stylesheets/actiontext.css @@ -0,0 +1,33 @@ +/* + * Provides a drop-in pointer for the default Trix stylesheet that will format the toolbar and + * the trix-editor content (whether displayed or under editing). Feel free to incorporate this + * inclusion directly in any other asset bundle and remove this file. + * + *= require trix/dist/trix +*/ + +/* + * We need to override trix.css’s image gallery styles to accommodate the + * element we wrap around attachments. Otherwise, + * images in galleries will be squished by the max-width: 33%; rule. +*/ +.trix-content .attachment-gallery > action-text-attachment, +.trix-content .attachment-gallery > .attachment { + flex: 1 0 33%; + padding: 0 0.5em; + max-width: 33%; +} + +.trix-content .attachment-gallery.attachment-gallery--2 > action-text-attachment, +.trix-content .attachment-gallery.attachment-gallery--2 > .attachment, .trix-content .attachment-gallery.attachment-gallery--4 > action-text-attachment, +.trix-content .attachment-gallery.attachment-gallery--4 > .attachment { + flex-basis: 50%; + max-width: 50%; +} + +.trix-content action-text-attachment .attachment { + padding: 0 !important; + max-width: 100% !important; +} + +.trix-button-group--file-tools, .trix-button--icon-attach {display: none !important; visibility: none !important;} diff --git a/app/assets/stylesheets/application.bulma.scss b/app/assets/stylesheets/application.bulma.scss new file mode 100644 index 0000000..3408796 --- /dev/null +++ b/app/assets/stylesheets/application.bulma.scss @@ -0,0 +1,103 @@ +// @charset "utf-8"; + +// Import a Google Font +@import url('https://fonts.googleapis.com/css?family=Open+Sans:400,700&display=swap'); + +// Colors +$black: hsl(0, 0%, 4%) !default; +$black-bis: hsl(0, 0%, 7%) !default; +$black-ter: hsl(0, 0%, 14%) !default; + +$grey-darker: hsl(0, 0%, 21%) !default; +$grey-dark: hsl(0, 0%, 29%) !default; +$grey: hsl(0, 0%, 48%) !default; +$grey-light: hsl(0, 0%, 71%) !default; +$grey-lighter: hsl(0, 0%, 86%) !default; + +$white-ter: hsl(0, 0%, 96%) !default; +$white-bis: hsl(0, 0%, 98%) !default; +$white: hsl(0, 0%, 100%) !default; + +$orange: hsl(37, 100%, 60%) !default; +$orange-light: hsl(37, 100%, 71%) !default; +$orange-darker: hsl(37, 100%, 21%) !default; +$orange-dark: hsl(37, 100%, 35%) !default; +$yellow: hsl(60, 100%, 66%) !default; +$yellow-light: hsl(60, 100%, 80%) !default; +$yellow-lighter: hsl(60, 100%, 93%) !default; +$yellow-darker: hsl(60, 100%, 21%) !default; +$yellow-dark: hsl(60, 100%, 35%) !default; +$green: hsl(140, 52%, 66%) !default; +$green-darker: hsl(140, 52%, 21%) !default; +$green-dark: hsl(140, 52%, 35%) !default; +$turquoise: hsl(171, 100%, 41%) !default; +$cyan: hsl(204, 86%, 53%) !default; +$blue: hsl(217, 71%, 53%) !default; +$purple: hsl(271, 100%, 11%) !default; +$red: hsl(8, 59%, 51%) !default; + +// Colori pannello +$subtitle-color: hsl(0, 0%, 48%) !default; +$panel-heading-background-color: hsl(0, 0%, 50%) !default; +$panel-heading-color: #fff !default; +$panel-heading-size: 1rem !default; + +// Typography +$family-sans-serif: BlinkMacSystemFont, -apple-system, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif !default; +$family-monospace: monospace !default; + +$rgba-primary-0: rgba(135, 38, 67,1); // Main Primary color */ +$rgba-primary-1: rgba(209,125,151,1); +$rgba-primary-2: rgba(169, 72,101,1); +$rgba-primary-3: rgba( 98, 12, 38,1); +$rgba-primary-4: rgba( 62, 0, 19,1); + +$rgba-secondary-1-0: rgba(151,101, 43,1); // Main Secondary color (1); */ +$rgba-secondary-1-1: rgba(233,190,140,1); +$rgba-secondary-1-2: rgba(189,139, 81,1); +$rgba-secondary-1-3: rgba(110, 65, 13,1); +$rgba-secondary-1-4: rgba( 69, 37, 0,1); + +$rgba-secondary-2-0: rgba( 30, 74, 96,1); // Main Secondary color (2); */ +$rgba-secondary-2-1: rgba( 92,129,148,1); +$rgba-secondary-2-2: rgba( 55, 98,119,1); +$rgba-secondary-2-3: rgba( 11, 50, 69,1); +$rgba-secondary-2-4: rgba( 2, 30, 44,1); + +$rgba-complement-0: rgba( 82,134, 38,1); // Main Complement color */ +$rgba-complement-1: rgba(162,208,125,1); +$rgba-complement-2: rgba(116,168, 72,1); +$rgba-complement-3: rgba( 51, 98, 12,1); +$rgba-complement-4: rgba( 28, 62, 0,1); + +// Variabili globali +$primary: rgba(135, 38, 67,1); +$primary-darker: rgb(102, 18, 36); +$primary-dark: rgb(102, 18, 36); +$link: rgb(30, 74, 96); +$info: $cyan; +$success: $green; +$warning: $orange; +$danger: $red; +$dark: $grey-darker; +$text: $grey-dark; +$background: $white-ter; +$family-primary: 'Titillium Web', sans-serif; +$family-sans-serif: 'Titillium Web', sans-serif; // 'Roboto Mono', monospace + + +// Import only what you need from Bulma +// @import "bulma/sass/utilities/_all.sass"; +// @import "bulma/sass/base/_all.sass"; +// @import "bulma/sass/elements/button.sass"; +// @import "bulma/sass/elements/container.sass"; +// @import "bulma/sass/elements/title.sass"; +// @import "bulma/sass/form/_all.sass"; +// @import "bulma/sass/components/navbar.sass"; +// @import "bulma/sass/layout/hero.sass"; +// @import "bulma/sass/layout/section.sass"; + +@import 'bulma/bulma.sass'; +@import './application.sass'; + +.pagy-bulma-nav {margin-top: 0.5rem; padding-top: 0.5rem; border-top: 2px solid #f5f5f5;} diff --git a/app/assets/stylesheets/application.sass b/app/assets/stylesheets/application.sass new file mode 100644 index 0000000..3901ea8 --- /dev/null +++ b/app/assets/stylesheets/application.sass @@ -0,0 +1,22 @@ +@charset 'utf-8' +@import url('https://fonts.googleapis.com/css?family=Open+Sans:400,700&display=swap') +@import 'trix/dist/trix.css' +@import 'actiontext.css' +@import '@creativebulma/bulma-tooltip/src/sass/index' +@import 'bulma-quickview/src/sass/index' +@import 'bulma-switch/src/sass/index' +@import 'slim-select/src/slim-select/slimselect.scss' +@import 'animate.css/animate.css' + +@import 'header' +@import 'nav1' +@import 'nav2' +@import 'nav3' +@import 'footer' +@import 'menu' +@import 'list' + +@import 'custom' +@import 'swal2_custom' +@import 'manual' +@import 'form' diff --git a/app/assets/stylesheets/custom.sass b/app/assets/stylesheets/custom.sass new file mode 100644 index 0000000..9acb271 --- /dev/null +++ b/app/assets/stylesheets/custom.sass @@ -0,0 +1,168 @@ +html, body + margin: 0 + +html + font-family: 'Open Sans', sans-serif + font-size: 13px + color: $black + +body + line-height: 1.6 + +@media print + table, .top-bar + display: none !important + .no-print, .no-print * + display: none !important + +header + .hero-body + padding: 1rem 1.5rem + +.fa5-text + padding-left: 0.5rem + +.is-nowrap + white-space: nowrap + +.has-spaced + padding: 2rem 0 + +.is-spaced + margin: 1.5rem 0 + +.is-borderless + border: 0 !important + +.has-background-trasparent + background-color: transparent !important + +.is-capitalized-first + text-transform: lowercase + &::first-letter + text-transform: uppercase + +.is-small-caps + font-variant: small-caps + +.is-hidden-touch.is-active + display: block !important + +.is-inline + display: inline !important + +.has-text-purple + color: $purple + +.has-text-denied + color: $black + +i.fa + padding-right: 0.5rem + +.turbo-progress-bar + width: 0 + opacity: 0 + height: 5px + background: $danger +.turbo-progress-bar--in-progress + width: 50% + opacity: 1 +.turbo-progress-bar--finalizing + width: 100% + opacity: 0 + +nav.breadcrumb + letter-spacing: 1px + margin: 0.7rem 0.5rem 2rem !important + li + &:before + padding-right: 0.5rem + a, a:link, a:visited, a:hover + color: #444 + +.is-hoverable + tbody + &:hover + background-color: $grey-lighter + +.is-sticky + position: sticky + top: 0 + z-index: 1 + +.is-required + &:after + content: '*' + margin-left: 0.1rem + color: $danger + &:hover + content: 'required' + cursor: 'help' + +.message-body + .columns + margin-bottom: 0px + +.is-disabled, .disabled, :disabled, [disabled] + pointer-events: none !important + +#menu + .quickview-body + .section + padding: 1rem !important + +.has-cursor-help + cursor: help + +.has-shadow + box-shadow: 0 .5em 1em -.125em rgba(10,10,10,.2), 0 0 0 1px rgba(10,10,10,.02) + +nav.panel.filters + span.panel-container + max-height: 180px + overflow-y: auto + display: inline-block + width: 100% + a.panel-block + margin: 0 !important + padding: 0em 0.75em + .panel-icon + visibility: hidden + &.is-active + .panel-icon + visibility: visible + .icon + display: inline-table + .column + padding: 0.3rem + +input[pattern]:invalid + box-shadow: 0 0 5px 1px $danger + +/* Chrome, Safari, Edge, Opera */ +input.is-arrowless::-webkit-outer-spin-button, +input.is-arrowless::-webkit-inner-spin-button + -webkit-appearance: none + margin: 0 + +/* Firefox */ +input.is-arrowless[type=number] + -moz-appearance: textfield + +// span[data-trix-button-group=file-tools] +// visibility: hidden +// display: none + +fieldset + border: 1px solid $grey-lighter !important + border-radius: 4px + padding: .5rem auto + .no-border + border: 0px !important + legend + font-weight: bold + margin-left: .4rem + +a.button.is-static + height: 30px !important \ No newline at end of file diff --git a/app/assets/stylesheets/dashboard.sass b/app/assets/stylesheets/dashboard.sass new file mode 100644 index 0000000..41e592a --- /dev/null +++ b/app/assets/stylesheets/dashboard.sass @@ -0,0 +1,7 @@ +#dashboard + .is-hoverable + i:hover, svg:hover + color: $warning + .card + .card-content + border-top: 1px solid grey \ No newline at end of file diff --git a/app/assets/stylesheets/footer.sass b/app/assets/stylesheets/footer.sass new file mode 100644 index 0000000..c270bef --- /dev/null +++ b/app/assets/stylesheets/footer.sass @@ -0,0 +1,31 @@ +footer + #footer-first + padding: 41px 0px + .column.menu + border-left: $primary-darker 1px solid + p.menu-label + font-weight: bold + color: $primary + font-size: 0.8rem + margin-bottom: 4px + padding-left: 18px + ul.menu-list + li + span.fa-li + font-size: 0.8rem + a + font-size: 0.8rem + font-weight: bold + color: $grey-dark + &:hover + background: $primary-darker + color: $white + #footer-second + background: $primary-darker + padding: 41px 0px + text-align: center + color: $white + strong + color: $white + a + color: $white diff --git a/app/assets/stylesheets/form.sass b/app/assets/stylesheets/form.sass new file mode 100644 index 0000000..f01cb88 --- /dev/null +++ b/app/assets/stylesheets/form.sass @@ -0,0 +1,9 @@ +form + nav.panel + label.panel-block + input[type="radio"] + margin-right: 5px + + p.helper + @extend .help + cursor: default \ No newline at end of file diff --git a/app/assets/stylesheets/header.sass b/app/assets/stylesheets/header.sass new file mode 100644 index 0000000..b73fb9d --- /dev/null +++ b/app/assets/stylesheets/header.sass @@ -0,0 +1,2 @@ +body > header + box-shadow: $panel-shadow diff --git a/app/assets/stylesheets/home.scss b/app/assets/stylesheets/home.scss new file mode 100644 index 0000000..9e167d0 --- /dev/null +++ b/app/assets/stylesheets/home.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the Home controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: https://sass-lang.com/ diff --git a/app/assets/stylesheets/list.sass b/app/assets/stylesheets/list.sass new file mode 100644 index 0000000..0c2ddec --- /dev/null +++ b/app/assets/stylesheets/list.sass @@ -0,0 +1,35 @@ +.list + @extend .is-sticky + background-color: #f0f8ff + padding: 1rem + .head + .column + font-size: 0.9rem + font-weight: bold + text-transform: lowercase + &::first-letter + text-transform: uppercase +.rows + margin-bottom: 1rem + turbo-frame + &:nth-child(even) + .row, .has-background-parent + background: $white + &:nth-child(odd) + .row, .has-background-parent + background: #fafafa + .row + margin: 0.3rem 0 + border: 1px solid #f5f5f5 + background: $white + &:hover + background: $yellow-lighter + &.is-newer + background: $yellow-lighter !important + border: 1px solid $red !important + .column + & > a.button, & > form + margin-top: 0.3rem !important + .list + background-color: #faebd7 + diff --git a/app/assets/stylesheets/manual.sass b/app/assets/stylesheets/manual.sass new file mode 100644 index 0000000..0073669 --- /dev/null +++ b/app/assets/stylesheets/manual.sass @@ -0,0 +1,51 @@ +@media print + #manual + .pagebreak + page-break-before: always + table + display: table !important +#manual + .manual-menu + &.sticky + position: sticky + top: 30px + .manual-body + line-height: 2rem + h6.title.is-6 + text-decoration-line: underline + text-decoration-style: solid + span.icon + margin-right: 1rem + a, a:link, a:visited, a:hover + color: #4a4a4a + ul.child + margin: 0.5rem auto + list-style-type: none + &.circle + list-style-type: circle + &.disc + list-style-type: disc + li + margin-left: 2rem + margin-bottom: 0.5rem + padding-left: 1rem + img + border: 1px solid #4a4a4a + margin: 1rem auto + table + width: 100% + margin-top: 1rem + margin-bottom: 1rem + caption + text-align: left + font-weight: bold + padding: .5rem 0 + th, td + border: 1px solid black + padding: 0 .5rem + &:first-child + min-width: 15% + thead + th + background-color: #9CC2E5 + diff --git a/app/assets/stylesheets/menu.sass b/app/assets/stylesheets/menu.sass new file mode 100644 index 0000000..df404c0 --- /dev/null +++ b/app/assets/stylesheets/menu.sass @@ -0,0 +1,36 @@ +.quickview + direction: rtl + .quickview-header + min-height: 32px !important + background-color: $primary + .title + color: $white + .quickview-body + direction: rtl + .quickview-block + direction: ltr + .menu-label + padding: 0 1rem + ul.menu-list + overflow-y: auto + li + padding: 0 1rem + &:hover + background: $yellow-lighter + a + font-size: .9rem + li:last-child + margin-bottom: 1rem + li.total + border-top: 2px solid $black + margin: 0 0.5rem + .level + margin-left: -0.5rem + .level-left + width: 18vw +.quickview.is-left + direction: ltr + .quickview-body + direction: ltr + .quickview-block + direction: ltr diff --git a/app/assets/stylesheets/nav1.sass b/app/assets/stylesheets/nav1.sass new file mode 100644 index 0000000..19f11dd --- /dev/null +++ b/app/assets/stylesheets/nav1.sass @@ -0,0 +1,8 @@ +#nav1 + .navbar + min-height: 30px + a.navbar-item + font-size: 14px + line-height: 20px + padding: 0rem 0.75rem + diff --git a/app/assets/stylesheets/nav2.sass b/app/assets/stylesheets/nav2.sass new file mode 100644 index 0000000..d3578cd --- /dev/null +++ b/app/assets/stylesheets/nav2.sass @@ -0,0 +1,13 @@ +#nav2 + background: $primary + padding: 10px + h1 + margin: 0px + font-size: 1.40em + line-height: 1.40em + padding: 9px 0px + a + color: $white + img + max-height: 83px + vertical-align: middle diff --git a/app/assets/stylesheets/nav3.sass b/app/assets/stylesheets/nav3.sass new file mode 100644 index 0000000..2acf51e --- /dev/null +++ b/app/assets/stylesheets/nav3.sass @@ -0,0 +1,102 @@ +$nav3-font-size: 1.1em +$nav3-text-color: $white +$nav3-background-color: $primary-darker +$nav3-background-color-hover: $primary +$nav3-dropdown-font-size: 0.85rem +$nav3-dropdown-submenu-font-size: 0.75rem +$nav3-dropdown-submenu-max-width: 120px +$nav3-dropdown-submenu-margin-left: calc( -100% + ( #{$nav3-dropdown-submenu-max-width} ) ) + +#nav3 + background: $nav3-background-color + nav.navbar + background: transparent + .navbar-brand + font-size: calc(#{$nav3-font-size} - 0.1em) + .icon + margin: 0 + a.navbar-item + span + color: #74a848 + a.button + &:not(.is-textless) + width: 6rem !important + &:after + padding-left: .3rem + content: 'Menù' + a.navbar-item + white-space: nowrap + color: $nav3-text-color + font-size: $nav3-font-size + &:hover, &.is-active + transition: all 0.5s ease + background: $nav3-background-color-hover + &:focus, &:active + background: transparent + span.icon + margin-right: .5rem + &.is-right + margin-left: .5rem + margin-right: 0 + &.is-hidden-menu + background: $nav3-background-color !important + div.navbar-dropdown + background: $nav3-background-color + border-top: .5rem + min-width: 12.2rem + a.navbar-item + font-size: $nav3-dropdown-font-size + hr.navbar-divider + background-color: $nav3-text-color + height: 1px + a.navbar-link + white-space: nowrap + color: $nav3-text-color + font-size: $nav3-font-size + background: $nav3-background-color + &:not(.is-arrowless)::after + border-color: $nav3-text-color + &:hover + background: transparent + &.is-active + background: $nav3-background-color-hover + + .nested.dropdown + font-size: .75rem + &:hover + background: $nav3-background-color-hover + &:hover > .dropdown-menu + display: block + .dropdown-trigger + a, button + color: $nav3-text-color + font-size: $nav3-dropdown-font-size + &:not(.is-arrowless):hover::after + content: ' >' + .icon + margin-right: 0.3rem + .dropdown-menu + top: -15px + margin-left: 100% + max-width: $nav3-dropdown-submenu-max-width + .dropdown-content + background: $nav3-background-color + margin-top: 0.4rem + .dropdown-item + font-size: $nav3-dropdown-submenu-font-size + color: $nav3-text-color + background: $nav3-background-color + &:hover + background: $nav3-background-color-hover + &.has-menu-left + .dropdown-trigger + a, button + color: $nav3-text-color + font-size: $nav3-dropdown-font-size + &:not(.is-arrowless):hover + &::before + content: '< ' + &::after + content: '' + .dropdown-menu + margin-left: $nav3-dropdown-submenu-margin-left diff --git a/app/assets/stylesheets/summary.sass b/app/assets/stylesheets/summary.sass new file mode 100644 index 0000000..00c2004 --- /dev/null +++ b/app/assets/stylesheets/summary.sass @@ -0,0 +1,2 @@ +.summary + padding: 0.3rem 0rem 1rem 0 \ No newline at end of file diff --git a/app/assets/stylesheets/swal2_custom.sass b/app/assets/stylesheets/swal2_custom.sass new file mode 100644 index 0000000..e10c706 --- /dev/null +++ b/app/assets/stylesheets/swal2_custom.sass @@ -0,0 +1,9 @@ +.swal2-html-container + text-align: left !important +.ss-content + z-index: 10100 !important +.swal2-close + background-color: $red !important + border-top-left-radius: 0px !important + border-bottom-right-radius: 0px !important + border-top-right-radius: 0px !important \ No newline at end of file diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb new file mode 100644 index 0000000..d672697 --- /dev/null +++ b/app/channels/application_cable/channel.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb new file mode 100644 index 0000000..0ff5442 --- /dev/null +++ b/app/channels/application_cable/connection.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Connection < ActionCable::Connection::Base + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 0000000..757e671 --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,95 @@ +class ApplicationController < ActionController::Base + protect_from_forgery with: :exception + layout :set_layout + before_action :set_locale + before_action :set_user, if: :user_signed_in? + + rescue_from CanCan::AccessDenied do |exception| + Rails.logger.debug "Access denied on #{exception.action} #{exception.subject.inspect}" + respond_to do |format| + format.turbo_stream { + flash.now[:error] = "Access denied on #{exception.action} #{exception.subject.inspect}" + render turbo_stream: turbo_stream.replace(:flashes, partial: "flashes") + } + format.html { redirect_to page_path('notallowed') } + end + end + + rescue_from ActionController::InvalidAuthenticityToken do |exception| + sign_out_user # Example method that will destroy the user cookies + end + + rescue_from ActiveRecord::RecordNotFound do + record_not_found! + end + + rescue_from RackCAS::ActiveRecordStore::Session do + access_denied! + end + + private + + # Select the current_user + # @return [class] User + def set_user + @user = current_user + end + + # Set locale from `params[:locale]`. + # If params[:locale] is unset or is not available, + # the method set the default locale + # @return [Symbol,String] new locale definition + def set_locale + I18n.locale = params[:locale] || I18n.default_locale + end + + # Render 404 page and stop the work + # @return [nil] + def record_not_found! + render partial: 'errors/404', status: 404 && return + end + + # Select the layout name based on request type: xhr request or other + # @return [String] the layout name + def set_layout + request.xhr? ? 'empty' : 'application' + end + + # Render 401 page and stop the work + # @return [nil] + def access_denied! + render partial: 'pages/notallowed', status: :unauthorized and return + end + + # {access_denied!} unless the request.xhr == true + # @return [nil] + def xhr_required! + access_denied! unless request.xhr? + end + + # Localize a fieldName if #obj is present + # @param [Text] field_label + # @param [Text] obj + # @return [String] localized + def t_field(field_label = nil, obj = '') + return '' if field_label.blank? + + case obj + when Class + I18n.t(field_label, scope: "activerecord.attributes.#{obj.class}", default: field_label).try(:capitalize) + when String + I18n.t(field_label, scope: "activerecord.attributes.#{obj}", default: field_label).try(:capitalize) + else + I18n.t(field_label, default: field_label).try(:capitalize) + end + end + + # Create callback with class error messages + # @param [Class] obj + # @param [Text] scope + # @return [String] errors localized messages + def write_errors(obj, scope: false) + obj.errors.map { |e| "#{t_field(e.attribute, scope || obj.class.table_name.singularize)} #{e.message}" }.join(', ') + end + +end diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/controllers/faqs_controller.rb b/app/controllers/faqs_controller.rb new file mode 100644 index 0000000..2563b15 --- /dev/null +++ b/app/controllers/faqs_controller.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +# This controller manage the views refering to {Faq} model +class FaqsController < ApplicationController + include Pagy::Backend + before_action :authenticate_editor! + load_and_authorize_resource + before_action :set_view + before_action :set_faq, only: %i[ show edit update destroy ] + + # GET /faqs or /faqs.json + def index; end + + def list + faqs + end + + # GET /faqs/1 or /faqs/1.json + def show + @view = params[:view] || '' + @current_ability.cannot(:manage, @faq) if @view == 'modal' + end + + # GET /faqs/new + def new + @faq = Faq.new + @sample = Faq.accessible_by(current_ability).find(Faq.accessible_by(current_ability).pluck(:id).sample) + end + + # GET /faqs/1/edit + def edit; end + + # POST /faqs or /faqs.json + def create + @faq = Faq.new(faq_params) + + if @faq.save + render turbo_stream: [ + turbo_stream.replace(:flashes, partial: "flashes"), + turbo_stream.update(:yield, partial: "faqs/index") + ] + else + flash.now[:error] = write_errors(@faq) + render turbo_stream: turbo_stream.replace(:flashes, partial: "flashes") + end + end + + # PATCH/PUT /faqs/1 or /faqs/1.json + def update + if @faq.update(faq_params) + flash.now[:success] = 'Modifica avvenuta con successo' + else + flash.now[:error] = write_errors(@faq) + end + render turbo_stream: turbo_stream.replace(:flashes, partial: "flashes") + end + + # DELETE /faqs/1 or /faqs/1.json + def destroy + + if @faq.destroy + flash.now[:success] = 'Cancellazione avvenuta con successo' + render turbo_stream: [ + turbo_stream.remove("faq_#{@faq.id}"), + turbo_stream.replace(:flashes, partial: "flashes") + ] + else + flash.now[:error] = write_errors(@faq) + render turbo_stream: turbo_stream.replace(:flashes, partial: "flashes") + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_faq + @faq = Faq.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def faq_params + params.require(:faq).permit(:title, :content, :structure, :counter, :approve, :evidence, :active, :visibility, :created_by, :updated_by, :approved_by, category_list: [], instructions: [], models: [], files: []) + end + + # Only allow a list of trusted parameters through. + def filter_params + params.fetch(:filter, {}).permit(:text, :view) + end + + # Set callback view + def set_view + @view = filter_params[:view] || '' + end + + # Set @pagy, @faqs for paginate all {User} + def faqs + @filters = filter_params + @page = params[:page] || 1 + faqs = Faq.accessible_by(current_ability) + faqs = faqs.where(structure: current_user.structure) if current_user.editor? + faqs = faqs.where("title ilike ?", "%#{@filters[:text]}%") if @filters[:text].present? + @pagy, @faqs = pagy(faqs, page: @page, link_extra: "data-turbo-frame='faqs'") + end + + # deny access unless current_user is an editor + def authenticate_editor! + redirect_to page_path('notallowed') unless user_signed_in? && (current_user.editor? || current_user.admin?) + end +end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb new file mode 100644 index 0000000..72ffda8 --- /dev/null +++ b/app/controllers/home_controller.rb @@ -0,0 +1,172 @@ +# frozen_string_literal: true + +# This controller manage the views refering to user +class HomeController < ApplicationController + include Pagy::Backend + before_action :authenticate_user!, only: %i[propose propose_send] + load_and_authorize_resource class: Faq + before_action :set_view, only: %i[propose show] + before_action :set_faq, only: %i[show favorite_create favorite_destroy] + before_action :get_user_faqs, only: %i[list favorite_list search_by_title], if: :user_signed_in? + + # GET /home/index + # + # render faqs + # @return [Object] render home/index + def index + @categories = ActsAsTaggableOn::Tag.most_used(Settings.faq.categories_on_top) + @faqs = Faq.accessible_by(current_ability).actived.approved.tagged_with(@categories, any: true) + @sample = Faq.accessible_by(current_ability).actived.approved.find(Faq.accessible_by(current_ability).actived.approved.pluck(:id).sample) + end + + # GET /home/list + # + # render user faqs + # @return [Object] render home/faqs + def list + @filters = filter_params + @page = params[:page] || 1 + faqs = Faq.accessible_by(current_ability).actived.approved + @evidences = faqs.evidenced.limit(Settings.faq.max_faqs_in_evidence) + @tops = faqs.approved.where('counter > 0').on_top(Settings.faq.most_requested) + if @filters[:text].present? || @filters[:category].present? + faqs = faqs.search_by_title(@filters[:text].gsub(/[^\w\s]+/, ' ').remove(/[^\w\s]+/)) if @filters[:text].present? + faqs = faqs.tagged_with(@filters[:category].remove(/[^\w\s]+/), any: true) if @filters[:category].present? + @faqs = faqs + end + flash.now[:success] = 'Caricamento completato' + end + + # GET /home/1/show + # + # render faq + # @return [Object] render home/show + def show + FaqCounterJob.perform_later(faq: @faq) + end + + # GET /:text + # + # render index + # @return [Object] render home/index + def search + @filters = filter_params + @filters[:text] = params[:text].gsub(/[^\w\s]+/, ' ').remove(/[^\w\s]+/) if params[:text].present? + flash.now[:success] = 'Caricamento completato' + render :index + end + + # GET /faq/:find_by_title + # + # render show + # @return [Object] render home/show + def search_by_title + @filters = filter_params + @filters[:text] = params[:text].gsub(/[^\w\s]+/, ' ').remove(/[^\w\s]+/) if params[:text].present? + faqs = Faq.accessible_by(current_ability) + @faq = faqs.find_by("LOWER(title) = ?", @filters[:text].gsub(/[^\w\s]+/, ' ').remove(/[^\w\s]+/).downcase) if @filters[:text].present? + FaqCounterJob.perform_later(faq: @faq) + render :show + end + + # GET /home/propose + # + # render propose + # @return [Object] render home/propose + def propose; end + + # GET /home/favorite + # + # render favorite + # @return [Object] render home/favorite + def favorite; end + + # GET /home/favorite_list + # + # render favorite_list + # @return [Object] render home/favorite_list + def favorite_list + @filters = filter_params + @page = params[:page] || 1 + @faqs = @user.faqs + @faqs = @faqs.where("title ilike '%?%'", @filters[:text]) if @filters[:text].present? + @pagy, @faqs = pagy(@faqs, page: @page, link_extra: "data-turbo-frame='faqs'") + end + + def favorite_create + if UserFaq.create(user_id: current_user.id, faq_id: @faq.id) + flash.now[:success] = 'Faq aggiunta tra i preferiti!' + get_user_faqs + else + flash.now[:success] = 'Si è verificato un errore durante la creazione della Faq nei preferiti!' + end + render turbo_stream: [ + turbo_stream.replace(:flashes, partial: "flashes"), + turbo_stream.replace("faq_#{@faq.id}", partial: "home/faq", locals: {faq: @faq, user_faq_ids: @user_faq_ids}) + ] + end + + + def favorite_destroy + user_faq = UserFaq.find_by(user_id: current_user.id, faq_id: @faq.id) + if user_faq.present? && user_faq.destroy + flash.now[:success] = 'Faq rimossa dai preferiti!' + get_user_faqs + else + flash.now[:success] = 'Si è verificato un errore durante la cancellazione della Faq dai preferiti!' + end + render turbo_stream: [ + turbo_stream.replace(:flashes, partial: "flashes"), + turbo_stream.replace("faq_#{@faq.id}", partial: "home/faq", locals: {faq: @faq, user_faq_ids: @user_faq_ids}) + ] + end + + # POST /home/propose + # + # render propose_send + # @return [Object] render home/propose_send + def propose_send + @author = propose_params[:author_id].present? ? User.find(propose_params[:author_id]) : '' + if @author.present? && propose_params[:title].present? + UserMailer.with(author: @author, propose: propose_params).propose_email.deliver_now + User.admins.map { |u| FaqMailer.with(user: u, author: @author, propose: propose_params).propose_email.deliver_now } + flash.now[:success] = 'Invio proposta completato' + else + flash.now[:error] = 'Non è stato possibile inviare la richiesta' if @author.blank? + flash.now[:error] = 'Il titolo è obbligatorio!' if propose_params[:title].blank? + end + render turbo_stream: turbo_stream.replace(:flashes, partial: "flashes") + end + + private + + # Use callbacks to share common setup or constraints between actions. + def set_faq + @faq = Faq.accessible_by(current_ability).find(params[:id]) + end + + def get_user_faqs + @user_faq_ids = current_user.faqs.ids + end + + # Only allow a list of trusted parameters through. + def faq_params + params.require(:faq).permit(:title) + end + + # Only allow a list of trusted parameters through. + def filter_params + params.fetch(:filter, {}).permit(:text, :category, :view) + end + + # Only allow a list of trusted parameters through. + def propose_params + params.fetch(:propose, {}).permit(:author_id, :title, :text, :visibility, :structure) + end + + # Set callback view + def set_view + @view = filter_params[:view] || '' + end + +end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb new file mode 100644 index 0000000..937b244 --- /dev/null +++ b/app/controllers/users_controller.rb @@ -0,0 +1,182 @@ +# frozen_string_literal: true + +# This controller manage the views refering to {User} model +# === before_action :{set_user}, only: [:show, :edit, :update, :destroy, unlock, trash} +class UsersController < ApplicationController + include Pagy::Backend + load_and_authorize_resource + before_action :authenticate_admin! + before_action :set_user, only: %i[show edit update destroy unlock trash] + + # GET /users + # + # render users index + # set @pagy, @users for the users pagination + # @return [Object] render users/index + def index; end + + # GET /users/list + # + # render users list + # set @pagy, @users for the users pagination + # @return [Object] render users/list + def list + users + flash.now[:success] = 'Caricamento completato' + end + + # GET /users/1 + # + # render users show + # @return [Object] render users/show + def show + @view = params[:view] || '' + @current_ability.cannot(:manage, @user) if @view == 'modal' + end + + # GET /users/new + # + # Render the form for create a new user + # set @user as new {User} + # @return [Object] rendeer users/new + def new + @user = User.new + end + + # GET /users/1/edit + # + # Render the form for edit a user + # {set_user} has set @user for edit + # @return [Object] render users/edit + def edit; end + + # POST /users + # + # Create a new User + # set @user as new {User} with {user_params} as param + # @return [Object] render users/index if @user is created or users/new if fail + def create + @user = User.new(user_params) + if @user.save + UsersCheckJob.perform_now(username: @user.username) + flash.now[:success] = 'Creazione avvenuta con successo' + render turbo_stream: [ + turbo_stream.replace(:flashes, partial: "flashes"), + turbo_stream.replace(:yield, partial: "users/show", locals: { user: @user.reload, tab: @tab }) + ] + else + flash.now[:error] = write_errors(@user) + render turbo_stream: turbo_stream.replace(:flashes, partial: "flashes") + end + end + + # PATCH/PUT /users/1 + # + # update a user + # {set_user} has set @user for update + # and update with {user_params} as param + # @return [Object] render users/index if @user is updated or users/new if fail + def update + if @user.update(user_params) + flash.now[:success] = 'Modifica avvenuta con successo' + else + flash.now[:error] = write_errors(@user) + end + render turbo_stream: turbo_stream.replace(:flashes, partial: "flashes") + end + + # DELETE /users/1 + # + # lock a user + # + # * {set_user} has set @user for destroy + # @return [Object] render users/list + def destroy + if @user.disable! + flash.now[:success] = 'Disattivazione avvenuta con successo' + render turbo_stream: [ + turbo_stream.replace("user_#{@user.id}", partial: 'users/user', locals: {user: @user, current_user: current_user}), + turbo_stream.replace(:flashes, partial: "flashes") + ] + else + flash.now[:error] = write_errors(@user) + format.turbo_stream { render turbo_stream: turbo_stream.replace(:flashes, partial: "flashes") } + end + end + + # PATCH /users/1/unlock + # + # unlock a user + # + # * {set_user} has set @user for unlock + # @return [Object] render users/list + def unlock + if @user.enable! + flash.now[:success] = 'Riattivazione avvenuta con successo' + render turbo_stream: [ + turbo_stream.replace("user_#{@user.id}", partial: 'users/user', locals: {user: @user, current_user: current_user}), + turbo_stream.replace(:flashes, partial: "flashes") + ] + else + flash.now[:error] = write_errors(@user) + render turbo_stream: turbo_stream.replace(:flashes, partial: "flashes") + end + end + + # DELETE /users/1/trash + # + # destroy a user + # + # * {set_user} has set @user for destroy + # @return [Object] render users/list + def trash + @user.forced = 'true' + if @user.destroy + flash.now[:success] = 'Cancellazione avvenuta con successo' + render turbo_stream: [ + turbo_stream.remove("user_#{@user.id}"), + turbo_stream.replace(:flashes, partial: "flashes") + ] + else + flash.now[:error] = write_errors(@user) + render turbo_stream: turbo_stream.replace(:flashes, partial: "flashes") + end + end + + private + + # Use callbacks to share common setup or constraints between actions. + def set_user + @user = User.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def user_params + params.fetch(:user, {}).permit(:username, :label, :email, :structure, :admin, :editor) + end + + # Only allow a list of trusted parameters through. + def filter_params + params.fetch(:filter, {}).permit(:text) + end + + # Only allow a list of trusted parameters through. + def sort_params + params.fetch(:sort, {}).permit(:label, :username, :last_sign_in_at, :admin, :editor, :locked_at) + end + + # Set @pagy, @users for paginate all {User} + def users + @filters = filter_params + @sorted = sort_params + @page = params[:page] || 1 + users = User.all + users = users.where("label ilike ? or username ilike ? or email ilike ?", "%#{@filters[:text]}%", "%#{@filters[:text]}%", "%#{@filters[:text]}%") if @filters[:text].present? + @pagy, @users = pagy(users, page: @page, link_extra: "data-turbo-frame='users'") + end + + # deny access unless current_user is an editor + def authenticate_admin! + access_denied! unless user_signed_in? && current_user.admin? + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100644 index 0000000..734ed81 --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +# This helper contain the methods shared for all views +# +# * include Pagy::Frontend +module ApplicationHelper + include Pagy::Frontend + + # make a div for the font-awesome icons + # @param [String] fa_style style of icon + # @param [String] span_style other style for container + # @param [String] style extra css params + # @param [String] text inside container + # @param [String] tooltip on mouseover + # @param [String] title other mouseover tooltip + # @return [String] + def fas_icon(fa_style, span_style: nil, style: false, text: '', tooltip: false, title: '') + content_tag_i = tag.i('', class: "fas fa-#{fa_style}", aria: { hidden: 'true' }) + span = if tooltip.present? + tag.span(content_tag_i, class: "icon #{span_style}", style: style, title: title, data: { tooltip: tooltip }) + else + tag.span(content_tag_i, class: "icon #{span_style}", style: style, title: title) + end + return span if text.blank? + + span + tag.span(text) + end + + # generate a list for a select from an enum + # @param list [Hash], enum option list, default {} + # @param scope [String] scope of localization, default '' + # @return [List] + def t_enum(list = {}, scope = '') + list.map { |k, _| [t(k, scope: scope), k] } + end + + # Localize a DateTime with format :long if #obj is present + # @param [DateTime] obj + # @return [String] localized and formatted date + def l_long(obj = nil) + l(obj.try(:to_time), format: :long) if obj.present? + end + + # Localize a DateTime with format :time if #obj is present + # @param [DateTime] obj + # @return [String] localized and formatted date + def l_time(obj = nil) + l(obj.try(:to_time), format: :time) if obj.present? + end + + # Localize a DateTime with format :date if #obj is present + # @param [DateTime,Date] obj + # @return [String] localized and formatted date + def l_date(obj = nil) + obj.present? ? l(obj.try(:to_date), format: :date) : '-' + end + + # Localize a fieldName if #obj is present + # @param [Text] field_label + # @param [Text] obj + # @return [String] localized + def t_field(field_label = nil, obj = '') + return '' if field_label.blank? + + case obj + when Class + t(field_label, scope: "activerecord.attributes.#{obj.class}") + when String + t(field_label, scope: "activerecord.attributes.#{obj}") + else + t(field_label, default: field_label) + end + end + + # Convert string into Math formula + # Require MathJax.js + # @param [String] text + # @return [String] localized + def t_formula(text = '') + text.present? ? tag.span("$#{ActionView::Base.full_sanitizer.sanitize(text)}$", class: 'is-inline is-formula') : '-' + end + + # @param [String] text + # @return [String] + def t_value(text = '') + text.presence || '-' + end + + # Format qta into Integer if unit is equal pz + # @param [Integer] qta + # @param [String] unit default 'pz' + # @param [Bool] with_unit default true + # @return [String] + def format_qta(qta: 0, unit: 'pz', with_unit: true) + new_qta = if unit == 'pz' + qta.try(:to_i) + else + strip_trailing_zero(qta) + end + "#{new_qta}#{unit if with_unit}" + end + + # Remove zeros + # @param [Integer] number + # @return [String] + def strip_trailing_zero(number, precision: 2) + # number.to_s.sub(/\.?0+$/, '') + number_with_precision(number, precision: precision, strip_insignificant_zeros: true) + end + + # Remove HTML tags + # @param [String] text + # @return [String] + def to_plain_text(body) + Nokogiri::HTML(body).text.strip + end + + # Render errors form + # @param [Class] model resource + # @return [String] with error tags + def form_errors_for(resource) + errors_for(resource.errors, scope: resource.class.table_name.singularize) if resource.errors.present? + rescue + '' + end + + # Render errors + # @param [Class] model resource + # @return [String] with error tags + def errors_for(errors, scope: '') + errors.map { |e| "#{t_field(e.attribute, scope)} #{e.message}" } + end +end diff --git a/app/javascript/application.js b/app/javascript/application.js new file mode 100644 index 0000000..06f3a6d --- /dev/null +++ b/app/javascript/application.js @@ -0,0 +1,10 @@ +// Entry point for the build script in your package.json +import "@hotwired/turbo-rails" +import "./controllers" + +require("@rails/ujs").start() +require("./awesome.js") + +require("trix") + + diff --git a/app/javascript/awesome.js b/app/javascript/awesome.js new file mode 100644 index 0000000..ad80f9e --- /dev/null +++ b/app/javascript/awesome.js @@ -0,0 +1,102 @@ +//import '@fortawesome/fontawesome-free/js/solid' +import { config, library, dom } from '@fortawesome/fontawesome-svg-core' + +// Change the config to fix the flicker +config.mutateApproach = 'sync' + +// Import required icons +import { + faBan, + faBars, + faBook, + faBug, + faCheck, + faChevronDown, + faCog, + faCogs, + faDownload, + faEdit, + faExclamationCircle, + faExternalLinkAlt, + faFilter, + faHome, + faImage, + faImages, + faInfoCircle, + faLaptop, + faLink, + faList, + faLock, + faPlus, + faPrint, + faQuestionCircle, + faLifeRing, + faSave, + faSearch, + faSearchPlus, + faSignInAlt, + faSignOutAlt, + faSort, + faSortDown, + faSortUp, + faStar, + faTimes, + faTrash, + faUnlock, + faUpload, + faUser, + faUserCog, + faUsers, + faWarningCircle +} from '@fortawesome/free-solid-svg-icons' + +import { faStar as farFaStar } from '@fortawesome/free-regular-svg-icons' + +// add incons to library +library.add( + faBan, + faBars, + faBook, + faBug, + faCheck, + faChevronDown, + faCog, + faCogs, + faDownload, + faEdit, + faExclamationCircle, + faExternalLinkAlt, + faFilter, + faHome, + faImage, + faImages, + faInfoCircle, + faLaptop, + faLink, + faList, + faLock, + faPlus, + faPrint, + faQuestionCircle, + faLifeRing, + faSave, + faSearch, + faSearchPlus, + faSignInAlt, + faSignOutAlt, + faSort, + faSortDown, + faSortUp, + faStar, + farFaStar, + faTimes, + faTrash, + faUnlock, + faUpload, + faUser, + faUserCog, + faUsers +) + +// watch hatml +dom.watch() diff --git a/app/javascript/controllers/application.js b/app/javascript/controllers/application.js new file mode 100644 index 0000000..c32b3d2 --- /dev/null +++ b/app/javascript/controllers/application.js @@ -0,0 +1,18 @@ +import "@hotwired/turbo-rails" +import { Application } from "@hotwired/stimulus" +import * as ActiveStorage from '@rails/activestorage' +import SlimSelect from 'slim-select' + +const application = Application.start() +ActiveStorage.start() + +document.addEventListener('trix-file-accept', function(event) { event.preventDefault(); }); +document.querySelectorAll('[disabled]').forEach(function(obj) { + return obj.classList.add('is-disabled'); +}); + +// Configure Stimulus development experience +application.debug = true +window.Stimulus = application + +export { application } diff --git a/app/javascript/controllers/form_controller.coffee.erb b/app/javascript/controllers/form_controller.coffee.erb new file mode 100644 index 0000000..acea6a3 --- /dev/null +++ b/app/javascript/controllers/form_controller.coffee.erb @@ -0,0 +1,83 @@ +import { Controller } from "@hotwired/stimulus" +import Rails from '@rails/ujs' +import Timeout from 'smart-timeout' +import SlimSelect from 'slim-select' +import Swal from 'sweetalert2' + +export default class extends Controller + @targets = ['form', 'slimselect'] + + connect: => + # Per disattivare l'evento click dei bottoni dopo il passaggio a Bulma + # che non ha la gestione eventi via Javascript + document.querySelectorAll('[disabled]').forEach (obj) -> + obj.classList.add('is-disabled') + if this.hasSlimselectTarget + this.slimselectTargets.forEach (slim) => + this.slimSelect slim + + send: (event) => + Rails.fire(event.target.closest('form'), 'submit') + + delayedSend: (event) => + if Timeout.exists('textDelay') + Timeout.set( 'textDelay', true ) + Timeout.set('textDelay', (() => this.send(event)), 750) + + reset: (event) => + Rails.fire(event.target.closest('form'), 'reset') + + close: (event) => + Swal.close() + + toggleTagActive: (event) => + tags_container = event.target.closest('.field') + tags = tags_container.querySelectorAll('.tag') + tags.forEach (tag) => + if event.target.dataset.type != 'reset' + tag.classList.remove('is-info') + if event.target.dataset.type != 'reset' + event.target.closest('.tag').classList.add('is-info') + if tags[tags.length - 1].dataset.type == 'reset' + tags[tags.length - 1].classList.remove('is-hidden') + + desactiveTagFilter: (event) => + tags_container = event.target.closest('.field') + tags = tags_container.querySelectorAll('.tag') + tags.forEach (tag) => + tag.classList.remove('is-info') + if tags[tags.length - 1].dataset.type == 'reset' + tags[tags.length - 1].classList.add('is-hidden') + + toggleVisible: (event) => + document.getElementById(event.currentTarget.dataset.id).classList.toggle('is-hidden') + if event.currentTarget.querySelector('i.fas') + event.currentTarget.querySelector('i.fas').classList.toggle('fa-chevron-down') + + slimSelect: (select) -> + if select.dataset.formAddable == 'true' + new SlimSelect + addToBody: true, + select: "##{select.id}", + searchingText: 'Ricerca in corso...', + searchText: 'Nessun record trovato', + searchPlaceholder: 'Cerca', + placeholder: 'Seleziona uno o più valori', + text: 'Seleziona uno o più valori', + closeOnSelect: if select.dataset.formCloseonselect == 'true' then true else false + addable: (value) => + displayData = [] + if value == '' + this.send 'Inserire un valore prima di cliccare sul bottone', 'error' + else + return value + else + new SlimSelect + addToBody: true, + select: "##{select.id}", + searchingText: 'Ricerca in corso...', + searchText: 'Nessun record trovato', + searchPlaceholder: 'Cerca', + placeholder: 'Seleziona uno o più valori', + text: 'Seleziona uno o più valori' + diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js new file mode 100644 index 0000000..fb7f9fd --- /dev/null +++ b/app/javascript/controllers/index.js @@ -0,0 +1,28 @@ +// This file is auto-generated by ./bin/rails stimulus:manifest:update +// Run that command whenever you add a new controller or create them with +// ./bin/rails generate stimulus controllerName + +import { application } from "./application" + +import FormController from "./form_controller.coffee.erb" +application.register("form", FormController) + +import MenuController from "./menu_controller.coffee.erb" +application.register("menu", MenuController) + +import MessageController from "./message_controller.coffee.erb" +application.register("message", MessageController) + +import ModalController from "./modal_controller.coffee.erb" +application.register("modal", ModalController) + +import { AttachmentUpload } from "@rails/actiontext/app/javascript/actiontext/attachment_upload" + +addEventListener("trix-attachment-add", event => { + const { attachment, target } = event + + if (attachment.file) { + const upload = new AttachmentUpload(attachment, target) + upload.start() + } +}) \ No newline at end of file diff --git a/app/javascript/controllers/menu_controller.coffee.erb b/app/javascript/controllers/menu_controller.coffee.erb new file mode 100644 index 0000000..8c98f0b --- /dev/null +++ b/app/javascript/controllers/menu_controller.coffee.erb @@ -0,0 +1,23 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller + + open: (event) => + menuId = event.target.dataset.menuId + console.log menuId + if menuId + menu = document.getElementById(menuId) + if menu + menu.classList.toggle('is-active') + + close: (event) => + menuId = event.target.dataset.menuId + if menuId + menu = document.getElementById(menuId) + if menu + menu.classList.remove('is-active') + + focus: (event) => + if event.currentTarget.dataset.menuId + target = document.getElementById(event.currentTarget.dataset.menuId) + target.scrollIntoView() if target diff --git a/app/javascript/controllers/message_controller.coffee.erb b/app/javascript/controllers/message_controller.coffee.erb new file mode 100644 index 0000000..8499933 --- /dev/null +++ b/app/javascript/controllers/message_controller.coffee.erb @@ -0,0 +1,32 @@ +import { Controller } from "@hotwired/stimulus" +import Swal from 'sweetalert2' + +export default class extends Controller + @targets = ['text'] + + connect: => + if this.hasTextTarget + this.send this.textTarget.innerHTML, this.element.dataset.messageStatus || 'success' + this.element.outerHTML = '' + + disconnect: => + this.outerHTML = '' + + send: (message, level = 'success', timeout = 2000, toast = true) -> + options = { + toast: if level == 'error' then false else toast + icon: level + timerProgressBar: true + position: if level == 'error' then 'center' else 'top-end' + text: message + timer: if level == 'error' then false else timeout + showConfirmButton: if level == 'error' then true else false + didOpen: (toast) => + toast.addEventListener('mouseenter', Swal.stopTimer) + toast.addEventListener('mouseleave', Swal.resumeTimer) + showClass: + popup: if level == 'error' then '' else 'animate__animated animate__bounceInRight' + hideClass: + popup: if level == 'error' then '' else 'animate__animated animate__bounceOutRight' + } + Swal.fire(options) \ No newline at end of file diff --git a/app/javascript/controllers/modal_controller.coffee.erb b/app/javascript/controllers/modal_controller.coffee.erb new file mode 100644 index 0000000..ca4dc3a --- /dev/null +++ b/app/javascript/controllers/modal_controller.coffee.erb @@ -0,0 +1,44 @@ +import { Controller } from "@hotwired/stimulus" +import Swal from 'sweetalert2' + +export default class extends Controller + + connect: => + body = this.element.innerHTML + if body + this.send body + this.element.outerHTML = '' + + disconnect: => + this.outerHTML = '' + + send: (body = '') => + width = this.element.dataset.modalSize || '90%' + position = this.element.dataset.modalPosition || 'top' + options = { + width: width + heightAuto: true + height: true + toast: false + icon: false + timerProgressBar: false + position: position + title: false + html: body + footer: false + timer: false + showConfirmButton: false + showCloseButton: true + showCancelButton: false + cancelButtonText: "<%= I18n.t( 'close' ) %>" + showClass: + popup: 'animated fadeIn' + hideClass: + popup: '' + } + if Swal.isVisible() + Swal.update(options) + else + Swal.fire(options) + if Swal.getPopup().querySelector('.is-formula') + MathJax.typeset() diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 0000000..d394c3d --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,7 @@ +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/app/jobs/faq_counter_job.rb b/app/jobs/faq_counter_job.rb new file mode 100644 index 0000000..9749be7 --- /dev/null +++ b/app/jobs/faq_counter_job.rb @@ -0,0 +1,7 @@ +class FaqCounterJob < ApplicationJob + queue_as :default + + def perform(faq: '') + faq.update(counter: faq.counter + 1) if faq.present? + end +end diff --git a/app/jobs/users_check_job.rb b/app/jobs/users_check_job.rb new file mode 100644 index 0000000..eb599da --- /dev/null +++ b/app/jobs/users_check_job.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +# This job contain the methods for sync users with REST-API json +class UsersCheckJob < ApplicationJob + queue_as :urgent + require 'open-uri' + + # get users data from remote api. + # For each user received run {set_data} + def perform(username: '') + return if Rails.env.test? + + users = User.all.order(label: :asc) + users = users.where(username: username) if username.present? + uri = URI.parse(Settings.api.url) + opts = { + http_basic_authentication: [ + Rails.application.credentials.api[:user] || Settings.api.username.to_s, + Rails.application.credentials.api[:secret_access_key] || Settings.api.secret_access_key.to_s + ], + ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE + } + users.each do |user| + next if user.username.blank? + + uri.query = "login=#{user.username}&struttura=true" + json_data = URI.parse(uri.to_s).open(opts).read + user_data = JSON.parse(json_data) + set_data(user, user_data) + uri.query = '' + end + end + + # update a user with api data + # @param [Object] user istance of user to update + # @param [Hash] data all user's data from the api. Default: {} + # @return [Boolean] true if user is updated + def set_data(user, data = {}) + return if data.blank? + + user.label = data['nominativo'].presence || [user.username.split('.').second, user.username.split('.').first].join(' ').titleize + user.email = data['email'] + user.structure = data['assegnazione'] + user.responsabile = data['struttura']['ufficio']['responsabile']['nominativi'] + user.locked_at = Time.zone.today if !user.locked_at? && data['stato'] == 'scaduto' + user.save + end +end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 0000000..5525841 --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: Settings.bug.email + layout 'mailer' +end diff --git a/app/mailers/faq_mailer.rb b/app/mailers/faq_mailer.rb new file mode 100644 index 0000000..94d36ae --- /dev/null +++ b/app/mailers/faq_mailer.rb @@ -0,0 +1,8 @@ +class FaqMailer < ApplicationMailer + def propose_email + @user = params[:user] + @author = params[:author] + @propose = params[:propose] + mail(to: email_address_with_name(@user.email, @user.label), subject: "[Faq] Inviata nuova proposta") + end +end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb new file mode 100644 index 0000000..65210d0 --- /dev/null +++ b/app/mailers/user_mailer.rb @@ -0,0 +1,7 @@ +class UserMailer < ApplicationMailer + def propose_email + @author = params[:author] + @propose = params[:propose] + mail(to: email_address_with_name(@author.email, @author.label), subject: "[Faq] Inviata nuova proposta") + end +end diff --git a/app/models/ability.rb b/app/models/ability.rb new file mode 100644 index 0000000..6f082ff --- /dev/null +++ b/app/models/ability.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class Ability + include CanCan::Ability + + def initialize(user) + # Define abilities for the passed in user here. For example: + + # Guest user can read, list and propose a Faq if this is approved, actived and has a public visibility + can %i[read list search search_by_title], Faq, approve: true, active: true, visibility: 0 + # can :create, Faq, approve: false + if user.present? + can %i[propose propose_send favorite favorite_list favorite_create favorite_destroy], Faq + # Auhenticated user can has all guest ability and can read and list a Faq if this is approved, actived and has a restricted visibility + can %i[read list], Faq, approve: true, active: true, visibility: 1 + # Auhenticated user can has all guest ability and can read and list a Faq if this is approved, actived and has a restricted visibility for user structure + can %i[read list], Faq, approve: true, active: true, visibility: 2, structure: user.structure + if user.admin? + # Admin user can manage all Models + can :manage, :all + elsif user.editor? + # Editor user can only manage the faqs of his structure + can :manage, Faq, structure: user.structure + end + end + end +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 0000000..e2b6a20 --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# This model contain the methods shared for all models +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/models/faq.rb b/app/models/faq.rb new file mode 100644 index 0000000..f31d9c7 --- /dev/null +++ b/app/models/faq.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +# {Faq} is model responsible for storing faq informations. +# +# @!attribute [rw] title +# @return [String] Title of {Faq} +# @!attribute [rw] content +# @return [Rich Text] content of {Faq} +# @!attribute [rw] structure +# @return [CiText] structure of {Faq} +# @!attribute [rw] counter +# @return [Integer] counter number of requests of {Faq} +# @!attribute [rw] visibility +# @return [Integer] visibility enum with security visibility of {Faq} +# @!attribute [rw] approve +# @return [Boolean] if {Faq} is approved +# @!attribute [rw] evidence +# @return [Boolean] if {Faq} is in evidence +# @!attribute [rw] active +# @return [Boolean] if {Faq} is active +# @!attribute [rw] metadata +# @return [JSONB] content of {Faq} +# @!attribute [rw] created_at +# @return [DateTime] date when {Faq} was created +# @!attribute [rw] updated_at +# @return [DateTime] date when {Faq} was updated +# +# === Relations +# +# * acts_as_taggable_on :categories +# * has_rich_text :content +# * has_many_attached :files +# * has_many_attached :instructions +# * has_many_attached :models +# * has_many :favorites, class_name: 'UserFaq', inverse_of: :faq +# * has_many :users, through: :favorites, source: :user +# +# === Validations +# +# * validate presence of {title} +# * validate presence of {content} +# * validate presence of {categories} +# +class Faq < ApplicationRecord + include PgSearch::Model + acts_as_taggable_on :categories + has_rich_text :content + has_many_attached :files + has_many_attached :instructions + has_many_attached :models + has_many :favorites, class_name: 'UserFaq', inverse_of: :faq + has_many :users, through: :favorites, source: :user + + enum visibility: Settings.faq.visibility.to_hash + + store_accessor :metadata, :created_by, :updated_by, :approved_by + + scope :actived, -> { where(active: true) } + scope :unactived, -> { where(active: false) } + scope :approved, -> { where(approve: true) } + scope :unapproved, -> { where(approve: false) } + scope :evidenced, -> { where(evidence: true) } + scope :on_top, ->(top = Settings.faq.top) { actived.approved.where("counter >= ?", 1).order(counter: :desc).limit(top) } + pg_search_scope :search_by_title, + against: :title, + ignoring: :accents, + using: { + tsearch: { prefix: true, normalization: 2, any_word: true }, + trigram: { threshold: 0.5, word_similarity: true }, + dmetaphone: {} + }, + order_within_rank: "faqs.counter DESC" + + before_validation :prerequisite + validates :title, presence: true, uniqueness: true + # validates :content, presence: true + validates :structure, presence: true + validates :visibility, presence: true + validates_inclusion_of :visibility, in: visibilities.keys, + message: "state must be one of #{visibilities.keys}" + validate :must_have_valid_categories + + private + + def prerequisite + self.created_by ||= Settings.system.username if created_by.blank? && new_record? + self.updated_by ||= Settings.system.username + end + + def must_have_valid_categories + errors.add(:category_list, I18n.t('is_required', scope: '', default: "is required!")) if category_list.blank? && !Rails.env.test? + end + +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000..b57679a --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +# {User} is model responsible for storing user informations. +# +# @!attribute [rw] sign_in_count +# @return [Integer] Counter of {User} sign in +# @!attribute [rw] current_sign_in_at +# @return [DateTime] date of {User} current sign in +# @!attribute [rw] last_sign_in_at +# @return [DateTime] date of {User} last sign in +# @!attribute [rw] current_sign_in_ip +# @return [String] ip of {User} current sign in +# @!attribute [rw] last_sign_in_ip +# @return [String] ip of {User} last sign in +# @!attribute [rw] locked_at +# @return [Datetime] date when {User} was blocked +# @!attribute [rw] username +# @return [String] unique {User} name +# @!attribute [rw] label +# @return [String] long {User} name +# @!attribute [rw] admin +# @return [Boolean[ true if {User} is a admin otherwise false +# @!attribute [rw] created_at +# @return [DateTime] date when {User} was created +# @!attribute [rw] updated_at +# @return [DateTime] date when {User} was updated +# +# === Relations +# +# === Validations +# +# * validate presence of {username} +# * validate presence of {label} +# +# === Before destroy +# {abort_destroy} +# +# +# @!method locked() +# @return [Bool] if {User} with {locked_at} present +class User < ApplicationRecord + store_accessor :metadata, :matr, :responsabile, :data_aggiornamento + attr_accessor :forced + + devise Settings.auth.devise, :trackable, :timeoutable, :lockable + + default_scope { order(:label) } + scope :avaibilities, -> { where(locked_at: nil) } + scope :locked, -> { where.not(locked_at: nil) } + scope :admins, -> { where(admin: true) } + + has_many :favorites, class_name: 'UserFaq', inverse_of: :user + has_many :faqs, through: :favorites, source: :faq + + before_validation :presequisite + before_destroy :abort_destroy, unless: :forced? + validates :username, presence: true, uniqueness: true + validates :structure, presence: true + after_create_commit {UsersCheckJob.perform_now(username: username) unless Rails.env.test?} + + # update self with {locked_at} as Time.zone.now + # @return [Boolean] true if is updated + def disable! + update(locked_at: Time.zone.now) + end + + # update self with {locked_at} as nil + # @return [Boolean] true if is updated + def enable! + update(locked_at: nil) + end + + def locked? + locked_at? + end + + private + + def presequisite + self.label = username if username.blank? + end + + # is executed before destroy, add an error to :base and abort the action + # @return [False] + def abort_destroy + errors.add :base, 'Can\'t be destroyed' + throw :abort + end + + def forced? + forced == 'true' + end +end diff --git a/app/models/user_faq.rb b/app/models/user_faq.rb new file mode 100644 index 0000000..30eadcc --- /dev/null +++ b/app/models/user_faq.rb @@ -0,0 +1,6 @@ +class UserFaq < ApplicationRecord + belongs_to :user, class_name: 'User', foreign_key: :user_id, primary_key: :id, required: true + belongs_to :faq, class_name: 'Faq', foreign_key: :faq_id, primary_key: :id, required: true + + validates_uniqueness_of :faq_id, scope: :user_id +end diff --git a/app/views/active_storage/blobs/_blob.html.haml b/app/views/active_storage/blobs/_blob.html.haml new file mode 100644 index 0000000..3a4e99d --- /dev/null +++ b/app/views/active_storage/blobs/_blob.html.haml @@ -0,0 +1,15 @@ +-# + %figure{class: "attachment attachment--#{blob.representable? ? "preview" : "file"} attachment--#{blob.filename.extension}"} + - if blob.representable? + = image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 100, 100 ] : [ 200, 200 ]) + %figcaption.attachment__caption + - if caption = blob.try(:caption) + = caption + - else + %span.attachment__name= blob.filename + %span.attachment__size= number_to_human_size blob.byte_size + - else + %a{ href: rails_blob_path(blob), download: true, download: blob.filename, target: "_blank" }= blob.filename + %br +%a.button.is-ghost{ href: rails_blob_path(blob), download: true, download: blob.filename, target: "_blank" }= blob.filename +%br \ No newline at end of file diff --git a/app/views/application/_error.html.haml b/app/views/application/_error.html.haml new file mode 100644 index 0000000..1f897ed --- /dev/null +++ b/app/views/application/_error.html.haml @@ -0,0 +1,9 @@ +#error.modal.modal-container{ data: { modal_target: 'container', erase: 'false' } } + .modal-background + .modal-card + %header.modal-card-head.has-background-danger + %p.modal-card-title.has-text-white + %i.fas.fa-exclamation-triangle.mr-3 + ATTENZIONE + %button.delete{ data: { controller: 'modal', action: 'click->modal#close' }, aria: { label: 'close' } } + %section.modal-card-body E' stato rilevato un errore di trasmissione, ricaricare la pagina e riprovare.
Se il problema persiste, contattare il supporto tecnico. \ No newline at end of file diff --git a/app/views/application/_files.html.haml b/app/views/application/_files.html.haml new file mode 100644 index 0000000..88c52f0 --- /dev/null +++ b/app/views/application/_files.html.haml @@ -0,0 +1,19 @@ +.content.mt-4 + - if faq.instructions.attached? + .tags + .subtitle.is-6.mb-2.mr-3= t_field 'instructions', 'faq' + - faq.instructions.each do |file| + %span.tag + %a{ href: rails_blob_path(file), download: true, download: file.filename, target: "_blank" }= file.filename + - if faq.models.attached? + .tags + .subtitle.is-6.mb-2.mr-3= t_field 'models', 'faq' + - faq.models.each do |file| + %span.tag + %a{ href: rails_blob_path(file), download: true, download: file.filename, target: "_blank" }= file.filename + - if faq.files.attached? + .tags + .subtitle.is-6.mb-2.mr-3= t_field 'files', 'faq' + - faq.files.each do |file| + %span.tag + %a{ href: rails_blob_path(file), download: true, download: file.filename, target: "_blank" }= file.filename \ No newline at end of file diff --git a/app/views/application/_flashes.html.haml b/app/views/application/_flashes.html.haml new file mode 100644 index 0000000..42a5ebc --- /dev/null +++ b/app/views/application/_flashes.html.haml @@ -0,0 +1,6 @@ +%turbo-frame#flashes + .container.is-hidden{data: {controller: 'message', message_status: flash.now[:error].present? ? 'error' : 'success'}} + - if flash.now[:error].present? + %span{data: {message_target: 'text'}}= flash.now[:error] + - elsif flash.now[:success].present? + %span{data: {message_target: 'text'}}= flash.now[:success] diff --git a/app/views/application/_menu.html.haml b/app/views/application/_menu.html.haml new file mode 100644 index 0000000..4cbc225 --- /dev/null +++ b/app/views/application/_menu.html.haml @@ -0,0 +1,27 @@ +%turbo-frame#menu + #menu_col.quickview.is-left + %header.quickview-header + %p.title.has-text-weight-bold= fas_icon('bars', text: 'Menù' ) + %span.delete{ data: { dismiss: "quickview", controller: 'menu', menu_id: 'menu_col', action: 'click->menu#close' } } + .quickview-body.pt-4 + .quickview-block + %aside.menu + %ul.menu-list + %li + %p.menu-label= fas_icon 'user-cog', text: user_signed_in? ? current_user.label : t('visitor', scope: 'user', default: 'Visitor') + %ul.menu-list + %li + %a.navbar-item{href: page_path('manual'), data: {turbo_frame: 'yield'}}= fas_icon 'book', text: 'Manuale' + %li + %a.navbar-item{href: page_path('notifications'), data: {turbo_frame: 'yield'}}= fas_icon 'bug', text: Settings.bug.title + - if current_user.editor? || current_user.admin? + %li + %a.navbar-item{href: faqs_path, data: {turbo_frame: 'yield'}}= fas_icon 'faq-circle', text: t('manage', scope: 'faq', default: 'Manage faqs') + - if current_user.admin? + %li + %a.navbar-item{href: users_path, data: {turbo_frame: 'yield'}}= fas_icon 'users', text: t('manage', scope: 'user', default: 'Manage users') + %footer.quickview-footer + - if user_signed_in? + %a{href: destroy_user_session_path, data: {method: :delete, confirm: "Are you sure?"}}= fas_icon('sign-out-alt', text: t('logout', default: 'Logout')) + - else + %a{href: new_user_session_path}= fas_icon('sign-in-alt', text: t('login', default: 'Login')) diff --git a/app/views/application/_nav_3.html.haml b/app/views/application/_nav_3.html.haml new file mode 100644 index 0000000..4ad733b --- /dev/null +++ b/app/views/application/_nav_3.html.haml @@ -0,0 +1,41 @@ +#nav3 + .container + %nav.navbar{role: 'navigation', aria_label: 'main navigation'} + .navbar-brand + %a.navbar-item.has-text-weight-bold.is-italic.is-size-5{href: root_path, data: {turbo_frame: 'yield'}} + %span.icon + %i.fas.fa-search.fa-fw + %span D + omando + %a.button.is-primary.navbar-burger.is-hidden-menu.navbar-item.is-radiusless.is-hidden-desktop.is-textless{ data: { controller: 'menu', action: 'click->menu#open', menu_id: 'menu_col' } } + %span.has-background-white{ aria_hidden: 'true' } + %span.has-background-white{ aria_hidden: 'true' } + %span.has-background-white{ aria_hidden: 'true' } + .navbar-menu + .navbar-start + - if user_signed_in? + %a.navbar-item{href: favorite_home_index_path, data: {turbo_frame: 'yield'}} + %i.fas.fa-star.mr-2 + = t('favorite', scope: 'faq', default: 'Favorite faqs') + .navbar-end + %a.navbar-item.tooltip{href: root_path, data: {turbo_frame: 'yield', tooltip: t('search')}} + %i.fas.fa-search.mr-0 + %a.navbar-item.tooltip{href: page_path('manual'), data: {turbo_frame: 'yield', tooltip: t('manual')}} + %i.fas.fa-book.mr-0 + %a.navbar-item.tooltip.has-tooltip-multiline{href: page_path('notifications'), data: {turbo_frame: 'yield', tooltip: Settings.bug.title}} + %i.fas.fa-bug.mr-0 + - if user_signed_in? + - if can?(:manage, Faq) || can?(:manage, User) + .navbar-item.has-dropdown.is-hoverable + %a.navbar-link= fas_icon 'home', text: user_signed_in? ? current_user.label : t('visitor', scope: 'user', default: 'Visitor') + .navbar-dropdown + - if can? :manage, Faq + %a.navbar-item{href: faqs_path, data: {turbo_frame: 'yield'}}= fas_icon 'question-circle', text: t('manage', scope: 'faq', default: 'Manage faqs') + - if can? :manage, User + %a.navbar-item{href: users_path, data: {turbo_frame: 'yield'}}= fas_icon 'users', text: t('manage', scope: 'user', default: 'Manage users') + %hr.navbar-divider + %a.navbar-item{href: destroy_user_session_path(allow_other_host: true), data: {method: :delete, confirm: t('logout_confirm', scope: '', default: "Are you sure?")}}= fas_icon('sign-out-alt', text: t('logout', default: 'Logout')) + - else + %a.navbar-item{href: destroy_user_session_path(allow_other_host: true), data: {method: :delete, confirm: t('logout_confirm', scope: '', default: "Are you sure?")}}= fas_icon('sign-out-alt', text: t('logout', default: 'Logout')) + - else + %a.navbar-item{href: new_user_session_path}= fas_icon('sign-in-alt', text: t('login', default: 'Login')) diff --git a/app/views/application/_timeout.html.haml b/app/views/application/_timeout.html.haml new file mode 100644 index 0000000..7a574f6 --- /dev/null +++ b/app/views/application/_timeout.html.haml @@ -0,0 +1,10 @@ +#timeout.modal.modal-container{ data: { modal_target: 'container', erase: 'false' } } + .modal-background + .modal-card + %header.modal-card-head.has-background-danger + %p.modal-card-title.has-text-white + %i.fas.fa-exclamation-triangle.mr-3 + ATTENZIONE + %section.modal-card-body + La sessione e` scaduta. Ricaricare la pagina per continuare. + .buttons.is-centered.mt-5.is-uppercase= link_to t('reload'), root_path, class: 'button is-danger' \ No newline at end of file diff --git a/app/views/faq_mailer/propose_email.html.haml b/app/views/faq_mailer/propose_email.html.haml new file mode 100644 index 0000000..509649c --- /dev/null +++ b/app/views/faq_mailer/propose_email.html.haml @@ -0,0 +1,13 @@ +%p Buongiorno #{@user.label}, +%p il sistema ti comunica che #{@author.label} ha inviato una richiesta di inserire una nuova faq. +%p + Di seguito un breve riepilogo: + %ul + %li #{t('title', scope: 'activerecord.attributes.faq', default: 'Title')}: #{@propose[:title]} + %li #{t('content', scope: 'activerecord.attributes.faq', default: 'Content')}: #{@propose[:text]} + %li #{t('visibility', scope: 'activerecord.attributes.faq', default: 'Visibility')}: #{t(@propose[:visibility], scope: 'activerecord.attributes.faq.visibilities', default: @propose[:visibility])} + %li #{t('structure', scope: 'activerecord.attributes.faq', default: 'Structure')}: #{@propose[:structure]} + +%p La e-mail è generata automaticamente dal sistema. Si prega di non rispondere a questa e-mail poichè la casella non è gestita e nessuna delle risposte verrà recapitata al mittente e quindi letta. +%p Distinti saluti, +%p #{Settings.title} - #{Settings.subtitle} \ No newline at end of file diff --git a/app/views/faq_mailer/propose_email.text.haml b/app/views/faq_mailer/propose_email.text.haml new file mode 100644 index 0000000..5caead0 --- /dev/null +++ b/app/views/faq_mailer/propose_email.text.haml @@ -0,0 +1,14 @@ +Buongiorno #{@user.label}, +il sistema ti comunica che #{@author.label} ha inviato una richiesta di inserire una nuova faq. + +Di seguito un breve riepilogo: + +#{t('title', scope: 'activerecord.attributes.faq', default: 'Title')}: #{@propose[:title]} +#{t('content', scope: 'activerecord.attributes.faq', default: 'Content')}: #{@propose[:text]} +#{t('visibility', scope: 'activerecord.attributes.faq', default: 'Visibility')}: #{t(@propose[:visibility], scope: 'activerecord.attributes.faq.visibilities', default: @propose[:visibility])} +#{t('structure', scope: 'activerecord.attributes.faq', default: 'Structure')}: #{@propose[:structure]} + +La e-mail è generata automaticamente dal sistema. Si prega di non rispondere a questa e-mail poichè la casella non è gestita e nessuna delle risposte verrà recapitata al mittente e quindi letta. + +Distinti saluti, +#{Settings.title} - #{Settings.subtitle} \ No newline at end of file diff --git a/app/views/faqs/_faq.html.haml b/app/views/faqs/_faq.html.haml new file mode 100644 index 0000000..28a2e36 --- /dev/null +++ b/app/views/faqs/_faq.html.haml @@ -0,0 +1,13 @@ += turbo_frame_tag dom_id(faq) do + .columns.row + .column.is-2= faq.title + .column.has-text-justified= faq.content.to_plain_text + .column.is-1= faq.structure + .column.is-2= faq.category_list + .column.is-1.has-text-centered= fas_icon('check', span_style: 'has-text-success-dark') if faq.approve? + .column.is-1.has-text-centered= fas_icon('check', span_style: 'has-text-success-dark') if faq.active? + .column.is-1-desktop.is-2-tablet.is-vcentered.is-paddingless + .buttons.has-addons.is-right.is-flex-wrap-nowrap + = link_to fas_icon('search-plus'), faq_path(id: faq.id, filter: {view: :modal}), class: 'button is-borderless has-background-trasparent tooltip ', data: { tooltip: t('show', scope: 'faq', default: 'Show faq'), turbo_frame: 'modal' } + = link_to fas_icon('edit'), edit_faq_path(id: faq.id), disabled: cannot?(:edit, faq), class: "button is-borderless has-background-trasparent tooltip #{ 'disabled' if cannot?(:edit, faq) }", data: { tooltip: t('edit', scope: 'faq', default: 'Edit faq'), turbo_frame: 'yield' } + = link_to fas_icon('trash'), faq_path(id: faq.id), method: :delete, disabled: cannot?(:delete, faq), class: "button is-borderless has-background-trasparent tooltip #{ 'disabled' if cannot?(:delete, faq) }", data: { tooltip: t('delete', scope: 'faq', default: 'Delete faq'), turbo_frame: dom_id(faq), turbo_method: 'DELETE', confirm: t('destroy', scope: 'faq.confirmations', default: 'Destroy faq from system?') } \ No newline at end of file diff --git a/app/views/faqs/_form.html.haml b/app/views/faqs/_form.html.haml new file mode 100644 index 0000000..f99bf08 --- /dev/null +++ b/app/views/faqs/_form.html.haml @@ -0,0 +1,97 @@ += form_with model: faq, data: { controller: 'form', turbo_frame: 'yield' } do |f| + .form-inputs + .field.is-horizontal + .field-label + %label.label.is-capitalized= f.label :title, class: 'label is-required' + .field-body + .field + .control.is-expanded= f.text_field :title, placeholder: "es: #{@sample.present? ? @sample.title.try(:downcase) : 'nuovo computer'}", class: 'input', required: true + %p.help Inserire un testo chiaro e non eccessivamente lungo in modo che sia facilmente identificabile da chi lo cerca + .field.is-horizontal + .field-label + %label.label= f.label :category_list, class: 'label is-required' + .field-body + .field + .control + %span.is-fullwidth= f.select :category_list, ActsAsTaggableOn::Tag.all.pluck(:name), {selected: f.object.category_list, default: ''}, {id: "faq_#{f.object.id}_category_list", class: 'slimselect', multiple: true, data: { form_target: 'slimselect', form_addable: true, form_closeOnSelect: false}} + %p.help Inserire una o più categorie per questa Faq. Le categorie verranno utilizzate per filtrare le ricerche. Nota: se la categoria è assente nell'elenco visualizzato, scrivilo e poi clicca sul bottone + per aggiungerlo all'elenco. + .field.is-horizontal + .field-label + %label.label.is-capitalized= f.label :content, class: 'label is-required' + .field-body + .field + .control.is-expanded= f.rich_text_area :content, class: 'textarea', rows: '10', required: true + %p.help E' possibile inserire sia test che allegati che links. Si consiglia, di dividere bene le istruzioni dai moduli e dalla documentazione. + .field.is-horizontal + .field-label + %label.label.is-capitalized= f.label :structure, class: 'label is-required' + .field-body + .field + .control.is-expanded= f.text_field :structure, class: "input #{'is-static' if current_user.editor?}", readonly: current_user.editor?, value: f.object.structure.presence || current_user.structure, required: true + %p.help Indicare il nome della struttura a cui afferisce questa Faq. In futuro, solo gli editor che afferiscono a questa struttura potranno modificare questa faq. Nota bene: è possibile indicare una sola struttura! + .field.is-horizontal + .field-label + %label.label= f.label :visibility, class: 'label' + .field-body + .field + .control + %span.is-fullwidth= f.select :visibility, Faq.visibilities.map {|k,v| [t_field(k, 'faq.visibilities'), k] }, {selected: f.object.visibility, default: ''}, {id: "faq_#{f.object.id}_visibility", class: 'slimselect', data: { form_target: 'slimselect' }} + %p.help Decidere se rendere questa Faq pubblicamente visibile oppure visibile previa autenticazione oppure visibile esclusivamente alla struttura indicata nell'apposito campo. + .field.is-horizontal.my-4 + .field-label + %label.label.is-capitalized= f.label :files, class: 'label is-required' + %p.help Allega i file da visualizzare nelle istruzioni rinominandoli preventivamente in modo che i nomi file siano comprensibili + .field-body + .field.is-horizontal.is-pulled-left + .field-label + %label.label.is-capitalized= f.label :instructions, class: 'label', value: t_field('instructions', 'faq') + .field + .control.is-expanded + .file + .file-label= f.file_field :instructions, class: '', multiple: true + .field.is-horizontal.is-pulled-left + .field-label + %label.label.is-capitalized= f.label :models, class: 'label', value: t_field('models', 'faq') + .field + .control.is-expanded + .file + .file-label= f.file_field :models, multiple: true + .field.is-horizontal.is-pulled-left + .field-label + %label.label.is-capitalized= f.label :files, class: 'label', value: t_field('others', 'faq') + .field + .control.is-expanded + .file + .file-label= f.file_field :files, multiple: true + + .field.is-horizontal + .field-label + %label.label= f.label :evidence, class: 'label' + .field-body + .field + = f.check_box :evidence, { id: "#{dom_id(faq)}_evidence", class: 'switch is-success is-unchecked-danger' }, '1', '0' + %label.has-text-inside.has-text-white{ for: "#{dom_id(faq)}_evidence" } + %span.switch-active{ aria: { hidden: 'true' } }= t('yes') + %span.switch-inactive{ aria: { hidden: 'true' } }= t('no') + .field.is-horizontal + .field-label + %label.label= f.label :approve, class: 'label' + .field-body + .field + = f.check_box :approve, { id: "#{dom_id(faq)}_approve", class: 'switch is-success is-unchecked-danger' }, '1', '0' + %label.has-text-inside.has-text-white{ for: "#{dom_id(faq)}_approve" } + %span.switch-active{ aria: { hidden: 'true' } }= t('yes') + %span.switch-inactive{ aria: { hidden: 'true' } }= t('no') + .field.is-horizontal + .field-label + %label.label= f.label :active, class: 'label' + .field-body + .field + = f.check_box :active, { id: "#{dom_id(faq)}_active", class: 'switch is-success is-unchecked-danger' }, '1', '0' + %label.has-text-inside.has-text-white{ for: "#{dom_id(faq)}_active" } + %span.switch-active{ aria: { hidden: 'true' } }= t('yes') + %span.switch-inactive{ aria: { hidden: 'true' } }= t('no') + .form-actions + .buttons.is-right + = f.submit icon: 'save', value: t('save'), class: 'button is-link is-capitalized' + = link_to t('cancel'), faqs_path, class: 'button is-danger is-capitalized', data: { turbo_frame: 'yield' } diff --git a/app/views/faqs/_index.html.haml b/app/views/faqs/_index.html.haml new file mode 100644 index 0000000..33ded3f --- /dev/null +++ b/app/views/faqs/_index.html.haml @@ -0,0 +1,28 @@ +.container + %h2.title.is-2.has-text-centered= t('index', scope: 'faq', default: 'List faqs') + - if current_user.editor? + %h5.subtitle.is-5.has-text-centered Puoi gestire solo le Faq appartenenti #{current_user.structure.present? ? "a #{current_user.structure}" : 'alla tua struttura'} + .box + .level + .level-left + + .level-right + .level-item= link_to fas_icon('plus', text: t('new', scope: 'faq', default: 'New faq')), new_faq_path, class: 'button box is-link', data: {turbo_frame: 'yield'} if can? :create, Faq + .level-item + = form_with scope: :filter, url: list_faqs_path, method: :get, data: {turbo_frame: 'faqs'} do |f| + %p.control.has-icons-left + = f.text_field :text, placeholder: 'Ricerca veloce', class: 'input', data: {controller: 'form', action: "keyup->form#delayedSend"} + %span.icon.is-left + %i.fas.fa-search{ aria: { hidden: 'true' } } + %hr + .list + .columns.head + .column.is-2.is-vcentered #{t_field 'title', 'faq'} + .column.is-vcentered #{t_field 'content', 'faq'} + .column.is-1 #{t_field 'structure', 'faq'} + .column.is-2.has-text-centered #{t_field 'category_list', 'faq'} + .column.is-1.has-text-centered #{t_field 'approve', 'faq'} + .column.is-1.has-text-centered #{t_field 'active', 'faq'} + .column.is-1 + = turbo_frame_tag 'faqs', class: 'rows', src: list_faqs_path, alt: 'Nessuna riga trovata' do + .loading.has-text-centered= fas_icon 'cog fa-pulse', text: 'Caricamento in corso' diff --git a/app/views/faqs/edit.html.haml b/app/views/faqs/edit.html.haml new file mode 100644 index 0000000..98b6dcb --- /dev/null +++ b/app/views/faqs/edit.html.haml @@ -0,0 +1,4 @@ +.container + = turbo_frame_tag dom_id(@faq), data: {controller: @view} do + %h2.title.is-2.has-text-centered.is-capitalized-first= t('edit', scope: 'faq', default: 'Edit faq') + = render 'form', faq: @faq \ No newline at end of file diff --git a/app/views/faqs/index.html.haml b/app/views/faqs/index.html.haml new file mode 100644 index 0000000..d1601e8 --- /dev/null +++ b/app/views/faqs/index.html.haml @@ -0,0 +1 @@ += render 'faqs/index' \ No newline at end of file diff --git a/app/views/faqs/list.html.haml b/app/views/faqs/list.html.haml new file mode 100644 index 0000000..daca5c2 --- /dev/null +++ b/app/views/faqs/list.html.haml @@ -0,0 +1,8 @@ += turbo_frame_tag 'faqs', class: 'rows' do + - unless @faqs.blank? + = render @faqs + .columns + .column.is-full= "#{pagy_bulma_nav(@pagy)}".html_safe + - else + .columns + .column.is-full.has-text-centered Nessuna faq trovata diff --git a/app/views/faqs/new.html.haml b/app/views/faqs/new.html.haml new file mode 100644 index 0000000..8eefe69 --- /dev/null +++ b/app/views/faqs/new.html.haml @@ -0,0 +1,4 @@ +.container + = turbo_frame_tag dom_id(@faq), data: {controller: @view} do + %h2.title.is-2.has-text-centered.is-capitalized-first= t('new', scope: 'faq', default: 'New faq') + = render 'form', faq: @faq diff --git a/app/views/faqs/show.html.haml b/app/views/faqs/show.html.haml new file mode 100644 index 0000000..3888a88 --- /dev/null +++ b/app/views/faqs/show.html.haml @@ -0,0 +1,5 @@ += turbo_frame_tag dom_id(@faq), data: {controller: 'modal', modal_size: '50%'} do + %h4.subtitle.is-4.has-text-centered= @faq.title + %hr/ + = @faq.rich_text_content + = render 'files', faq: @faq \ No newline at end of file diff --git a/app/views/home/_categories.html.haml b/app/views/home/_categories.html.haml new file mode 100644 index 0000000..ce63e2b --- /dev/null +++ b/app/views/home/_categories.html.haml @@ -0,0 +1,16 @@ +.field.is-grouped.is-grouped-multiline + .control + .subtitle.is-6.mb-2.mr-3= t('categories_top', scope: 'faq', default: 'Top categories', top: Settings.faq.categories_on_top) + - categories.each do |category| + .control + .tags.has-addons + %span.tag.is-dark + %label.radio + = f.radio_button :category, category.name, data: {action: 'click->form#send click->form#toggleTagActive'}, class: 'is-hidden' + = category.name + %span.tag.is-link= @faqs.tagged_with(category.name).count + .control + %span.tag.is-danger.is-hidden.is-pulled-right{data: {type: 'reset'}} + %label.radio + = f.radio_button :category, '', data: {type: 'reset', action: 'click->form#send click->form#desactiveTagFilter'}, class: 'is-hidden' + = t 'delete', scope: '', default: 'remove' diff --git a/app/views/home/_faq.html.haml b/app/views/home/_faq.html.haml new file mode 100644 index 0000000..ddbad88 --- /dev/null +++ b/app/views/home/_faq.html.haml @@ -0,0 +1,40 @@ += turbo_frame_tag dom_id(faq) do + .card.mt-2.is-unselectable + .card-header + %p.card-header-title + %a{data: {controller: 'form', action: 'form#toggleVisible', id: "#{dom_id(faq)}_content"}}= faq.title + - if user_signed_in? + %button.card-header-icon + %span.icon + - if user_faq_ids.include?(faq.id) + %a.is-vcentered.tooltip.is-multiline{href: favorite_destroy_home_index_path(id: faq.id), data: {method: :delete, turbo_method: 'delete', tooltip: t('remove_to_preferite', scope: 'faq', default: 'Remove faq to preferite list')} } + %i.fas.fa-star + - else + %a.is-vcentered.tooltip.is-multiline{href: favorite_create_home_index_path(id: faq.id), data: {method: :put, turbo_method: 'put', tooltip: t('add_to_preferite', scope: 'faq', default: 'Add faq to preferite list')} } + %i.far.fa-star + %button.card-header-icon + %span.icon + = link_to fas_icon('external-link-alt'), home_path(id: faq.id, filter: {view: :modal}), data: {turbo_frame: 'modal'} + .card-content.is-hidden{id: "#{dom_id(faq)}_content"} + - if !faq.evidence? || faq.category_list.present? + .level.mb-1 + .level-left + - if !faq.evidence? + .level-item + .subtitle.mb-2.mr-3{style: 'font-size: 0.8rem;'}= fas_icon('exclamation-circle has-text-danger', text: t_field('evidence', 'faq')) + .level-right + - if faq.category_list.present? + .level-item + .subtitle.mb-2.mr-3{style: 'font-size: 0.8rem;'}= faq.category_list + - if faq.content.present? + .content.has-text-justified.mb-4 + = faq.content + = render 'files', faq: faq + .content + .level + .level-left + .level-item.subtitle.mb-2.mr-3{style: 'font-size: 0.8rem;'} + %a.pl-0.button.is-text.is-small.has-text-transparent.tooltip{onclick: "javascript: navigator.clipboard.writeText(this.text); alert('Link copiato negli appunti!')", data: {tooltip: 'Clicca per copiare il link'} }= search_by_title_url(text: faq.title.downcase.gsub(' ', '-')) + .level-right + .level-item + .subtitle.mb-2.mr-3{style: 'font-size: 0.8rem;'}= "#{t_field('updated_at', 'faq')} #{l(faq.updated_at, format: :long)}" diff --git a/app/views/home/_favorite.html.haml b/app/views/home/_favorite.html.haml new file mode 100644 index 0000000..ef6f962 --- /dev/null +++ b/app/views/home/_favorite.html.haml @@ -0,0 +1,9 @@ += turbo_frame_tag dom_id(faq) do + .columns.row + .column.is-2= faq.title + .column.has-text-justified= faq.content.to_plain_text + .column.is-1= faq.structure + .column.is-2= faq.category_list + .column.is-1-desktop.is-2-tablet.is-vcentered.is-paddingless + .buttons.has-addons.is-right.is-flex-wrap-nowrap + = link_to fas_icon('search-plus'), faq_path(id: faq.id, filter: {view: :modal}), class: 'button is-borderless has-background-trasparent tooltip ', data: { tooltip: t('show', scope: 'faq', default: 'Show faq'), turbo_frame: 'modal' } diff --git a/app/views/home/_panels.html.haml b/app/views/home/_panels.html.haml new file mode 100644 index 0000000..cd0a493 --- /dev/null +++ b/app/views/home/_panels.html.haml @@ -0,0 +1,37 @@ +.columns.mt-6 + .column + %nav.panel + %p.panel-heading + %span.level + %span.level-left + %span.level-item In evidenza + %span.level-right + %span.level-item #{evidences.present? ? evidences.count : '0'} + - if evidences.present? + - evidences.each do |evidence| + %a.panel-block{ href: home_path(id: evidence.id, filter: {view: :modal}), data: {turbo_frame: 'modal'} } + .columns.is-vcentered.is-gapless{style: 'width: 100%;'} + .column.is-11= evidence.title + .column.is-1.is-hidden-mobile + .panel-icon.is-hidden-mobile.is-pulled-right.mr-0 + %i.fas.fa-external-link-alt{'aria-hidden': 'true'} + - else + .panel-block Nessuna faq in evidenza + .column + %nav.panel + %p.panel-heading + %span.level + %span.level-left + %span.level-item Le più richieste + %span.level-right + %span.level-item #{tops.present? ? tops.count : '0'} + - if tops.present? + - tops.each do |top| + %a.panel-block{ href: home_path(id: top.id, filter: {view: :modal}), data: {turbo_frame: 'modal'} } + .columns.is-vcentered.is-gapless{style: 'width: 100%;'} + .column.is-11= top.title + .column.is-1.is-hidden-mobile + .panel-icon.is-hidden-mobile.is-pulled-right.mr-0 + %i.fas.fa-external-link-alt{'aria-hidden': 'true'} + - else + .panel-block Nessuna faq trovata diff --git a/app/views/home/favorite.html.haml b/app/views/home/favorite.html.haml new file mode 100644 index 0000000..3315b6c --- /dev/null +++ b/app/views/home/favorite.html.haml @@ -0,0 +1,15 @@ +.container + %h2.title.is-2.has-text-centered= t('favorite', scope: 'faq', default: 'Favorite faqs') + .level + .level-left + + .level-right + .level-item + = form_with scope: :filter, url: list_favorite_home_index_path, method: :get, data: {turbo_frame: 'faqs'} do |f| + %p.control.has-icons-left + = f.text_field :text, placeholder: 'Ricerca veloce', class: 'input', data: {controller: 'form', action: "keyup->form#delayedSend"} + %span.icon.is-left + %i.fas.fa-search{ aria: { hidden: 'true' } } + %hr + = turbo_frame_tag 'faqs', class: 'rows', src: list_favorite_home_index_path, alt: 'Nessuna riga trovata' do + .loading.has-text-centered= fas_icon 'cog fa-pulse', text: 'Caricamento in corso' diff --git a/app/views/home/favorite_list.html.haml b/app/views/home/favorite_list.html.haml new file mode 100644 index 0000000..658c169 --- /dev/null +++ b/app/views/home/favorite_list.html.haml @@ -0,0 +1,8 @@ += turbo_frame_tag 'faqs', class: 'rows' do + - unless @faqs.blank? + = render partial: 'home/faq', collection: @faqs, as: :faq, locals: {user_faq_ids: @user_faq_ids} + .columns + .column.is-full= "#{pagy_bulma_nav(@pagy)}".html_safe + - else + .columns + .column.is-full.has-text-centered Nessuna faq trovata diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml new file mode 100644 index 0000000..274ac2d --- /dev/null +++ b/app/views/home/index.html.haml @@ -0,0 +1,15 @@ +.container + .columns + .column.is-8.is-offset-2 + = form_with scope: :filter, url: list_home_index_path, method: :get, class: 'mb-2', data: {controller: 'form', turbo_frame: 'faqs'} do |f| + %h3.title.is-3.has-text-centered Cosa fare per + .field.has-addons + .control.has-icons-left.is-expanded + = f.search_field :text, value: @filters.present? && @filters[:text].present? ? @filters[:text] : '', class: 'input is-large', placeholder: "es: #{@sample.present? ? @sample.title.try(:downcase) : 'nuovo computer'}", data: {action: 'form#delayedSend'} + %span.icon.is-medium.is-left + %i.fas.fa-search + %p.control + %a.button.is-link.is-large{ data: {action: 'form#send'} } Cerca + %p.help.mt-3= render 'home/categories', categories: @categories, f: f if @categories.present? + = turbo_frame_tag 'faqs', class: 'rows', src: list_home_index_path(filter: @filters) do + .loading.has-text-centered= fas_icon 'cog fa-pulse', text: 'Caricamento in corso' diff --git a/app/views/home/list.html.haml b/app/views/home/list.html.haml new file mode 100644 index 0000000..e0d80d0 --- /dev/null +++ b/app/views/home/list.html.haml @@ -0,0 +1,22 @@ += turbo_frame_tag 'faqs', class: 'rows' do + - unless @faqs.blank? + = render partial: 'home/faq', collection: @faqs, as: :faq, locals: {user_faq_ids: @user_faq_ids} + - else + - if @filters[:text].present? + %p.box.mt-2 + %span.level + %span.level-left + %span.level-item + Nessuna informazione trovata per + - if @filters[:text].present? + "#{@filters[:text].gsub(/[^\w\s]+/, ' ').remove(/[^\w\s]+/)}" + - if @filters[:category].present? + = ' e ' if @filters[:text].present? + categoria "#{@filters[:category]}" + %span.level-right + %span.level-item + - if can? :propose, Faq + = link_to 'proponi', propose_home_index_path(filter: {view: :modal}), class: 'button is-ghost', data: {turbo_frame: 'modal'} + - else + %a.button.is-ghost{href: new_user_session_path, data: {turbo: 'false'}} collegati e proponi + = render 'home/panels', evidences: @evidences, tops: @tops \ No newline at end of file diff --git a/app/views/home/propose.html.haml b/app/views/home/propose.html.haml new file mode 100644 index 0000000..82d6c28 --- /dev/null +++ b/app/views/home/propose.html.haml @@ -0,0 +1,45 @@ += turbo_frame_tag :propose, data: {controller: @view, modal_size: '60%', modal_position: 'center'} do + %h3.subtitle.is-3.has-text-centered Proponi + .content.has-text-justified.px-6.is-size-6 + Buongiorno #{@user.label}, + %br/ + questa sezione è dedicata alle proposte per domande a cui non riesci a trovare risposta nell'applicativo. + %br/ + Per inviare + %b segnalazioni, + qualora tu avessi trovato un bug durante la navigazione nel sito, ti chiediamo chiudere e di seguire le istruzioni indicate alla pagina dedicata alle segnalazioni seguendo l'icona #{fas_icon 'bug'} + %br/ + Infine, per + %b richiedere assistenza + , su problematiche riscontrate negli applicativi o di altro genere, + %b devi aprire un ticket. + .content.pr-6 + = form_with scope: :propose, url: proposed_home_index_path, data: {controller: 'form', action: 'submit->form#close'} do |f| + = f.hidden_field :author_id, value: @user.id, required: true + .form-inputs + .field.is-horizontal + .field-label + %label.label.is-capitalized= f.label :title, t('title', scope: 'propose', default: 'Title'), class: 'label is-required' + .field-body + .field + .control.is-expanded= f.text_field :title, class: 'input', required: true + %p.help Inserire un titolo, per questa faq che proponi, chiaro e non eccessivamente lungo in modo che sia facilmente identificabile da chi lo cerca + .field.is-horizontal + .field-label + %label.label.is-capitalized= f.label :text, t('text', scope: 'propose', default: 'Description'), class: 'label' + .field-body + .field + .control.is-expanded= f.text_area :text, row: 10, class: 'textarea has-fixed-size is-small' + %p.help Inserire un testo chiaro e non eccessivamente lungo in modo che sia facilmente comprensibile da chi lo dovrà leggere + .field.is-horizontal + .field-label + %label.label= f.label :visibility, t('visibility', scope: 'propose', default: 'Visibility'), class: 'label' + .field-body + .field + .control + %span= f.select :visibility, Faq.visibilities.map {|k,v| [t_field(k, 'faq.visibilities'), k] }, {selected: '', default: ''}, {id: "faq_visibility", class: 'slimselect', data: { form_target: 'slimselect' }} + %p.help Decidere se rendere questa Faq pubblicamente visibile oppure visibile previa autenticazione oppure visibile esclusivamente alla struttura indicata nell'apposito campo. + .field + .control= f.text_field :structure, class: 'input disabled', readonly: true, value: current_user.structure + .buttons.is-centered.mt-5 + = f.submit icon: 'save', value: t('send'), class: 'button is-link is-capitalized' diff --git a/app/views/home/show.html.haml b/app/views/home/show.html.haml new file mode 100644 index 0000000..5cca598 --- /dev/null +++ b/app/views/home/show.html.haml @@ -0,0 +1,26 @@ +- if @faq.present? + = turbo_frame_tag dom_id(@faq), data: {controller: @view, modal_size: '70%'} do + .container + %h4.title.is-4.has-text-centered.is-capitalized-first + = @faq.title + - if user_signed_in? && @view != 'modal' + %span.icon + - if @user_faq_ids.present? && @user_faq_ids.include?(@faq.id) + %a.is-vcentered.tooltip.is-multiline{href: favorite_destroy_home_index_path(id: @faq.id), data: {method: :delete, turbo_method: 'delete', tooltip: t('remove_to_preferite', scope: 'faq', default: 'Remove faq to preferite list')} } + %i.fas.fa-star + - else + %a.is-vcentered.tooltip.is-multiline{href: favorite_create_home_index_path(id: @faq.id), data: {method: :put, turbo_method: 'put', tooltip: t('add_to_preferite', scope: 'faq', default: 'Add faq to preferite list')} } + %i.far.fa-star + - unless @view == 'modal' + .has-text-centered= link_to fas_icon('search', text: t('search')), root_path, class: 'button is-text', data: {turbo_frame: 'yield'} + %hr/ + = @faq.rich_text_content.presence || 'Nessuna descrizione trovata' + = render 'files', faq: @faq +- else + .container + .message.is-danger + .message-body + .has-text-centered + %h1.title.is-size-5.has-text-danger-dark Spiacente, ma non è stata trovata nessuna faq + %p.mt-4 + Se invece ritieni che questo sia un messaggio errato, ti preghiamo di segnalarcelo seguendo le istruzioni indicate alla pagina dedicata alle segnalazioni seguendo l'icona #{fas_icon 'bug'} \ No newline at end of file diff --git a/app/views/layouts/action_text/contents/_content.html.erb b/app/views/layouts/action_text/contents/_content.html.erb new file mode 100644 index 0000000..9e3c0d0 --- /dev/null +++ b/app/views/layouts/action_text/contents/_content.html.erb @@ -0,0 +1,3 @@ +
+ <%= yield -%> +
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml new file mode 100644 index 0000000..28ec542 --- /dev/null +++ b/app/views/layouts/application.html.haml @@ -0,0 +1,30 @@ +!!! 5 +%html.no-js{lang: "#{I18n.locale || I18n.default_locale}"} + %head + -# + %meta{ charset: 'utf-8' } + %meta{ 'http-equiv': 'X-UA-Compatible', content: 'IE=edge' } + %meta{ name: 'viewport', content: 'width=device-width, initial-scale=1.0' } + %meta{ name: 'description', content: "#{Settings.subtitle}" } + = favicon_link_tag 'icon' + %title #{Settings.title} v#{Settings.app.version} #{(' - ' + Settings.env) if Settings.env.present?} + = csrf_meta_tags + = csp_meta_tag + = stylesheet_link_tag 'application', 'data-turbo-track': 'reload' + = javascript_include_tag 'application', 'data-turbo-track': 'reload', defer: true + %body + #loader.is-hidden + .is-loading + .hero.is-fullheight + .hero-head + %header.no-print + = turbo_frame_tag 'nav1', src: Settings.header + = render partial: 'nav_3' + %turbo-frame#flashes + %section.no-print + + %section.section{style: "min-height:350px;"} + %turbo-frame#yield= yield + %turbo-frame#modal.is-hidden= yield + .no-print= render partial: 'menu' if user_signed_in? + = turbo_frame_tag 'footer', src: Settings.footer diff --git a/app/views/layouts/empty.html.haml b/app/views/layouts/empty.html.haml new file mode 100644 index 0000000..f1d0cc8 --- /dev/null +++ b/app/views/layouts/empty.html.haml @@ -0,0 +1 @@ += yield \ No newline at end of file diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb new file mode 100644 index 0000000..cbd34d2 --- /dev/null +++ b/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ + + + + + + + + + <%= yield %> + + diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000..37f0bdd --- /dev/null +++ b/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/app/views/pages/error.html.haml b/app/views/pages/error.html.haml new file mode 100644 index 0000000..1f897ed --- /dev/null +++ b/app/views/pages/error.html.haml @@ -0,0 +1,9 @@ +#error.modal.modal-container{ data: { modal_target: 'container', erase: 'false' } } + .modal-background + .modal-card + %header.modal-card-head.has-background-danger + %p.modal-card-title.has-text-white + %i.fas.fa-exclamation-triangle.mr-3 + ATTENZIONE + %button.delete{ data: { controller: 'modal', action: 'click->modal#close' }, aria: { label: 'close' } } + %section.modal-card-body E' stato rilevato un errore di trasmissione, ricaricare la pagina e riprovare.
Se il problema persiste, contattare il supporto tecnico. \ No newline at end of file diff --git a/app/views/pages/manual.html.haml b/app/views/pages/manual.html.haml new file mode 100644 index 0000000..1ce8d05 --- /dev/null +++ b/app/views/pages/manual.html.haml @@ -0,0 +1,12 @@ +%turbo-frame#yield + #manual.container.is-unselectable{data: {controller: 'menu'}} + .has-spaced.px-2.my-4 + .content.is-large.has-text-centered + %h2.title.is-2 Manuale utente + .columns.has-text-black + .column.is-3-desktop.is-4-tablet.no-print= render partial: 'pages/manual/menu' + .column.ml-4 + %ul.manual-body + %li.mt-4{id: 'introduction'}= render partial: 'pages/manual/introduction' + %li.mt-4{id: 'requirements'}= render partial: 'pages/manual/requirements' + %li.mt-4{id: 'help'}= render partial: 'pages/manual/help' \ No newline at end of file diff --git a/app/views/pages/manual/_definitions.html.haml b/app/views/pages/manual/_definitions.html.haml new file mode 100644 index 0000000..b567121 --- /dev/null +++ b/app/views/pages/manual/_definitions.html.haml @@ -0,0 +1,43 @@ +%h4.title.is-4.mt-6.mb-3= fas_icon('fas fa-book', text: t('definitions', scope: 'manual.definitions')) +%table.is-bordered.is-striped.mx-5.is-hoverable + %thead + %tr + %th Termine, acronimo + %th Descrizione + %tbody + %tr + %td.has-text-weight-bold DSTR + %td Responsabile struttura (responsabile di CN-LAB, responsabile di CN-COST, Capo Dipartimento BIO) + %tr + %td.has-text-weight-bold RSTR + %td Referente struttura + %tr + %td.has-text-weight-bold DMAG + %td Responsabile di una sub-struttura su cui ricade un Magazzino (Capo Area di CN-LAB, Responsabile Laboratori di Ozzano, Chioggia e Livorno) + %tr + %td.has-text-weight-bold RMAG + %td Referente magazzino + %tr + %td.has-text-weight-bold OL + %td Operatore di laboratorio + %tr + %td.has-text-weight-bold SGSP + %td Scheda Generale singolo prodotto + %tr + %td.has-text-weight-bold SSic + %td Scheda di Sicurezza del prodotto + %tr + %td.has-text-weight-bold SOI + %td Scheda ordinativi interni + %tr + %td.has-text-weight-bold SCP + %td Scheda consegna prodotti + %tr + %td.has-text-weight-bold SRnA + %td Scheda richiesta nuovi acquisti + %tr + %td.has-text-weight-bold Magazzino + %td Inventario virtuale del materiale acquistato + %tr + %td.has-text-weight-bold Web GUI + %td Interfaccia grafica web del front end dell’applicativo diff --git a/app/views/pages/manual/_help.html.haml b/app/views/pages/manual/_help.html.haml new file mode 100644 index 0000000..21bc40b --- /dev/null +++ b/app/views/pages/manual/_help.html.haml @@ -0,0 +1,5 @@ +%h4.title.is-4.mt-6.mb-3= fas_icon('fas fa-life-ring', text: t('help', scope: 'manual.help')) +%p.has-text-justify + Nel menù è possibile consultare il manuale e il link per segnalare gli errori riscontrati. + %ul.child + %li.mt-5{id: 'help_bug'}= render partial: 'pages/manual/help/bug' diff --git a/app/views/pages/manual/_introduction.html.haml b/app/views/pages/manual/_introduction.html.haml new file mode 100644 index 0000000..222b294 --- /dev/null +++ b/app/views/pages/manual/_introduction.html.haml @@ -0,0 +1,18 @@ +%h4.title.is-4.mt-2.mb-3= fas_icon('fas fa-book', text: t('introduction', scope: 'manual.introduction')) +%p.has-text-justify Il progetto #{Settings.title.to_s} nasce dal'esigenza di gestione delle domande frequenti per permettere a tutti gli utenti di trovare aiuto il più velocemente possibile. +%p.has-text-justify L’utente avrà la disponibilità di strumenti per ricercare le domande frequenti e scaricare eventuali allegati. +%p.has-text-justify Il programma è stato appositamente sviluppato su piattaforma web per consentire l'accesso alle risorse tramite l'utilizzo di un comune browser web. +%p.has-text-justify.mt-3 + Nello specifico il sistema permette: + %ul.child.disc.manual-body.mt-1 + %li.pl-4 Ricerca delle domande frequenti(faq) tramite testo libero e/o le categorie; + %li.pl-4 Visualizzazione delle domande frequenti in evidenza; + %li.pl-4 Visualizzazione delle domande frequenti più ricercate; + %li.pl-4 Possibilità per gli utenti autenticati di inviare proposte da inserire nell'applicativo; + %li.pl-4 Gestione delle domande frequenti per ciascun dipendente con diritti di editor; + %li.pl-4 Possibilità di generare faq visibili a tutti, visibili solo al personale autenticato oppure visibili solo ai dipendenti assegnati a una specifico gruppo o struttura; + %li.pl-4 Gestione degli utenti e dei ruoli; + %li.pl-4 Comunicazioni tramite e-mail con gli utenti; + %li.pl-4 Autenticazione SingleSignOn su sistema CAS Server; + %li.pl-4 Documentazione applicativa completa(html); + %li.pl-4 Manuale utente. diff --git a/app/views/pages/manual/_menu.html.haml b/app/views/pages/manual/_menu.html.haml new file mode 100644 index 0000000..3cf765e --- /dev/null +++ b/app/views/pages/manual/_menu.html.haml @@ -0,0 +1,16 @@ +%aside.menu.manual-menu + %ul.menu-list + %li + %a.panel-block{data: {action: 'click->menu#focus', menu_id: 'introduction'}}= fas_icon('fas fa-book', text: t('introduction', scope: 'manual.introduction')) + %li + %a.panel-block{data: {action: 'click->menu#focus', menu_id: 'requirements'}}= fas_icon('fas fa-laptop', text: t('requirements', scope: 'manual.requirements')) + %ul + %li + %a.panel-block{data: {action: 'click->menu#focus', menu_id: 'introduction_min_requirement'}}= fas_icon('fas fa-laptop', text: t('minimal', scope: 'manual.requirements')) + %li + %a.panel-block{data: {action: 'click->menu#focus', menu_id: 'introduction_requirement'}}= fas_icon('fas fa-laptop', text: t('recommended', scope: 'manual.requirements')) + %li + %a.panel-block{data: {action: 'click->menu#focus', menu_id: 'help'}}= fas_icon('fas fa-life-ring', text: t('help', scope: 'manual.help')) + %ul + %li + %a.panel-block{data: {action: 'click->menu#focus', menu_id: 'help_bug'}}= fas_icon(Settings.bug.icon, text: Settings.bug.title) diff --git a/app/views/pages/manual/_requirements.html.haml b/app/views/pages/manual/_requirements.html.haml new file mode 100644 index 0000000..a965be5 --- /dev/null +++ b/app/views/pages/manual/_requirements.html.haml @@ -0,0 +1,27 @@ +%h4.title.is-4.mt-6.mb-3= fas_icon('fas fa-laptop', text: t('requirements', scope: 'manual.requirements')) +%ul.child + %li{id: 'introduction_min_requirement'} + %h5.title.is-5= fas_icon('fas fa-laptop', text: t('minimal', scope: 'manual.requirements')) + %ul.circle.child + %li Browser web liberamente scelto tra: Edge(ultime 2 versioni), Firefox(ultime 3 versioni), Chrome(ultime 3 versioni), Opera(ultime 2 versioni) e Safari(ultime 2 versioni); + %li Internet Explorer 11 non è compatibile. Il suo utilizzo potrebbe non permettere una visualizzare corretta delle pagine; + %li La modalità di compatibilità deve essere disattivata; + %li Javascript abilitato; + %li Cookie abilitati; + %li Supporto ai certificati SSL; + %li Accesso alla rete intranet ISPRA; + %li Indirizzo di posta elettronica sul dominio isprambiente.it; + %li Risoluzione schermo 1024x768. + + %li{id: 'introduction_requirement'} + %h5.title.is-5= fas_icon('fas fa-laptop', text: t('recommended', scope: 'manual.requirements')) + %ul.circle.child + %li Browser web liberamente scelto tra: Edge(ultime 2 versioni), Firefox(ultime 3 versioni), Chrome(ultime 3 versioni), Opera(ultime 2 versioni) e Safari(ultime 2 versioni); + %li Internet Explorer 11 non è compatibile. Il suo utilizzo potrebbe non permettere una visualizzare corretta delle pagine; + %li La modalità di compatibilità deve essere disattivata; + %li Javascript abilitato; + %li Cookie abilitati; + %li Supporto ai certificati SSL; + %li Accesso alla rete intranet ISPRA; + %li Indirizzo di posta elettronica sul dominio isprambiente.it; + %li Risoluzione schermo 1280x1024. \ No newline at end of file diff --git a/app/views/pages/manual/help/_bug.html.haml b/app/views/pages/manual/help/_bug.html.haml new file mode 100644 index 0000000..7776305 --- /dev/null +++ b/app/views/pages/manual/help/_bug.html.haml @@ -0,0 +1,3 @@ +%h5.title.is-5.mb-2= fas_icon(Settings.bug.icon, text: Settings.bug.title) +%p.has-text-justify + Cliccando sul link indicante la dicitura "#{Settings.bug.title}" sarai reindirizzato alla pagina del programma dedicata all'assistenza in cui troverai le indicazioni per segnalare il problema che hai trovato. Sarà considerata cosa gradita, se oltre a una descrizione il più dettagliata possibile, venisse allegato anche un file contenente l'immagine dell'errore generato. Appena l'errore sarà stato risolto, verrà prontamente rilasciata una patch correttiva.
Grazie in anticipo per l'aiuto. \ No newline at end of file diff --git a/app/views/pages/notallowed.html.erb b/app/views/pages/notallowed.html.erb new file mode 100644 index 0000000..f0a10a8 --- /dev/null +++ b/app/views/pages/notallowed.html.erb @@ -0,0 +1,70 @@ + + + + Non autorizzato ad accedere a questa pagina + + + + + + +
+
+
+
+

Spiacente, ma tu non sei autorizzato ad accedere a questa pagina e/o a questo sito.

+
+

Se invece ritieni di dovervi accedere, scrivi una email a <%= mail_to(Settings.bug.email) %> indicando tutti i riferimenti necessari all'identificazione del problema e le informazioni relative alla risorsa a cui stai tentando di accedere.

+
+
+
+ + diff --git a/app/views/pages/notifications.html.haml b/app/views/pages/notifications.html.haml new file mode 100644 index 0000000..374b60e --- /dev/null +++ b/app/views/pages/notifications.html.haml @@ -0,0 +1,7 @@ +%turbo-frame#yield + #segnalazioni.container + .has-spaced.box.px-4.my-4 + %h3.title.is-3.is-medium.has-text-centered Segnala un problema riscontrato durante l'utilizzo del programma + %p.content.has-text-justified Qualora tu avessi riscontrato un problema tecnico durante la visualizzazione del programma e desideri segnalarlo ai nostri tecnici, scrivi una email a #{mail_to Settings.bug.email} indicando tutti i riferimenti necessari all'identificazione del problema. Il primo tecnico disponibile cercherà di rispondere al più presto. + %p.content.has-text-justified Sarà considerata cosa gradita, se oltre a una descrizione il più dettagliata, fosse allegato uno o più file contenenti l'immagine dell'errore generato, ove chiaramente possibile. + %p.content.has-text-justified.has-text-weight-bold Le segnalazioni non di natura tecnica relativamente a problematiche non attinenti a bug o a problemi esterni al programma, saranno rimosse e cancellate senza risoluzione della problematica. diff --git a/app/views/user_mailer/propose_email.html.haml b/app/views/user_mailer/propose_email.html.haml new file mode 100644 index 0000000..b6d0080 --- /dev/null +++ b/app/views/user_mailer/propose_email.html.haml @@ -0,0 +1,13 @@ +%p Buongiorno #{@author.label}, +%p il sistema ti comunica che la tua richiesta di inserire una nuova faq e' stata inviata e sara' valutata il prima possibile +%p + Di seguito un breve riepilogo: + %ul + %li #{t('title', scope: 'activerecord.attributes.faq', default: 'Title')}: #{@propose[:title]} + %li #{t('content', scope: 'activerecord.attributes.faq', default: 'Content')}: #{@propose[:text]} + %li #{t('visibility', scope: 'activerecord.attributes.faq', default: 'Visibility')}: #{t(@propose[:visibility], scope: 'activerecord.attributes.faq.visibilities', default: @propose[:visibility])} + %li #{t('structure', scope: 'activerecord.attributes.faq', default: 'Structure')}: #{@propose[:structure]} + +%p La e-mail è generata automaticamente dal sistema. Si prega di non rispondere a questa e-mail poichè la casella non è gestita e nessuna delle risposte verrà recapitata al mittente e quindi letta. +%p Distinti saluti, +%p #{Settings.title} - #{Settings.subtitle} \ No newline at end of file diff --git a/app/views/user_mailer/propose_email.text.haml b/app/views/user_mailer/propose_email.text.haml new file mode 100644 index 0000000..9644983 --- /dev/null +++ b/app/views/user_mailer/propose_email.text.haml @@ -0,0 +1,14 @@ +Buongiorno #{@author.label}, +il sistema ti comunica che la tua richiesta di inserire una nuova faq e' stata inviata e sara' valutata il prima possibile + +Di seguito un breve riepilogo: + +#{t('title', scope: 'activerecord.attributes.faq', default: 'Title')}: #{@propose[:title]} +#{t('content', scope: 'activerecord.attributes.faq', default: 'Content')}: #{@propose[:text]} +#{t('visibility', scope: 'activerecord.attributes.faq', default: 'Visibility')}: #{t(@propose[:visibility], scope: 'activerecord.attributes.faq.visibilities', default: @propose[:visibility])} +#{t('structure', scope: 'activerecord.attributes.faq', default: 'Structure')}: #{@propose[:structure]} + +La e-mail è generata automaticamente dal sistema. Si prega di non rispondere a questa e-mail poichè la casella non è gestita e nessuna delle risposte verrà recapitata al mittente e quindi letta. + +Distinti saluti, +#{Settings.title} - #{Settings.subtitle} \ No newline at end of file diff --git a/app/views/users/_form.html.haml b/app/views/users/_form.html.haml new file mode 100644 index 0000000..d4c5b5f --- /dev/null +++ b/app/views/users/_form.html.haml @@ -0,0 +1,53 @@ +%turbo-frame#user + - if can? :read, User + .buttons.is-centered + %a.button.is-ghost.is-small-caps{href: users_path, data: {turbo_frame: 'yield'}}= t('list', scope: 'user', default: 'List users') + = form_with model: user, data: { turbo_frame: 'yield' } do |f| + .form-inputs + .field.is-horizontal + .field-label + %label.label.is-capitalized= f.label :username, class: 'label is-required' + .field-body + .field + .control.is-expanded= f.text_field :username, required: true, placeholder: t('username', scope: 'activerecord.user'), class: 'input' + - if user.persisted? + .field.is-horizontal + .field-label + %label.label= f.label :label, class: 'label is-required' + .field-body + .field + .control.is-expanded= f.text_field :label, required: true, placeholder: t('label', scope: 'activerecord.user'), class: 'input' + .field.is-horizontal + .field-label + %label.label= f.label :email, class: 'label is-required' + .field-body + .field + .control.is-expanded= f.text_field :email, required: true, placeholder: t('email', scope: 'activerecord.user'), class: 'input' + .field.is-horizontal + .field-label + %label.label= f.label :structure, class: 'label is-required' + .field-body + .field + .control.is-expanded= f.text_field :structure, required: true, placeholder: t('email', scope: 'activerecord.user'), class: 'input' + .field.is-horizontal + .field-label + %label.label= f.label :editor, class: 'label' + .field-body + .field + = f.check_box :editor, { id: "#{dom_id(user)}_editor", class: 'switch is-success is-unchecked-danger' }, '1', '0' + %label.has-text-inside.has-text-white{ for: "#{dom_id(user)}_editor" } + %span.switch-active{ aria: { hidden: 'true' } }= t('yes') + %span.switch-inactive{ aria: { hidden: 'true' } }= t('no') + .field.is-horizontal + .field-label + %label.label= f.label :admin, class: 'label' + .field-body + .field + = f.check_box :admin, { id: "#{dom_id(user)}_admin", class: 'switch is-success is-unchecked-danger' }, '1', '0' + %label.has-text-inside.has-text-white{ for: "#{dom_id(user)}_admin" } + %span.switch-active{ aria: { hidden: 'true' } }= t('yes') + %span.switch-inactive{ aria: { hidden: 'true' } }= t('no') + .form-actions + .buttons.is-right + = f.submit icon: 'save', value: t('save'), class: 'button is-link is-capitalized' + = link_to t('cancel'), users_path, class: 'button is-danger is-capitalized', data: { turbo_frame: 'yield' } diff --git a/app/views/users/_show.html.haml b/app/views/users/_show.html.haml new file mode 100644 index 0000000..7cd2f34 --- /dev/null +++ b/app/views/users/_show.html.haml @@ -0,0 +1,30 @@ +.container.is-unselectable + - unless @view == 'modal' + %h2.title.is-2.has-text-centered.is-capitalized #{t('show')} #{user.label} + - if can? :read, User + .buttons.is-centered + %a.button.is-ghost.is-small-caps{href: users_path, data: {turbo_frame: 'yield'}}= t('list', scope: 'user', default: 'List users') + .box.pt-5 + .buttons.is-right.mb-4= link_to fas_icon('edit', text: t('edit')), edit_user_path(id: user.id), class: 'button is-small tooltip', data: { tooltip: t('edit', scope: 'user', default: 'Edit user'), turbo_frame: 'yield' } if can? :edit, user + #user.container{ id: dom_id(user) } + .columns + .column.is-3.is-capitalized.has-text-weight-bold.has-text-right= t_field 'label', 'user' + .column= user.label + .columns + .column.is-3.is-capitalized.has-text-weight-bold.has-text-right= t_field 'username', 'user' + .column= user.username + .columns + .column.is-3.is-capitalized.has-text-weight-bold.has-text-right= t_field 'email', 'user' + .column= user.email + .columns + .column.is-3.is-capitalized.has-text-weight-bold.has-text-right= t_field 'last_sign_in_at', 'user' + .column= l_long(user.last_sign_in_at) || t('never') + .columns + .column.is-3.is-capitalized.has-text-weight-bold.has-text-right= t_field 'editor', 'user' + .column.has-text-weight-bold{class: "has-text-#{user.editor? ? 'success' : 'danger'}"}= t user.editor + .columns + .column.is-3.is-capitalized.has-text-weight-bold.has-text-right= t_field 'admin', 'user' + .column.has-text-weight-bold{class: "has-text-#{user.admin? ? 'success' : 'danger'}"}= t user.admin + .columns + .column.is-3.is-capitalized.has-text-weight-bold.has-text-right= t_field 'locked?', 'user' + .column= t user.locked? diff --git a/app/views/users/_user.html.haml b/app/views/users/_user.html.haml new file mode 100644 index 0000000..0a9575b --- /dev/null +++ b/app/views/users/_user.html.haml @@ -0,0 +1,18 @@ += turbo_frame_tag dom_id(user) do + .columns.row + .column= user.label + .column.is-2= user.username + .column.is-2= user.email + .column.is-1.has-text-centered= l_long user.last_sign_in_at if user.last_sign_in_at.present? + .column.is-1.has-text-centered= fas_icon('check', span_style: 'has-text-success-dark') if user.editor? + .column.is-1.has-text-centered= fas_icon('check', span_style: 'has-text-success-dark') if user.admin? + .column.is-1.has-text-centered= fas_icon('check', span_style: 'has-text-success-dark') if user.locked? + .column.is-1-desktop.is-2-tablet.is-vcentered.is-paddingless + .buttons.has-addons.is-right.is-flex-wrap-nowrap + = link_to fas_icon('search-plus'), user_path(user), class: 'button is-borderless has-background-trasparent tooltip ', data: { tooltip: t('show', scope: 'user', default: 'Show user'), turbo_frame: 'yield', title: "#{t('show', scope: 'user', default: 'Show user')} #{user.label}" } + - if user.locked? + = link_to fas_icon('unlock'), unlock_user_path(user), method: :patch, class: 'button is-borderless has-background-trasparent tooltip', data: { tooltip: t('unlock', scope: 'user'), turbo_frame: dom_id(user), turbo_method: 'PATCH' } + = link_to fas_icon('trash'), trash_user_path(user), method: :delete, class: 'button is-borderless has-background-trasparent tooltip', data: {icon: 'warning', title: 'ATTENZIONE', confirmation: t('destroy', scope: 'user.confirmations', default: 'Destroy user from system'), tooltip: t('delete', scope: 'user', default: 'Destroy user from system'), turbo_frame: dom_id(user), turbo_method: 'DELETE' } + - else + = link_to fas_icon('edit'), edit_user_path(user), class: 'button is-borderless has-background-trasparent tooltip ', data: { tooltip: t('edit', scope: 'user', default: 'Edit user'), turbo_frame: 'yield' } + = link_to fas_icon('trash'), user_path(user), method: :delete, class: "button is-borderless has-background-trasparent tooltip #{ 'disabled' if user == current_user }", disabled: user == current_user, data: { tooltip: t('delete', scope: 'user', default: 'Delete user'), turbo_frame: dom_id(user), turbo_method: 'DELETE', icon: 'warning', title: 'ATTENZIONE', confirmation: t('disable', scope: 'user.confirmations', default: 'Disable user from system?') } diff --git a/app/views/users/edit.html.haml b/app/views/users/edit.html.haml new file mode 100644 index 0000000..7e26da6 --- /dev/null +++ b/app/views/users/edit.html.haml @@ -0,0 +1,3 @@ +#user.container + %h2.title.is-2.has-text-centered.is-capitalized #{t('edit')} #{@user.label} + %turbo-frame#user= render 'form', user: @user, tab: @tab \ No newline at end of file diff --git a/app/views/users/index.html.haml b/app/views/users/index.html.haml new file mode 100644 index 0000000..8d2cc9c --- /dev/null +++ b/app/views/users/index.html.haml @@ -0,0 +1,27 @@ +.container + %h2.title.is-2.has-text-centered= t('index', scope: 'user', default: 'List users') + .box + .level + .level-left + + .level-right + .level-item= link_to fas_icon('plus', text: t('new', scope: 'user', default: 'New user')), new_user_path, class: 'button box is-link', data: {turbo_frame: 'yield'} if can? :create, User + .level-item + = form_with scope: :filter, url: list_users_path, method: :get, data: {turbo_frame: 'users'} do |f| + %p.control.has-icons-left + = f.text_field :text, placeholder: 'Ricerca veloce', class: 'input', data: {controller: 'form', action: "keyup->form#delayedSend"} + %span.icon.is-left + %i.fas.fa-search{ aria: { hidden: 'true' } } + %hr + .list + .columns.head + .column.is-vcentered #{t_field 'label', 'user'} + .column.is-2.is-vcentered #{t_field 'username', 'user'} + .column.is-2 #{t_field 'email', 'user'} + .column.is-1.has-text-centered #{t_field 'last_sign_in_at', 'user'} + .column.is-1.has-text-centered #{t_field 'editor', 'user'} + .column.is-1.has-text-centered #{t_field 'admin', 'user'} + .column.is-1.has-text-centered #{t_field 'locked?', 'user'} + .column.is-1 + = turbo_frame_tag 'users', class: 'rows', src: list_users_path, alt: 'Nessuna riga trovata' do + .loading.has-text-centered= fas_icon 'cog fa-pulse', text: 'Caricamento in corso' diff --git a/app/views/users/list.html.haml b/app/views/users/list.html.haml new file mode 100644 index 0000000..b4e56db --- /dev/null +++ b/app/views/users/list.html.haml @@ -0,0 +1,8 @@ += turbo_frame_tag 'users', class: 'rows' do + - unless @users.blank? + = render @users + .columns + .column.is-full= "#{pagy_bulma_nav(@pagy)}".html_safe + - else + .columns + .column.is-full.has-text-centered Nessun utente trovato diff --git a/app/views/users/new.html.haml b/app/views/users/new.html.haml new file mode 100644 index 0000000..329df01 --- /dev/null +++ b/app/views/users/new.html.haml @@ -0,0 +1,3 @@ +.container + %h2.title.is-2.has-text-centered.is-capitalized= t('new', scope: 'user', default: 'New user') + = render 'form', user: @user, tab: @tab \ No newline at end of file diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml new file mode 100644 index 0000000..db85dbc --- /dev/null +++ b/app/views/users/show.html.haml @@ -0,0 +1 @@ += render 'show', user: @user, tab: @tab \ No newline at end of file diff --git a/bin/bundle b/bin/bundle new file mode 100644 index 0000000..5b593cb --- /dev/null +++ b/bin/bundle @@ -0,0 +1,114 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'bundle' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "rubygems" + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($0) == File.expand_path(__FILE__) + end + + def env_var_version + ENV["BUNDLER_VERSION"] + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN + bundler_version = a + end + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ + bundler_version = $1 + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV["BUNDLE_GEMFILE"] + return gemfile if gemfile && !gemfile.empty? + + File.expand_path("../../Gemfile", __FILE__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + Regexp.last_match(1) + end + + def bundler_requirement + @bundler_requirement ||= + env_var_version || cli_arg_version || + bundler_requirement_for(lockfile_version) + end + + def bundler_requirement_for(version) + return "#{Gem::Requirement.default}.a" unless version + + bundler_gem_version = Gem::Version.new(version) + + requirement = bundler_gem_version.approximate_recommendation + + return requirement unless Gem.rubygems_version < Gem::Version.new("2.7.0") + + requirement += ".a" if bundler_gem_version.prerelease? + + requirement + end + + def load_bundler! + ENV["BUNDLE_GEMFILE"] ||= gemfile + + activate_bundler + end + + def activate_bundler + gem_error = activation_error_handling do + gem "bundler", bundler_requirement + end + return if gem_error.nil? + require_error = activation_error_handling do + require "bundler/version" + end + return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +if m.invoked_as_script? + load Gem.bin_path("bundler", "bundle") +end diff --git a/bin/bundle.cmd b/bin/bundle.cmd new file mode 100644 index 0000000..69ff6fa --- /dev/null +++ b/bin/bundle.cmd @@ -0,0 +1,117 @@ +@ruby -x "%~f0" %* +@exit /b %ERRORLEVEL% + +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'bundle' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "rubygems" + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($0) == File.expand_path(__FILE__) + end + + def env_var_version + ENV["BUNDLER_VERSION"] + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN + bundler_version = a + end + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ + bundler_version = $1 + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV["BUNDLE_GEMFILE"] + return gemfile if gemfile && !gemfile.empty? + + File.expand_path("../../Gemfile", __FILE__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + Regexp.last_match(1) + end + + def bundler_requirement + @bundler_requirement ||= + env_var_version || cli_arg_version || + bundler_requirement_for(lockfile_version) + end + + def bundler_requirement_for(version) + return "#{Gem::Requirement.default}.a" unless version + + bundler_gem_version = Gem::Version.new(version) + + requirement = bundler_gem_version.approximate_recommendation + + return requirement unless Gem.rubygems_version < Gem::Version.new("2.7.0") + + requirement += ".a" if bundler_gem_version.prerelease? + + requirement + end + + def load_bundler! + ENV["BUNDLE_GEMFILE"] ||= gemfile + + activate_bundler + end + + def activate_bundler + gem_error = activation_error_handling do + gem "bundler", bundler_requirement + end + return if gem_error.nil? + require_error = activation_error_handling do + require "bundler/version" + end + return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +if m.invoked_as_script? + load Gem.bin_path("bundler", "bundle") +end diff --git a/bin/dev b/bin/dev new file mode 100644 index 0000000..2daf776 --- /dev/null +++ b/bin/dev @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +if ! command -v foreman &> /dev/null +then + echo "Installing foreman..." + gem install foreman +fi + +foreman start -f Procfile.dev diff --git a/bin/rails b/bin/rails new file mode 100644 index 0000000..7bcc36e --- /dev/null +++ b/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby.exe +APP_PATH = File.expand_path("../config/application", __dir__) +require_relative "../config/boot" +require "rails/commands" diff --git a/bin/rake b/bin/rake new file mode 100644 index 0000000..01f7fc0 --- /dev/null +++ b/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby.exe +require_relative "../config/boot" +require "rake" +Rake.application.run diff --git a/bin/setup b/bin/setup new file mode 100644 index 0000000..5e463ee --- /dev/null +++ b/bin/setup @@ -0,0 +1,33 @@ +#!/usr/bin/env ruby.exe +require "fileutils" + +# path to your application root. +APP_ROOT = File.expand_path("..", __dir__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. + + puts "== Installing dependencies ==" + system! "gem install bundler --conservative" + system("bundle check") || system!("bundle install") + + # puts "\n== Copying sample files ==" + # unless File.exist?("config/database.yml") + # FileUtils.cp "config/database.yml.sample", "config/database.yml" + # end + + puts "\n== Preparing database ==" + system! "bin/rails db:prepare" + + puts "\n== Removing old logs and tempfiles ==" + system! "bin/rails log:clear tmp:clear" + + puts "\n== Restarting application server ==" + system! "bin/rails restart" +end diff --git a/bin/yarn b/bin/yarn new file mode 100644 index 0000000..3095207 --- /dev/null +++ b/bin/yarn @@ -0,0 +1,17 @@ +#!/usr/bin/env ruby.exe +APP_ROOT = File.expand_path('..', __dir__) +Dir.chdir(APP_ROOT) do + yarn = ENV["PATH"].split(File::PATH_SEPARATOR). + select { |dir| File.expand_path(dir) != __dir__ }. + product(["yarn", "yarn.cmd", "yarn.ps1"]). + map { |dir, file| File.expand_path(file, dir) }. + find { |file| File.executable?(file) } + + if yarn + exec yarn, *ARGV + else + $stderr.puts "Yarn executable was not detected in the system." + $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" + exit 1 + end +end diff --git a/config.ru b/config.ru new file mode 100644 index 0000000..4a3c09a --- /dev/null +++ b/config.ru @@ -0,0 +1,6 @@ +# This file is used by Rack-based servers to start the application. + +require_relative "config/environment" + +run Rails.application +Rails.application.load_server diff --git a/config/application.rb b/config/application.rb new file mode 100644 index 0000000..fbf60b0 --- /dev/null +++ b/config/application.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require_relative "boot" + +require 'rails/all' +require 'rack-cas/session_store/active_record' + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module Domando + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 7.0 + config.rack_cas.session_store = RackCAS::ActiveRecordStore + config.rack_cas.server_url = Settings.auth.url + config.encoding = 'utf-8' + config.i18n.default_locale = :it + config.i18n.available_locales = [:it] + config.i18n.enforce_available_locales = true + config.i18n.fallbacks = true + config.time_zone = 'Rome' + config.generators do |g| + g.template_engine :haml + end + # Don't generate system test files. + # config.generators.system_tests = nil + end +end diff --git a/config/boot.rb b/config/boot.rb new file mode 100644 index 0000000..3cda23b --- /dev/null +++ b/config/boot.rb @@ -0,0 +1,4 @@ +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) + +require "bundler/setup" # Set up gems listed in the Gemfile. +require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/config/cable.yml b/config/cable.yml new file mode 100644 index 0000000..dd47f56 --- /dev/null +++ b/config/cable.yml @@ -0,0 +1,11 @@ +development: + adapter: async + url: redis://localhost:6379/1 + +test: + adapter: test + +production: + adapter: redis + url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> + channel_prefix: domando_production diff --git a/config/database.yml b/config/database.yml new file mode 100644 index 0000000..089701a --- /dev/null +++ b/config/database.yml @@ -0,0 +1,29 @@ +default: &default + adapter: postgresql + encoding: unicode + schema_search_path: public + pool: 5 + +development: + <<: *default + database: domando_development + username: postgres + password: qwerty + host: localhost + sslmode: prefer + +test: + <<: *default + database: domando_test + username: postgres + password: qwerty + host: localhost + sslmode: prefer + +production: + <<: *default + database: domando_production + username: postgres + password: qwerty + host: localhost + sslmode: prefer diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 0000000..cac5315 --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative "application" + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb new file mode 100644 index 0000000..58613d8 --- /dev/null +++ b/config/environments/development.rb @@ -0,0 +1,75 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # In the development environment your application's code is reloaded any time + # it changes. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.cache_classes = false + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable server timing + config.server_timing = true + + # Enable/disable caching. By default caching is disabled. + # Run rails dev:cache to toggle caching. + if Rails.root.join("tmp/caching-dev.txt").exist? + config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true + + config.cache_store = :memory_store + config.public_file_server.headers = { + "Cache-Control" => "public, max-age=#{2.days.to_i}" + } + else + config.action_controller.perform_caching = false + + config.cache_store = :null_store + end + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false + + config.action_mailer.perform_caching = false + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + + # Suppress logger output for asset requests. + config.assets.quiet = true + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true + + config.default_url_options = { host: 'localhost:3000' } + config.action_mailer.delivery_method = :letter_opener + config.action_mailer.perform_deliveries = true + config.action_mailer.default_url_options = { host: 'localhost:3000' } +end diff --git a/config/environments/production.rb b/config/environments/production.rb new file mode 100644 index 0000000..42aa5e2 --- /dev/null +++ b/config/environments/production.rb @@ -0,0 +1,108 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.cache_classes = true + + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both threaded web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true + + # Full error reports are disabled and caching is turned on. + config.consider_all_requests_local = false + config.action_controller.perform_caching = true + + # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] + # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). + # config.require_master_key = true + + # Disable serving static files from the `/public` folder by default since + # Apache or NGINX already handles this. + config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present? + + # Compress CSS using a preprocessor. + # config.assets.css_compressor = :sass + + # Do not fallback to assets pipeline if a precompiled asset is missed. + config.assets.compile = false + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.asset_host = "http://assets.example.com" + + # Specifies the header that your server uses for sending files. + # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache + # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Mount Action Cable outside main process or domain. + # config.action_cable.mount_path = nil + # config.action_cable.url = "wss://example.com/cable" + # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + # config.force_ssl = true + + # Include generic and useful information about system operation, but avoid logging too much + # information to avoid inadvertent exposure of personally identifiable information (PII). + config.log_level = :info + + # Prepend all log lines with the following tags. + config.log_tags = [ :request_id ] + + # Use a different cache store in production. + # config.cache_store = :mem_cache_store + + # Use a real queuing backend for Active Job (and separate queues per environment). + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "domando_production" + + config.action_mailer.perform_caching = false + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Don't log any deprecations. + config.active_support.report_deprecations = false + + # Use default logging formatter so that PID and timestamp are not suppressed. + config.log_formatter = ::Logger::Formatter.new + + # Use a different logger for distributed setups. + # require "syslog/logger" + # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name") + + if ENV["RAILS_LOG_TO_STDOUT"].present? + logger = ActiveSupport::Logger.new(STDOUT) + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + end + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + config.action_mailer.perform_caching = false + config.action_mailer.delivery_method = :smtp + config.action_mailer.default_url_options = { + host: Rails.application.credentials.default_url_host || Settings.action_mailer.default_url_option.host || '', + protocol: Rails.application.credentials.default_url_protocol || Settings.action_mailer.default_url_option.protocol || '' + } + config.action_mailer.smtp_settings = { + address: Rails.application.credentials.smtp_address || Settings.action_mailer.smtp_settings.address, + port: Rails.application.credentials.smtp_port || Settings.action_mailer.smtp_settings.port, + domain: Rails.application.credentials.smtp_domain || Settings.action_mailer.smtp_settings.domain, + ssl: Rails.application.credentials.smtp_ssl || Settings.action_mailer.smtp_settings.ssl, + user_name: Rails.application.credentials.smtp_username || Settings.action_mailer.smtp_settings.username, + password: Rails.application.credentials.smtp_password || Settings.action_mailer.smtp_settings.password + } +end diff --git a/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 0000000..6ea4d1e --- /dev/null +++ b/config/environments/test.rb @@ -0,0 +1,60 @@ +require "active_support/core_ext/integer/time" + +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Turn false under Spring and add config.action_view.cache_template_loading = true. + config.cache_classes = true + + # Eager loading loads your whole application. When running a single test locally, + # this probably isn't necessary. It's a good idea to do in a continuous integration + # system, or in some way before deploying your code. + config.eager_load = ENV["CI"].present? + + # Configure public file server for tests with Cache-Control for performance. + config.public_file_server.enabled = true + config.public_file_server.headers = { + "Cache-Control" => "public, max-age=#{1.hour.to_i}" + } + + # Show full error reports and disable caching. + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + config.cache_store = :null_store + + # Raise exceptions instead of rendering exception templates. + config.action_dispatch.show_exceptions = false + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + # Store uploaded files on the local file system in a temporary directory. + config.active_storage.service = :test + + config.action_mailer.perform_caching = false + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true +end diff --git a/config/initializers/active_record_logger.rb b/config/initializers/active_record_logger.rb new file mode 100644 index 0000000..3246c68 --- /dev/null +++ b/config/initializers/active_record_logger.rb @@ -0,0 +1,8 @@ +class CacheFreeLogger < ActiveSupport::Logger + def add(severity, message = nil, progname = nil, &block) + return true if progname&.include? "CACHE" + super + end +end + +ActiveRecord::Base.logger = CacheFreeLogger.new(STDOUT) diff --git a/config/initializers/acts_as_taggable_on.rb b/config/initializers/acts_as_taggable_on.rb new file mode 100644 index 0000000..3772c38 --- /dev/null +++ b/config/initializers/acts_as_taggable_on.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# ActsAsTaggableOn.delimiter = ';' +ActsAsTaggableOn.strict_case_match = true +ActsAsTaggableOn.remove_unused_tags = true +ActsAsTaggableOn.force_lowercase = true diff --git a/config/initializers/application_controller_renderer.rb b/config/initializers/application_controller_renderer.rb new file mode 100644 index 0000000..89d2efa --- /dev/null +++ b/config/initializers/application_controller_renderer.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# ActiveSupport::Reloader.to_prepare do +# ApplicationController.renderer.defaults.merge!( +# http_host: 'example.org', +# https: false +# ) +# end diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb new file mode 100644 index 0000000..f8223b0 --- /dev/null +++ b/config/initializers/assets.rb @@ -0,0 +1,15 @@ +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = '1.0' + +# Add additional assets to the asset load path. +# Rails.application.config.assets.paths << Emoji.images_path +# Add Yarn node_modules folder to the asset load path. + +Rails.application.config.assets.paths << Rails.root.join('node_modules') + +# Precompile additional assets. +# application.js, application.css, and all non-JS/CSS in the app/assets +# folder are already added. +# Rails.application.config.assets.precompile += %w( admin.js admin.css ) diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb new file mode 100644 index 0000000..33699c3 --- /dev/null +++ b/config/initializers/backtrace_silencers.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. +# Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) } + +# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code +# by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'". +Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"] diff --git a/config/initializers/config.rb b/config/initializers/config.rb new file mode 100644 index 0000000..46ccb7b --- /dev/null +++ b/config/initializers/config.rb @@ -0,0 +1,58 @@ +Config.setup do |config| + # Name of the constant exposing loaded settings + config.const_name = 'Settings' + + # Ability to remove elements of the array set in earlier loaded settings file. For example value: '--'. + # + # config.knockout_prefix = nil + + # Overwrite an existing value when merging a `nil` value. + # When set to `false`, the existing value is retained after merge. + # + # config.merge_nil_values = true + + # Overwrite arrays found in previously loaded settings file. When set to `false`, arrays will be merged. + # + # config.overwrite_arrays = true + + # Load environment variables from the `ENV` object and override any settings defined in files. + # + # config.use_env = false + + # Define ENV variable prefix deciding which variables to load into config. + # + # Reading variables from ENV is case-sensitive. If you define lowercase value below, ensure your ENV variables are + # prefixed in the same way. + # + # When not set it defaults to `config.const_name`. + # + config.env_prefix = 'SETTINGS' + + # What string to use as level separator for settings loaded from ENV variables. Default value of '.' works well + # with Heroku, but you might want to change it for example for '__' to easy override settings from command line, where + # using dots in variable names might not be allowed (eg. Bash). + # + # config.env_separator = '.' + + # Ability to process variables names: + # * nil - no change + # * :downcase - convert to lower case + # + # config.env_converter = :downcase + + # Parse numeric values as integers instead of strings. + # + # config.env_parse_values = true + + # Validate presence and type of specific config values. Check https://github.com/dry-rb/dry-validation for details. + # + # config.schema do + # required(:name).filled + # required(:age).maybe(:int?) + # required(:email).filled(format?: EMAIL_REGEX) + # end + + # Evaluate ERB in YAML config files at load time. + # + # config.evaluate_erb_in_yaml = true +end diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb new file mode 100644 index 0000000..3621f97 --- /dev/null +++ b/config/initializers/content_security_policy.rb @@ -0,0 +1,26 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy +# For further information see the following documentation +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy + +# Rails.application.configure do +# config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end +# +# # Generate session nonces for permitted importmap and inline scripts +# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } +# config.content_security_policy_nonce_directives = %w(script-src) +# +# # Report CSP violations to a specified URI. See: +# # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only +# # config.content_security_policy_report_only = true +# end diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb new file mode 100644 index 0000000..5a6a32d --- /dev/null +++ b/config/initializers/cookies_serializer.rb @@ -0,0 +1,5 @@ +# Be sure to restart your server when you modify this file. + +# Specify a serializer for the signed and encrypted cookie jars. +# Valid options are :json, :marshal, and :hybrid. +Rails.application.config.action_dispatch.cookies_serializer = :json diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb new file mode 100644 index 0000000..298f5ae --- /dev/null +++ b/config/initializers/devise.rb @@ -0,0 +1,314 @@ +# frozen_string_literal: true + +# Assuming you have not yet modified this file, each configuration option below +# is set to its default value. Note that some are commented out while others +# are not: uncommented lines are intended to protect your configuration from +# breaking changes in upgrades (i.e., in the event that future versions of +# Devise change the default values for those options). +# +# Use this hook to configure devise mailer, warden hooks and so forth. +# Many of these configuration options can be set straight in your model. +Devise.setup do |config| + # The secret key used by Devise. Devise uses this key to generate + # random tokens. Changing this key will render invalid all existing + # confirmation, reset password and unlock tokens in the database. + # Devise will use the `secret_key_base` as its `secret_key` + # by default. You can change it below and use your own secret key. + # config.secret_key = 'e8f1ff8b255b5399c5bace51ecf8a959065d828506d7bd7b7fc2e8afe931e8da014a8c80f000fa3181ca32bbcca62e354c4d62c4f4d6f59e7debbd8f2db92224' + # cas authenticable + config.cas_create_user = true + + # ==> Controller configuration + # Configure the parent class to the devise controllers. + # config.parent_controller = 'DeviseController' + + # ==> Mailer Configuration + # Configure the e-mail address which will be shown in Devise::Mailer, + # note that it will be overwritten if you use your own mailer class + # with default "from" parameter. + # config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' + config.mailer_sender = Settings.email + + # Configure the class responsible to send e-mails. + # config.mailer = 'Devise::Mailer' + + # Configure the parent class responsible to send e-mails. + # config.parent_mailer = 'ActionMailer::Base' + + # ==> ORM configuration + # Load and configure the ORM. Supports :active_record (default) and + # :mongoid (bson_ext recommended) by default. Other ORMs may be + # available as additional gems. + require 'devise/orm/active_record' + + # ==> Configuration for any authentication mechanism + # Configure which keys are used when authenticating a user. The default is + # just :email. You can configure it to use [:username, :subdomain], so for + # authenticating a user, both parameters are required. Remember that those + # parameters are used only when authenticating and not when retrieving from + # session. If you need permissions, you should implement that in a before filter. + # You can also supply a hash where the value is a boolean determining whether + # or not authentication should be aborted when the value is not present. + # config.authentication_keys = [:email] + + # Configure parameters from the request object used for authentication. Each entry + # given should be a request method and it will automatically be passed to the + # find_for_authentication method and considered in your model lookup. For instance, + # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. + # The same considerations mentioned for authentication_keys also apply to request_keys. + # config.request_keys = [] + + # Configure which authentication keys should be case-insensitive. + # These keys will be downcased upon creating or modifying a user and when used + # to authenticate or find a user. Default is :email. + config.case_insensitive_keys = [:username] + + # Configure which authentication keys should have whitespace stripped. + # These keys will have whitespace before and after removed upon creating or + # modifying a user and when used to authenticate or find a user. Default is :email. + config.strip_whitespace_keys = [:username] + + # Tell if authentication through request.params is enabled. True by default. + # It can be set to an array that will enable params authentication only for the + # given strategies, for example, `config.params_authenticatable = [:database]` will + # enable it only for database (email + password) authentication. + # config.params_authenticatable = true + + # Tell if authentication through HTTP Auth is enabled. False by default. + # It can be set to an array that will enable http authentication only for the + # given strategies, for example, `config.http_authenticatable = [:database]` will + # enable it only for database authentication. + # For API-only applications to support authentication "out-of-the-box", you will likely want to + # enable this with :database unless you are using a custom strategy. + # The supported strategies are: + # :database = Support basic authentication with authentication key + password + # config.http_authenticatable = false + + # If 401 status code should be returned for AJAX requests. True by default. + # config.http_authenticatable_on_xhr = true + + # The realm used in Http Basic Authentication. 'Application' by default. + # config.http_authentication_realm = 'Application' + + # It will change confirmation, password recovery and other workflows + # to behave the same regardless if the e-mail provided was right or wrong. + # Does not affect registerable. + # config.paranoid = true + + # By default Devise will store the user in session. You can skip storage for + # particular strategies by setting this option. + # Notice that if you are skipping storage for all authentication paths, you + # may want to disable generating routes to Devise's sessions controller by + # passing skip: :sessions to `devise_for` in your config/routes.rb + config.skip_session_storage = [:http_auth] + + # By default, Devise cleans up the CSRF token on authentication to + # avoid CSRF token fixation attacks. This means that, when using AJAX + # requests for sign in and sign up, you need to get a new CSRF token + # from the server. You can disable this option at your own risk. + # config.clean_up_csrf_token_on_authentication = true + + # When false, Devise will not attempt to reload routes on eager load. + # This can reduce the time taken to boot the app but if your application + # requires the Devise mappings to be loaded during boot time the application + # won't boot properly. + # config.reload_routes = true + + # ==> Configuration for :database_authenticatable + # For bcrypt, this is the cost for hashing the password and defaults to 12. If + # using other algorithms, it sets how many times you want the password to be hashed. + # The number of stretches used for generating the hashed password are stored + # with the hashed password. This allows you to change the stretches without + # invalidating existing passwords. + # + # Limiting the stretches to just one in testing will increase the performance of + # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use + # a value less than 10 in other environments. Note that, for bcrypt (the default + # algorithm), the cost increases exponentially with the number of stretches (e.g. + # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). + config.stretches = Rails.env.test? ? 1 : 12 + + # Set up a pepper to generate the hashed password. + # config.pepper = '8e3eec519bf24fd9bdc8579163897f905eae62926347ae3101dd9172905aa881acf398f51876f02031afc157be3ced536e8cf6da15d5e529fa1a0b7c25b7a0bc' + + # Send a notification to the original email when the user's email is changed. + # config.send_email_changed_notification = false + + # Send a notification email when the user's password is changed. + # config.send_password_change_notification = false + + # ==> Configuration for :confirmable + # A period that the user is allowed to access the website even without + # confirming their account. For instance, if set to 2.days, the user will be + # able to access the website for two days without confirming their account, + # access will be blocked just in the third day. + # You can also set it to nil, which will allow the user to access the website + # without confirming their account. + # Default is 0.days, meaning the user cannot access the website without + # confirming their account. + # config.allow_unconfirmed_access_for = 2.days + + # A period that the user is allowed to confirm their account before their + # token becomes invalid. For example, if set to 3.days, the user can confirm + # their account within 3 days after the mail was sent, but on the fourth day + # their account can't be confirmed with the token any more. + # Default is nil, meaning there is no restriction on how long a user can take + # before confirming their account. + # config.confirm_within = 3.days + + # If true, requires any email changes to be confirmed (exactly the same way as + # initial account confirmation) to be applied. Requires additional unconfirmed_email + # db field (see migrations). Until confirmed, new email is stored in + # unconfirmed_email column, and copied to email column on successful confirmation. + config.reconfirmable = true + + # Defines which key will be used when confirming an account + # config.confirmation_keys = [:email] + + # ==> Configuration for :rememberable + # The time the user will be remembered without asking for credentials again. + # config.remember_for = 2.weeks + + # Invalidates all the remember me tokens when the user signs out. + config.expire_all_remember_me_on_sign_out = true + + # If true, extends the user's remember period when remembered via cookie. + # config.extend_remember_period = false + + # Options to be passed to the created cookie. For instance, you can set + # secure: true in order to force SSL only cookies. + # config.rememberable_options = {} + + # ==> Configuration for :validatable + # Range for password length. + config.password_length = 6..128 + + # Email regex used to validate email formats. It simply asserts that + # one (and only one) @ exists in the given string. This is mainly + # to give user feedback and not to assert the e-mail validity. + config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ + + # ==> Configuration for :timeoutable + # The time you want to timeout the user session without activity. After this + # time the user will be asked for credentials again. Default is 30 minutes. + # config.timeout_in = 30.minutes + + # ==> Configuration for :lockable + # Defines which strategy will be used to lock an account. + # :failed_attempts = Locks an account after a number of failed attempts to sign in. + # :none = No lock strategy. You should handle locking by yourself. + # config.lock_strategy = :failed_attempts + + # Defines which key will be used when locking and unlocking an account + # config.unlock_keys = [:email] + + # Defines which strategy will be used to unlock an account. + # :email = Sends an unlock link to the user email + # :time = Re-enables login after a certain amount of time (see :unlock_in below) + # :both = Enables both strategies + # :none = No unlock strategy. You should handle unlocking by yourself. + # config.unlock_strategy = :both + + # Number of authentication tries before locking an account if lock_strategy + # is failed attempts. + # config.maximum_attempts = 20 + + # Time interval to unlock the account if :time is enabled as unlock_strategy. + # config.unlock_in = 1.hour + + # Warn on the last attempt before the account is locked. + # config.last_attempt_warning = true + + # ==> Configuration for :recoverable + # + # Defines which key will be used when recovering the password for an account + # config.reset_password_keys = [:email] + + # Time interval you can reset your password with a reset password key. + # Don't put a too small interval or your users won't have the time to + # change their passwords. + config.reset_password_within = 6.hours + + # When set to false, does not sign a user in automatically after their password is + # reset. Defaults to true, so a user is signed in automatically after a reset. + # config.sign_in_after_reset_password = true + + # ==> Configuration for :encryptable + # Allow you to use another hashing or encryption algorithm besides bcrypt (default). + # You can use :sha1, :sha512 or algorithms from others authentication tools as + # :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20 + # for default behavior) and :restful_authentication_sha1 (then you should set + # stretches to 10, and copy REST_AUTH_SITE_KEY to pepper). + # + # Require the `devise-encryptable` gem when using anything other than bcrypt + # config.encryptor = :sha512 + + # ==> Scopes configuration + # Turn scoped views on. Before rendering "sessions/new", it will first check for + # "users/sessions/new". It's turned off by default because it's slower if you + # are using only default views. + # config.scoped_views = false + + # Configure the default scope given to Warden. By default it's the first + # devise role declared in your routes (usually :user). + # config.default_scope = :user + + # Set this configuration to false if you want /users/sign_out to sign out + # only the current scope. By default, Devise signs out all scopes. + # config.sign_out_all_scopes = true + + # ==> Navigation configuration + # Lists the formats that should be treated as navigational. Formats like + # :html, should redirect to the sign in page when the user does not have + # access, but formats like :xml or :json, should return 401. + # + # If you have any extra navigational formats, like :iphone or :mobile, you + # should add them to the navigational formats lists. + # + # The "*/*" below is required to match Internet Explorer requests. + # config.navigational_formats = ['*/*', :html] + + # The default HTTP method used to sign out a resource. Default is :delete. + config.sign_out_via = :delete + + # ==> OmniAuth + # Add a new OmniAuth provider. Check the wiki for more information on setting + # up on your models and hooks. + # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' + + # ==> Warden configuration + # If you want to use other strategies, that are not supported by Devise, or + # change the failure app, you can configure them inside the config.warden block. + # + # config.warden do |manager| + # manager.intercept_401 = false + # manager.default_strategies(scope: :user).unshift :some_external_strategy + # end + + # ==> Mountable engine configurations + # When using Devise inside an engine, let's call it `MyEngine`, and this engine + # is mountable, there are some extra configurations to be taken into account. + # The following options are available, assuming the engine is mounted as: + # + # mount MyEngine, at: '/my_engine' + # + # The router that invoked `devise_for`, in the example above, would be: + # config.router_name = :my_engine + # + # When using OmniAuth, Devise cannot automatically set OmniAuth path, + # so you need to do it manually. For the users scope, it would be: + # config.omniauth_path_prefix = '/my_engine/users/auth' + + # ==> Turbolinks configuration + # If your app is using Turbolinks, Turbolinks::Controller needs to be included to make redirection work correctly: + # + # ActiveSupport.on_load(:devise_failure_app) do + # include Turbolinks::Controller + # end + + # ==> Configuration for :registerable + + # When set to false, does not sign a user in automatically after their password is + # changed. Defaults to true, so a user is signed in automatically after changing a password. + # config.sign_in_after_change_password = true +end diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000..4b34a03 --- /dev/null +++ b/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,6 @@ +# Be sure to restart your server when you modify this file. + +# Configure sensitive parameters which will be filtered from the log file. +Rails.application.config.filter_parameters += [ + :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn +] diff --git a/config/initializers/high_voltage.rb b/config/initializers/high_voltage.rb new file mode 100644 index 0000000..320e4fd --- /dev/null +++ b/config/initializers/high_voltage.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +HighVoltage.configure do |config| + config.layout = 'empty' +end diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb new file mode 100644 index 0000000..3860f65 --- /dev/null +++ b/config/initializers/inflections.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, "\\1en" +# inflect.singular /^(ox)en/i, "\\1" +# inflect.irregular "person", "people" +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym "RESTful" +# end diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb new file mode 100644 index 0000000..dc18996 --- /dev/null +++ b/config/initializers/mime_types.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Add new mime types for use in respond_to blocks: +# Mime::Type.register "text/richtext", :rtf diff --git a/config/initializers/new_framework_defaults_7_0.rb b/config/initializers/new_framework_defaults_7_0.rb new file mode 100644 index 0000000..a579326 --- /dev/null +++ b/config/initializers/new_framework_defaults_7_0.rb @@ -0,0 +1,117 @@ +# Be sure to restart your server when you modify this file. +# +# This file eases your Rails 7.0 framework defaults upgrade. +# +# Uncomment each configuration one by one to switch to the new default. +# Once your application is ready to run with all new defaults, you can remove +# this file and set the `config.load_defaults` to `7.0`. +# +# Read the Guide for Upgrading Ruby on Rails for more info on each option. +# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html + +# `button_to` view helper will render `