Compare commits

..

No commits in common. "main" and "with-bulma" have entirely different histories.

56 changed files with 260 additions and 1250 deletions

View file

@ -18,7 +18,7 @@ gem "stimulus-rails"
gem "jbuilder" gem "jbuilder"
# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
gem "bcrypt", "~> 3.1.7" # gem "bcrypt", "~> 3.1.7"
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: %i[ windows jruby ] gem "tzinfo-data", platforms: %i[ windows jruby ]
@ -51,7 +51,6 @@ group :development, :test do
gem "rubocop-rails-omakase", require: false gem "rubocop-rails-omakase", require: false
gem "rspec-rails", "~> 8.0.0" gem "rspec-rails", "~> 8.0.0"
gem "factory_bot_rails"
end end
group :development do group :development do
@ -59,7 +58,6 @@ group :development do
gem "web-console" gem "web-console"
gem "solargraph", require: false gem "solargraph", require: false
gem "solargraph-rails", require: false gem "solargraph-rails", require: false
gem "letter_opener_web"
end end
group :test do group :test do
@ -70,5 +68,3 @@ end
gem "cssbundling-rails", "~> 1.4" gem "cssbundling-rails", "~> 1.4"
gem "rails-i18n" gem "rails-i18n"
gem "haml"

View file

