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/Gemfile b/Gemfile index eee3e6c02..01c68600b 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", "~>8.1.0" -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.7", 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,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 b0fc6918d..bf99aa945 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,29 +1,31 @@ -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: 2fa1a742d8c20cd3394d60f1c54dcbcd27b39ded specs: - listen (3.1.5) + listen (3.10.0) + logger 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: 649652f4f992a09a880cac04adf780bdea5082f2 specs: - actionpack-page_caching (1.2.2) - actionpack (>= 5.0.0) + french_rails (0.7.0) + rails (~> 8.0) + +GIT + remote: https://github.com/linuxfrorg/html-pipeline-linuxfr.git + revision: 9927b673c06672c20a871664a85a45575cae1e9c + specs: + 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/nono/diff_match_patch-ruby.git @@ -33,325 +35,383 @@ 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.17) + railties + actioncable (8.1.2.1) + actionpack (= 8.1.2.1) + activesupport (= 8.1.2.1) 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.1) + actionpack (= 8.1.2.1) + activejob (= 8.1.2.1) + activerecord (= 8.1.2.1) + activestorage (= 8.1.2.1) + activesupport (= 8.1.2.1) + mail (>= 2.8.0) + actionmailer (8.1.2.1) + actionpack (= 8.1.2.1) + actionview (= 8.1.2.1) + activejob (= 8.1.2.1) + activesupport (= 8.1.2.1) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (8.1.2.1) + actionview (= 8.1.2.1) + activesupport (= 8.1.2.1) + 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.1) + action_text-trix (~> 2.1.15) + actionpack (= 8.1.2.1) + activerecord (= 8.1.2.1) + activestorage (= 8.1.2.1) + activesupport (= 8.1.2.1) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (8.1.2.1) + activesupport (= 8.1.2.1) 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.1) + activesupport (= 8.1.2.1) 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.1) + activesupport (= 8.1.2.1) + activerecord (8.1.2.1) + activemodel (= 8.1.2.1) + activesupport (= 8.1.2.1) + timeout (>= 0.4.0) + activestorage (8.1.2.1) + actionpack (= 8.1.2.1) + activejob (= 8.1.2.1) + activerecord (= 8.1.2.1) + activesupport (= 8.1.2.1) + marcel (~> 1.0) + activesupport (8.1.2.1) + 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.9) + 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.21.1) + msgpack (~> 1.2) + builder (3.3.0) + byebug (13.0.0) + reline (>= 0.6.0) canable (0.3.0) - capistrano-maintenance (0.0.5) - capistrano (~> 2.0) - capybara (3.15.1) - addressable - mini_mime (>= 0.1.3) - nokogiri (~> 1.8) - rack (>= 1.6.0) - rack-test (>= 0.6.3) - regexp_parser (~> 1.2) - xpath (~> 3.2) - carrierwave (1.2.3) - activemodel (>= 4.0.0) - activesupport (>= 4.0.0) - mime-types (>= 1.16) - childprocess (3.0.0) - 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.2) + 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.17.0) + pp (>= 0.6.0) + prism (>= 1.3.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.19.2) + 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.1) 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 (6.0.2) + drb (~> 2.0) + prism (~> 1.5) + msgpack (1.8.0) + multi_json (1.19.1) + mysql2 (0.5.7) + bigdecimal + net-imap (0.6.3) + 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.2-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) + prism (1.9.0) + psych (5.3.1) + date + stringio + public_suffix (7.0.5) + puma (7.2.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.5) + 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.1) + actioncable (= 8.1.2.1) + actionmailbox (= 8.1.2.1) + actionmailer (= 8.1.2.1) + actionpack (= 8.1.2.1) + actiontext (= 8.1.2.1) + actionview (= 8.1.2.1) + activejob (= 8.1.2.1) + activemodel (= 8.1.2.1) + activerecord (= 8.1.2.1) + activestorage (= 8.1.2.1) + activesupport (= 8.1.2.1) + bundler (>= 1.15.0) + railties (= 8.1.2.1) + 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.7.0) + loofah (~> 2.25) + 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.1) + actionpack (= 8.1.2.1) + activesupport (= 8.1.2.1) + 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) - regexp_parser (1.8.2) - responders (2.4.1) - actionpack (>= 4.2.0, < 6.0) - railties (>= 4.2.0, < 6.0) - rinku (2.0.4) - rubyzip (1.3.0) - sanitize (4.6.6) + rdoc (7.2.0) + 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) - selenium-webdriver (3.142.7) - childprocess (>= 0.5, < 4.0) - rubyzip (>= 1.2.2) - 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.1) + 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) - xpath (3.2.0) - nokogiri (~> 1.8) + zeitwerk (2.7.5) 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 @@ -360,20 +420,15 @@ DEPENDENCIES bootsnap (~> 1.3) byebug canable (~> 0.1) - capistrano (~> 2.15)! - capistrano-maintenance - capybara (>= 2.15) - 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) @@ -383,25 +438,20 @@ 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 (~> 8.1.0) + redis (~> 5.0) rinku (~> 2.0) - sass-rails (~> 5.0) - selenium-webdriver - 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 fc07ee15c..60fd00f6d 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 - -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 +## Install Debian packages -Packages to install from main Stretch source: +Packages to install from main Debian 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: -``` -~ $ sudo apt install -t stretch-backports nodejs npm -``` +- you can use libcurl4-gnutls-dev instead of libcurl4-openssl-dev. +- the `default-mysql-server` package will install MariaDB on Debian -# 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,23 +116,23 @@ 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 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`. + service and the image caching service. + By default both domain names are `dlfp.lo`. 2. `MY_PUBLIC_PORT` and `IMG_PUBLIC_PORT` which define the public HTTP port for - both services. - By default both ports are `80`. + 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`. @@ -172,13 +143,13 @@ 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; @@ -191,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; } @@ -225,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. @@ -241,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' ; ``` @@ -273,3 +244,16 @@ You can override the configuration with the following environment variables: Warning: the current configuration doesn't support authentication or TLS. Be careful with a remote Redis because everything will be exchange in plain text, and you exposed your Redis server on the network. + +## Make CSS works in alpha/production + +LinuxFr.org allows to select custom CSS styles. + +On production environments, like on alpha.linuxfr.org, system administrator +needs to compile SCSS files as CSS with this command: + +```sh +RAILS_ENV=alpha RAILS_GROUPS=assets bundle exec rake assets:precompile +``` + +For production, you need to change the `RAILS_ENV` value. 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..533fdc604 --- /dev/null +++ b/app/assets/javascripts/application.js @@ -0,0 +1,210 @@ +//= 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 + }); + }); +}; +$("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 0ebccc3a5..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..dd07700e4 --- /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 = $("