diff --git a/.dockerignore b/.dockerignore index cb7c59de7..ba55aea73 100644 --- a/.dockerignore +++ b/.dockerignore @@ -23,3 +23,4 @@ /uploads /vendor/ruby /vendor/bundle +/coverage diff --git a/.gitignore b/.gitignore index 2d5e10349..9ed0f7c5d 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ /vendor/bundle /.env /docker-compose.*.yaml +.rubocop.yml +Guardfile +coverage diff --git a/Capfile b/Capfile deleted file mode 100644 index f3634e86a..000000000 --- a/Capfile +++ /dev/null @@ -1,2 +0,0 @@ -load 'deploy' -load 'config/deploy' diff --git a/Docker.md b/Container.md similarity index 57% rename from Docker.md rename to Container.md index 02234fe74..7bae0b621 100644 --- a/Docker.md +++ b/Container.md @@ -1,19 +1,47 @@ -LinuxFr on Docker ------------------ +LinuxFr with Containers +----------------------- -To simplify set up of a developement environment, LinuxFr.org can be -run on Docker with `docker compose up`. +To simplify set up of a development environment, LinuxFr.org can be +run with a container engine like Docker or Podman with the [`compose.yml`](./compose.yaml) +file which describes how to build all needed services. -To init the SQL database schema, you need to wait upto the `database` -container to be ready to listen MySQL connections. +By default, the LinuxFr.org services will be provided under the domain names +`dlfp.lo` and `image.dlfp.lo`. So you'll need to add the +following line into the `/etc/hosts` file of your machine: + + ``` + 127.0.0.1 dlfp.lo image.dlfp.lo + ``` + +Then, if you use the Docker engine, you can use the `docker compose up` +command to start the system (you need to install the +[Docker compose plugin](https://docs.docker.com/compose/) first). + +> Note: with the Docker engine, you need to enable the Docker BuildKit builder. +> Either you have a Docker version which uses it by default, or you set the +> environment variable `export DOCKER_BUILDKIT=1`. + +If you use the Podman engine, you can either use the same Docker compose plugin +or the [podman-compose](https://github.com/containers/podman-compose/) +tool. The podman cli itself provides a wrapper of one of these two tools +through the +[`podman compose` command](https://docs.podman.io/en/latest/markdown/podman-compose.1.html). +Thus you need to use the `podman compose up` command to start the system. + +At this point, this documentation will give you `docker compose` commands, +but you should be able to use `podman compose` without any issue. + +To setup the SQL database schema, you need to wait until the `database` +container becomes ready to listen MySQL connections. For example, you should see in the logs: -> database_1 | 2020-09-21 16:03:12 139820938893312 [Note] mysqld: ready for connections. +> database_1 | 2020-09-21 16:03:12 139820938893312 [Note] *mysqld: ready for connections.* > > database_1 | Version: '10.1.46-MariaDB-1\~bionic' socket: '/var/run/mysqld/mysqld.sock' port: 3306 mariadb.org binary distribution -Or you can check the `database` container status to be "healthy". +Or you can check the `database` container status to be *healthy* with the +`docker compose ps` command. Then, open a second terminal and run: @@ -21,16 +49,9 @@ Then, open a second terminal and run: docker compose exec linuxfr.org bin/rails db:setup ``` -Finally, the environment is ready and you can open [http://dlfp.lo](http://dlfp.lo) +Finally, the environment is ready and you can open [http://dlfp.lo:9000](http://dlfp.lo:9000) in your favorite browser. -Note: to be able to access this URL, you'll need to add the following line -into the `/etc/hosts` file of your machine: - -``` -127.0.0.1 dlfp.lo image.dlfp.lo -``` - Personalize configuration ========================= @@ -40,24 +61,22 @@ If you want, you can change the domain names used by the LinuxFr.org web application. To do this, you can setup `DOMAIN` and `IMAGE_DOMAIN` variables in the `deployment/default.env` file. -You can also configure your own Redis service and your own MySQL -service. - -If you want to change the application port and/or other configurations, you can -[override](https://docs.docker.com/compose/extends/) -the docker compose configuration (in particular the `nginx` service for -the port). +Within the same file, you can update the HTTP listening ports by updating the +`DOMAIN_HTTP_PORT` and `IMAGE_DOMAIN_HTTP_PORT` variables (both are set to +`9000` by default). If you modify them, don't forget to add the new values as +published ports for the `nginx` service in the `compose.yaml` file (they have +to target the `8080` container port). -Notice, that if LinuxFr.org doesn't run on port 80, the image cache -service won't work well and so you won't be able to see images in the news. +You can also configure your own Redis service and your own MySQL +service by updating environment variables in the same file. Test modifications ================== -The docker compose is currently configured to share `./app`, `./db` and -`./public` directories with the docker container. +The compose file currently shares `./app`, `./db` and +`./public` directories with the container. -So you can update files with your prefered IDE on your machine. Rails +So you can update files with your preferred IDE on your machine. Rails will directly detect changes and apply them on next page reload. Furthermore, if you need to access the Rails console, you need a second @@ -75,13 +94,13 @@ Run application tests ===================== To help maintainers, we are in the process of adding tests to check the -application has still the expected behaviour. +application has still the expected behavior. To get help about writing tests, see the [Ruby on Rails documentation](https://guides.rubyonrails.org/testing.html#the-rails-test-runner) . -To run tests with Docker environment, you need to use this command: +To run tests with containers, you need to use this command: ``` docker compose exec linuxfr.org bin/rails test -v @@ -114,10 +133,10 @@ use: docker compose exec linuxfr.org bin/rails db:reset ``` -Services provided by the docker compose +Services provided by the compose file ======================================= -Currently, these services are directly enabled by docker compose: +Currently, these services are directly enabled by compose: 1. The [LinuxFr.org](https://github.com/linuxfrorg/linuxfr.org) ruby on rails application itself diff --git a/Gemfile b/Gemfile index eee3e6c02..3a7e69cc2 100644 --- a/Gemfile +++ b/Gemfile @@ -1,49 +1,50 @@ -source 'https://rubygems.org' +source "https://rubygems.org" git_source(:github) do |repo_name| repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") "https://github.com/#{repo_name}.git" end -gem "rails", "~>5.2" -gem "nio4r", "2.5.2" # 2.5.3 is not compatible with ruby 2.3 +gem "rails", "~>8.1.0" -gem "actionpack-page_caching", github: "linuxfrorg/actionpack-page_caching" +# Latest 6.0.1 is not starting any tests +gem 'minitest', '< 6.0' + +gem "actionpack-page_caching" +gem "acts_as_list", "~>1.1" gem "ansi", "~>1.4", require: false -gem "acts_as_list", "~>0.4" gem "bitfields", "~>0.4" gem "bootsnap", "~>1.3", require: false gem "canable", "~>0.1" -gem "carrierwave", "~>1.1" +gem "carrierwave", "~>3.0" gem "devise", "~>4.3" gem "diff_match_patch", github: "nono/diff_match_patch-ruby", require: "diff_match_patch" -gem "doorkeeper", "~>4.2" +gem "doorkeeper" gem "ffi-hunspell", github: "postmodern/ffi-hunspell" -gem "french_rails", "~>0.4" +gem "french_rails", "~>0.7", github: "echarp/french-rails" gem "friendly_id", "~>5.1" -gem "haml", "~>5.0" -gem "html-pipeline-linuxfr", "~>0.15" +gem "haml", "~>6.3" +gem "htmlentities", "~>4.3" +gem "html-pipeline-linuxfr", "~>0.17", github: "echarp/html-pipeline-linuxfr" gem "html_spellchecker", "~>0.1" gem "html_truncator", "~>0.4" -gem "htmlentities", "~>4.3" gem "inherited_resources", "~>1.8" gem "kaminari", "~>1.2" gem "mini_magick", "~>4.9" gem "mysql2", "~>0.5.0" gem "nokogiri", "~>1.10" +gem "redis", "~>5.0" gem "rinku", "~>2.0" -gem "redis", "~>4.0" -gem "sitemap_generator", "~>2.1" -gem "state_machine", "~>1.2" +gem "sitemap_generator" +gem "state_machines-activerecord" # Gems used for assets assets = !%w(production alpha).include?(ENV['RAILS_ENV']) assets = true if ENV['RAILS_GROUPS'] == "assets" -gem "jquery-rails", "~>4.0", require: assets -gem "coffee-rails", "~>4.1", require: assets -gem "sass-rails", "~>5.0", require: assets -gem "rails-sass-images", require: assets -gem "uglifier", require: assets +gem "jquery-rails", "~>4.0", require: assets +gem "normalize-rails", "~>8.0", require: assets +gem "sassc-rails", require: assets +gem "terser", "~> 1.2", require: assets group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console @@ -54,25 +55,16 @@ group :development do gem "annotate" gem "better_errors" gem "binding_of_caller" - gem "capistrano", "~>2.15", github: 'capistrano', branch: 'legacy-v2' - gem "capistrano-maintenance" gem "letter_opener" gem "listen", github: "guard/listen" - gem "mo" - gem "pry-rails" - gem "spring" - gem "sushi" - gem "thin" + gem "puma" gem "web-console" end group :test do - # Adds support for Capybara system testing and selenium driver - gem "capybara", ">= 2.15" - gem "selenium-webdriver" + gem "simplecov" end group :production, :alpha do - gem "unicorn", "~>5.1" - gem "gctools", "~>0.2" + gem "unicorn", "~>6.1" end diff --git a/Gemfile.lock b/Gemfile.lock index e1732fcd3..567120651 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,29 +1,31 @@ GIT - remote: https://github.com/capistrano/capistrano.git - revision: 08a82f3618425eae7539e2bbd34a87e35bac2800 - branch: legacy-v2 + remote: https://github.com/echarp/french-rails.git + revision: 6a839c8fe21417a8e335059925ea8b36535dbef9 specs: - capistrano (2.15.9) - highline - net-scp (>= 1.0.0) - net-sftp (>= 2.0.0) - net-ssh (>= 2.0.14) - net-ssh-gateway (>= 1.1.0) + french_rails (0.7.0) + rails (~> 8.0) GIT - remote: https://github.com/guard/listen.git - revision: 587f4a7edb75fac80faa3408c4513af715dace87 + remote: https://github.com/echarp/html-pipeline-linuxfr.git + revision: 33972788f2c7131b03c5a498d31fe3267dc3b6e4 specs: - listen (3.1.5) - rb-fsevent (~> 0.10, >= 0.10.3) - rb-inotify (~> 0.9, >= 0.9.10) + html-pipeline-linuxfr (0.17.2) + activesupport (~> 8.0) + escape_utils (~> 1.2) + nokogiri (~> 1.6) + patron (~> 0.8) + pygments.rb (~> 1.1) + redcarpet (~> 3.4) + sanitize (~> 6.1) GIT - remote: https://github.com/linuxfrorg/actionpack-page_caching.git - revision: 13a51101deb396eeb089a0d36a06653bd0d046a9 + remote: https://github.com/guard/listen.git + revision: 7c6d39e17d4ca8aef8cf72890176cc56fa90b236 specs: - actionpack-page_caching (1.2.2) - actionpack (>= 5.0.0) + listen (3.9.0) + logger + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) GIT remote: https://github.com/nono/diff_match_patch-ruby.git @@ -33,309 +35,378 @@ GIT GIT remote: https://github.com/postmodern/ffi-hunspell.git - revision: e5dd37ea70f9dc73ab21b68fa20ad567e9ae9b18 + revision: c6161f5f84247e2bdb2f9ca161a096d632df0432 specs: - ffi-hunspell (0.4.0) + ffi-hunspell (0.6.1) ffi (~> 1.0) GEM remote: https://rubygems.org/ specs: - actioncable (5.2.5) - actionpack (= 5.2.5) + action_text-trix (2.1.16) + railties + actioncable (8.1.2) + actionpack (= 8.1.2) + activesupport (= 8.1.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailer (5.2.5) - actionpack (= 5.2.5) - actionview (= 5.2.5) - activejob (= 5.2.5) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (5.2.5) - actionview (= 5.2.5) - activesupport (= 5.2.5) - rack (~> 2.0, >= 2.0.8) + zeitwerk (~> 2.6) + actionmailbox (8.1.2) + actionpack (= 8.1.2) + activejob (= 8.1.2) + activerecord (= 8.1.2) + activestorage (= 8.1.2) + activesupport (= 8.1.2) + mail (>= 2.8.0) + actionmailer (8.1.2) + actionpack (= 8.1.2) + actionview (= 8.1.2) + activejob (= 8.1.2) + activesupport (= 8.1.2) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (8.1.2) + actionview (= 8.1.2) + activesupport (= 8.1.2) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.2.5) - activesupport (= 5.2.5) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actionpack-page_caching (1.2.4) + actionpack (>= 4.0.0) + actiontext (8.1.2) + action_text-trix (~> 2.1.15) + actionpack (= 8.1.2) + activerecord (= 8.1.2) + activestorage (= 8.1.2) + activesupport (= 8.1.2) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (8.1.2) + activesupport (= 8.1.2) builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (5.2.5) - activesupport (= 5.2.5) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (8.1.2) + activesupport (= 8.1.2) globalid (>= 0.3.6) - activemodel (5.2.5) - activesupport (= 5.2.5) - activerecord (5.2.5) - activemodel (= 5.2.5) - activesupport (= 5.2.5) - arel (>= 9.0) - activestorage (5.2.5) - actionpack (= 5.2.5) - activerecord (= 5.2.5) - marcel (~> 1.0.0) - activesupport (5.2.5) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) - acts_as_list (0.9.15) - activerecord (>= 3.0) - addressable (2.5.2) - public_suffix (>= 2.0.2, < 4.0) - annotate (2.7.4) - activerecord (>= 3.2, < 6.0) - rake (>= 10.4, < 13.0) + activemodel (8.1.2) + activesupport (= 8.1.2) + activerecord (8.1.2) + activemodel (= 8.1.2) + activesupport (= 8.1.2) + timeout (>= 0.4.0) + activestorage (8.1.2) + actionpack (= 8.1.2) + activejob (= 8.1.2) + activerecord (= 8.1.2) + activesupport (= 8.1.2) + marcel (~> 1.0) + activesupport (8.1.2) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + json + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + acts_as_list (1.2.6) + activerecord (>= 6.1) + activesupport (>= 6.1) + addressable (2.8.8) + public_suffix (>= 2.0.2, < 8.0) + annotate (2.6.5) + activerecord (>= 2.3.0) + rake (>= 0.8.7) ansi (1.5.0) - arel (9.0.0) - bcrypt (3.1.12) - better_errors (2.5.0) - coderay (>= 1.0.0) + base64 (0.3.0) + bcrypt (3.1.21) + better_errors (2.10.1) erubi (>= 1.0.0) rack (>= 0.9.0) - bindex (0.5.0) - binding_of_caller (0.8.0) - debug_inspector (>= 0.0.1) - bitfields (0.8.0) - bootsnap (1.4.6) - msgpack (~> 1.0) - boson (1.3.0) - builder (3.2.4) - byebug (10.0.2) + rouge (>= 1.0.0) + bigdecimal (4.0.1) + bindex (0.8.1) + binding_of_caller (1.0.1) + debug_inspector (>= 1.2.0) + bitfields (0.14.0) + activerecord (>= 5.1) + bootsnap (1.20.1) + msgpack (~> 1.2) + builder (3.3.0) + byebug (12.0.0) canable (0.3.0) - capistrano-maintenance (0.0.5) - capistrano (~> 2.0) - carrierwave (1.2.3) - activemodel (>= 4.0.0) - activesupport (>= 4.0.0) - mime-types (>= 1.16) - coderay (1.1.2) - coffee-rails (4.2.2) - coffee-script (>= 2.2.0) - railties (>= 4.0.0) - coffee-script (2.4.1) - coffee-script-source - execjs - coffee-script-source (1.12.2) - concurrent-ruby (1.1.8) + carrierwave (3.1.2) + activemodel (>= 6.0.0) + activesupport (>= 6.0.0) + addressable (~> 2.6) + image_processing (~> 1.1) + marcel (~> 1.0.0) + ssrf_filter (~> 1.0) + childprocess (5.1.0) + logger (~> 1.5) + concurrent-ruby (1.3.6) + connection_pool (3.0.2) crass (1.0.6) - daemons (1.2.6) - debug_inspector (0.0.3) - devise (4.6.2) + date (3.5.1) + debug_inspector (1.2.0) + devise (4.9.4) bcrypt (~> 3.0) orm_adapter (~> 0.1) - railties (>= 4.1.0, < 6.0) + railties (>= 4.1.0) responders warden (~> 1.2.3) - dimensions (1.3.0) - doorkeeper (4.4.2) - railties (>= 4.2) - erubi (1.9.0) - escape_utils (1.2.1) - eventmachine (1.2.7) - execjs (2.7.0) - ffi (1.12.2) - french_rails (0.4.0) - rails (~> 5.0) - friendly_id (5.2.4) + docile (1.4.1) + doorkeeper (5.8.2) + railties (>= 5) + drb (2.2.3) + erb (6.0.1) + erubi (1.13.1) + escape_utils (1.3.0) + execjs (2.10.0) + ffi (1.17.3-x86_64-linux-gnu) + friendly_id (5.6.0) activerecord (>= 4.0.0) - gctools (0.2.4) - globalid (0.4.2) - activesupport (>= 4.2.0) - haml (5.0.4) - temple (>= 0.8.0) + globalid (1.3.0) + activesupport (>= 6.1) + haml (6.4.0) + temple (>= 0.8.2) + thor tilt - has_scope (0.7.2) - actionpack (>= 4.1) - activesupport (>= 4.1) - highline (2.0.0) - html-pipeline-linuxfr (0.15.7) - activesupport (~> 5.0) - escape_utils (~> 1.2) - nokogiri (~> 1.6) - patron (~> 0.8) - pygments.rb (~> 1.1) - redcarpet (~> 3.4) - sanitize (~> 4.0) + has_scope (0.9.0) + actionpack (>= 7.0) + activesupport (>= 7.0) html_spellchecker (0.1.9) ffi-hunspell (~> 0.4) nokogiri (~> 1.4) html_truncator (0.4.2) nokogiri (~> 1.5) - htmlentities (4.3.4) - i18n (1.8.10) + htmlentities (4.4.2) + i18n (1.14.8) concurrent-ruby (~> 1.0) - inherited_resources (1.9.0) - actionpack (>= 4.2, < 5.3) - has_scope (~> 0.6) - railties (>= 4.2, < 5.3) - responders - jquery-rails (4.3.3) + image_processing (1.14.0) + mini_magick (>= 4.9.5, < 6) + ruby-vips (>= 2.0.17, < 3) + inherited_resources (1.14.0) + actionpack (>= 6.0) + has_scope (>= 0.6) + railties (>= 6.0) + responders (>= 2) + io-console (0.8.2) + irb (1.16.0) + pp (>= 0.6.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + jquery-rails (4.6.1) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - kaminari (1.2.1) + json (2.18.0) + kaminari (1.2.2) activesupport (>= 4.1.0) - kaminari-actionview (= 1.2.1) - kaminari-activerecord (= 1.2.1) - kaminari-core (= 1.2.1) - kaminari-actionview (1.2.1) + kaminari-actionview (= 1.2.2) + kaminari-activerecord (= 1.2.2) + kaminari-core (= 1.2.2) + kaminari-actionview (1.2.2) actionview - kaminari-core (= 1.2.1) - kaminari-activerecord (1.2.1) + kaminari-core (= 1.2.2) + kaminari-activerecord (1.2.2) activerecord - kaminari-core (= 1.2.1) - kaminari-core (1.2.1) - kgio (2.11.2) - launchy (2.4.3) - addressable (~> 2.3) - letter_opener (1.6.0) - launchy (~> 2.2) - loofah (2.7.0) + kaminari-core (= 1.2.2) + kaminari-core (1.2.2) + kgio (2.11.4) + launchy (3.1.1) + addressable (~> 2.8) + childprocess (~> 5.0) + logger (~> 1.6) + letter_opener (1.10.0) + launchy (>= 2.2, < 4) + logger (1.7.0) + loofah (2.25.0) crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) + nokogiri (>= 1.12.0) + mail (2.9.0) + logger mini_mime (>= 0.1.1) - marcel (1.0.1) - method_source (0.9.2) - mime-types (3.2.2) - mime-types-data (~> 3.2015) - mime-types-data (3.2018.0812) - mini_magick (4.9.4) - mini_mime (1.0.3) - mini_portile2 (2.4.0) - minitest (5.14.4) - mo (1.4.0) - boson - msgpack (1.3.3) - multi_json (1.13.1) - mysql2 (0.5.2) - net-scp (1.2.1) - net-ssh (>= 2.6.5) - net-sftp (2.1.2) - net-ssh (>= 2.6.5) - net-ssh (5.0.2) - net-ssh-gateway (2.0.0) - net-ssh (>= 4.0.0) - nio4r (2.5.2) - nokogiri (1.10.10) - mini_portile2 (~> 2.4.0) - nokogumbo (1.5.0) - nokogiri + net-imap + net-pop + net-smtp + marcel (1.0.4) + mini_magick (4.13.2) + mini_mime (1.1.5) + minitest (5.27.0) + msgpack (1.8.0) + multi_json (1.19.1) + mysql2 (0.5.7) + bigdecimal + net-imap (0.6.2) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.1) + net-protocol + nio4r (2.7.5) + nokogiri (1.19.0-x86_64-linux-gnu) + racc (~> 1.4) + normalize-rails (8.0.1) orm_adapter (0.5.0) - patron (0.13.1) - pry (0.11.3) - coderay (~> 1.1.0) - method_source (~> 0.9.0) - pry-rails (0.3.6) - pry (>= 0.10.4) - public_suffix (3.0.3) + patron (0.13.4) + pp (0.6.3) + prettyprint + prettyprint (0.2.0) + psych (5.3.1) + date + stringio + public_suffix (7.0.2) + puma (7.1.0) + nio4r (~> 2.0) pygments.rb (1.2.1) multi_json (>= 1.0.0) - rack (2.2.3) - rack-test (1.1.0) - rack (>= 1.0, < 3) - rails (5.2.5) - actioncable (= 5.2.5) - actionmailer (= 5.2.5) - actionpack (= 5.2.5) - actionview (= 5.2.5) - activejob (= 5.2.5) - activemodel (= 5.2.5) - activerecord (= 5.2.5) - activestorage (= 5.2.5) - activesupport (= 5.2.5) - bundler (>= 1.3.0) - railties (= 5.2.5) - sprockets-rails (>= 2.0.0) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) + racc (1.8.1) + rack (3.2.4) + rack-session (2.1.1) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.3.1) + rack (>= 3) + rails (8.1.2) + actioncable (= 8.1.2) + actionmailbox (= 8.1.2) + actionmailer (= 8.1.2) + actionpack (= 8.1.2) + actiontext (= 8.1.2) + actionview (= 8.1.2) + activejob (= 8.1.2) + activemodel (= 8.1.2) + activerecord (= 8.1.2) + activestorage (= 8.1.2) + activesupport (= 8.1.2) + bundler (>= 1.15.0) + railties (= 8.1.2) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.3.0) - loofah (~> 2.3) - rails-sass-images (1.0.3) - dimensions (> 0) - mime-types (> 0) - sass (> 0) - railties (5.2.5) - actionpack (= 5.2.5) - activesupport (= 5.2.5) - method_source - rake (>= 0.8.7) - thor (>= 0.19.0, < 2.0) - raindrops (0.19.0) - rake (12.3.3) - rb-fsevent (0.10.3) - rb-inotify (0.10.0) + rails-html-sanitizer (1.6.2) + loofah (~> 2.21) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (8.1.2) + actionpack (= 8.1.2) + activesupport (= 8.1.2) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) + zeitwerk (~> 2.6) + raindrops (0.20.1) + rake (13.3.1) + rb-fsevent (0.11.2) + rb-inotify (0.11.1) ffi (~> 1.0) - redcarpet (3.4.0) - redis (4.0.2) - responders (2.4.1) - actionpack (>= 4.2.0, < 6.0) - railties (>= 4.2.0, < 6.0) - rinku (2.0.4) - sanitize (4.6.6) + rdoc (7.0.3) + erb + psych (>= 4.0.0) + tsort + redcarpet (3.6.1) + redis (5.4.1) + redis-client (>= 0.22.0) + redis-client (0.26.3) + connection_pool + reline (0.6.3) + io-console (~> 0.5) + responders (3.2.0) + actionpack (>= 7.0) + railties (>= 7.0) + rinku (2.0.6) + rouge (4.7.0) + ruby-vips (2.3.0) + ffi (~> 1.12) + logger + sanitize (6.1.3) crass (~> 1.0.2) - nokogiri (>= 1.4.4) - nokogumbo (~> 1.4) - sass (3.5.7) - sass-listen (~> 4.0.0) - sass-listen (4.0.0) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - sass-rails (5.0.7) - railties (>= 4.0.0, < 6) - sass (~> 3.1) - sprockets (>= 2.8, < 4.0) - sprockets-rails (>= 2.0, < 4.0) - tilt (>= 1.1, < 3) - sitemap_generator (2.2.1) - spring (2.0.2) - activesupport (>= 4.2) - sprockets (3.7.2) + nokogiri (>= 1.12.0) + sassc (2.4.0) + ffi (~> 1.9) + sassc-rails (2.1.2) + railties (>= 4.0.0) + sassc (>= 2.0) + sprockets (> 3.0) + sprockets-rails + tilt + securerandom (0.4.1) + simplecov (0.22.0) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-html (0.13.2) + simplecov_json_formatter (0.1.4) + sitemap_generator (6.3.0) + builder (~> 3.0) + sprockets (4.2.2) concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.2.2) - actionpack (>= 4.0) - activesupport (>= 4.0) + logger + rack (>= 2.2.4, < 4) + sprockets-rails (3.5.2) + actionpack (>= 6.1) + activesupport (>= 6.1) sprockets (>= 3.0.0) - state_machine (1.2.0) - sushi (0.0.4) - temple (0.8.0) - thin (1.7.2) - daemons (~> 1.0, >= 1.0.9) - eventmachine (~> 1.0, >= 1.0.4) - rack (>= 1, < 3) - thor (1.0.1) - thread_safe (0.3.6) - tilt (2.0.8) - tzinfo (1.2.9) - thread_safe (~> 0.1) - uglifier (4.1.18) + ssrf_filter (1.3.0) + state_machines (0.100.4) + state_machines-activemodel (0.101.0) + activemodel (>= 7.2) + state_machines (>= 0.100.4) + state_machines-activerecord (0.100.0) + activerecord (>= 7.2) + state_machines-activemodel (>= 0.100.0) + stringio (3.2.0) + temple (0.10.4) + terser (1.2.6) execjs (>= 0.3.0, < 3) - unicorn (5.4.1) + thor (1.5.0) + tilt (2.7.0) + timeout (0.6.0) + tsort (0.2.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicorn (6.1.0) kgio (~> 2.6) raindrops (~> 0.7) - warden (1.2.8) - rack (>= 2.0.6) - web-console (3.6.2) - actionview (>= 5.0) - activemodel (>= 5.0) + uri (1.1.1) + useragent (0.16.11) + warden (1.2.9) + rack (>= 2.0.9) + web-console (4.2.1) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) bindex (>= 0.4.0) - railties (>= 5.0) - websocket-driver (0.7.3) + railties (>= 6.0.0) + websocket-driver (0.8.0) + base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) + zeitwerk (2.7.4) PLATFORMS - ruby + x86_64-linux DEPENDENCIES - actionpack-page_caching! - acts_as_list (~> 0.4) + actionpack-page_caching + acts_as_list (~> 1.1) annotate ansi (~> 1.4) better_errors @@ -344,19 +415,15 @@ DEPENDENCIES bootsnap (~> 1.3) byebug canable (~> 0.1) - capistrano (~> 2.15)! - capistrano-maintenance - carrierwave (~> 1.1) - coffee-rails (~> 4.1) + carrierwave (~> 3.0) devise (~> 4.3) diff_match_patch! - doorkeeper (~> 4.2) + doorkeeper ffi-hunspell! - french_rails (~> 0.4) + french_rails (~> 0.7)! friendly_id (~> 5.1) - gctools (~> 0.2) - haml (~> 5.0) - html-pipeline-linuxfr (~> 0.15) + haml (~> 6.3) + html-pipeline-linuxfr (~> 0.17)! html_spellchecker (~> 0.1) html_truncator (~> 0.4) htmlentities (~> 4.3) @@ -366,24 +433,21 @@ DEPENDENCIES letter_opener listen! mini_magick (~> 4.9) - mo + minitest (< 6.0) mysql2 (~> 0.5.0) - nio4r (= 2.5.2) nokogiri (~> 1.10) - pry-rails - rails (~> 5.2) - rails-sass-images - redis (~> 4.0) + normalize-rails (~> 8.0) + puma + rails (~> 8.1.0) + redis (~> 5.0) rinku (~> 2.0) - sass-rails (~> 5.0) - sitemap_generator (~> 2.1) - spring - state_machine (~> 1.2) - sushi - thin - uglifier - unicorn (~> 5.1) + sassc-rails + simplecov + sitemap_generator + state_machines-activerecord + terser (~> 1.2) + unicorn (~> 6.1) web-console BUNDLED WITH - 1.17.3 + 2.6.7 diff --git a/INSTALL.md b/INSTALL.md index 693d0e999..7b08cb521 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,67 +1,44 @@ -# Introduction +# LinuxFr.org installation + +## Introduction This document will give you steps to install LinuxFr.org website on your -Debian Stretch development machine. +Debian Trixie (stable) development machine. Note that all commands which require root access are prefixed by `sudo`. -# Use stretch-backports +## Install Debian packages -LinuxFr.org requires to add `stretch-backports` source package as it -needs the `npm`package. +Packages to install from main Debian source: -``` -~ $ sudo bash -c "echo 'deb http://deb.debian.org/debian stretch-backports main' >> /etc/apt/sources.list.d/linuxfr.list" -~ $ sudo apt update -``` - -# Install Debian packages - -Packages to install from main Stretch source: - -``` -~ $ sudo apt install mysql-server mysql-client libmysql++-dev git \ -build-essential openssl libreadline-dev curl libcurl4-openssl-dev zlib1g \ -zlib1g-dev libssl-dev libxml2-dev libxslt-dev autoconf libgmp-dev libyaml-dev \ -ncurses-dev bison automake libtool imagemagick libc6-dev hunspell \ -hunspell-fr-comprehensive redis-server ruby ruby-dev ruby-rack +```sh +~ $ sudo apt install default-mysql-server default-libmysqlclient-dev \ +libmysql++-dev git build-essential openssl libreadline-dev curl \ +libcurl4-openssl-dev zlib1g zlib1g-dev libssl-dev libxml2-dev libxslt-dev \ +autoconf libgmp-dev libyaml-dev ncurses-dev bison automake libtool \ +imagemagick libc6-dev hunspell hunspell-fr-comprehensive redis-server \ +ruby ruby-dev ``` Note: - * you can use libcurl4-gnutls-dev instead of libcurl4-openssl-dev. - * the `mysql` packages will install MariaDB on Debian Stretch -Packages to install from backports: +- you can use libcurl4-gnutls-dev instead of libcurl4-openssl-dev. +- the `default-mysql-server` package will install MariaDB on Debian -``` -~ $ sudo apt install -t stretch-backports nodejs npm -``` - -# Get LinuxFr.org code and external resources +## Get LinuxFr.org code and external resources Use git to get LinuxFr.org sources: -``` +```sh ~ $ git clone git://github.com/linuxfrorg/linuxfr.org.git ``` To be able to reach ruby external resources, you need the `bundler` packager. -The minimal required version is written at the end of the `Gemfile.lock` file. -Actually, Debian has the `1.13.6` version and the lock file requires already -the `1.16.4` version. - -So we first install the `bundler` packager directly from `rubygems.org`: - -``` -~ $ gem install --user-install bundler -``` - -Don't forget to update your PATH environment according to the warning message -shown during installation. +It is now installed by default with Ruby. Now, we can reach external Ruby resources: -``` +```sh ~ $ cd linuxfr.org ~/linuxfr.org $ bundle config set path 'vendor/bundle' ~/linuxfr.org $ bundle install @@ -70,12 +47,6 @@ Now, we can reach external Ruby resources: The `check` command above should say you there's no problem. -LinuxFr.org uses also some nodejs resources: - -``` -~/linuxfr.org $ npm install -``` - ## Install the LinuxFr.org board The `board-linuxfr` gem server is used to allow users chat on the `/boards` and @@ -83,11 +54,11 @@ the collaborative `redaction` space to work asynchronously. To install it, the simplest is to run `gem install --user-install board-linuxfr` -# Configure LinuxFr.org +## Configure LinuxFr.org To configure LinuxFr.org, you need to copy both sample configuration files: -``` +```sh ~/linuxfr.org $ cp config/database.yml{.sample,} ~/linuxfr.org $ cp config/secrets.yml{.sample,} ``` @@ -102,17 +73,17 @@ In this document we'll assume you took the default values for database names: Note that, on production, you'll need to customize secrets inside the `secrets.yml` file. -# Configure SQL data base +## Configure SQL data base Prepare time zones information inside the `mysql` database: -``` +```sh ~ $ mysql_tzinfo_to_sql /usr/share/zoneinfo | sudo mysql mysql ``` Create LinuxFr.org databases and MySQL users: -``` +```sql ~/linuxfr.org $ sudo mysql > CREATE DATABASE linuxfr_rails CHARACTER SET utf8mb4; > CREATE USER linuxfr_rails IDENTIFIED BY 'linuxfr_rails password'; @@ -128,15 +99,15 @@ Create LinuxFr.org databases and MySQL users: Now every thing is ready and we can ask `rails` to setup our databases with every structures needed: -``` +```sh ~/linuxfr.org $ bin/rails db:setup ``` -# Run LinuxFr.org +## Run LinuxFr.org Now, you can run LinuxFr.org server with: -``` +```sh ~/linuxfr.org $ bin/rails server ``` @@ -145,31 +116,40 @@ virtual machine using the `http://localhost:3000` URL. Additionally, you run the boards within another terminal: -``` +```sh ~/linuxfr.org $ board-linuxfr -s -a localhost -p 9000 ``` -# Configure redirection +## Configure redirection This extra step isn't really needed to be able to use LinuxFr.org. -In the `config/environments/development.rb` file, there are two domains set -inside variables `MY_DOMAIN` and `IMG_DOMAIN`. -By default both domains are `dlfp.lo`. +In the `config/environments/development.rb` file, there are these variables: + +1. `MY_DOMAIN` and `IMG_DOMAIN` which define the domain name for the LinuxFr + service and the image caching service. + By default both domain names are `dlfp.lo`. + +2. `MY_PUBLIC_URL` and `IMG_PUBLIC_PORT` which define the public HTTP port for + both services. + By default both ports are `80`. + +These two set of variables are used to build the public url of the two +services. By default both public urls are `http://dlfp.lo`. -You'll find this domain inside some documents like emails to confirm user +You'll find this public url inside some documents like emails to confirm user subscription. To simplify your usage of LinuxFr.org, you should consider install a website locally using this domain name. Set the domain `dlfp.lo` to target `localhost`: -``` +```sh ~ $ sudo bash -c 'echo "127.0.0.1 dlfp.lo" >> /etc/hosts' ``` For Nginx, create a new server configuration with content like: -``` +```nginx server { server_name dlfp.lo; access_log /var/log/nginx/dlfp.access.log; @@ -182,7 +162,7 @@ server { # Avatars files uploaded on linuxfr server are stored in partitions # with folder name containing 3 digits - location ~ ^/avatars/\d\d\d/ { + location ~ ^/avatars/\d\d\d/ { root /home/linuxfr/linuxfr.org/uploads; } @@ -216,14 +196,14 @@ server { and restart the web server: -``` +```sh ~ $ sudo systemctl restart nginx ``` Now, on the virtual machine, you can access LinuxFr.org with the `http://dlfp.lo` URL. -# Create administrator +## Create administrator To create new users, you can use directly the website. @@ -232,7 +212,7 @@ in the database. For example, to set as admin the user with login `admin_login`: -``` +```sql $ sudo mysql linuxfr_rails > update accounts set role = 'admin' where login = 'admin_login' ; ``` diff --git a/README.md b/README.md index ed27f6db9..159336532 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,9 @@ Install See [INSTALL.md](INSTALL.md) to set up LinuxFr.org on a Debian environment. -Alternatively, you can read [Docker.md](Docker.md) to setup easily -LinuxFr.org development environment with the Docker engine and -[docker-compose](https://docs.docker.com/compose/). +Alternatively, you can read [Container.md](Container.md) to setup easily +LinuxFr.org development environment with a container engine like Docker or +Podman and use the [container composer](https://docs.docker.com/compose/). See also -------- diff --git a/app/assets/javascripts/application.coffee b/app/assets/javascripts/application.coffee deleted file mode 100644 index 4e724e8bc..000000000 --- a/app/assets/javascripts/application.coffee +++ /dev/null @@ -1,148 +0,0 @@ -#= require jquery2 -#= require jquery_ujs -#= require jquery.autocomplete -#= require jquery.caret-range -#= require jquery.cookie -#= require jquery.hotkeys -#= require jquery.notice -#= require jquery.markitup -#= require markitup-markdown -#= require_tree . - -$ = window.jQuery - -$("body").on "ajax:success", "form[data-remote]", (e, data) -> - $.noticeAdd text: data.notice if data and data.notice - $("#nb_votes").text data.nb_votes if data and data.nb_votes - $(@).parent().hide() unless $(@).data("hidden") - -$(".markItUp").markItUp window.markItUpSettings - -$("a.hit_counter[data-hit]").each -> - @href = "/redirect/" + $(@).data("hit") - -# Ready to moule -$("input[autofocus=autofocus]").focus() -$(".board").chat() -$("#news_revisions").redaction() - -# Force people to preview their modified contents -$("textarea, #form_answers input").keypress (event) -> - $(@).parents("form").find("input[value=Prévisualiser]").next("input[type=submit]").hide() - $(@).off event - -# Add/Remove dynamically links in the news form -langs = - xx: "!? hmmm ?!" - fr: "Français" - de: "Allemand" - en: "Anglais" - eu: "Basque" - ct: "Catalan" - cn: "Chinois" - wq: "Code/binaire" - ko: "Coréen" - da: "Danois" - es: "Espagnol" - ee: "Estonien" - fi: "Finnois" - el: "Grec" - it: "Italien" - ja: "Japonais" - nl: "Néerlandais" - no: "Norvégien" - pl: "Polonais" - pt: "Portugais" - ru: "Russe" - sv: "Suédois" - -$("#form_links").nested_fields "news", "link", "lien", "fieldset", title: "text", url: "url", lang: langs -$("#form_answers").nested_fields "poll", "answer", "choix", "p", answer: "text" - -# Mask the contributors if they are too many -$("article.news .edited_by").each -> - field = $(@) - nb = field.find("a").length - if nb > 3 - was = field.html() - field.html "#{nb} personnes" - field.one "click", -> field.html was - -# Toolbar preferences -$("#account_visible_toolbar") - .prop("checked", Toolbar.storage.visible != "false") - .click -> - Toolbar.storage.visible = $(@).is(":checked") - true - -# Show the toolbar -$.fn.reverse = [].reverse -if $("body").hasClass("logged") - if $("#comments").length - $("#comments .new-comment") - .toolbar("Nouveaux commentaires", folding: "#comments .comment") - .additional $("#comments .comment").sort((a,b) -> a.id.localeCompare(b.id)), "Commentaires par ordre chronologique" - else if $("main .node").length - $("#phare .new-node, main .new-node:not(.ppp)") - .toolbar("Contenus jamais visités") - .additional $("#phare .new_comments, main .node:not(.ppp) .new_comments").parents("article").reverse(), "Contenus lus avec + de commentaires" - -# Redaction -$(".edition_in_place").editionInPlace() -$("#redaction .new_link").editionInPlace() -$("#redaction .new_paragraph").on "ajax:success", false -$("#redaction .link, #redaction .paragraph").lockableEditionInPlace() - -# Tags -$.fn.autocompleter = () -> - @each -> - input = $(@) - input.autocomplete input.data("url"), - multiple: true - multipleSeparator: " " - dataType: "text" - matchSubset: false - @ -$("input#tags").autocompleter() -$(".tag_in_place").on("in_place:form", -> - $("input.autocomplete").autocompleter().focus() -).on("in_place:success", -> - $.noticeAdd text: "Étiquettes ajoutées" -).editionInPlace() -$(".add_tag, .remove_tag").click( -> - $(@).blur().parents("form").data hidden: "true" -).parents("form").on "ajax:success", -> - $(@).find("input").attr disabled: "disabled" - -# Hotkeys -$(document).bind("keypress", "g", -> - $("html,body").animate scrollTop: 0, 500 - false -).bind("keypress", "shift+g", -> - $("html,body").animate scrollTop: $("body").attr("scrollHeight"), 500 - false -).bind "keypress", "shift+?", -> - $.noticeAdd - text: """ - Raccourcis clavier : - """ - stay: true - false - -$("#account_user_attributes_avatar").change -> - return if window.URL? - url = window.URL.createObjectURL(@files[0]) - $(@).parents("form").find(".avatar").attr "src", url - -# Follow-up, admins, plonk... -$("button.more").click -> - $(@).next('.more_actions').show() - $(@).hide() - false diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js new file mode 100644 index 000000000..d203f8390 --- /dev/null +++ b/app/assets/javascripts/application.js @@ -0,0 +1,211 @@ +//= require jquery2 +//= require jquery_ujs +//= require jquery.autocomplete +//= require jquery.caret-range +//= require jquery.cookie +//= require jquery.hotkeys +//= require jquery.notice +//= require jquery.markitup +//= require markitup-markdown +//= require_tree . + +$ = window.jQuery; + +$("body").on("ajax:success", "form[data-remote]", function(e, data) { + if (data && data.notice) { + $.noticeAdd({ text: data.notice }); + } + if (data && data.nb_votes) { + $("#nb_votes").text(data.nb_votes); + } + if (!$(this).data("hidden")) { + $(this) + .parent() + .hide(); + } +}); + +$(".markItUp").markItUp(window.markItUpSettings); + +$("a.hit_counter[data-hit]").each(function() { + this.href = "/redirect/" + $(this).data("hit"); +}); + +// Ready to moule +$("input[autofocus=autofocus]").focus(); +$(".board").chat(); +$("#news_revisions").redaction(); + +// Force people to preview their modified contents +$("textarea, #form_answers input").keypress(function(event) { + $(this) + .parents("form") + .find("input[value=Prévisualiser]") + .next("input[type=submit]") + .hide(); + $(this).off(event); +}); + +// Add/Remove dynamically links in the news form +const langs = { + xx: "!? hmmm ?!", + fr: "Français", + de: "Allemand", + en: "Anglais", + eu: "Basque", + ct: "Catalan", + cn: "Chinois", + wq: "Code/binaire", + ko: "Coréen", + da: "Danois", + es: "Espagnol", + ee: "Estonien", + fi: "Finnois", + el: "Grec", + it: "Italien", + ja: "Japonais", + nl: "Néerlandais", + no: "Norvégien", + pl: "Polonais", + pt: "Portugais", + ru: "Russe", + sv: "Suédois" +}; + +$("#form_links").nested_fields("news", "link", "lien", "fieldset", { + title: "text", + url: "url", + lang: langs +}); +$("#form_answers").nested_fields("poll", "answer", "choix", "p", { + answer: "text" +}); + +// Mask the contributors if they are too many +$("article.news .edited_by").each(function() { + const field = $(this); + const nb = field.find("a").length; + if (nb > 3) { + const was = field.html(); + field.html(`${nb} personnes`); + field.one("click", () => field.html(was)); + } +}); + +// Toolbar preferences +$("#account_visible_toolbar") + .prop("checked", Toolbar.storage.visible !== "false") + .click(function() { + Toolbar.storage.visible = $(this).is(":checked"); + return true; + }); + +// Show the toolbar +$.fn.reverse = [].reverse; +if ($("body").hasClass("logged")) { + if ($("#comments").length) { + $("#comments .new-comment") + .toolbar("Nouveaux commentaires", { folding: "#comments .comment" }) + .additional( + $("#comments .comment").sort((a, b) => a.id.localeCompare(b.id)), + "Commentaires par ordre chronologique" + ); + } else if ($("main .node").length) { + $("#phare .new-node, main .new-node:not(.ppp)") + .toolbar("Contenus jamais visités") + .additional( + $("#phare .new_comments, main .node:not(.ppp) .new_comments") + .parents("article") + .reverse(), + "Contenus lus avec + de commentaires" + ); + } +} + +// Redaction +$(".edition_in_place").editionInPlace(); +$("#redaction .new_link").editionInPlace(); +$("#redaction .new_paragraph").on("ajax:success", false); +$("#redaction .link, #redaction .paragraph").lockableEditionInPlace(); + +// Tags +$.fn.autocompleter = function() { + return this.each(function() { + const input = $(this); + return input.autocomplete(input.data("url"), { + multiple: true, + multipleSeparator: " ", + dataType: "text", + matchSubset: false + }); + }); + return this; +}; +$("input#tags").autocompleter(); +$(".tag_in_place") + .on("in_place:form", () => + $("input.autocomplete") + .autocompleter() + .focus() + ) + .on("in_place:success", () => $.noticeAdd({ text: "Étiquettes ajoutées" })) + .editionInPlace(); +$(".add_tag, .remove_tag") + .click(function() { + $(this) + .blur() + .parents("form") + .data({ hidden: "true" }); + }) + .parents("form") + .on("ajax:success", function() { + $(this) + .find("input") + .attr({ disabled: "disabled" }); + }); + +// Hotkeys +$(document) + .bind("keypress", "g", function() { + $("html,body").animate({ scrollTop: 0 }, 500); + return false; + }) + .bind("keypress", "shift+g", function() { + $("html,body").animate({ scrollTop: document.body.offsetHeight }, 500); + return false; + }) + .bind("keypress", "shift+?", function() { + $.noticeAdd({ + text: `\ +Raccourcis clavier : \ +`, + stay: true + }); + return false; + }); + +$("#account_user_attributes_avatar").change(function() { + if (!window.URL) { + return; + } + const url = window.URL.createObjectURL(this.files[0]); + return $(this) + .parents("form") + .find(".avatar") + .attr("src", url); +}); + +// Follow-up, admins, plonk... +$("button.more").click(function() { + $(this) + .next(".more_actions") + .show(); + $(this).hide(); +}); diff --git a/app/assets/javascripts/chat.coffee b/app/assets/javascripts/chat.coffee deleted file mode 100644 index 0d1a06899..000000000 --- a/app/assets/javascripts/chat.coffee +++ /dev/null @@ -1,158 +0,0 @@ -#= require push - -$ = window.jQuery - -class Chat - constructor: (@board) -> - @input = @board.find("input[type=text]") - @inbox = @board.find(".inbox") - @isInboxLarge = @inbox.hasClass("large") - @inboxContainer = @board.find(".inbox-container") - @inboxContainer.scrollTop(@inbox.height()) - @chan = @board.data("chan") - @board.find(".board-left .norloge").click @norloge - @board.find("form").submit @postMessage - @totoz_type = $.cookie("totoz-type") - @totoz_url = $.cookie("totoz-url") or "https://totoz.eu/img/" - @norlogize right for right in @board.find(".board-right") - @norlogize_left left for left in @board.find(".board-left time").get().reverse() - @board.on("mouseenter", ".board-left time", @left_highlitizer) - .on("mouseleave", "time", @deshighlitizer) - @board.on("mouseenter", ".board-right time", @right_highlitizer) - .on("mouseleave", "time", @deshighlitizer) - if @totoz_type == "popup" - @totoz = @board.append($("
")).find("#les-totoz") - @board.on("mouseenter", ".totoz", @createTotoz) - .on("mouseleave", ".totoz", @destroyTotoz) - $.push(@chan).on("chat", @onChat).start() - - onChat: (msg) => - existing = $("#board_" + msg.id) - return if existing.length > 0 - if @isInboxLarge - @inbox.append(msg.large).find(".board-left:last .norloge").click @norloge - @inboxContainer.scrollTop(@inbox.height()) - @norlogize right for right in @inbox.find(".board-right:last") - @norlogize_left left for left in @inbox.find(".board-left time:last") - else - @inbox.prepend(msg.message).find(".board-left:first .norloge").click @norloge - @norlogize right for right in @inbox.find(".board-right:first") - @norlogize_left left for left in @inbox.find(".board-left time:first") - - postMessage: (event) => - form = $(event.target) - data = form.serialize() - @input.val("").select() - $.ajax url: form.attr("action"), data: data, type: "POST", dataType: "script" - false - - norloge: (event) => - string = $(event.target).attr("norloge") - index = $(event.target).data("clockIndex") - if index > 1 || (index == 1 && @board.find(".board-left time[data-clock-time=\"" + $(event.target).data("clockTime") + "\"]").length > 1) - switch index - when 1 then string += "¹" - when 2 then string += "²" - when 3 then string += "³" - else string += ":" + index - value = @input.val() - range = @input.caret() - unless range.start? - range.start = 0 - range.end = 0 - @input.val value.substr(0, range.start) + string + " " + value.substr(range.end, value.length) - @input.caret range.start + string.length + 1 - @input.focus() - - norlogize: (x) -> - tmp = $('
') - escape = (txt) -> tmp.text(txt).html() - $(x).contents().filter(-> @nodeType == 3).each -> - r = /(\d{4}-\d{2}-\d{2} )?(\d{2}:\d{2}(:\d{2})?)([⁰¹²³⁴⁵⁶⁷⁸⁹]+|[:\^]\d+)?/g - orig = escape @data - html = "" - while matches = r.exec orig - [match, datematch, timematch, minutes, index] = matches - if index - switch index.substr(0, 1) - when ":", "^" then index = index.substr(1) - when "¹" then index = 1 - when "²" then index = 2 - when "³" then index = 3 - when "⁴" then index = 4 - when "⁵" then index = 5 - when "⁶" then index = 6 - when "⁷" then index = 7 - when "⁸" then index = 8 - when "⁹" then index = 9 - else index = 1 - else - index = 1 - stop = matches.index - html = html + orig.slice(idx, stop) + "" - idx = r.lastIndex - $(@).replaceWith html+orig.slice(idx) if html - if @totoz_type == "popup" || @totoz_type == "inline" - cfg = @ - $(x).contents().filter(-> @nodeType == 3).each -> - totoz = /\[:([0-9a-zA-Z \*\$@':_-]+)\]/g - orig = escape @data - html = "" - while matches = totoz.exec orig - [title, name] = matches - stop = matches.index - html = html + orig.slice(idx, stop) + - if cfg.totoz_type == "popup" - "#{title}" - else if cfg.totoz_type == "inline" - "\"#{title}\"" - idx = totoz.lastIndex - $(@).replaceWith html+orig.slice(idx) if html - - norlogize_left: (x) -> - norlogeDatetime = $(x).attr("datetime") - date = /\d{4}-\d{2}-\d{2}/.exec(norlogeDatetime) - time = /\d{2}:\d{2}:\d{2}/.exec(norlogeDatetime) - index = @board.find(".board-left time[data-clock-date=\"" + date + "\"][data-clock-time=\"" + time + "\"]").length + 1 - $(x).attr("data-clock-date", date) - $(x).attr("data-clock-time", time) - $(x).attr("data-clock-index", index) - - left_highlitizer: (event) => - time = $(event.target).data("clockTime") - index = $(event.target).data("clockIndex") - @inbox.find("time[data-clock-time=\"" + time + "\"][data-clock-index=\"" + index + "\"]").addClass "highlighted" - @inbox.find("time[data-clock-time=\"" + time.substr(0, 5) + "\"][data-clock-index=\"" + index + "\"]").addClass "highlighted" - - right_highlitizer: (event) => - time = $(event.target).data("clockTime") - index = $(event.target).data("clockIndex") - if time.length = 5 - @inbox.find("time[data-clock-time*=\"" + time + "\"]").addClass "highlighted" - else - @inbox.find("time[data-clock-time=\"" + time + "\"][data-clock-index=\"" + index + "\"]").addClass "highlighted" - - deshighlitizer: => - @inbox.find("time.highlighted").removeClass "highlighted" - - createTotoz: (event) => - totozName = event.target.getAttribute("data-totoz-name") - totozId = encodeURIComponent(totozName).replace(/[%']/g, "") - totoz = @totoz.find("#totoz-" + totozId).first() - if totoz.size() == 0 - totoz = $("
") - .css(display: "none", position: "absolute") - .append("") - @totoz.append totoz - position = $(event.target).position() - [x, y] = [position.left, position.top + event.target.offsetHeight] - totoz.css "z-index": "15", display: "block", top: y + 5, left: x + 5 - - destroyTotoz: (event) => - totozId = encodeURIComponent(event.target.getAttribute("data-totoz-name")).replace(/[%']/g, "") - totoz = @totoz.find("#totoz-" + totozId).first() - totoz.css display: "none" - -$.fn.chat = -> - @each -> - new Chat($(@)) diff --git a/app/assets/javascripts/chat.js b/app/assets/javascripts/chat.js new file mode 100644 index 000000000..458751281 --- /dev/null +++ b/app/assets/javascripts/chat.js @@ -0,0 +1,344 @@ +//= require push + +$ = window.jQuery; + +class Chat { + constructor(board) { + this.onChat = this.onChat.bind(this); + this.postMessage = this.postMessage.bind(this); + this.norloge = this.norloge.bind(this); + this.left_highlitizer = this.left_highlitizer.bind(this); + this.right_highlitizer = this.right_highlitizer.bind(this); + this.deshighlitizer = this.deshighlitizer.bind(this); + this.createTotoz = this.createTotoz.bind(this); + this.destroyTotoz = this.destroyTotoz.bind(this); + this.board = board; + this.input = this.board.find("input[type=text]"); + this.inbox = this.board.find(".inbox"); + this.isInboxLarge = this.inbox.hasClass("large"); + this.inboxContainer = this.board.find(".inbox-container"); + this.inboxContainer.scrollTop(this.inbox.height()); + this.chan = this.board.data("chan"); + this.board.find(".board-left .norloge").click(this.norloge); + this.board.find("form").submit(this.postMessage); + this.totoz_type = $.cookie("totoz-type"); + this.totoz_url = $.cookie("totoz-url") || "https://totoz.eu/img/"; + for (var right of this.board.find(".board-right")) { + this.norlogize(right); + } + for (var left of this.board + .find(".board-left time") + .get() + .reverse()) { + this.norlogize_left(left); + } + this.board + .on("mouseenter", ".board-left time", this.left_highlitizer) + .on("mouseleave", "time", this.deshighlitizer); + this.board + .on("mouseenter", ".board-right time", this.right_highlitizer) + .on("mouseleave", "time", this.deshighlitizer); + if (this.totoz_type === "popup") { + this.totoz = this.board + .append($('
')) + .find("#les-totoz"); + this.board + .on("mouseenter", ".totoz", this.createTotoz) + .on("mouseleave", ".totoz", this.destroyTotoz); + } + $.push(this.chan) + .on("chat", this.onChat) + .start(); + } + + onChat(msg) { + let right; + const existing = $("#board_" + msg.id); + if (existing.length > 0) { + return; + } + if (this.isInboxLarge) { + this.inbox + .append(msg.large) + .find(".board-left:last .norloge") + .click(this.norloge); + this.inboxContainer.scrollTop(this.inbox.height()); + for (right of this.inbox.find(".board-right:last")) { + this.norlogize(right); + } + for (var left of this.inbox.find(".board-left time:last")) { + this.norlogize_left(left); + } + } else { + this.inbox + .prepend(msg.message) + .find(".board-left:first .norloge") + .click(this.norloge); + for (right of this.inbox.find(".board-right:first")) { + this.norlogize(right); + } + for (var left of this.inbox.find(".board-left time:first")) { + this.norlogize_left(left); + } + } + } + + postMessage(event) { + const form = $(event.target); + const data = form.serialize(); + this.input.val("").select(); + $.ajax({ + url: form.attr("action"), + data, + type: "POST", + dataType: "script" + }); + return false; + } + + norloge(event) { + let string = $(event.target).attr("norloge"); + const index = $(event.target).data("clockIndex"); + if ( + index > 1 || + (index === 1 && + this.board.find( + '.board-left time[data-clock-time="' + + $(event.target).data("clockTime") + + '"]' + ).length > 1) + ) { + switch (index) { + case 1: + string += "¹"; + break; + case 2: + string += "²"; + break; + case 3: + string += "³"; + break; + default: + string += ":" + index; + } + } + const value = this.input.val(); + const range = this.input.caret(); + if (!range.start) { + range.start = 0; + range.end = 0; + } + this.input.val( + value.substr(0, range.start) + + string + + " " + + value.substr(range.end, value.length) + ); + this.input.caret(range.start + string.length + 1); + return this.input.focus(); + } + + norlogize(x) { + const tmp = $("
"); + const escape = txt => tmp.text(txt).html(); + $(x) + .contents() + .filter(function() { + return this.nodeType === 3; + }) + .each(function() { + let matches; + let idx; + const r = /(\d{4}-\d{2}-\d{2} )?(\d{2}:\d{2}(:\d{2})?)([⁰¹²³⁴⁵⁶⁷⁸⁹]+|[:\^]\d+)?/g; + const orig = escape(this.data); + let html = ""; + while ((matches = r.exec(orig))) { + var [match, datematch, timematch, minutes, index] = matches; + if (index) { + switch (index.substr(0, 1)) { + case ":": + case "^": + index = index.substr(1); + break; + case "¹": + index = 1; + break; + case "²": + index = 2; + break; + case "³": + index = 3; + break; + case "⁴": + index = 4; + break; + case "⁵": + index = 5; + break; + case "⁶": + index = 6; + break; + case "⁷": + index = 7; + break; + case "⁸": + index = 8; + break; + case "⁹": + index = 9; + break; + default: + index = 1; + } + } else { + index = 1; + } + var stop = matches.index; + html = + html + + orig.slice(idx, stop) + + '"; + idx = r.lastIndex; + } + if (html) { + return $(this).replaceWith(html + orig.slice(idx)); + } + }); + if (this.totoz_type === "popup" || this.totoz_type === "inline") { + const cfg = this; + return $(x) + .contents() + .filter(function() { + return this.nodeType === 3; + }) + .each(function() { + let matches; + let idx; + const totoz = /\[:([0-9a-zA-Z \*\$@':_-]+)\]/g; + const orig = escape(this.data); + let html = ""; + while ((matches = totoz.exec(orig))) { + var [title, name] = matches; + var stop = matches.index; + html = + html + + orig.slice(idx, stop) + + (() => { + if (cfg.totoz_type === "popup") { + return `${title}`; + } else if (cfg.totoz_type === "inline") { + return `\"${title}\"`; + } + })(); + idx = totoz.lastIndex; + } + if (html) { + return $(this).replaceWith(html + orig.slice(idx)); + } + }); + } + } + + norlogize_left(x) { + const norlogeDatetime = $(x).attr("datetime"); + const date = /\d{4}-\d{2}-\d{2}/.exec(norlogeDatetime); + const time = /\d{2}:\d{2}:\d{2}/.exec(norlogeDatetime); + const index = + this.board.find( + '.board-left time[data-clock-date="' + + date + + '"][data-clock-time="' + + time + + '"]' + ).length + 1; + $(x).attr("data-clock-date", date); + $(x).attr("data-clock-time", time); + return $(x).attr("data-clock-index", index); + } + + left_highlitizer(event) { + const time = $(event.target).data("clockTime"); + const index = $(event.target).data("clockIndex"); + this.inbox + .find( + 'time[data-clock-time="' + time + '"][data-clock-index="' + index + '"]' + ) + .addClass("highlighted"); + return this.inbox + .find( + 'time[data-clock-time="' + + time.substr(0, 5) + + '"][data-clock-index="' + + index + + '"]' + ) + .addClass("highlighted"); + } + + right_highlitizer(event) { + const time = $(event.target).data("clockTime"); + const index = $(event.target).data("clockIndex"); + if ((time.length = 5)) { + return this.inbox + .find('time[data-clock-time*="' + time + '"]') + .addClass("highlighted"); + } else { + return this.inbox + .find( + 'time[data-clock-time="' + + time + + '"][data-clock-index="' + + index + + '"]' + ) + .addClass("highlighted"); + } + } + + deshighlitizer() { + return this.inbox.find("time.highlighted").removeClass("highlighted"); + } + + createTotoz(event) { + const totozName = event.target.getAttribute("data-totoz-name"); + const totozId = encodeURIComponent(totozName).replace(/[%']/g, ""); + let totoz = this.totoz.find("#totoz-" + totozId).first(); + if (totoz.size() === 0) { + totoz = $(`
`) + .css({ display: "none", position: "absolute" }) + .append(``); + this.totoz.append(totoz); + } + const position = $(event.target).position(); + const x = position.left; + const y = position.top + event.target.offsetHeight; + return totoz.css({ + "z-index": "15", + display: "block", + top: y + 5, + left: x + 5 + }); + } + + destroyTotoz(event) { + const totozId = encodeURIComponent( + event.target.getAttribute("data-totoz-name") + ).replace(/[%']/g, ""); + const totoz = this.totoz.find("#totoz-" + totozId).first(); + return totoz.css({ display: "none" }); + } +} + +$.fn.chat = function() { + return this.each(function() { + return new Chat($(this)); + }); +}; diff --git a/app/assets/javascripts/edition_in_place.coffee b/app/assets/javascripts/edition_in_place.coffee deleted file mode 100644 index 9436b2843..000000000 --- a/app/assets/javascripts/edition_in_place.coffee +++ /dev/null @@ -1,70 +0,0 @@ -$ = window.jQuery - -class EditionInPlace - constructor: (@el, @edit) -> - @url = @el.data("url") or (document.location.pathname + "/modifier") - @button().click @loadForm - - button: -> - if @edit then @el.find(@edit) else @el - - loadForm: => - @button().off "click" - @old = @el.html() - @xhr = $.ajax(url: @url, type: "get", dataType: "html").fail(@cantEdit).done(@showForm) - false - - cantEdit: => - @el.trigger "in_place:cant_edit", @xhr - @button().click @loadForm - @xhr = null - - showForm: => - form = @el.html(@xhr.responseText).find("form") - form.find(".cancel").click @reset - form.find("textarea, input, select")[0].select() - form.find(".markItUp").markItUp window.markItUpSettings - form.submit @submitForm - @el.trigger "in_place:form", @xhr - @xhr = null - - reset: (event) => - @el.html @old - @button().click @loadForm - @el.trigger "in_place:reset", event - false - - submitForm: => - form = @el.find("form") - url = form.attr("action") - data = form.serialize() - @xhr = $.ajax(url: url, type: "post", data: data, dataType: "html").fail(@error).done(@success) - false - - error: => - try - error = @el.find("ul.error") - response = $.parseJSON(@xhr.responseText) - messages = [] - for attribute, errors of response.errors - for message in errors - messages.push(message) - if messages.length == 1 - error.text("Erreur : " + messages[0]) - else - error.text("Erreurs :") - for message in messages - error.append($("
  • ").append(message)) - error.show() - @el.trigger "in_place:error", @xhr - @xhr = null - - success: => - @el = $(@xhr.responseText).replaceAll @el - @button().click @loadForm - @el.trigger "in_place:success", @xhr - @xhr = null - -$.fn.editionInPlace = (edit_selector) -> - @each -> - new EditionInPlace($(@), edit_selector) diff --git a/app/assets/javascripts/edition_in_place.js b/app/assets/javascripts/edition_in_place.js new file mode 100644 index 000000000..58b6dda8e --- /dev/null +++ b/app/assets/javascripts/edition_in_place.js @@ -0,0 +1,106 @@ +$ = window.jQuery; + +class EditionInPlace { + constructor(el, edit) { + this.loadForm = this.loadForm.bind(this); + this.cantEdit = this.cantEdit.bind(this); + this.showForm = this.showForm.bind(this); + this.reset = this.reset.bind(this); + this.submitForm = this.submitForm.bind(this); + this.error = this.error.bind(this); + this.success = this.success.bind(this); + this.el = el; + this.edit = edit; + this.url = this.el.data("url") || document.location.pathname + "/modifier"; + this.button().click(this.loadForm); + } + + button() { + if (this.edit) { + return this.el.find(this.edit); + } else { + return this.el; + } + } + + loadForm() { + this.button().off("click"); + this.old = this.el.html(); + this.xhr = $.ajax({ url: this.url, type: "get", dataType: "html" }) + .fail(this.cantEdit) + .done(this.showForm); + return false; + } + + cantEdit() { + this.el.trigger("in_place:cant_edit", this.xhr); + this.button().click(this.loadForm); + this.xhr = null; + } + + showForm() { + const form = this.el.html(this.xhr.responseText).find("form"); + form.find(".cancel").click(this.reset); + form.find("textarea, input, select")[0].select(); + form.find(".markItUp").markItUp(window.markItUpSettings); + form.submit(this.submitForm); + this.el.trigger("in_place:form", this.xhr); + this.xhr = null; + } + + reset(event) { + this.el.html(this.old); + this.button().click(this.loadForm); + this.el.trigger("in_place:reset", event); + return false; + } + + submitForm() { + const form = this.el.find("form"); + const url = form.attr("action"); + const data = form.serialize(); + this.xhr = $.ajax({ url, type: "post", data, dataType: "html" }) + .fail(this.error) + .done(this.success); + return false; + } + + error() { + try { + let message; + const error = this.el.find("ul.error"); + const response = $.parseJSON(this.xhr.responseText); + const messages = []; + for (var attribute in response.errors) { + var errors = response.errors[attribute]; + for (message of errors) { + messages.push(message); + } + } + if (messages.length === 1) { + error.text("Erreur : " + messages[0]); + } else { + error.text("Erreurs :"); + for (message of messages) { + error.append($("
  • ").append(message)); + } + } + error.show(); + } catch (error1) {} + this.el.trigger("in_place:error", this.xhr); + this.xhr = null; + } + + success() { + this.el = $(this.xhr.responseText).replaceAll(this.el); + this.button().click(this.loadForm); + this.el.trigger("in_place:success", this.xhr); + this.xhr = null; + } +} + +$.fn.editionInPlace = function(edit_selector) { + return this.each(function() { + return new EditionInPlace($(this), edit_selector); + }); +}; diff --git a/app/assets/javascripts/lockable_edition_in_place.coffee b/app/assets/javascripts/lockable_edition_in_place.coffee deleted file mode 100644 index 9213b45a4..000000000 --- a/app/assets/javascripts/lockable_edition_in_place.coffee +++ /dev/null @@ -1,13 +0,0 @@ -$ = window.jQuery - -$.fn.lockableEditionInPlace = -> - $(@).on("in_place:form", -> - self = $(@) - self.data cancel: self.find(".cancel").data("url") - self.data token: self.find('input[name="authenticity_token"]').serialize() - ).on("in_place:reset", -> - self = $(@) - $.ajax url: self.data("cancel"), type: "post", data: self.data("token") - ).on("in_place:cant_edit", (event, xhr) -> - $.noticeAdd text: xhr.responseText - ).editionInPlace("button.edit") diff --git a/app/assets/javascripts/lockable_edition_in_place.js b/app/assets/javascripts/lockable_edition_in_place.js new file mode 100644 index 000000000..82ae1dc17 --- /dev/null +++ b/app/assets/javascripts/lockable_edition_in_place.js @@ -0,0 +1,24 @@ +$ = window.jQuery; + +$.fn.lockableEditionInPlace = function() { + return $(this) + .on("in_place:form", function() { + const self = $(this); + self.data({ cancel: self.find(".cancel").data("url") }); + self.data({ + token: self.find('input[name="authenticity_token"]').serialize() + }); + }) + .on("in_place:reset", function() { + const self = $(this); + $.ajax({ + url: self.data("cancel"), + type: "post", + data: self.data("token") + }); + }) + .on("in_place:cant_edit", (event, xhr) => + $.noticeAdd({ text: xhr.responseText }) + ) + .editionInPlace("button.edit"); +}; diff --git a/app/assets/javascripts/nested_fields.coffee b/app/assets/javascripts/nested_fields.coffee deleted file mode 100644 index 4f4f29ca0..000000000 --- a/app/assets/javascripts/nested_fields.coffee +++ /dev/null @@ -1,48 +0,0 @@ -$ = window.jQuery - -class NestedFields - constructor: (@el, @parent, @nested, @text, @tag, @attributes) -> - @create() - - create: -> - items = @el.children(".#{@nested}") - @counter = items.length - @bind_item item, i for item, i in items - @el.append $("<#{@tag}/>", html: $("""" - it.children(".remove").click => - if counter - name = "#{@parent}[#{@nested}s_attributes][#{counter}][_destroy]" - it.replaceWith $("", name: name, type: "hidden", value: 1) - else - it.remove() - false - - add_item: => - last = @el.children(".#{@nested}:last") - last = @el.children("#{@tag}:first") if last.length == 0 - fset = $("<#{@tag}/>", class: @nested) - last.after fset - for i,type of @attributes - name = "#{@parent}[#{@nested}s_attributes][#{@counter}][#{i}]" - if typeof (type) == "string" - elem = $("", name: name, type: type, size: 30, autocomplete: "off") - else - elem = $("", { name, type: "hidden", value: 1 })); + } else { + it.remove(); + } + return false; + }); + } + + add_item() { + let last = this.el.children(`.${this.nested}:last`); + if (last.length === 0) { + last = this.el.children(`${this.tag}:first`); + } + const fset = $(`<${this.tag}/>`, { class: this.nested }); + last.after(fset); + for (var i in this.attributes) { + var elem; + var type = this.attributes[i]; + var name = `${this.parent}[${this.nested}s_attributes][${ + this.counter + }][${i}]`; + if (typeof type === "string") { + elem = $("", { name, type, size: 30, autocomplete: "off" }); + } else { + elem = $("