@ -1,29 +1,29 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (8.0.4) actioncable (8.0.2)
actionpack (= 8.0.4) actionpack (= 8.0.2)
activesupport (= 8.0.4) activesupport (= 8.0.2)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
actionmailbox (8.0.4) actionmailbox (8.0.2)
actionpack (= 8.0.4) actionpack (= 8.0.2)
activejob (= 8.0.4) activejob (= 8.0.2)
activerecord (= 8.0.4) activerecord (= 8.0.2)
activestorage (= 8.0.4) activestorage (= 8.0.2)
activesupport (= 8.0.4) activesupport (= 8.0.2)
mail (>= 2.8.0) mail (>= 2.8.0)
actionmailer (8.0.4) actionmailer (8.0.2)
actionpack (= 8.0.4) actionpack (= 8.0.2)
actionview (= 8.0.4) actionview (= 8.0.2)
activejob (= 8.0.4) activejob (= 8.0.2)
activesupport (= 8.0.4) activesupport (= 8.0.2)
mail (>= 2.8.0) mail (>= 2.8.0)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
actionpack (8.0.4) actionpack (8.0.2)
actionview (= 8.0.4) actionview (= 8.0.2)
activesupport (= 8.0.4) activesupport (= 8.0.2)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
rack (>= 2.2.4) rack (>= 2.2.4)
rack-session (>= 1.0.1) rack-session (>= 1.0.1)
@ -31,35 +31,35 @@ GEM
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
useragent (~> 0.16) useragent (~> 0.16)
actiontext (8.0.4) actiontext (8.0.2)
actionpack (= 8.0.4) actionpack (= 8.0.2)
activerecord (= 8.0.4) activerecord (= 8.0.2)
activestorage (= 8.0.4) activestorage (= 8.0.2)
activesupport (= 8.0.4) activesupport (= 8.0.2)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (8.0.4) actionview (8.0.2)
activesupport (= 8.0.4) activesupport (= 8.0.2)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.11) erubi (~> 1.11)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
activejob (8.0.4) activejob (8.0.2)
activesupport (= 8.0.4) activesupport (= 8.0.2)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (8.0.4) activemodel (8.0.2)
activesupport (= 8.0.4) activesupport (= 8.0.2)
activerecord (8.0.4) activerecord (8.0.2)
activemodel (= 8.0.4) activemodel (= 8.0.2)
activesupport (= 8.0.4) activesupport (= 8.0.2)
timeout (>= 0.4.0) timeout (>= 0.4.0)
activestorage (8.0.4) activestorage (8.0.2)
actionpack (= 8.0.4) actionpack (= 8.0.2)
activejob (= 8.0.4) activejob (= 8.0.2)
activerecord (= 8.0.4) activerecord (= 8.0.2)
activesupport (= 8.0.4) activesupport (= 8.0.2)
marcel (~> 1.0) marcel (~> 1.0)
activesupport (8.0.4) activesupport (8.0.2)
base64 base64
benchmark (>= 0.3) benchmark (>= 0.3)
bigdecimal bigdecimal
@ -72,19 +72,18 @@ GEM
securerandom (>= 0.3) securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5) tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1) uri (>= 0.13.1)
addressable (2.8.8) addressable (2.8.7)
public_suffix (>= 2.0.2, < 8.0) public_suffix (>= 2.0.2, < 7.0)
ast (2.4.3) ast (2.4.3)
backport (1.2.0) backport (1.2.0)
base64 (0.3.0) base64 (0.3.0)
bcrypt (3.1.21) bcrypt_pbkdf (1.1.1)
bcrypt_pbkdf (1.1.2) benchmark (0.4.1)
benchmark (0.5.0) bigdecimal (3.2.2)
bigdecimal (4.0.1)
bindex (0.8.1) bindex (0.8.1)
bootsnap (1.23.0) bootsnap (1.18.6)
msgpack (~> 1.2) msgpack (~> 1.2)
brakeman (8.0.2) brakeman (7.0.2)
racc racc
builder (3.3.0) builder (3.3.0)
capybara (3.40.0) capybara (3.40.0)
@ -96,57 +95,45 @@ GEM
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0) regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2) xpath (~> 3.2)
childprocess (5.1.0) concurrent-ruby (1.3.5)
logger (~> 1.5) connection_pool (2.5.3)
concurrent-ruby (1.3.6)
connection_pool (3.0.2)
crass (1.0.6) crass (1.0.6)
cssbundling-rails (1.4.3) cssbundling-rails (1.4.3)
railties (>= 6.0.0) railties (>= 6.0.0)
date (3.5.1) date (3.4.1)
debug (1.11.1) debug (1.11.0)
irb (~> 1.10) irb (~> 1.10)
reline (>= 0.3.8) reline (>= 0.3.8)
diff-lcs (1.6.2) diff-lcs (1.6.2)
dotenv (3.2.0) dotenv (3.1.8)
drb (2.2.3) drb (2.2.3)
ed25519 (1.4.0) ed25519 (1.4.0)
erb (6.0.1) erb (5.0.1)
erubi (1.13.1) erubi (1.13.1)
et-orbi (1.4.0) et-orbi (1.2.11)
tzinfo tzinfo
factory_bot (6.5.6) fugit (1.11.1)
activesupport (>= 6.1.0) et-orbi (~> 1, >= 1.2.11)
factory_bot_rails (6.5.1)
factory_bot (~> 6.5)
railties (>= 6.1.0)
fugit (1.12.1)
et-orbi (~> 1.4)
raabro (~> 1.4) raabro (~> 1.4)
globalid (1.3.0) globalid (1.2.1)
activesupport (>= 6.1) activesupport (>= 6.1)
haml (7.2.0) i18n (1.14.7)
temple (>= 0.8.2)
thor
tilt
i18n (1.14.8)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
importmap-rails (2.2.3) importmap-rails (2.1.0)
actionpack (>= 6.0.0) actionpack (>= 6.0.0)
activesupport (>= 6.0.0) activesupport (>= 6.0.0)
railties (>= 6.0.0) railties (>= 6.0.0)
io-console (0.8.2) io-console (0.8.0)
irb (1.17.0) irb (1.15.2)
pp (>= 0.6.0) pp (>= 0.6.0)
prism (>= 1.3.0)
rdoc (>= 4.0.0) rdoc (>= 4.0.0)
reline (>= 0.4.2) reline (>= 0.4.2)
jaro_winkler (1.7.0) jaro_winkler (1.6.1)
jbuilder (2.14.1) jbuilder (2.13.0)
actionview (>= 7.0.0) actionview (>= 5.0.0)
activesupport (>= 7.0.0) activesupport (>= 5.0.0)
json (2.18.1) json (2.12.2)
kamal (2.10.1) kamal (2.7.0)
activesupport (>= 7.0) activesupport (>= 7.0)
base64 (~> 0.2) base64 (~> 0.2)
bcrypt_pbkdf (~> 1.0) bcrypt_pbkdf (~> 1.0)
@ -157,40 +144,27 @@ GEM
sshkit (>= 1.23.0, < 2.0) sshkit (>= 1.23.0, < 2.0)
thor (~> 1.3) thor (~> 1.3)
zeitwerk (>= 2.6.18, < 3.0) zeitwerk (>= 2.6.18, < 3.0)
kramdown (2.5.2) kramdown (2.5.1)
rexml (>= 3.4.4) rexml (>= 3.3.9)
kramdown-parser-gfm (1.1.0) kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0) kramdown (~> 2.0)
language_server-protocol (3.17.0.5) language_server-protocol (3.17.0.5)
launchy (3.1.1)
addressable (~> 2.8)
childprocess (~> 5.0)
logger (~> 1.6)
letter_opener (1.10.0)
launchy (>= 2.2, < 4)
letter_opener_web (3.0.0)
actionmailer (>= 6.1)
letter_opener (~> 1.9)
railties (>= 6.1)
rexml
lint_roller (1.1.0) lint_roller (1.1.0)
logger (1.7.0) logger (1.7.0)
loofah (2.25.0) loofah (2.24.1)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
mail (2.9.0) mail (2.8.1)
logger
mini_mime (>= 0.1.1) mini_mime (>= 0.1.1)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
marcel (1.1.0) marcel (1.0.4)
matrix (0.4.3) matrix (0.4.3)
mini_mime (1.1.5) mini_mime (1.1.5)
minitest (6.0.1) minitest (5.25.5)
prism (~> 1.5)
msgpack (1.8.0) msgpack (1.8.0)
net-imap (0.6.3) net-imap (0.5.9)
date date
net-protocol net-protocol
net-pop (0.1.2) net-pop (0.1.2)
@ -204,64 +178,64 @@ GEM
net-smtp (0.5.1) net-smtp (0.5.1)
net-protocol net-protocol
net-ssh (7.3.0) net-ssh (7.3.0)
nio4r (2.7.5) nio4r (2.7.4)
nokogiri (1.19.1-aarch64-linux-gnu) nokogiri (1.18.8-aarch64-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.19.1-aarch64-linux-musl) nokogiri (1.18.8-aarch64-linux-musl)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.19.1-arm-linux-gnu) nokogiri (1.18.8-arm-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.19.1-arm-linux-musl) nokogiri (1.18.8-arm-linux-musl)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.19.1-x86_64-linux-gnu) nokogiri (1.18.8-x86_64-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.19.1-x86_64-linux-musl) nokogiri (1.18.8-x86_64-linux-musl)
racc (~> 1.4) racc (~> 1.4)
observer (0.1.2) observer (0.1.2)
open3 (0.2.1) ostruct (0.6.2)
ostruct (0.6.3)
parallel (1.27.0) parallel (1.27.0)
parser (3.3.10.2) parser (3.3.8.0)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
pp (0.6.3) pp (0.6.2)
prettyprint prettyprint
prettyprint (0.2.0) prettyprint (0.2.0)
prism (1.9.0) prism (1.4.0)
propshaft (1.3.1) propshaft (1.1.0)
actionpack (>= 7.0.0) actionpack (>= 7.0.0)
activesupport (>= 7.0.0) activesupport (>= 7.0.0)
rack rack
psych (5.3.1) railties (>= 7.0.0)
psych (5.2.6)
date date
stringio stringio
public_suffix (7.0.2) public_suffix (6.0.2)
puma (7.2.0) puma (6.6.0)
nio4r (~> 2.0) nio4r (~> 2.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.8.1) racc (1.8.1)
rack (3.2.5) rack (3.1.16)
rack-session (2.1.1) rack-session (2.1.1)
base64 (>= 0.1.0) base64 (>= 0.1.0)
rack (>= 3.0.0) rack (>= 3.0.0)
rack-test (2.2.0) rack-test (2.2.0)
rack (>= 1.3) rack (>= 1.3)
rackup (2.3.1) rackup (2.2.1)
rack (>= 3) rack (>= 3)
rails (8.0.4) rails (8.0.2)
actioncable (= 8.0.4) actioncable (= 8.0.2)
actionmailbox (= 8.0.4) actionmailbox (= 8.0.2)
actionmailer (= 8.0.4) actionmailer (= 8.0.2)
actionpack (= 8.0.4) actionpack (= 8.0.2)
actiontext (= 8.0.4) actiontext (= 8.0.2)
actionview (= 8.0.4) actionview (= 8.0.2)
activejob (= 8.0.4) activejob (= 8.0.2)
activemodel (= 8.0.4) activemodel (= 8.0.2)
activerecord (= 8.0.4) activerecord (= 8.0.2)
activestorage (= 8.0.4) activestorage (= 8.0.2)
activesupport (= 8.0.4) activesupport (= 8.0.2)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 8.0.4) railties (= 8.0.2)
rails-dom-testing (2.3.0) rails-dom-testing (2.3.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
minitest minitest
@ -272,39 +246,36 @@ GEM
rails-i18n (8.1.0) rails-i18n (8.1.0)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 8.0.0, < 9) railties (>= 8.0.0, < 9)
railties (8.0.4) railties (8.0.2)
actionpack (= 8.0.4) actionpack (= 8.0.2)
activesupport (= 8.0.4) activesupport (= 8.0.2)
irb (~> 1.13) irb (~> 1.13)
rackup (>= 1.0.0) rackup (>= 1.0.0)
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0, >= 1.2.2) thor (~> 1.0, >= 1.2.2)
tsort (>= 0.2)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.3.1) rake (13.3.0)
rbs (3.10.3) rbs (3.9.5)
logger logger
tsort rdoc (6.14.2)
rdoc (7.2.0)
erb erb
psych (>= 4.0.0) psych (>= 4.0.0)
tsort regexp_parser (2.10.0)
regexp_parser (2.11.3) reline (0.6.1)
reline (0.6.3)
io-console (~> 0.5) io-console (~> 0.5)
reverse_markdown (3.0.2) reverse_markdown (3.0.0)
nokogiri nokogiri
rexml (3.4.4) rexml (3.4.1)
rspec-core (3.13.6) rspec-core (3.13.5)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-expectations (3.13.5) rspec-expectations (3.13.5)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-mocks (3.13.7) rspec-mocks (3.13.5)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-rails (8.0.3) rspec-rails (8.0.2)
actionpack (>= 7.2) actionpack (>= 7.2)
activesupport (>= 7.2) activesupport (>= 7.2)
railties (>= 7.2) railties (>= 7.2)
@ -312,8 +283,8 @@ GEM
rspec-expectations (~> 3.13) rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13) rspec-mocks (~> 3.13)
rspec-support (~> 3.13) rspec-support (~> 3.13)
rspec-support (3.13.7) rspec-support (3.13.6)
rubocop (1.84.2) rubocop (1.77.0)
json (~> 2.3) json (~> 2.3)
language_server-protocol (~> 3.17.0.2) language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0) lint_roller (~> 1.1.0)
@ -321,17 +292,17 @@ GEM
parser (>= 3.3.0.2) parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0) regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.49.0, < 2.0) rubocop-ast (>= 1.45.1, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0) unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.49.0) rubocop-ast (1.45.1)
parser (>= 3.3.7.2) parser (>= 3.3.7.2)
prism (~> 1.7) prism (~> 1.4)
rubocop-performance (1.26.1) rubocop-performance (1.25.0)
lint_roller (~> 1.1) lint_roller (~> 1.1)
rubocop (>= 1.75.0, < 2.0) rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.47.1, < 2.0) rubocop-ast (>= 1.38.0, < 2.0)
rubocop-rails (2.34.3) rubocop-rails (2.32.0)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
lint_roller (~> 1.1) lint_roller (~> 1.1)
rack (>= 1.1) rack (>= 1.1)
@ -342,26 +313,24 @@ GEM
rubocop-performance (>= 1.24) rubocop-performance (>= 1.24)
rubocop-rails (>= 2.30) rubocop-rails (>= 2.30)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
rubyzip (3.2.2) rubyzip (2.4.1)
securerandom (0.4.1) securerandom (0.4.1)
selenium-webdriver (4.41.0) selenium-webdriver (4.34.0)
base64 (~> 0.2) base64 (~> 0.2)
logger (~> 1.4) logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5) rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 4.0) rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0) websocket (~> 1.0)
solargraph (0.58.2) solargraph (0.57.0)
ast (~> 2.4.3)
backport (~> 1.2) backport (~> 1.2)
benchmark (~> 0.4) benchmark (~> 0.4)
bundler (>= 2.0) bundler (~> 2.0)
diff-lcs (~> 1.4) diff-lcs (~> 1.4)
jaro_winkler (~> 1.6, >= 1.6.1) jaro_winkler (~> 1.6, >= 1.6.1)
kramdown (~> 2.3) kramdown (~> 2.3)
kramdown-parser-gfm (~> 1.1) kramdown-parser-gfm (~> 1.1)
logger (~> 1.6) logger (~> 1.6)
observer (~> 0.1) observer (~> 0.1)
open3 (~> 0.2.1)
ostruct (~> 0.6) ostruct (~> 0.6)
parser (~> 3.0) parser (~> 3.0)
prism (~> 1.4) prism (~> 1.4)
@ -373,32 +342,32 @@ GEM
yard (~> 0.9, >= 0.9.24) yard (~> 0.9, >= 0.9.24)
yard-activesupport-concern (~> 0.0) yard-activesupport-concern (~> 0.0)
yard-solargraph (~> 0.1) yard-solargraph (~> 0.1)
solargraph-rails (1.3) solargraph-rails (1.2.4)
activesupport activesupport
solargraph (>= 0.48.0) solargraph (>= 0.48.0, <= 0.57)
solid_cable (3.0.12) solid_cable (3.0.11)
actioncable (>= 7.2) actioncable (>= 7.2)
activejob (>= 7.2) activejob (>= 7.2)
activerecord (>= 7.2) activerecord (>= 7.2)
railties (>= 7.2) railties (>= 7.2)
solid_cache (1.0.10) solid_cache (1.0.7)
activejob (>= 7.2) activejob (>= 7.2)
activerecord (>= 7.2) activerecord (>= 7.2)
railties (>= 7.2) railties (>= 7.2)
solid_queue (1.3.2) solid_queue (1.1.5)
activejob (>= 7.1) activejob (>= 7.1)
activerecord (>= 7.1) activerecord (>= 7.1)
concurrent-ruby (>= 1.3.1) concurrent-ruby (>= 1.3.1)
fugit (~> 1.11) fugit (~> 1.11.0)
railties (>= 7.1) railties (>= 7.1)
thor (>= 1.3.1) thor (~> 1.3.1)
sqlite3 (2.9.0-aarch64-linux-gnu) sqlite3 (2.7.2-aarch64-linux-gnu)
sqlite3 (2.9.0-aarch64-linux-musl) sqlite3 (2.7.2-aarch64-linux-musl)
sqlite3 (2.9.0-arm-linux-gnu) sqlite3 (2.7.2-arm-linux-gnu)
sqlite3 (2.9.0-arm-linux-musl) sqlite3 (2.7.2-arm-linux-musl)
sqlite3 (2.9.0-x86_64-linux-gnu) sqlite3 (2.7.2-x86_64-linux-gnu)
sqlite3 (2.9.0-x86_64-linux-musl) sqlite3 (2.7.2-x86_64-linux-musl)
sshkit (1.25.0) sshkit (1.24.0)
base64 base64
logger logger
net-scp (>= 1.1.2) net-scp (>= 1.1.2)
@ -407,24 +376,22 @@ GEM
ostruct ostruct
stimulus-rails (1.3.4) stimulus-rails (1.3.4)
railties (>= 6.0.0) railties (>= 6.0.0)
stringio (3.2.0) stringio (3.1.7)
temple (0.10.4) thor (1.3.2)
thor (1.5.0) thruster (0.1.14)
thruster (0.1.18) thruster (0.1.14-aarch64-linux)
thruster (0.1.18-aarch64-linux) thruster (0.1.14-x86_64-linux)
thruster (0.1.18-x86_64-linux) tilt (2.6.1)
tilt (2.7.0) timeout (0.4.3)
timeout (0.6.0) turbo-rails (2.0.16)
tsort (0.2.0)
turbo-rails (2.0.23)
actionpack (>= 7.1.0) actionpack (>= 7.1.0)
railties (>= 7.1.0) railties (>= 7.1.0)
tzinfo (2.0.6) tzinfo (2.0.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
unicode-display_width (3.2.0) unicode-display_width (3.1.4)
unicode-emoji (~> 4.1) unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.2.0) unicode-emoji (4.0.4)
uri (1.1.1) uri (1.0.3)
useragent (0.16.11) useragent (0.16.11)
web-console (4.2.1) web-console (4.2.1)
actionview (>= 6.0.0) actionview (>= 6.0.0)
@ -438,12 +405,12 @@ GEM
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
yard (0.9.38) yard (0.9.37)
yard-activesupport-concern (0.0.1) yard-activesupport-concern (0.0.1)
yard (>= 0.8) yard (>= 0.8)
yard-solargraph (0.1.0) yard-solargraph (0.1.0)
yard (~> 0.9) yard (~> 0.9)
zeitwerk (2.7.5) zeitwerk (2.7.3)
PLATFORMS PLATFORMS
aarch64-linux aarch64-linux
@ -456,18 +423,14 @@ PLATFORMS
x86_64-linux-musl x86_64-linux-musl
DEPENDENCIES DEPENDENCIES
bcrypt (~> 3.1.7)
bootsnap bootsnap
brakeman brakeman
capybara capybara
cssbundling-rails (~> 1.4) cssbundling-rails (~> 1.4)
debug debug
factory_bot_rails
haml
importmap-rails importmap-rails
jbuilder jbuilder
kamal kamal
letter_opener_web
propshaft propshaft
puma (>= 5.0) puma (>= 5.0)
rails (~> 8.0.2) rails (~> 8.0.2)

