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..cea236121 100644 --- a/Gemfile +++ b/Gemfile @@ -1,49 +1,47 @@ -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", "~>7.1" -gem "actionpack-page_caching", github: "linuxfrorg/actionpack-page_caching" +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.5", github: "linuxfrorg/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: "linuxfrorg/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 +52,17 @@ 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 "puma" gem "spring" - gem "sushi" - gem "thin" 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..b028983bf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,29 +1,30 @@ -GIT - remote: https://github.com/capistrano/capistrano.git - revision: 08a82f3618425eae7539e2bbd34a87e35bac2800 - branch: legacy-v2 - 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) - GIT remote: https://github.com/guard/listen.git - revision: 587f4a7edb75fac80faa3408c4513af715dace87 + revision: f186b2fa159a2458f3ff7e8680c3a4fcbdc636d1 specs: - listen (3.1.5) + listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) GIT - remote: https://github.com/linuxfrorg/actionpack-page_caching.git - revision: 13a51101deb396eeb089a0d36a06653bd0d046a9 + remote: https://github.com/linuxfrorg/french-rails.git + revision: 9fd86a206d4ee90eed0205eb0e9047934dcb20bb + specs: + french_rails (0.6.0) + rails (~> 7.0) + +GIT + remote: https://github.com/linuxfrorg/html-pipeline-linuxfr.git + revision: 5d3b7a41555a63c23fdd51bd1d2cb5107a2b9d5f specs: - actionpack-page_caching (1.2.2) - actionpack (>= 5.0.0) + html-pipeline-linuxfr (0.17.0) + activesupport (~> 7.0) + escape_utils (~> 1.2) + nokogiri (~> 1.6) + patron (~> 0.8) + pygments.rb (~> 1.1) + redcarpet (~> 3.4) + sanitize (~> 5.0) GIT remote: https://github.com/nono/diff_match_patch-ruby.git @@ -33,309 +34,367 @@ 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) + actioncable (7.1.3.4) + actionpack (= 7.1.3.4) + activesupport (= 7.1.3.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailer (5.2.5) - actionpack (= 5.2.5) - actionview (= 5.2.5) - activejob (= 5.2.5) + zeitwerk (~> 2.6) + actionmailbox (7.1.3.4) + actionpack (= 7.1.3.4) + activejob (= 7.1.3.4) + activerecord (= 7.1.3.4) + activestorage (= 7.1.3.4) + activesupport (= 7.1.3.4) + mail (>= 2.7.1) + net-imap + net-pop + net-smtp + actionmailer (7.1.3.4) + actionpack (= 7.1.3.4) + actionview (= 7.1.3.4) + activejob (= 7.1.3.4) + activesupport (= 7.1.3.4) 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) + net-imap + net-pop + net-smtp + rails-dom-testing (~> 2.2) + actionpack (7.1.3.4) + actionview (= 7.1.3.4) + activesupport (= 7.1.3.4) + nokogiri (>= 1.8.5) + racc + 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) + actionpack-page_caching (1.2.4) + actionpack (>= 4.0.0) + actiontext (7.1.3.4) + actionpack (= 7.1.3.4) + activerecord (= 7.1.3.4) + activestorage (= 7.1.3.4) + activesupport (= 7.1.3.4) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (7.1.3.4) + activesupport (= 7.1.3.4) 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 (7.1.3.4) + activesupport (= 7.1.3.4) 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) + activemodel (7.1.3.4) + activesupport (= 7.1.3.4) + activerecord (7.1.3.4) + activemodel (= 7.1.3.4) + activesupport (= 7.1.3.4) + timeout (>= 0.4.0) + activestorage (7.1.3.4) + actionpack (= 7.1.3.4) + activejob (= 7.1.3.4) + activerecord (= 7.1.3.4) + activesupport (= 7.1.3.4) + marcel (~> 1.0) + activesupport (7.1.3.4) + base64 + bigdecimal 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) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + minitest (>= 5.1) + mutex_m + tzinfo (~> 2.0) + acts_as_list (1.2.1) + activerecord (>= 6.1) + activesupport (>= 6.1) + addressable (2.8.6) + public_suffix (>= 2.0.2, < 6.0) + annotate (3.2.0) + activerecord (>= 3.2, < 8.0) + rake (>= 10.4, < 14.0) ansi (1.5.0) - arel (9.0.0) - bcrypt (3.1.12) - better_errors (2.5.0) - coderay (>= 1.0.0) + base64 (0.2.0) + bcrypt (3.1.20) + 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 (3.1.8) + bindex (0.8.1) + binding_of_caller (1.0.1) + debug_inspector (>= 1.2.0) + bitfields (0.14.0) + activerecord (>= 5.1) + bootsnap (1.18.3) + msgpack (~> 1.2) + builder (3.3.0) + byebug (11.1.3) 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.0.7) + 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.0.0) + concurrent-ruby (1.3.2) + connection_pool (2.4.1) crass (1.0.6) - daemons (1.2.6) - debug_inspector (0.0.3) - devise (4.6.2) + date (3.3.4) + 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.0) + doorkeeper (5.7.0) + railties (>= 5) + drb (2.2.1) + erubi (1.12.0) + escape_utils (1.3.0) + execjs (2.9.1) + ffi (1.16.3) + friendly_id (5.5.1) 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.2.1) + activesupport (>= 6.1) + haml (6.3.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.8.2) + actionpack (>= 5.2) + activesupport (>= 5.2) 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) + i18n (1.14.5) 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.12.2) + mini_magick (>= 4.9.5, < 5) + 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.7.2) + irb (1.13.1) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + jquery-rails (4.6.0) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - kaminari (1.2.1) + 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.0.1) + addressable (~> 2.8) + childprocess (~> 5.0) + letter_opener (1.10.0) + launchy (>= 2.2, < 4) + loofah (2.22.0) crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) + nokogiri (>= 1.12.0) + mail (2.8.1) 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.12.0) + mini_mime (1.1.5) + mini_portile2 (2.8.7) + minitest (5.23.1) + msgpack (1.7.2) + multi_json (1.15.0) + mutex_m (0.2.0) + mysql2 (0.5.6) + net-imap (0.4.12) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.0) + net-protocol + nio4r (2.7.3) + nokogiri (1.16.5) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) + nokogiri (1.16.5-x86_64-linux) + racc (~> 1.4) + nokogumbo (2.0.5) + nokogiri (~> 1.8, >= 1.8.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.3) + psych (5.1.2) + stringio + public_suffix (5.0.5) + puma (6.4.2) + 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.0) + rack (3.0.11) + rack-session (2.0.0) + rack (>= 3.0.0) + rack-test (2.1.0) + rack (>= 1.3) + rackup (2.1.0) + rack (>= 3) + webrick (~> 1.8) + rails (7.1.3.4) + actioncable (= 7.1.3.4) + actionmailbox (= 7.1.3.4) + actionmailer (= 7.1.3.4) + actionpack (= 7.1.3.4) + actiontext (= 7.1.3.4) + actionview (= 7.1.3.4) + activejob (= 7.1.3.4) + activemodel (= 7.1.3.4) + activerecord (= 7.1.3.4) + activestorage (= 7.1.3.4) + activesupport (= 7.1.3.4) + bundler (>= 1.15.0) + railties (= 7.1.3.4) + rails-dom-testing (2.2.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.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + railties (7.1.3.4) + actionpack (= 7.1.3.4) + activesupport (= 7.1.3.4) + irb + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) + raindrops (0.20.1) + rake (13.2.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 (6.7.0) + psych (>= 4.0.0) + redcarpet (3.6.0) + redis (5.2.0) + redis-client (>= 0.22.0) + redis-client (0.22.2) + connection_pool + reline (0.5.8) + io-console (~> 0.5) + responders (3.1.1) + actionpack (>= 5.2) + railties (>= 5.2) + rinku (2.0.6) + rouge (4.2.1) + ruby-vips (2.2.1) + ffi (~> 1.12) + sanitize (5.2.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.8.0) + nokogumbo (~> 2.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 + simplecov (0.22.0) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-html (0.12.3) + simplecov_json_formatter (0.1.4) + sitemap_generator (6.3.0) + builder (~> 3.0) + spring (4.2.1) + sprockets (4.2.1) concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.2.2) - actionpack (>= 4.0) - activesupport (>= 4.0) + rack (>= 2.2.4, < 4) + sprockets-rails (3.5.1) + 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.1.2) + state_machines (0.6.0) + state_machines-activemodel (0.9.0) + activemodel (>= 6.0) + state_machines (>= 0.6.0) + state_machines-activerecord (0.9.0) + activerecord (>= 6.0) + state_machines-activemodel (>= 0.9.0) + stringio (3.1.0) + temple (0.10.3) + terser (1.2.2) execjs (>= 0.3.0, < 3) - unicorn (5.4.1) + thor (1.3.1) + tilt (2.3.0) + timeout (0.4.1) + 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) + 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) + webrick (1.8.1) + websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) + zeitwerk (2.6.15) 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 +403,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.5)! 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 +421,21 @@ DEPENDENCIES letter_opener listen! mini_magick (~> 4.9) - mo 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 (~> 7.1) + redis (~> 5.0) rinku (~> 2.0) - sass-rails (~> 5.0) - sitemap_generator (~> 2.1) + sassc-rails + simplecov + sitemap_generator spring - state_machine (~> 1.2) - sushi - thin - uglifier - unicorn (~> 5.1) + state_machines-activerecord + terser (~> 1.2) + unicorn (~> 6.1) web-console BUNDLED WITH - 1.17.3 + 2.4.20 diff --git a/INSTALL.md b/INSTALL.md index 693d0e999..8445b23f8 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -5,16 +5,6 @@ Debian Stretch development machine. Note that all commands which require root access are prefixed by `sudo`. -# Use stretch-backports - -LinuxFr.org requires to add `stretch-backports` source package as it -needs the `npm`package. - -``` -~ $ 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: @@ -31,12 +21,6 @@ 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: - -``` -~ $ sudo apt install -t stretch-backports nodejs npm -``` - # Get LinuxFr.org code and external resources Use git to get LinuxFr.org sources: @@ -70,12 +54,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 @@ -153,11 +131,20 @@ Additionally, you run the boards within another terminal: 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. diff --git a/README.md b/README.md index cdee161e5..d263fd2e5 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 = $("