View file

@ -1,2 +1,2 @@
web: env RUBY_DEBUG_OPEN=true bin/rails server web: env RUBY_DEBUG_OPEN=true bin/rails server
css: node_modules/yarn/bin/yarn build:css --watch css: yarn build:css --watch

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

View file

@ -20,12 +20,6 @@ main {
height: 100vh; height: 100vh;
} }
#navbar-top {
position: sticky;
top: 0;
}
.tracker { .tracker {
align-content: center; align-content: center;
} }
@ -33,12 +27,13 @@ main {
.logs { .logs {
height: 60vh; height: 60vh;
overflow: scroll; overflow: scroll;
scrollbar-width: none;
} }
.current-day { .current-day {
background-color: white; background-color: white;
position: sticky; position: sticky;
top: 64px; top: 0;
align-content: center; align-content: center;
} }
@ -79,22 +74,10 @@ main {
height: 15px; height: 15px;
} }
.selected-day {
border: 2px double white;
margin: 4px;
width: 15px;
height: 15px;
}
.creatif { .creatif {
background-color: red; background-color: red;
} }
.unknown {
border: 2px double grey;
background-color: white;
}
.en-charge { .en-charge {
background-color: orange; background-color: orange;
} }

View file

@ -1,5 +1,4 @@
class ApplicationController < ActionController::Base class ApplicationController < ActionController::Base
include Authentication
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
# allow_browser versions: :modern # allow_browser versions: :modern
end end

View file

@ -1,57 +0,0 @@
module Authentication
extend ActiveSupport::Concern
included do
before_action :require_authentication
helper_method :authenticated?
helper_method :current_user
end
class_methods do
def allow_unauthenticated_access(**options)
skip_before_action :require_authentication, **options
end
end
private
def authenticated?
resume_session
end
def current_user
authenticated?&.user
end
def require_authentication
resume_session || request_authentication
end
def resume_session
Current.session ||= find_session_by_cookie
end
def find_session_by_cookie
Session.find_by(id: cookies.signed[:session_id]) if cookies.signed[:session_id]
end
def request_authentication
session[:return_to_after_authenticating] = request.url
redirect_to new_session_path
end
def after_authentication_url
session.delete(:return_to_after_authenticating) || root_url
end
def start_new_session_for(user)
user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
Current.session = session
cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax }
end
end
def terminate_session
Current.session.destroy
cookies.delete(:session_id)
end
end

View file

@ -1,12 +0,0 @@
class HomeController < ApplicationController
allow_unauthenticated_access
def index
@user = User.find_by(username: request.subdomain)
if @user&.public?
render "moods/index"
else
redirect_to dashboard_path
end
end
end

View file

@ -1,25 +0,0 @@
class InvitationsController < ApplicationController
before_action :set_user_by_token, only: %i[ edit update ]
allow_unauthenticated_access
def edit
end
def update
password_params = params.expect(user: [ :password, :password_confirmation ])
if @user.update(password_params.merge(invitation_accepted_at: Time.current))
redirect_to root_path, notice: "Account activated!"
else
redirect_to invitation_path(params[:token]), alert: "Passwords did not match."
end
end
private
def set_user_by_token
@user = User.find_by_invitation_token(params[:token])
rescue ActiveSupport::MessageVerifier::InvalidSignature
redirect_to root_path, alert: "Invitation link is invalid or has expired."
end
end

View file

@ -1,5 +1,6 @@
class MoodsController < ApplicationController class MoodsController < ApplicationController
def index def index
@user = Current.user @mode = Mood.last&.mode || "croisiere"
@mood_log = Mood.history_for_a_year || []
end end
end end

View file

@ -1,33 +0,0 @@
class PasswordsController < ApplicationController
allow_unauthenticated_access
before_action :set_user_by_token, only: %i[ edit update ]
def new
end
def create
if user = User.find_by(email_address: params[:email_address])
PasswordsMailer.reset(user).deliver_later
end
redirect_to new_session_path, notice: "Password reset instructions sent (if user with that email address exists)."
end
def edit
end
def update
if @user.update(params.permit(:password, :password_confirmation))
redirect_to new_session_path, notice: "Password has been reset."
else
redirect_to edit_password_path(params[:token]), alert: "Passwords did not match."
end
end
private
def set_user_by_token
@user = User.find_by_password_reset_token!(params[:token])
rescue ActiveSupport::MessageVerifier::InvalidSignature
redirect_to new_password_path, alert: "Password reset link is invalid or has expired."
end
end

View file

@ -1,10 +1,9 @@
class RfidTagsController < ApplicationController class RfidTagsController < ApplicationController
skip_forgery_protection skip_forgery_protection
allow_unauthenticated_access
def create def create
rfid_tag_params = params.expect(rfid_tag: [ :chip_id, :identifier ]) identifier = params.expect(:identifier)
rfid_tag = RfidTag.find_or_initialize_by(chip_id: rfid_tag_params[:chip_id], identifier: rfid_tag_params[:identifier]) rfid_tag = RfidTag.find_or_initialize_by(identifier:)
if rfid_tag.new_record? if rfid_tag.new_record?
register rfid_tag register rfid_tag
return return
@ -16,14 +15,13 @@ class RfidTagsController < ApplicationController
private private
def register(rfid_tag) def register(rfid_tag)
rfid_tag.user = RfidTag.where(chip_id: rfid_tag.chip_id).first&.user
rfid_tag.save! rfid_tag.save!
head :created, code: :registered head :created, code: :registered
end end
def create_mood(rfid_tag) def create_mood(rfid_tag)
if rfid_tag.mode if rfid_tag.mode
Mood.create!(recorded_at: DateTime.now, mode: rfid_tag.mode, user: rfid_tag.user) Mood.create!(recorded_at: DateTime.now, mode: rfid_tag.mode)
head :created, code: :recorded head :created, code: :recorded
else else
head :unprocessable_entity head :unprocessable_entity

View file

@ -1,21 +0,0 @@
class SessionsController < ApplicationController
allow_unauthenticated_access only: %i[ new create ]
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_url, alert: "Try again later." }
def new
end
def create
if user = User.authenticate_by(params.permit(:email_address, :password))
start_new_session_for user
redirect_to after_authentication_url
else
redirect_to new_session_path, alert: "Try another email address or password."
end
end
def destroy
terminate_session
redirect_to new_session_path
end
end

View file

@ -1,9 +1,2 @@
module MoodsHelper module MoodsHelper
def mode_for(mood, user)
if user.guess?
mood[:mode] || mood[:guess] || "unknown"
else
mood[:mode] || "unknown"
end
end
end end

View file

@ -1,11 +0,0 @@
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
static targets = [ 'burger', 'navbar' ];
toggle(event) {
[ this.burgerTarget, this.navbarTarget ].forEach((target) => {
target.classList.toggle("is-active");
});
}
}

View file

@ -1,18 +1,15 @@
import { Controller } from '@hotwired/stimulus' import { Controller } from '@hotwired/stimulus'
export default class extends Controller { export default class extends Controller {
static targets = [ "image", "modeDay" ]; static targets = [ "image", "modeDay", "modeDayMobile" ]
updateDayInfo(event) { updateDayInfo(event) {
const image = this.imageTarget; const image = this.imageTarget;
const modeDay = this.modeDayTarget; const modeDay = this.modeDayTarget;
const modeDayMobile = this.modeDayMobileTarget;
const modeDayContent = event.target.dataset.day + ' : ' + event.target.dataset.mode; const modeDayContent = event.target.dataset.day + ' : ' + event.target.dataset.mode;
image.src = event.target.dataset.image; image.src = event.target.dataset.image;
modeDayMobile.textContent = modeDayContent;
modeDay.textContent = modeDayContent; modeDay.textContent = modeDayContent;
event.target.className = "selected-day " + event.target.dataset.mode;
}
connect() {
window.location = "#end";
} }
} }

View file

@ -1,6 +0,0 @@
class PasswordsMailer < ApplicationMailer
def reset(user)
@user = user
mail subject: "Reset your password", to: user.email_address
end
end

View file

@ -1,8 +0,0 @@
class UserMailer < ApplicationMailer
def invitation_email(user, token)
@user = user
@invite_url = edit_invitation_url(token: token)
mail(to: @user.email_address, subject: "Vous êtes invité à rejoindre la kluk")
end
end

View file

@ -1,4 +0,0 @@
class Current < ActiveSupport::CurrentAttributes
attribute :session
delegate :user, to: :session, allow_nil: true
end

View file

@ -1,3 +1,45 @@
class Mood < ApplicationRecord class Mood < ApplicationRecord
belongs_to :user class << self
def history_for_a_year
history(Date.today - 1.year, Date.today)
end
private
def history(from, to)
return [] if Mood.count < 1
first_monday = monday_of_the_week(first_mood_date(from))
current_date = first_monday
current_mode = last_mode_before(current_date)
log_mood = []
mood_range = Mood.order(:recorded_at).where(recorded_at: (first_monday..to))
mood_range.each do |mood|
while current_date < mood.recorded_at.to_date do
log_mood << [ current_date.to_s, current_mode ]
current_date += 1
end
current_mode = mood.mode
end
while current_date <= to.to_date do
log_mood << [ current_date.to_s, current_mode ]
current_date += 1
end
log_mood.each_slice(7).to_a
end
def first_mood_date(from)
Mood.order(recorded_at: :asc).where("recorded_at >= ?", from).first.recorded_at.to_date
end
def monday_of_the_week(date)
date - date.wday + 1
end
def last_mode_before(date)
mood = Mood.where("recorded_at < ?", date).last
mood&.mode || "croisiere"
end
end
end end

View file

@ -1,3 +1,2 @@
class RfidTag < ApplicationRecord class RfidTag < ApplicationRecord
belongs_to :user, required: false
end end

View file

@ -1,3 +0,0 @@
class Session < ApplicationRecord
belongs_to :user
end

View file

@ -1,40 +0,0 @@
class User < ApplicationRecord
has_secure_password
has_many :sessions, dependent: :destroy
has_many :moods, -> { order "recorded_at" }
normalizes :email_address, with: ->(e) { e.strip.downcase }
generates_token_for :invitation, expires_in: 5.days do
invitation_accepted_at
end
def invite_user!(email)
invited_user = User.create!(
email_address: email,
password: SecureRandom.hex(16) # temporary password
)
# Send invitation email using the generated token
token = invited_user.generate_token_for(:invitation)
UserMailer.invitation_email(invited_user, token).deliver_later
invited_user
end
def self.find_by_invitation_token(token)
find_by_token_for(:invitation, token)
end
def invitation_token
generate_token_for(:invitation)
end
def history
MoodCalendarService.generate_calendar(moods)
end
def current_mood
self.moods.last&.mode || "croisiere"
end
end

View file

@ -1,78 +0,0 @@
# app/services/mood_calendar_service.rb
class MoodCalendarService
def self.generate_calendar(moods, start_date: nil, end_date: Date.current)
# Convertir la relation ActiveRecord en tableau de hash
data = moods.order(:recorded_at)
.pluck(:mode, :recorded_at)
.map { |mode, recorded_at| { mode: mode, recorded_at: recorded_at } }
if data.empty?
start_date = Date.current
else
start_date ||= data.first[:recorded_at].to_date
end
if end_date < (start_date + 5.months)
end_date = start_date + 5.months
end
# Convertir en Date si ce sont des DateTime ou Time
start_date = start_date.to_date
end_date = end_date&.to_date
# Grouper par jour et garder le plus récent pour chaque jour
data_by_date = data.group_by { |d| d[:recorded_at].to_date }
.transform_values { |entries| entries.max_by { |e| e[:recorded_at] } }
# Trouver le dernier mood avant start_date pour initialiser le guess
last_mode = data.select { |d| d[:recorded_at].to_date < start_date }
.max_by { |d| d[:recorded_at] }
&.[](:mode)
# Générer le tableau complet avec tous les jours
complete_data = (start_date..end_date).map do |date|
if data_by_date[date]
last_mode = data_by_date[date][:mode]
data_by_date[date]
else
{ mode: nil, recorded_at: date.to_datetime, guess: last_mode }
end
end
# Regrouper par mois avec semaines commençant au premier lundi
complete_data.group_by { |d| d[:recorded_at].to_date.beginning_of_month }
.map do |month_start, month_data|
# Trouver le premier lundi du mois (à partir du 1er du mois)
first_monday = month_start
first_monday = first_monday.next_occurring(:monday) unless first_monday.monday?
# Trouver le premier lundi du mois SUIVANT
next_month_start = month_start.next_month.beginning_of_month
next_first_monday = next_month_start
next_first_monday = next_first_monday.next_occurring(:monday) unless next_first_monday.monday?
# Le dernier jour du mois est le dimanche précédant le premier lundi du mois suivant
month_end = next_first_monday - 1.day
# Créer un hash pour accès rapide aux données de complete_data (qui contient déjà les guess)
data_hash = complete_data.index_by { |d| d[:recorded_at].to_date }
# Générer tous les jours du premier lundi jusqu'au dimanche avant le prochain lundi
all_days = (first_monday..month_end).map do |date|
# Utiliser directement les données de complete_data qui ont déjà le bon guess
data_hash[date] || { mode: nil, recorded_at: date.to_datetime, guess: nil }
end
# Grouper par semaines
weeks = all_days.group_by { |d| d[:recorded_at].to_date.beginning_of_week(:monday) }
.sort_by { |week_start, _| week_start }
.map { |_, week_moods| week_moods }
{
month: month_start,
weeks: weeks
}
end
end
end

View file

@ -1,27 +0,0 @@
<nav id="navbar-top" class="navbar has-background-primary" role="navigation" aria-label="main navigation" data-controller="menu">
<div class="navbar-brand">
<div class="navbar-item is-size-5">Comment ça </div>
<div class="navbar-item is-size-3"><strong>KLUK</strong></div>
<div class="navbar-item is-size-5">aujourd'hui ?</div>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-menu-target="burger" data-action="menu#toggle">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div class="navbar-menu" data-menu-target="navbar">
<div class="navbar-end">
<% if authenticated? %>
<div class="navbar-item">
<%= button_to "Se déconnecter", session_path, method: :delete, class: "button is-light" %>
</div>
<% else %>
<div class="navbar-item">
<%= link_to "Se connecter", new_session_path, class: "button is-light" %>
</div>
<% end %>
</div>
</div>
</nav>

View file

@ -1 +0,0 @@
%h1 Comment ça KLUK ?

View file

@ -1,24 +0,0 @@
<div>
<h1>Set up your password</h1>
<p>Please set your password to activate your account.</p>
<%= form_with model: @user, url: invitation_path(token: params[:token]), method: :patch do |form| %>
<div>
<%= form.password_field :password,
required: true,
autocomplete: "new-password",
placeholder: "Enter new password" %>
</div>
<div>
<%= form.password_field :password_confirmation,
required: true,
autocomplete: "new-password",
placeholder: "Repeat new password" %>
</div>
<div>
<%= form.submit "Activate Account" %>
</div>
<% end %>
</div>

View file

@ -22,15 +22,12 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Delius&family=Sour+Gummy:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Delius&family=Sour+Gummy:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
<script src="https://kit.fontawesome.com/079b06296f.js" crossorigin="anonymous"></script>
<%# Includes all stylesheet files in app/assets/stylesheets %> <%# Includes all stylesheet files in app/assets/stylesheets %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %> <%= javascript_importmap_tags %>
</head> </head>
<body> <body>
<%= render 'menu' %>
<%= yield %> <%= yield %>
</body> </body>
</html> </html>

View file

@ -1,17 +1,21 @@
<main data-controller="mood" class="columns m-auto"> <main data-controller="mood" class="columns m-auto">
<div class="current-day column is-hidden-mobile"> <div class="current-day column">
<section class="m-4"> <section class="m-4">
<figure class="image has-ratio"> <h1 class="title is-3 is-spaced">Comment ça KLUK ?</h1>
<%= image_tag(@user.current_mood + ".jpg", "data-mood-target": "image") %> <div class="is-hidden-tablet mb-4">
<div class="" data-mood-target="modeDayMobile">Aujourd'hui</div>
</div>
<figure class="image is-hidden-mobile has-ratio ">
<%= image_tag(@mode + ".jpg", "data-mood-target": "image") %>
</figure> </figure>
</section> </section>
</div> </div>
<section class="section column has-background-primary-light is-flex is-flex-direction-column is-justify-content-space-around"> <section class="section column has-background-primary-light is-flex is-flex-direction-column is-justify-content-space-around">
<div class="is-hidden-mobile mb-4">
<div class="title is-3" data-mood-target="modeDay">Aujourd'hui</div>
</div>
<div class="tracker"> <div class="tracker">
<div class="legend is-flex is-flex-wrap-wrap mb-5"> <div class="legend is-flex is-flex-wrap-wrap mb-5">
<div class="is-flex is-align-items-center mr-4">
<div class=""><strong>Légende</strong></div>
</div>
<div class="is-flex is-align-items-center mr-4"> <div class="is-flex is-align-items-center mr-4">
<div class="bar-frigo mr-1"></div> <div class="bar-frigo mr-1"></div>
<div class="">Triste</div> <div class="">Triste</div>
@ -33,32 +37,28 @@
<div class="">A fond</div> <div class="">A fond</div>
</div> </div>
</div> </div>
<div class="container mb-4 p-3 has-background-white day-info">
<div class="title is-4">
<span class="icon"><i class="fa-regular fa-calendar"></i></span>
<span data-mood-target="modeDay">Aujourd'hui : <%= @user.current_mood %></span>
</div>
</div>
<div class="logs"> <div class="logs">
<div class="is-flex is-flex-direction-row is-flex-wrap-wrap mb-3">
<% @user.history.each do |month| %>
<div class="mr-3">
<div> <%= I18n.l(month[:month], format: "%b %y").capitalize %></div>
<div class="is-flex is-flex-wrap-wrap"> <div class="is-flex is-flex-wrap-wrap">
<% month[:weeks].each do |week| %> <% @mood_log.each do |week| %>
<div class="is-flex is-flex-direction-column is-flex-wrap-wrap mb-3"> <div class="is-flex is-flex-direction-column is-flex-wrap-wrap mb-3">
<% week.each do |mood| %> <div class="is-size-7 day">
<% mode = mode_for(mood, @user) %> <% day = Date.parse(week.first[0]) %>
<div data-image="<%= asset_path(mode + ".jpg") %>" data-mode="<%= mode %>" data-day="<%= l mood[:recorded_at].to_date %>" data-action="click->mood#updateDayInfo" title="<%= l(mood[:recorded_at].to_date) %> : <%= mode %>" class="day <%= mode %>"></div> <% if day.day < 8 && day.monday? %>
<%= l(day, format: "%b") %>
<% end %>
</div>
<% week.each do |d| %>
<% day = Date.parse(d[0]) %>
<% if d[1] %>
<div data-image="<%= asset_path(d[1] + ".jpg") %>" data-mode="<%= d[1] %>" data-day="<%= d[0] %>" data-action="click->mood#updateDayInfo mouseover->mood#updateDayInfo mouseleave->mood#updateDayInfo" title="<%= d[0] %> : <%= d[1] %>" class="day <%= d[1] %>"></div>
<% else %>
<div class="day"></div>
<% end %>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
</div> </div>
</div> </div>
<% end %>
<div id="end"></>
</div>
</div>
</div> </div>
</section> </section>
</main> </main>

View file

@ -1,9 +0,0 @@
<h1>Update your password</h1>
<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
<%= form_with url: password_path(params[:token]), method: :put do |form| %>
<%= form.password_field :password, required: true, autocomplete: "new-password", placeholder: "Enter new password", maxlength: 72 %><br>
<%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", placeholder: "Repeat new password", maxlength: 72 %><br>
<%= form.submit "Save" %>
<% end %>

View file

@ -1,8 +0,0 @@
<h1>Forgot your password?</h1>
<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
<%= form_with url: passwords_path do |form| %>
<%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address] %><br>
<%= form.submit "Email reset instructions" %>
<% end %>

View file

@ -1,4 +0,0 @@
<p>
You can reset your password within the next 15 minutes on
<%= link_to "this password reset page", edit_password_url(@user.password_reset_token) %>.
</p>

View file

@ -1,2 +0,0 @@
You can reset your password within the next 15 minutes on this password reset page:
<%= edit_password_url(@user.password_reset_token) %>

View file

@ -0,0 +1,2 @@
<h1>RfidTags#post</h1>
<p>Find me in app/views/rfid_tags/post.html.erb</p>

View file

@ -1,22 +0,0 @@
%main.columns.m-auto{"data-controller" => "mood"}
.column.is-hidden-mobile
%section.m-4
%figure.image.has-ratio
= image_tag("croisiere.jpg")
%section.section.column.has-background-primary-light.is-flex.is-flex-direction-column.is-justify-content-center
%h1.title Connexion au tableau de bord de la KLUK
.flash.mb-2
.has-text-danger= flash[:alert] if flash[:alert]
.has-text-info= flash[:notice] if flash[:notice]
= form_with url: session_path do |form|
.field
= form.label :email_address, "Email"
.control
= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Entrez votre email", value: params[:email_address], class: "input"
.field
= form.label :password, "Mot de passe"
.control
= form.password_field :password, required: true, autocomplete: "current-password", placeholder: "Entrez votre mot de passe", maxlength: 72, class: "input"
.field
.control
%button.button.is-link Se connecter

View file

@ -1,3 +0,0 @@
<h1>You're invited!</h1>
<p>Click here to set up your account:</p>
<%= link_to "Set up account", @invite_url %>

View file

@ -34,9 +34,6 @@ Rails.application.configure do
# Don't care if the mailer can't send. # Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false config.action_mailer.raise_delivery_errors = false
config.action_mailer.delivery_method = :letter_opener
config.action_mailer.perform_deliveries = true
# Make template changes take effect immediately. # Make template changes take effect immediately.
config.action_mailer.perform_caching = false config.action_mailer.perform_caching = false
@ -72,8 +69,4 @@ Rails.application.configure do
# Apply autocorrection by RuboCop to files generated by `bin/rails generate`. # Apply autocorrection by RuboCop to files generated by `bin/rails generate`.
# config.generators.apply_rubocop_autocorrect_after_generate! # config.generators.apply_rubocop_autocorrect_after_generate!
#
config.hosts << "localhost.localdomain:3000"
config.hosts << "robi.localhost.localdomain:3000"
config.hosts << "marie.localhost.localdomain:3000"
end end

View file

@ -80,10 +80,10 @@ Rails.application.configure do
config.active_record.attributes_for_inspect = [ :id ] config.active_record.attributes_for_inspect = [ :id ]
# Enable DNS rebinding protection and other `Host` header attacks. # Enable DNS rebinding protection and other `Host` header attacks.
config.hosts = [ # config.hosts = [
"kluk.fr", # "example.com", # Allow requests from example.com
/.*\.kluk\.fr/ # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com`
] # ]
# #
# Skip DNS rebinding protection for the default health check endpoint. # Skip DNS rebinding protection for the default health check endpoint.
# config.host_authorization = { exclude: ->(request) { request.path == "/up" } } # config.host_authorization = { exclude: ->(request) { request.path == "/up" } }

View file

@ -1,7 +1,5 @@
Rails.application.routes.draw do Rails.application.routes.draw do
resource :session post '/rfid_tags' => 'rfid_tags#create'
resources :passwords, param: :token
post "/rfid_tags" => "rfid_tags#create"
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
@ -12,13 +10,6 @@ Rails.application.routes.draw do
# get "manifest" => "rails/pwa#manifest", as: :pwa_manifest # get "manifest" => "rails/pwa#manifest", as: :pwa_manifest
# get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker # get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker
mount LetterOpenerWeb::Engine, at: "/letter_opener" if Rails.env.development?
# Defines the root path route ("/") # Defines the root path route ("/")
root "home#index" root "moods#index"
get "/invite/:token", to: "invitations#edit", as: :edit_invitation
patch "/invite/:token", to: "invitations#update", as: :invitation
get "/moods", to: "moods#index", as: :dashboard
end end

View file

@ -1,11 +0,0 @@
class CreateUsers < ActiveRecord::Migration[8.0]
def change
create_table :users do |t|
t.string :email_address, null: false
t.string :password_digest, null: false
t.timestamps
end
add_index :users, :email_address, unique: true
end
end

View file

@ -1,11 +0,0 @@
class CreateSessions < ActiveRecord::Migration[8.0]
def change
create_table :sessions do |t|
t.references :user, null: false, foreign_key: true
t.string :ip_address
t.string :user_agent
t.timestamps
end
end
end

View file

@ -1,5 +0,0 @@
class AddInvitationAcceptedAtToUsers < ActiveRecord::Migration[8.0]
def change
add_column :users, :invitation_accepted_at, :datetime
end
end

View file

@ -1,6 +0,0 @@
class AddChipIdAndUsersToRfidTags < ActiveRecord::Migration[8.0]
def change
add_column :rfid_tags, :chip_id, :string
add_reference :rfid_tags, :user, foreign_key: true
end
end

View file

@ -1,5 +0,0 @@
class AddUserToMoods < ActiveRecord::Migration[8.0]
def change
add_reference :moods, :user, foreign_key: true
end
end

View file

@ -1,7 +0,0 @@
class AddUsernameToUsers < ActiveRecord::Migration[8.0]
def up
add_column :users, :username, :string
User.update_all(username: "temporary")
change_column_null :users, :username, false
end
end

View file

@ -1,5 +0,0 @@
class AddPublicToUsers < ActiveRecord::Migration[8.0]
def change
add_column :users, :public, :boolean, default: false
end
end

View file

@ -1,5 +0,0 @@
class AddGuessToUsers < ActiveRecord::Migration[8.0]
def change
add_column :users, :guess, :boolean, default: false
end
end

34
db/schema.rb generated
View file

@ -10,49 +10,19 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2026_02_26_101258) do ActiveRecord::Schema[8.0].define(version: 2025_08_06_151318) do
create_table "moods", force: :cascade do |t| create_table "moods", force: :cascade do |t|
t.string "mode" t.string "mode"
t.datetime "recorded_at" t.datetime "recorded_at"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.integer "user_id"
t.index ["user_id"], name: "index_moods_on_user_id"
end end
create_table "rfid_tags", force: :cascade do |t| create_table "rfid_tags", force: :cascade do |t|
t.string "identifier" t.string "identifier"
t.string "mode" t.string "mode"
t.datetime "created_at", default: -> { "CURRENT_DATE" }, null: false
t.datetime "updated_at", default: -> { "CURRENT_DATE" }, null: false
t.string "chip_id"
t.integer "user_id"
t.index ["identifier"], name: "index_rfid_tags_on_identifier"
t.index ["user_id"], name: "index_rfid_tags_on_user_id"
end
create_table "sessions", force: :cascade do |t|
t.integer "user_id", null: false
t.string "ip_address"
t.string "user_agent"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["user_id"], name: "index_sessions_on_user_id" t.index ["identifier"], name: "index_rfid_tags_on_identifier", unique: true
end end
create_table "users", force: :cascade do |t|
t.string "email_address", null: false
t.string "password_digest", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime "invitation_accepted_at"
t.string "username", null: false
t.boolean "public", default: false
t.boolean "guess", default: false
t.index ["email_address"], name: "index_users_on_email_address", unique: true
end
add_foreign_key "moods", "users"
add_foreign_key "rfid_tags", "users"
add_foreign_key "sessions", "users"
end end

View file

@ -1,7 +0,0 @@
FactoryBot.define do
factory :mood do
mode { "croisiere" }
recorded_at { DateTime.now }
association :user
end
end

View file

@ -1,6 +0,0 @@
FactoryBot.define do
factory :user do
sequence(:email_address) { |n| "user#{n}@example.com" }
password_digest { BCrypt::Password.create('password123') }
end
end

View file

@ -1,11 +0,0 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
<% password_digest = BCrypt::Password.create("password") %>
one:
email_address: one@example.com
password_digest: <%= password_digest %>
two:
email_address: two@example.com
password_digest: <%= password_digest %>

View file

@ -1,5 +0,0 @@
require 'rails_helper'
RSpec.describe User, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
end

View file

@ -9,23 +9,21 @@ abort("The Rails environment is running in production mode!") if Rails.env.produ
# return unless Rails.env.test? # return unless Rails.env.test?
require 'rspec/rails' require 'rspec/rails'
# Add additional requires below this line. Rails is not loaded until this point! # Add additional requires below this line. Rails is not loaded until this point!
require 'factory_bot_rails'
# Requires supporting ruby files with custom matchers and macros, etc, in # Requires supporting ruby files with custom matchers and macros, etc, in
# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
# run as spec files by default. This means that files in spec/support that end # run as spec files by default. This means that files in spec/support that end
# in _spec.rb will both be required and run as specs, causing the specs to be # in _spec.rb will both be required and run as specs, causing the specs to be
# run twice. It is recommended that you do not name files matching this glob to # run twice. It is recommended that you do not name files matching this glob to
# end with _spec.rb. You can configure this pattern with the --pattern # end with _spec.rb. You can configure this pattern with the --pattern
# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. # option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
# #
# The following line is provided for convenience purposes. It has the downside # The following line is provided for convenience purposes. It has the downside
# of increasing the boot-up time by auto-requiring all files in the support # of increasing the boot-up time by auto-requiring all files in the support
# directory. Alternatively, in the individual `*_spec.rb` files, manually # directory. Alternatively, in the individual `*_spec.rb` files, manually
# require only the support files necessary. # require only the support files necessary.
# #
# Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f } # Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f }
Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f }
# Ensures that the test database schema matches the current schema file. # Ensures that the test database schema matches the current schema file.
# If there are pending migrations it will invoke `db:test:prepare` to # If there are pending migrations it will invoke `db:test:prepare` to

View file

@ -1,422 +0,0 @@
# spec/services/mood_calendar_service_spec.rb
require 'rails_helper'
RSpec.describe MoodCalendarService do
describe '.generate_calendar' do
context 'avec un recorded_at pour chaque jour' do
it 'génère un calendrier complet sans jours nil' do
# Données du 1er au 7 février 2025 (une semaine complète)
(1..7).each do |day|
create(:mood, mode: "mood_#{day}", recorded_at: Time.zone.parse("2025-02-0#{day} 10:00:00"))
end
result = MoodCalendarService.generate_calendar(
Mood.all,
end_date: Date.parse("2025-02-28")
)
expect(result.length).to eq(1) # Un seul mois
expect(result[0][:month]).to eq(Date.parse("2025-02-01"))
# Première semaine commence le lundi 3 février
first_week = result[0][:weeks][0]
expect(first_week.length).to eq(7) # Du lundi 3 au dimanche 9
# Vérifier que tous les jours du 3 au 7 ont un mode
(3..7).each do |day|
mood = first_week.find { |m| m[:recorded_at].day == day }
expect(mood[:mode]).to eq("mood_#{day}")
expect(mood).not_to have_key(:guess)
end
# Le 8 et 9 février devraient avoir mode: nil avec guess: "mood_7"
mood_8 = first_week.find { |m| m[:recorded_at].day == 8 }
expect(mood_8[:mode]).to be_nil
expect(mood_8[:guess]).to eq("mood_7")
mood_9 = first_week.find { |m| m[:recorded_at].day == 9 }
expect(mood_9[:mode]).to be_nil
expect(mood_9[:guess]).to eq("mood_7")
end
end
context 'avec des jours manquants' do
it 'remplit les jours manquants avec mode: nil et guess égal au dernier mode' do
create(:mood, mode: "triste", recorded_at: Time.zone.parse("2025-02-12 08:30:00"))
create(:mood, mode: "content", recorded_at: Time.zone.parse("2025-02-12 14:20:00"))
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
create(:mood, mode: "calme", recorded_at: Time.zone.parse("2025-02-18 16:45:00"))
moods = Mood.all
result = MoodCalendarService.generate_calendar(
moods,
end_date: Date.parse("2025-02-28")
)
expect(result.length).to eq(1)
expect(result[0][:month]).to eq(Date.parse("2025-02-01"))
# Trouver tous les jours
all_days = result[0][:weeks].flatten
# Le 12 février devrait avoir le dernier mood (content à 14:20)
mood_12 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-12") }
expect(mood_12[:mode]).to eq("content")
expect(mood_12[:recorded_at].to_i).to eq(Time.zone.parse("2025-02-12 14:20:00").to_i)
# Le 13 février devrait être nil avec guess: "content"
mood_13 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-13") }
expect(mood_13[:mode]).to be_nil
expect(mood_13[:guess]).to eq("content")
# Le 14 février devrait être nil avec guess: "content"
mood_14 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-14") }
expect(mood_14[:mode]).to be_nil
expect(mood_14[:guess]).to eq("content")
# Le 15 février devrait avoir le mood "heureux"
mood_15 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-15") }
expect(mood_15[:mode]).to eq("heureux")
# Le 16 février devrait être nil avec guess: "heureux"
mood_16 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-16") }
expect(mood_16[:mode]).to be_nil
expect(mood_16[:guess]).to eq("heureux")
# Les jours avant le 12 (à partir du premier lundi 3 février) devraient avoir guess: nil
mood_3 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-03") }
expect(mood_3[:mode]).to be_nil
expect(mood_3[:guess]).to be_nil
end
it 'gère correctement plusieurs mois avec des jours manquants' do
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
create(:mood, mode: "joyeux", recorded_at: Time.zone.parse("2025-04-20 11:30:00"))
result = MoodCalendarService.generate_calendar(
Mood.all,
end_date: Date.parse("2025-04-30")
)
# Devrait avoir exactement février, mars et avril
expect(result.length).to eq(3)
# Vérifier février
february = result.find { |m| m[:month] == Date.parse("2025-02-01") }
expect(february).not_to be_nil
# Vérifier mars (tous les jours devraient avoir mode: nil et guess: "heureux")
march = result.find { |m| m[:month] == Date.parse("2025-03-01") }
expect(march).not_to be_nil
all_march_days = march[:weeks].flatten
# Tous les jours de mars (y compris ceux qui débordent début avril) devraient avoir mode: nil
expect(all_march_days.all? { |d| d[:mode].nil? }).to be true
# Tous les jours de mars devraient avoir guess: "heureux"
# (le mois de mars va du 3 mars au 6 avril, avant le mood du 20 avril)
all_march_days.each do |day|
expect(day[:guess]).to eq("heureux"),
"Le jour #{day[:recorded_at].to_date} devrait avoir guess: 'heureux' mais a: #{day[:guess].inspect}"
end
# Vérifier avril
april = result.find { |m| m[:month] == Date.parse("2025-04-01") }
expect(april).not_to be_nil
all_april_days = april[:weeks].flatten
mood_20_april = all_april_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-04-20") }
expect(mood_20_april[:mode]).to eq("joyeux")
# Les jours du 7 au 19 avril devraient avoir guess: "heureux"
mood_10_april = all_april_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-04-10") }
expect(mood_10_april[:guess]).to eq("heureux")
# Les jours après le 20 avril devraient avoir guess: "joyeux"
mood_21_april = all_april_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-04-21") }
expect(mood_21_april[:guess]).to eq("joyeux")
end
end
context 'sans aucun recorded_at' do
it 'retourne un tableau vide' do
result = MoodCalendarService.generate_calendar(Mood.none)
expect(result).to eq([])
end
end
context 'avec plusieurs moods le même jour' do
it 'garde uniquement le mood le plus récent' do
create(:mood, mode: "triste", recorded_at: Time.zone.parse("2025-02-12 08:30:00"))
create(:mood, mode: "neutre", recorded_at: Time.zone.parse("2025-02-12 12:00:00"))
create(:mood, mode: "content", recorded_at: Time.zone.parse("2025-02-12 14:20:00"))
create(:mood, mode: "fatigué", recorded_at: Time.zone.parse("2025-02-12 09:15:00"))
moods = Mood.all
result = MoodCalendarService.generate_calendar(
moods,
end_date: Date.parse("2025-02-28")
)
all_days = result[0][:weeks].flatten
mood_12 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-12") }
expect(mood_12[:mode]).to eq("content")
expect(mood_12[:recorded_at].to_i).to eq(Time.zone.parse("2025-02-12 14:20:00").to_i)
end
end
context 'structure du calendrier' do
it 'commence chaque mois par le premier lundi' do
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
result = MoodCalendarService.generate_calendar(
Mood.all,
end_date: Date.parse("2025-02-28")
)
february = result[0]
first_day = february[:weeks][0][0]
# Le premier jour de février 2025 devrait être le lundi 3 février
expect(first_day[:recorded_at].to_date).to eq(Date.parse("2025-02-03"))
expect(first_day[:recorded_at].to_date.monday?).to be true
end
it 'va du premier lundi du mois au dimanche avant le premier lundi du mois suivant' do
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
result = MoodCalendarService.generate_calendar(
Mood.all,
end_date: Date.parse("2025-03-31")
)
february = result[0]
all_days = february[:weeks].flatten
# Premier jour : lundi 3 février 2025
first_day = all_days.first
expect(first_day[:recorded_at].to_date).to eq(Date.parse("2025-02-03"))
expect(first_day[:recorded_at].to_date.monday?).to be true
# Dernier jour : dimanche 2 mars 2025 (veille du premier lundi de mars qui est le 3)
last_day = all_days.last
expect(last_day[:recorded_at].to_date).to eq(Date.parse("2025-03-02"))
expect(last_day[:recorded_at].to_date.sunday?).to be true
# Le 3 mars (premier lundi de mars) ne devrait PAS être dans février
expect(all_days.any? { |d| d[:recorded_at].to_date == Date.parse("2025-03-03") }).to be false
end
it 'toutes les semaines ont exactement 7 jours' do
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
result = MoodCalendarService.generate_calendar(
Mood.all,
end_date: Date.parse("2025-02-28")
)
february = result[0]
# Toutes les semaines devraient avoir exactement 7 jours
february[:weeks].each do |week|
expect(week.length).to eq(7)
end
end
it 'retourne month comme Date (le premier du mois)' do
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
result = MoodCalendarService.generate_calendar(
Mood.all,
end_date: Date.parse("2025-02-28")
)
february = result[0]
# month devrait être une Date
expect(february[:month]).to be_a(Date)
expect(february[:month]).to eq(Date.parse("2025-02-01"))
expect(february[:month].day).to eq(1) # Premier du mois
end
end
context 'avec paramètres par défaut' do
it 'utilise le premier mood comme start_date et aujourd\'hui comme end_date' do
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
result = MoodCalendarService.generate_calendar(Mood.all)
expect(result).not_to be_empty
# Devrait commencer en février
expect(result.first[:month]).to eq(Date.parse("2025-02-01"))
end
end
context 'avec start_date spécifié' do
it 'commence à la date spécifiée' do
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
result = MoodCalendarService.generate_calendar(
Mood.all,
start_date: Date.parse("2025-03-01"),
end_date: Date.parse("2025-03-31")
)
# Devrait commencer en mars même si le mood est en février
expect(result.first[:month]).to eq(Date.parse("2025-03-01"))
# Ne devrait pas contenir février
expect(result.any? { |m| m[:month] == Date.parse("2025-02-01") }).to be false
# Tous les jours de mars devraient avoir guess: "heureux" (du 15 février)
all_march_days = result.first[:weeks].flatten
puts all_march_days.inspect
expect(all_march_days.all? { |d| d[:guess] == "heureux" }).to be true
end
end
context 'avec end_date spécifié' do
it 'se termine à la date spécifiée' do
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
result = MoodCalendarService.generate_calendar(
Mood.all,
end_date: Date.parse("2025-02-28")
)
# Devrait s'arrêter en février
expect(result.last[:month]).to eq(Date.parse("2025-02-01"))
# Ne devrait pas contenir mars ou au-delà
expect(result.any? { |m| m[:month] == Date.parse("2025-03-01") }).to be false
end
end
context 'avec start_date et end_date spécifiés' do
it 'génère le calendrier pour la plage spécifiée' do
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
create(:mood, mode: "calme", recorded_at: Time.zone.parse("2025-04-20 10:00:00"))
result = MoodCalendarService.generate_calendar(
Mood.all,
start_date: Date.parse("2025-03-01"),
end_date: Date.parse("2025-03-31")
)
# Devrait contenir uniquement mars
expect(result.length).to eq(1)
expect(result.first[:month]).to eq(Date.parse("2025-03-01"))
# Tous les jours de mars devraient avoir guess: "heureux" (du 15 février)
all_days = result.first[:weeks].flatten
expect(all_days.all? { |d| d[:mode].nil? }).to be true
expect(all_days.all? { |d| d[:guess] == "heureux" }).to be true
end
it 'gère une plage de plusieurs mois' do
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
result = MoodCalendarService.generate_calendar(
Mood.all,
start_date: Date.parse("2025-02-01"),
end_date: Date.parse("2025-04-30")
)
# Devrait contenir février, mars et avril
months = result.map { |m| m[:month] }
expect(months).to include(
Date.parse("2025-02-01"),
Date.parse("2025-03-01"),
Date.parse("2025-04-01")
)
end
it 'fonctionne avec DateTime en paramètres' do
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
# Passer des DateTime au lieu de Date
result = MoodCalendarService.generate_calendar(
Mood.all,
start_date: Time.zone.parse("2025-02-01 00:00:00"),
end_date: Time.zone.parse("2025-02-28 23:59:59")
)
expect(result).not_to be_empty
expect(result.first[:month]).to eq(Date.parse("2025-02-01"))
end
end
context 'avec une plage ne contenant aucun mood' do
it 'génère un calendrier avec tous les jours à nil et guess du dernier mood avant la plage' do
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
result = MoodCalendarService.generate_calendar(
Mood.all,
start_date: Date.parse("2025-04-01"),
end_date: Date.parse("2025-04-30")
)
# Devrait contenir avril
expect(result.first[:month]).to eq(Date.parse("2025-04-01"))
# Tous les jours devraient avoir mode: nil et guess: "heureux"
all_days = result.first[:weeks].flatten
expect(all_days.all? { |d| d[:mode].nil? }).to be true
expect(all_days.all? { |d| d[:guess] == "heureux" }).to be true
end
end
context 'avec plusieurs mois' do
it 'chaque mois commence et finit correctement' do
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
create(:mood, mode: "calme", recorded_at: Time.zone.parse("2025-03-20 10:00:00"))
result = MoodCalendarService.generate_calendar(
Mood.all,
end_date: Date.parse("2025-03-31")
)
february = result.find { |m| m[:month] == Date.parse("2025-02-01") }
march = result.find { |m| m[:month] == Date.parse("2025-03-01") }
# Février se termine le dimanche 2 mars
last_day_february = february[:weeks].flatten.last
expect(last_day_february[:recorded_at].to_date).to eq(Date.parse("2025-03-02"))
# Mars commence le lundi 3 mars
first_day_march = march[:weeks].flatten.first
expect(first_day_march[:recorded_at].to_date).to eq(Date.parse("2025-03-03"))
# Pas de chevauchement
expect(last_day_february[:recorded_at].to_date + 1.day).to eq(first_day_march[:recorded_at].to_date)
end
end
context 'avec un scope spécifique' do
it 'fonctionne avec des moods filtrés par user' do
user1 = create(:user)
user2 = create(:user)
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"), user: user1)
create(:mood, mode: "triste", recorded_at: Time.zone.parse("2025-02-16 10:00:00"), user: user2)
moods = Mood.where(user: user1)
result = MoodCalendarService.generate_calendar(
moods,
end_date: Date.parse("2025-02-28")
)
all_days = result[0][:weeks].flatten
mood_15 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-15") }
expect(mood_15[:mode]).to eq("heureux")
# Le mood du user2 ne devrait pas apparaître
mood_16 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-16") }
expect(mood_16[:mode]).to be_nil
expect(mood_16[:guess]).to eq("heureux")
end
end
end
end

View file

@ -1,3 +0,0 @@
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
end

View file

@ -7,11 +7,6 @@
resolved "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz" resolved "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz"
integrity sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A== integrity sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==
"@parcel/watcher-linux-x64-musl@2.5.1":
version "2.5.1"
resolved "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz"
integrity sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==
"@parcel/watcher@^2.4.1": "@parcel/watcher@^2.4.1":
version "2.5.1" version "2.5.1"
resolved "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz" resolved "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz"