forked from fixin.me/fixin.me
		
	Merging from main master to my repo master. #4
							
								
								
									
										2
									
								
								Gemfile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Gemfile
									
									
									
									
									
								
							@ -1,7 +1,7 @@
 | 
				
			|||||||
source "https://rubygems.org"
 | 
					source "https://rubygems.org"
 | 
				
			||||||
ruby file: ".ruby-version"
 | 
					ruby file: ".ruby-version"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
gem "rails", "~> 7.1.2"
 | 
					gem "rails", "~> 7.2.2"
 | 
				
			||||||
gem "sprockets-rails"
 | 
					gem "sprockets-rails"
 | 
				
			||||||
gem "mysql2", "~> 0.5"
 | 
					gem "mysql2", "~> 0.5"
 | 
				
			||||||
gem "puma", "~> 6.0"
 | 
					gem "puma", "~> 6.0"
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										245
									
								
								Gemfile.lock
									
									
									
									
									
								
							
							
						
						
									
										245
									
								
								Gemfile.lock
									
									
									
									
									
								
							@ -1,124 +1,122 @@
 | 
				
			|||||||
GEM
 | 
					GEM
 | 
				
			||||||
  remote: https://rubygems.org/
 | 
					  remote: https://rubygems.org/
 | 
				
			||||||
  specs:
 | 
					  specs:
 | 
				
			||||||
    actioncable (7.1.3)
 | 
					    actioncable (7.2.2)
 | 
				
			||||||
      actionpack (= 7.1.3)
 | 
					      actionpack (= 7.2.2)
 | 
				
			||||||
      activesupport (= 7.1.3)
 | 
					      activesupport (= 7.2.2)
 | 
				
			||||||
      nio4r (~> 2.0)
 | 
					      nio4r (~> 2.0)
 | 
				
			||||||
      websocket-driver (>= 0.6.1)
 | 
					      websocket-driver (>= 0.6.1)
 | 
				
			||||||
      zeitwerk (~> 2.6)
 | 
					      zeitwerk (~> 2.6)
 | 
				
			||||||
    actionmailbox (7.1.3)
 | 
					    actionmailbox (7.2.2)
 | 
				
			||||||
      actionpack (= 7.1.3)
 | 
					      actionpack (= 7.2.2)
 | 
				
			||||||
      activejob (= 7.1.3)
 | 
					      activejob (= 7.2.2)
 | 
				
			||||||
      activerecord (= 7.1.3)
 | 
					      activerecord (= 7.2.2)
 | 
				
			||||||
      activestorage (= 7.1.3)
 | 
					      activestorage (= 7.2.2)
 | 
				
			||||||
      activesupport (= 7.1.3)
 | 
					      activesupport (= 7.2.2)
 | 
				
			||||||
      mail (>= 2.7.1)
 | 
					      mail (>= 2.8.0)
 | 
				
			||||||
      net-imap
 | 
					    actionmailer (7.2.2)
 | 
				
			||||||
      net-pop
 | 
					      actionpack (= 7.2.2)
 | 
				
			||||||
      net-smtp
 | 
					      actionview (= 7.2.2)
 | 
				
			||||||
    actionmailer (7.1.3)
 | 
					      activejob (= 7.2.2)
 | 
				
			||||||
      actionpack (= 7.1.3)
 | 
					      activesupport (= 7.2.2)
 | 
				
			||||||
      actionview (= 7.1.3)
 | 
					      mail (>= 2.8.0)
 | 
				
			||||||
      activejob (= 7.1.3)
 | 
					 | 
				
			||||||
      activesupport (= 7.1.3)
 | 
					 | 
				
			||||||
      mail (~> 2.5, >= 2.5.4)
 | 
					 | 
				
			||||||
      net-imap
 | 
					 | 
				
			||||||
      net-pop
 | 
					 | 
				
			||||||
      net-smtp
 | 
					 | 
				
			||||||
      rails-dom-testing (~> 2.2)
 | 
					      rails-dom-testing (~> 2.2)
 | 
				
			||||||
    actionpack (7.1.3)
 | 
					    actionpack (7.2.2)
 | 
				
			||||||
      actionview (= 7.1.3)
 | 
					      actionview (= 7.2.2)
 | 
				
			||||||
      activesupport (= 7.1.3)
 | 
					      activesupport (= 7.2.2)
 | 
				
			||||||
      nokogiri (>= 1.8.5)
 | 
					      nokogiri (>= 1.8.5)
 | 
				
			||||||
      racc
 | 
					      racc
 | 
				
			||||||
      rack (>= 2.2.4)
 | 
					      rack (>= 2.2.4, < 3.2)
 | 
				
			||||||
      rack-session (>= 1.0.1)
 | 
					      rack-session (>= 1.0.1)
 | 
				
			||||||
      rack-test (>= 0.6.3)
 | 
					      rack-test (>= 0.6.3)
 | 
				
			||||||
      rails-dom-testing (~> 2.2)
 | 
					      rails-dom-testing (~> 2.2)
 | 
				
			||||||
      rails-html-sanitizer (~> 1.6)
 | 
					      rails-html-sanitizer (~> 1.6)
 | 
				
			||||||
    actiontext (7.1.3)
 | 
					      useragent (~> 0.16)
 | 
				
			||||||
      actionpack (= 7.1.3)
 | 
					    actiontext (7.2.2)
 | 
				
			||||||
      activerecord (= 7.1.3)
 | 
					      actionpack (= 7.2.2)
 | 
				
			||||||
      activestorage (= 7.1.3)
 | 
					      activerecord (= 7.2.2)
 | 
				
			||||||
      activesupport (= 7.1.3)
 | 
					      activestorage (= 7.2.2)
 | 
				
			||||||
 | 
					      activesupport (= 7.2.2)
 | 
				
			||||||
      globalid (>= 0.6.0)
 | 
					      globalid (>= 0.6.0)
 | 
				
			||||||
      nokogiri (>= 1.8.5)
 | 
					      nokogiri (>= 1.8.5)
 | 
				
			||||||
    actionview (7.1.3)
 | 
					    actionview (7.2.2)
 | 
				
			||||||
      activesupport (= 7.1.3)
 | 
					      activesupport (= 7.2.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 (7.1.3)
 | 
					    activejob (7.2.2)
 | 
				
			||||||
      activesupport (= 7.1.3)
 | 
					      activesupport (= 7.2.2)
 | 
				
			||||||
      globalid (>= 0.3.6)
 | 
					      globalid (>= 0.3.6)
 | 
				
			||||||
    activemodel (7.1.3)
 | 
					    activemodel (7.2.2)
 | 
				
			||||||
      activesupport (= 7.1.3)
 | 
					      activesupport (= 7.2.2)
 | 
				
			||||||
    activerecord (7.1.3)
 | 
					    activerecord (7.2.2)
 | 
				
			||||||
      activemodel (= 7.1.3)
 | 
					      activemodel (= 7.2.2)
 | 
				
			||||||
      activesupport (= 7.1.3)
 | 
					      activesupport (= 7.2.2)
 | 
				
			||||||
      timeout (>= 0.4.0)
 | 
					      timeout (>= 0.4.0)
 | 
				
			||||||
    activestorage (7.1.3)
 | 
					    activestorage (7.2.2)
 | 
				
			||||||
      actionpack (= 7.1.3)
 | 
					      actionpack (= 7.2.2)
 | 
				
			||||||
      activejob (= 7.1.3)
 | 
					      activejob (= 7.2.2)
 | 
				
			||||||
      activerecord (= 7.1.3)
 | 
					      activerecord (= 7.2.2)
 | 
				
			||||||
      activesupport (= 7.1.3)
 | 
					      activesupport (= 7.2.2)
 | 
				
			||||||
      marcel (~> 1.0)
 | 
					      marcel (~> 1.0)
 | 
				
			||||||
    activesupport (7.1.3)
 | 
					    activesupport (7.2.2)
 | 
				
			||||||
      base64
 | 
					      base64
 | 
				
			||||||
 | 
					      benchmark (>= 0.3)
 | 
				
			||||||
      bigdecimal
 | 
					      bigdecimal
 | 
				
			||||||
      concurrent-ruby (~> 1.0, >= 1.0.2)
 | 
					      concurrent-ruby (~> 1.0, >= 1.3.1)
 | 
				
			||||||
      connection_pool (>= 2.2.5)
 | 
					      connection_pool (>= 2.2.5)
 | 
				
			||||||
      drb
 | 
					      drb
 | 
				
			||||||
      i18n (>= 1.6, < 2)
 | 
					      i18n (>= 1.6, < 2)
 | 
				
			||||||
 | 
					      logger (>= 1.4.2)
 | 
				
			||||||
      minitest (>= 5.1)
 | 
					      minitest (>= 5.1)
 | 
				
			||||||
      mutex_m
 | 
					      securerandom (>= 0.3)
 | 
				
			||||||
      tzinfo (~> 2.0)
 | 
					      tzinfo (~> 2.0, >= 2.0.5)
 | 
				
			||||||
    addressable (2.8.6)
 | 
					    addressable (2.8.7)
 | 
				
			||||||
      public_suffix (>= 2.0.2, < 6.0)
 | 
					      public_suffix (>= 2.0.2, < 7.0)
 | 
				
			||||||
    base64 (0.2.0)
 | 
					    base64 (0.2.0)
 | 
				
			||||||
    bcrypt (3.1.20)
 | 
					    bcrypt (3.1.20)
 | 
				
			||||||
    bigdecimal (3.1.6)
 | 
					    benchmark (0.4.0)
 | 
				
			||||||
 | 
					    bigdecimal (3.1.8)
 | 
				
			||||||
    bindex (0.8.1)
 | 
					    bindex (0.8.1)
 | 
				
			||||||
    builder (3.2.4)
 | 
					    builder (3.3.0)
 | 
				
			||||||
    byebug (11.1.3)
 | 
					    byebug (11.1.3)
 | 
				
			||||||
    capybara (3.39.2)
 | 
					    capybara (3.40.0)
 | 
				
			||||||
      addressable
 | 
					      addressable
 | 
				
			||||||
      matrix
 | 
					      matrix
 | 
				
			||||||
      mini_mime (>= 0.1.3)
 | 
					      mini_mime (>= 0.1.3)
 | 
				
			||||||
      nokogiri (~> 1.8)
 | 
					      nokogiri (~> 1.11)
 | 
				
			||||||
      rack (>= 1.6.0)
 | 
					      rack (>= 1.6.0)
 | 
				
			||||||
      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)
 | 
				
			||||||
    concurrent-ruby (1.2.3)
 | 
					    concurrent-ruby (1.3.4)
 | 
				
			||||||
    connection_pool (2.4.1)
 | 
					    connection_pool (2.4.1)
 | 
				
			||||||
    crass (1.0.6)
 | 
					    crass (1.0.6)
 | 
				
			||||||
    date (3.3.4)
 | 
					    date (3.4.1)
 | 
				
			||||||
    devise (4.9.3)
 | 
					    devise (4.9.4)
 | 
				
			||||||
      bcrypt (~> 3.0)
 | 
					      bcrypt (~> 3.0)
 | 
				
			||||||
      orm_adapter (~> 0.1)
 | 
					      orm_adapter (~> 0.1)
 | 
				
			||||||
      railties (>= 4.1.0)
 | 
					      railties (>= 4.1.0)
 | 
				
			||||||
      responders
 | 
					      responders
 | 
				
			||||||
      warden (~> 1.2.3)
 | 
					      warden (~> 1.2.3)
 | 
				
			||||||
    drb (2.2.0)
 | 
					    drb (2.2.1)
 | 
				
			||||||
      ruby2_keywords
 | 
					    erubi (1.13.0)
 | 
				
			||||||
    erubi (1.12.0)
 | 
					    ffi (1.17.0-x86_64-linux-gnu)
 | 
				
			||||||
    ffi (1.16.3)
 | 
					 | 
				
			||||||
    globalid (1.2.1)
 | 
					    globalid (1.2.1)
 | 
				
			||||||
      activesupport (>= 6.1)
 | 
					      activesupport (>= 6.1)
 | 
				
			||||||
    i18n (1.14.1)
 | 
					    i18n (1.14.6)
 | 
				
			||||||
      concurrent-ruby (~> 1.0)
 | 
					      concurrent-ruby (~> 1.0)
 | 
				
			||||||
    importmap-rails (2.0.1)
 | 
					    importmap-rails (2.0.3)
 | 
				
			||||||
      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.7.2)
 | 
					    io-console (0.8.0)
 | 
				
			||||||
    irb (1.11.1)
 | 
					    irb (1.14.1)
 | 
				
			||||||
      rdoc
 | 
					      rdoc (>= 4.0.0)
 | 
				
			||||||
      reline (>= 0.4.2)
 | 
					      reline (>= 0.4.2)
 | 
				
			||||||
    loofah (2.22.0)
 | 
					    logger (1.6.2)
 | 
				
			||||||
 | 
					    loofah (2.23.1)
 | 
				
			||||||
      crass (~> 1.0.2)
 | 
					      crass (~> 1.0.2)
 | 
				
			||||||
      nokogiri (>= 1.12.0)
 | 
					      nokogiri (>= 1.12.0)
 | 
				
			||||||
    mail (2.8.1)
 | 
					    mail (2.8.1)
 | 
				
			||||||
@ -126,79 +124,77 @@ GEM
 | 
				
			|||||||
      net-imap
 | 
					      net-imap
 | 
				
			||||||
      net-pop
 | 
					      net-pop
 | 
				
			||||||
      net-smtp
 | 
					      net-smtp
 | 
				
			||||||
    marcel (1.0.2)
 | 
					    marcel (1.0.4)
 | 
				
			||||||
    matrix (0.4.2)
 | 
					    matrix (0.4.2)
 | 
				
			||||||
    mini_mime (1.1.5)
 | 
					    mini_mime (1.1.5)
 | 
				
			||||||
    minitest (5.21.2)
 | 
					    minitest (5.25.4)
 | 
				
			||||||
    mutex_m (0.2.0)
 | 
					    mysql2 (0.5.6)
 | 
				
			||||||
    mysql2 (0.5.5)
 | 
					    net-imap (0.5.1)
 | 
				
			||||||
    net-imap (0.4.9.1)
 | 
					 | 
				
			||||||
      date
 | 
					      date
 | 
				
			||||||
      net-protocol
 | 
					      net-protocol
 | 
				
			||||||
    net-pop (0.1.2)
 | 
					    net-pop (0.1.2)
 | 
				
			||||||
      net-protocol
 | 
					      net-protocol
 | 
				
			||||||
    net-protocol (0.2.2)
 | 
					    net-protocol (0.2.2)
 | 
				
			||||||
      timeout
 | 
					      timeout
 | 
				
			||||||
    net-smtp (0.4.0.1)
 | 
					    net-smtp (0.5.0)
 | 
				
			||||||
      net-protocol
 | 
					      net-protocol
 | 
				
			||||||
    nio4r (2.7.0)
 | 
					    nio4r (2.7.4)
 | 
				
			||||||
    nokogiri (1.16.0-x86_64-linux)
 | 
					    nokogiri (1.16.8-x86_64-linux)
 | 
				
			||||||
      racc (~> 1.4)
 | 
					      racc (~> 1.4)
 | 
				
			||||||
    orm_adapter (0.5.0)
 | 
					    orm_adapter (0.5.0)
 | 
				
			||||||
    psych (5.1.2)
 | 
					    psych (5.2.1)
 | 
				
			||||||
 | 
					      date
 | 
				
			||||||
      stringio
 | 
					      stringio
 | 
				
			||||||
    public_suffix (5.0.4)
 | 
					    public_suffix (6.0.1)
 | 
				
			||||||
    puma (6.4.2)
 | 
					    puma (6.5.0)
 | 
				
			||||||
      nio4r (~> 2.0)
 | 
					      nio4r (~> 2.0)
 | 
				
			||||||
    racc (1.7.3)
 | 
					    racc (1.8.1)
 | 
				
			||||||
    rack (3.0.8)
 | 
					    rack (3.1.8)
 | 
				
			||||||
    rack-session (2.0.0)
 | 
					    rack-session (2.0.0)
 | 
				
			||||||
      rack (>= 3.0.0)
 | 
					      rack (>= 3.0.0)
 | 
				
			||||||
    rack-test (2.1.0)
 | 
					    rack-test (2.1.0)
 | 
				
			||||||
      rack (>= 1.3)
 | 
					      rack (>= 1.3)
 | 
				
			||||||
    rackup (2.1.0)
 | 
					    rackup (2.2.1)
 | 
				
			||||||
      rack (>= 3)
 | 
					      rack (>= 3)
 | 
				
			||||||
      webrick (~> 1.8)
 | 
					    rails (7.2.2)
 | 
				
			||||||
    rails (7.1.3)
 | 
					      actioncable (= 7.2.2)
 | 
				
			||||||
      actioncable (= 7.1.3)
 | 
					      actionmailbox (= 7.2.2)
 | 
				
			||||||
      actionmailbox (= 7.1.3)
 | 
					      actionmailer (= 7.2.2)
 | 
				
			||||||
      actionmailer (= 7.1.3)
 | 
					      actionpack (= 7.2.2)
 | 
				
			||||||
      actionpack (= 7.1.3)
 | 
					      actiontext (= 7.2.2)
 | 
				
			||||||
      actiontext (= 7.1.3)
 | 
					      actionview (= 7.2.2)
 | 
				
			||||||
      actionview (= 7.1.3)
 | 
					      activejob (= 7.2.2)
 | 
				
			||||||
      activejob (= 7.1.3)
 | 
					      activemodel (= 7.2.2)
 | 
				
			||||||
      activemodel (= 7.1.3)
 | 
					      activerecord (= 7.2.2)
 | 
				
			||||||
      activerecord (= 7.1.3)
 | 
					      activestorage (= 7.2.2)
 | 
				
			||||||
      activestorage (= 7.1.3)
 | 
					      activesupport (= 7.2.2)
 | 
				
			||||||
      activesupport (= 7.1.3)
 | 
					 | 
				
			||||||
      bundler (>= 1.15.0)
 | 
					      bundler (>= 1.15.0)
 | 
				
			||||||
      railties (= 7.1.3)
 | 
					      railties (= 7.2.2)
 | 
				
			||||||
    rails-dom-testing (2.2.0)
 | 
					    rails-dom-testing (2.2.0)
 | 
				
			||||||
      activesupport (>= 5.0.0)
 | 
					      activesupport (>= 5.0.0)
 | 
				
			||||||
      minitest
 | 
					      minitest
 | 
				
			||||||
      nokogiri (>= 1.6)
 | 
					      nokogiri (>= 1.6)
 | 
				
			||||||
    rails-html-sanitizer (1.6.0)
 | 
					    rails-html-sanitizer (1.6.1)
 | 
				
			||||||
      loofah (~> 2.21)
 | 
					      loofah (~> 2.21)
 | 
				
			||||||
      nokogiri (~> 1.14)
 | 
					      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 (7.1.3)
 | 
					    railties (7.2.2)
 | 
				
			||||||
      actionpack (= 7.1.3)
 | 
					      actionpack (= 7.2.2)
 | 
				
			||||||
      activesupport (= 7.1.3)
 | 
					      activesupport (= 7.2.2)
 | 
				
			||||||
      irb
 | 
					      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)
 | 
				
			||||||
      zeitwerk (~> 2.6)
 | 
					      zeitwerk (~> 2.6)
 | 
				
			||||||
    rake (13.1.0)
 | 
					    rake (13.2.1)
 | 
				
			||||||
    rdoc (6.6.2)
 | 
					    rdoc (6.8.1)
 | 
				
			||||||
      psych (>= 4.0.0)
 | 
					      psych (>= 4.0.0)
 | 
				
			||||||
    regexp_parser (2.9.0)
 | 
					    regexp_parser (2.9.3)
 | 
				
			||||||
    reline (0.4.2)
 | 
					    reline (0.5.12)
 | 
				
			||||||
      io-console (~> 0.5)
 | 
					      io-console (~> 0.5)
 | 
				
			||||||
    responders (3.1.1)
 | 
					    responders (3.1.1)
 | 
				
			||||||
      actionpack (>= 5.2)
 | 
					      actionpack (>= 5.2)
 | 
				
			||||||
      railties (>= 5.2)
 | 
					      railties (>= 5.2)
 | 
				
			||||||
    rexml (3.2.6)
 | 
					    rexml (3.3.9)
 | 
				
			||||||
    ruby2_keywords (0.0.5)
 | 
					 | 
				
			||||||
    rubyzip (2.3.2)
 | 
					    rubyzip (2.3.2)
 | 
				
			||||||
    sassc (2.4.0)
 | 
					    sassc (2.4.0)
 | 
				
			||||||
      ffi (~> 1.9)
 | 
					      ffi (~> 1.9)
 | 
				
			||||||
@ -208,27 +204,30 @@ GEM
 | 
				
			|||||||
      sprockets (> 3.0)
 | 
					      sprockets (> 3.0)
 | 
				
			||||||
      sprockets-rails
 | 
					      sprockets-rails
 | 
				
			||||||
      tilt
 | 
					      tilt
 | 
				
			||||||
    selenium-webdriver (4.16.0)
 | 
					    securerandom (0.4.0)
 | 
				
			||||||
 | 
					    selenium-webdriver (4.27.0)
 | 
				
			||||||
 | 
					      base64 (~> 0.2)
 | 
				
			||||||
 | 
					      logger (~> 1.4)
 | 
				
			||||||
      rexml (~> 3.2, >= 3.2.5)
 | 
					      rexml (~> 3.2, >= 3.2.5)
 | 
				
			||||||
      rubyzip (>= 1.2.2, < 3.0)
 | 
					      rubyzip (>= 1.2.2, < 3.0)
 | 
				
			||||||
      websocket (~> 1.0)
 | 
					      websocket (~> 1.0)
 | 
				
			||||||
    sprockets (4.2.1)
 | 
					    sprockets (4.2.1)
 | 
				
			||||||
      concurrent-ruby (~> 1.0)
 | 
					      concurrent-ruby (~> 1.0)
 | 
				
			||||||
      rack (>= 2.2.4, < 4)
 | 
					      rack (>= 2.2.4, < 4)
 | 
				
			||||||
    sprockets-rails (3.4.2)
 | 
					    sprockets-rails (3.5.2)
 | 
				
			||||||
      actionpack (>= 5.2)
 | 
					      actionpack (>= 6.1)
 | 
				
			||||||
      activesupport (>= 5.2)
 | 
					      activesupport (>= 6.1)
 | 
				
			||||||
      sprockets (>= 3.0.0)
 | 
					      sprockets (>= 3.0.0)
 | 
				
			||||||
    stringio (3.1.0)
 | 
					    stringio (3.1.2)
 | 
				
			||||||
    thor (1.3.0)
 | 
					    thor (1.3.2)
 | 
				
			||||||
    tilt (2.3.0)
 | 
					    tilt (2.4.0)
 | 
				
			||||||
    timeout (0.4.1)
 | 
					    timeout (0.4.2)
 | 
				
			||||||
    turbo-rails (2.0.0.pre.beta.3)
 | 
					    turbo-rails (2.0.11)
 | 
				
			||||||
      actionpack (>= 6.0.0)
 | 
					      actionpack (>= 6.0.0)
 | 
				
			||||||
      activejob (>= 6.0.0)
 | 
					 | 
				
			||||||
      railties (>= 6.0.0)
 | 
					      railties (>= 6.0.0)
 | 
				
			||||||
    tzinfo (2.0.6)
 | 
					    tzinfo (2.0.6)
 | 
				
			||||||
      concurrent-ruby (~> 1.0)
 | 
					      concurrent-ruby (~> 1.0)
 | 
				
			||||||
 | 
					    useragent (0.16.11)
 | 
				
			||||||
    warden (1.2.9)
 | 
					    warden (1.2.9)
 | 
				
			||||||
      rack (>= 2.0.9)
 | 
					      rack (>= 2.0.9)
 | 
				
			||||||
    web-console (4.2.1)
 | 
					    web-console (4.2.1)
 | 
				
			||||||
@ -236,14 +235,13 @@ GEM
 | 
				
			|||||||
      activemodel (>= 6.0.0)
 | 
					      activemodel (>= 6.0.0)
 | 
				
			||||||
      bindex (>= 0.4.0)
 | 
					      bindex (>= 0.4.0)
 | 
				
			||||||
      railties (>= 6.0.0)
 | 
					      railties (>= 6.0.0)
 | 
				
			||||||
    webrick (1.8.1)
 | 
					    websocket (1.2.11)
 | 
				
			||||||
    websocket (1.2.10)
 | 
					 | 
				
			||||||
    websocket-driver (0.7.6)
 | 
					    websocket-driver (0.7.6)
 | 
				
			||||||
      websocket-extensions (>= 0.1.0)
 | 
					      websocket-extensions (>= 0.1.0)
 | 
				
			||||||
    websocket-extensions (0.1.5)
 | 
					    websocket-extensions (0.1.5)
 | 
				
			||||||
    xpath (3.2.0)
 | 
					    xpath (3.2.0)
 | 
				
			||||||
      nokogiri (~> 1.8)
 | 
					      nokogiri (~> 1.8)
 | 
				
			||||||
    zeitwerk (2.6.12)
 | 
					    zeitwerk (2.7.1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
PLATFORMS
 | 
					PLATFORMS
 | 
				
			||||||
  x86_64-linux
 | 
					  x86_64-linux
 | 
				
			||||||
@ -255,7 +253,7 @@ DEPENDENCIES
 | 
				
			|||||||
  importmap-rails
 | 
					  importmap-rails
 | 
				
			||||||
  mysql2 (~> 0.5)
 | 
					  mysql2 (~> 0.5)
 | 
				
			||||||
  puma (~> 6.0)
 | 
					  puma (~> 6.0)
 | 
				
			||||||
  rails (~> 7.1.2)
 | 
					  rails (~> 7.2.2)
 | 
				
			||||||
  sassc-rails
 | 
					  sassc-rails
 | 
				
			||||||
  selenium-webdriver
 | 
					  selenium-webdriver
 | 
				
			||||||
  sprockets-rails
 | 
					  sprockets-rails
 | 
				
			||||||
@ -263,5 +261,8 @@ DEPENDENCIES
 | 
				
			|||||||
  tzinfo-data
 | 
					  tzinfo-data
 | 
				
			||||||
  web-console
 | 
					  web-console
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					RUBY VERSION
 | 
				
			||||||
 | 
					   ruby 3.3.0p0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
BUNDLED WITH
 | 
					BUNDLED WITH
 | 
				
			||||||
   2.5.3
 | 
					   2.5.3
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										19
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								README.md
									
									
									
									
									
								
							@ -9,8 +9,11 @@ Software requirements
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
* Server side:
 | 
					* Server side:
 | 
				
			||||||
    * Ruby version: developed on Ruby 3.x
 | 
					    * Ruby version: developed on Ruby 3.x
 | 
				
			||||||
    * database with recursive Common Table Expressions (CTE) support, e.g.
 | 
					    * database with:
 | 
				
			||||||
      MySQL >= 8.0, MariaDB >= 10.2.2
 | 
					        * recursive Common Table Expressions (CTE) support, e.g.
 | 
				
			||||||
 | 
					          MySQL >= 8.0, MariaDB >= 10.2.2
 | 
				
			||||||
 | 
					        * decimal type with precision of at least 30 (not sure if SQLite3
 | 
				
			||||||
 | 
					          supports this)
 | 
				
			||||||
    * for testing: browser as specified in _Client side_ requirements
 | 
					    * for testing: browser as specified in _Client side_ requirements
 | 
				
			||||||
* Client side:
 | 
					* Client side:
 | 
				
			||||||
    * browser supporting below requirements (e.g. Firefox >= 121):
 | 
					    * browser supporting below requirements (e.g. Firefox >= 121):
 | 
				
			||||||
@ -118,3 +121,15 @@ Tests need to be run from within toplevel application directory:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        bundle exec rails test test/system/users_test.rb --seed 1234
 | 
					        bundle exec rails test test/system/users_test.rb --seed 1234
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Icons
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Pictogrammers Material Design Icons: https://pictogrammers.com/library/mdi/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Rake tasks
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Exporting default settings defined in application to seed file (e.g. to send as
 | 
				
			||||||
 | 
					PR or share between installations):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        bundle exec rails db:seed:export
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										1
									
								
								app/assets/images/pictograms/close-outline.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								app/assets/images/pictograms/close-outline.svg
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" id="icon" viewBox="0 0 24 24"><path d="M3,16.74L7.76,12L3,7.26L7.26,3L12,7.76L16.74,3L21,7.26L16.24,12L21,16.74L16.74,21L12,16.24L7.26,21L3,16.74M12,13.41L16.74,18.16L18.16,16.74L13.41,12L18.16,7.26L16.74,5.84L12,10.59L7.26,5.84L5.84,7.26L10.59,12L5.84,16.74L7.26,18.16L12,13.41Z" /></svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 330 B  | 
@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" id="icon" viewBox="0 0 24 24"><path d="M12 14L19 7H15V1H9V7H5L12 14M12 11.17L9.83 9H11V3H13V9H14.17L12 11.17M5 16V18H19V16H5M5 22V20H19V22H5Z" /></svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 192 B  | 
							
								
								
									
										1
									
								
								app/assets/images/pictograms/download-outline.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								app/assets/images/pictograms/download-outline.svg
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" id="icon" viewBox="0 0 24 24"><path d="M13,5V11H14.17L12,13.17L9.83,11H11V5H13M15,3H9V9H5L12,16L19,9H15V3M19,18H5V20H19V18Z" /></svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 174 B  | 
							
								
								
									
										1
									
								
								app/assets/images/pictograms/upload-outline.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								app/assets/images/pictograms/upload-outline.svg
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" id="icon" viewBox="0 0 24 24"><path d="M9,10V16H15V10H19L12,3L5,10H9M12,5.8L14.2,8H13V14H11V8H9.8L12,5.8M19,18H5V20H19V18Z" /></svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 173 B  | 
@ -98,16 +98,21 @@ input[type=submit] {
 | 
				
			|||||||
  width: fit-content;
 | 
					  width: fit-content;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
input:not([type=submit]):not([type=checkbox]),
 | 
					input:not([type=submit]):not([type=checkbox]),
 | 
				
			||||||
select {
 | 
					select,
 | 
				
			||||||
 | 
					textarea {
 | 
				
			||||||
  padding: 0.2em 0.4em;
 | 
					  padding: 0.2em 0.4em;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
.button,
 | 
					.button,
 | 
				
			||||||
button,
 | 
					button,
 | 
				
			||||||
input,
 | 
					input,
 | 
				
			||||||
select {
 | 
					select,
 | 
				
			||||||
 | 
					textarea {
 | 
				
			||||||
  border: solid 1px var(--color-gray);
 | 
					  border: solid 1px var(--color-gray);
 | 
				
			||||||
  border-radius: 0.25em;
 | 
					  border-radius: 0.25em;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					textarea {
 | 
				
			||||||
 | 
					  margin: 0
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
.button > svg,
 | 
					.button > svg,
 | 
				
			||||||
.tab > svg,
 | 
					.tab > svg,
 | 
				
			||||||
button > svg {
 | 
					button > svg {
 | 
				
			||||||
@ -151,7 +156,8 @@ input[type=checkbox]:checked {
 | 
				
			|||||||
  -webkit-appearance: checkbox;
 | 
					  -webkit-appearance: checkbox;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
input:hover,
 | 
					input:hover,
 | 
				
			||||||
select:hover {
 | 
					select:hover,
 | 
				
			||||||
 | 
					textarea:hover {
 | 
				
			||||||
  border-color: #009ade;
 | 
					  border-color: #009ade;
 | 
				
			||||||
  outline: solid 1px #009ade;
 | 
					  outline: solid 1px #009ade;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -160,11 +166,13 @@ select:hover {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
input:focus-visible,
 | 
					input:focus-visible,
 | 
				
			||||||
select:focus-within,
 | 
					select:focus-within,
 | 
				
			||||||
select:focus-visible {
 | 
					select:focus-visible,
 | 
				
			||||||
 | 
					textarea:focus-visible {
 | 
				
			||||||
  accent-color: #006c9b;
 | 
					  accent-color: #006c9b;
 | 
				
			||||||
  background-color: var(--color-focus-gray);
 | 
					  background-color: var(--color-focus-gray);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
input[type=text]:read-only {
 | 
					input[type=text]:read-only,
 | 
				
			||||||
 | 
					textarea:read-only {
 | 
				
			||||||
  border: none;
 | 
					  border: none;
 | 
				
			||||||
  padding-left: 0;
 | 
					  padding-left: 0;
 | 
				
			||||||
  padding-right: 0;
 | 
					  padding-right: 0;
 | 
				
			||||||
@ -336,7 +344,7 @@ table.items th,
 | 
				
			|||||||
table.items td {
 | 
					table.items td {
 | 
				
			||||||
  padding-inline: 1em 0;
 | 
					  padding-inline: 1em 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
table.items td:has(input) {
 | 
					table.items td:has(input, textarea) {
 | 
				
			||||||
  padding-inline-start: calc(0.6em - 0.9px);
 | 
					  padding-inline-start: calc(0.6em - 0.9px);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
table.items th:last-child {
 | 
					table.items th:last-child {
 | 
				
			||||||
@ -367,7 +375,7 @@ table.items td.link a::after {
 | 
				
			|||||||
table.items td.subunit {
 | 
					table.items td.subunit {
 | 
				
			||||||
  padding-inline-start: 1.8em;
 | 
					  padding-inline-start: 1.8em;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
table.items td.subunit:has(input) {
 | 
					table.items td.subunit:has(input, textarea) {
 | 
				
			||||||
  padding-inline-start: calc(1.4em - 1px);
 | 
					  padding-inline-start: calc(1.4em - 1px);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
table.items td.actions {
 | 
					table.items td.actions {
 | 
				
			||||||
@ -390,6 +398,9 @@ table.items tr.dropzone::after {
 | 
				
			|||||||
table.items td.handle {
 | 
					table.items td.handle {
 | 
				
			||||||
  cursor: move;
 | 
					  cursor: move;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					table.items tr.form td {
 | 
				
			||||||
 | 
					  vertical-align: top;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* TODO: replace :hover:focus-visible combos with proper LOVE stye order */
 | 
					/* TODO: replace :hover:focus-visible combos with proper LOVE stye order */
 | 
				
			||||||
/* TODO: Update styling, including rem removal. */
 | 
					/* TODO: Update styling, including rem removal. */
 | 
				
			||||||
@ -410,7 +421,8 @@ table.items td.link a:hover:focus-visible {
 | 
				
			|||||||
  color: #006c9b;
 | 
					  color: #006c9b;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
table.items td:not(:first-child) {
 | 
					table.items td:not(:first-child),
 | 
				
			||||||
 | 
					.grayed {
 | 
				
			||||||
  color: var(--color-table-gray);
 | 
					  color: var(--color-table-gray);
 | 
				
			||||||
  fill: var(--color-table-gray);
 | 
					  fill: var(--color-table-gray);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -439,7 +451,8 @@ table.items input[type=submit] {
 | 
				
			|||||||
table.items .button:not(:hover),
 | 
					table.items .button:not(:hover),
 | 
				
			||||||
table.items button:not(:hover),
 | 
					table.items button:not(:hover),
 | 
				
			||||||
table.items input:not(:hover),
 | 
					table.items input:not(:hover),
 | 
				
			||||||
table.items select:not(:hover) {
 | 
					table.items select:not(:hover),
 | 
				
			||||||
 | 
					table.items textarea:not(:hover) {
 | 
				
			||||||
  border-color: var(--color-border-gray);
 | 
					  border-color: var(--color-border-gray);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
table.items .button:not(:hover),
 | 
					table.items .button:not(:hover),
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										54
									
								
								app/controllers/default/units_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								app/controllers/default/units_controller.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,54 @@
 | 
				
			|||||||
 | 
					class Default::UnitsController < ApplicationController
 | 
				
			||||||
 | 
					  navigation_tab :units
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  before_action :find_unit, only: :export
 | 
				
			||||||
 | 
					  before_action :find_unit_default, only: [:import, :destroy]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  before_action only: :import do
 | 
				
			||||||
 | 
					    raise AccessForbidden unless current_user.at_least(:active)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					  before_action except: [:index, :import] do
 | 
				
			||||||
 | 
					    raise AccessForbidden unless current_user.at_least(:admin)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def index
 | 
				
			||||||
 | 
					    @units = current_user.units.defaults_diff.includes(:base).ordered
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def import
 | 
				
			||||||
 | 
					    @unit.port!(current_user)
 | 
				
			||||||
 | 
					    flash.now[:notice] = t('.success', unit: @unit)
 | 
				
			||||||
 | 
					  ensure
 | 
				
			||||||
 | 
					    run_and_render :index
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  #def import_all
 | 
				
			||||||
 | 
					    # From defaults_diff return not only portability, but reason for not being
 | 
				
			||||||
 | 
					    # portable: missing_base and nesting_too_deep. Add portable and
 | 
				
			||||||
 | 
					    # missing_base, if possible in one query
 | 
				
			||||||
 | 
					  #end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def export
 | 
				
			||||||
 | 
					    @unit.port!(nil)
 | 
				
			||||||
 | 
					    flash.now[:notice] = t('.success', unit: @unit)
 | 
				
			||||||
 | 
					  ensure
 | 
				
			||||||
 | 
					    run_and_render :index
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def destroy
 | 
				
			||||||
 | 
					    @unit.destroy!
 | 
				
			||||||
 | 
					    flash.now[:notice] = t('.success', unit: @unit)
 | 
				
			||||||
 | 
					  ensure
 | 
				
			||||||
 | 
					    run_and_render :index
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def find_unit
 | 
				
			||||||
 | 
					    @unit = Unit.find_by!(id: params[:id], user: current_user)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def find_unit_default
 | 
				
			||||||
 | 
					    @unit = Unit.find_by!(id: params[:id], user: nil)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@ -1,10 +0,0 @@
 | 
				
			|||||||
class Units::DefaultsController < ApplicationController
 | 
					 | 
				
			||||||
  navigation_tab :units
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  before_action except: :index do
 | 
					 | 
				
			||||||
    raise AccessForbidden unless current_user.at_least(:admin)
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def index
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
end
 | 
					 | 
				
			||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
class UnitsController < ApplicationController
 | 
					class UnitsController < ApplicationController
 | 
				
			||||||
  before_action only: [:new] do
 | 
					  before_action only: :new do
 | 
				
			||||||
    find_unit if params[:id].present?
 | 
					    find_unit if params[:id].present?
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
  before_action :find_unit, only: [:edit, :update, :rebase, :destroy]
 | 
					  before_action :find_unit, only: [:edit, :update, :rebase, :destroy]
 | 
				
			||||||
@ -9,7 +9,7 @@ class UnitsController < ApplicationController
 | 
				
			|||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def index
 | 
					  def index
 | 
				
			||||||
    @units = current_user.units.includes(:subunits)
 | 
					    @units = current_user.units.includes(:subunits).ordered
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def new
 | 
					  def new
 | 
				
			||||||
@ -19,7 +19,7 @@ class UnitsController < ApplicationController
 | 
				
			|||||||
  def create
 | 
					  def create
 | 
				
			||||||
    @unit = current_user.units.new(unit_params)
 | 
					    @unit = current_user.units.new(unit_params)
 | 
				
			||||||
    if @unit.save
 | 
					    if @unit.save
 | 
				
			||||||
      flash.now[:notice] = t(".success")
 | 
					      flash.now[:notice] = t('.success', unit: @unit)
 | 
				
			||||||
      run_and_render :index
 | 
					      run_and_render :index
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
      render :new
 | 
					      render :new
 | 
				
			||||||
@ -31,7 +31,7 @@ class UnitsController < ApplicationController
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  def update
 | 
					  def update
 | 
				
			||||||
    if @unit.update(unit_params.except(:base_id))
 | 
					    if @unit.update(unit_params.except(:base_id))
 | 
				
			||||||
      flash.now[:notice] = t(".success")
 | 
					      flash.now[:notice] = t('.success', unit: @unit)
 | 
				
			||||||
      run_and_render :index
 | 
					      run_and_render :index
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
      render :edit
 | 
					      render :edit
 | 
				
			||||||
@ -40,25 +40,28 @@ class UnitsController < ApplicationController
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  def rebase
 | 
					  def rebase
 | 
				
			||||||
    permitted = params.require(:unit).permit(:base_id)
 | 
					    permitted = params.require(:unit).permit(:base_id)
 | 
				
			||||||
    if permitted[:base_id].blank? && @unit.multiplier != 1
 | 
					    permitted.merge!(multiplier: 1) if permitted[:base_id].blank? && @unit.multiplier != 1
 | 
				
			||||||
      permitted.merge!(multiplier: 1)
 | 
					 | 
				
			||||||
      flash.now[:notice] = t(".multiplier_reset", symbol: @unit.symbol)
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    run_and_render :index if @unit.update(permitted)
 | 
					    @unit.update!(permitted)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if @unit.multiplier_previously_changed?
 | 
				
			||||||
 | 
					      flash.now[:notice] = t(".multiplier_reset", unit: @unit)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  ensure
 | 
				
			||||||
 | 
					    run_and_render :index
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def destroy
 | 
					  def destroy
 | 
				
			||||||
    if @unit.destroy
 | 
					    @unit.destroy!
 | 
				
			||||||
      flash.now[:notice] = t(".success")
 | 
					    flash.now[:notice] = t('.success', unit: @unit)
 | 
				
			||||||
    end
 | 
					  ensure
 | 
				
			||||||
    run_and_render :index
 | 
					    run_and_render :index
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def unit_params
 | 
					  def unit_params
 | 
				
			||||||
    params.require(:unit).permit(:symbol, :name, :base_id, :multiplier)
 | 
					    params.require(:unit).permit(Unit::ATTRIBUTES)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def find_unit
 | 
					  def find_unit
 | 
				
			||||||
 | 
				
			|||||||
@ -2,12 +2,13 @@ class UsersController < ApplicationController
 | 
				
			|||||||
  helper_method :allow_disguise?
 | 
					  helper_method :allow_disguise?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  before_action :find_user, only: [:show, :update, :disguise]
 | 
					  before_action :find_user, only: [:show, :update, :disguise]
 | 
				
			||||||
  before_action except: :revert do
 | 
					
 | 
				
			||||||
    raise AccessForbidden unless current_user.at_least(:admin)
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
  before_action only: :revert do
 | 
					  before_action only: :revert do
 | 
				
			||||||
    raise AccessForbidden unless current_user_disguised?
 | 
					    raise AccessForbidden unless current_user_disguised?
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					  before_action except: :revert do
 | 
				
			||||||
 | 
					    raise AccessForbidden unless current_user.at_least(:admin)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def index
 | 
					  def index
 | 
				
			||||||
    @users = User.all
 | 
					    @users = User.all
 | 
				
			||||||
 | 
				
			|||||||
@ -84,6 +84,7 @@ module ApplicationHelper
 | 
				
			|||||||
  [:button_to, :link_to, :link_to_unless_current].each do |method_name|
 | 
					  [:button_to, :link_to, :link_to_unless_current].each do |method_name|
 | 
				
			||||||
    class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
 | 
					    class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
 | 
				
			||||||
      def image_#{method_name}(name, image = nil, options = nil, html_options = {}, &block)
 | 
					      def image_#{method_name}(name, image = nil, options = nil, html_options = {}, &block)
 | 
				
			||||||
 | 
					        name = name.to_s
 | 
				
			||||||
        name = svg_tag("pictograms/\#{image}") + name if image
 | 
					        name = svg_tag("pictograms/\#{image}") + name if image
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        html_options[:class] = class_names(
 | 
					        html_options[:class] = class_names(
 | 
				
			||||||
@ -95,6 +96,11 @@ module ApplicationHelper
 | 
				
			|||||||
          html_options[:onclick] = "return confirm('\#{html_options[:onclick][:confirm]}');"
 | 
					          html_options[:onclick] = "return confirm('\#{html_options[:onclick][:confirm]}');"
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if __method__.start_with?('image_link_to') &&
 | 
				
			||||||
 | 
					             !(html_options[:onclick] || html_options.dig(:data, :turbo_stream))
 | 
				
			||||||
 | 
					          name = name + '...'
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        send :#{method_name}, name, options, html_options, &block
 | 
					        send :#{method_name}, name, options, html_options, &block
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    RUBY_EVAL
 | 
					    RUBY_EVAL
 | 
				
			||||||
@ -123,27 +129,13 @@ module ApplicationHelper
 | 
				
			|||||||
    "Turbo.renderStreamMessage('#{j(render partial: partial, locals: locals)}'); return false;"
 | 
					    "Turbo.renderStreamMessage('#{j(render partial: partial, locals: locals)}'); return false;"
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private
 | 
					  def disabled_attributes(disabled)
 | 
				
			||||||
 | 
					    disabled ? {disabled: true, aria: {disabled: true}, tabindex: -1} : {}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # Converts value to HTML formatted scientific notation
 | 
					  def number_attributes(type)
 | 
				
			||||||
  def scientifize(d)
 | 
					    step = BigDecimal(10).power(-type.scale)
 | 
				
			||||||
    sign, coefficient, base, exponent = d.split
 | 
					    max = BigDecimal(10).power(type.precision - type.scale) - step
 | 
				
			||||||
    return 'NaN' unless sign
 | 
					    {min: -max, max: max, step: step}
 | 
				
			||||||
 | 
					 | 
				
			||||||
    result = (sign == -1 ? '-' : '')
 | 
					 | 
				
			||||||
    unless coefficient == '1' && sign == 1
 | 
					 | 
				
			||||||
      if coefficient.length > 1
 | 
					 | 
				
			||||||
        result += coefficient.insert(1, '.')
 | 
					 | 
				
			||||||
      elsif
 | 
					 | 
				
			||||||
        result += coefficient
 | 
					 | 
				
			||||||
      end
 | 
					 | 
				
			||||||
      if exponent != 1
 | 
					 | 
				
			||||||
        result += "×"
 | 
					 | 
				
			||||||
      end
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
    if exponent != 1
 | 
					 | 
				
			||||||
      result += "10<sup>% d</sup>" % [exponent-1]
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
    result.html_safe
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										2
									
								
								app/helpers/default/units_helper.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								app/helpers/default/units_helper.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					module Default::UnitsHelper
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@ -1,2 +0,0 @@
 | 
				
			|||||||
module Units::DefaultsHelper
 | 
					 | 
				
			||||||
end
 | 
					 | 
				
			||||||
@ -1,26 +1,83 @@
 | 
				
			|||||||
class Unit < ApplicationRecord
 | 
					class Unit < ApplicationRecord
 | 
				
			||||||
 | 
					  ATTRIBUTES = [:symbol, :description, :multiplier, :base_id]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  belongs_to :user, optional: true
 | 
					  belongs_to :user, optional: true
 | 
				
			||||||
  belongs_to :base, optional: true, class_name: "Unit"
 | 
					  belongs_to :base, optional: true, class_name: "Unit"
 | 
				
			||||||
  has_many :subunits, class_name: "Unit", dependent: :restrict_with_error, inverse_of: :base
 | 
					  has_many :subunits, class_name: "Unit", inverse_of: :base, dependent: :restrict_with_error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  validate if: ->{ base.present? } do
 | 
					  validate if: ->{ base.present? } do
 | 
				
			||||||
    errors.add(:base, :user_mismatch) unless user == base.user
 | 
					    errors.add(:base, :user_mismatch) unless user == base.user
 | 
				
			||||||
    errors.add(:base, :multilevel_nesting) if base.base.present?
 | 
					    errors.add(:base, :multilevel_nesting) if base.base.present?
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
  validates :symbol, presence: true, uniqueness: {scope: :user_id},
 | 
					  validates :symbol, presence: true, uniqueness: {scope: :user_id},
 | 
				
			||||||
    length: {maximum: columns_hash['symbol'].limit}
 | 
					    length: {maximum: type_for_attribute(:symbol).limit}
 | 
				
			||||||
  validates :name, length: {maximum: columns_hash['name'].limit}
 | 
					  validates :description, length: {maximum: type_for_attribute(:description).limit}
 | 
				
			||||||
  validates :multiplier, numericality: {equal_to: 1}, unless: :base
 | 
					  validates :multiplier, numericality: {equal_to: 1}, unless: :base
 | 
				
			||||||
  validates :multiplier, numericality: {other_than: 0}, if: :base
 | 
					  validates :multiplier, numericality: {other_than: 0, precision: true, scale: true}, if: :base
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  scope :defaults, ->{ where(user: nil) }
 | 
					  scope :defaults, ->{ where(user: nil) }
 | 
				
			||||||
 | 
					  scope :defaults_diff, ->{
 | 
				
			||||||
 | 
					    actionable_units = Arel::Table.new('actionable_units')
 | 
				
			||||||
 | 
					    units = actionable_units.alias('units')
 | 
				
			||||||
 | 
					    bases_units = arel_table.alias('bases_units')
 | 
				
			||||||
 | 
					    other_units = arel_table.alias('other_units')
 | 
				
			||||||
 | 
					    other_bases_units = arel_table.alias('other_bases_units')
 | 
				
			||||||
 | 
					    sub_units = arel_table.alias('sub_units')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # TODO: move inner 'with' CTE to outer 'with recursive' - it can have multiple
 | 
				
			||||||
 | 
					    # CTEs, even non recursive ones.
 | 
				
			||||||
 | 
					    Unit.with_recursive(actionable_units: [
 | 
				
			||||||
 | 
					      Unit.with(units: self.or(Unit.defaults)).left_joins(:base)
 | 
				
			||||||
 | 
					        .where.not(
 | 
				
			||||||
 | 
					          # Exclude Units that are/have default counterpart
 | 
				
			||||||
 | 
					          Arel::SelectManager.new.project(1).from(other_units)
 | 
				
			||||||
 | 
					            .outer_join(other_bases_units)
 | 
				
			||||||
 | 
					            .on(other_units[:base_id].eq(other_bases_units[:id]))
 | 
				
			||||||
 | 
					            .where(
 | 
				
			||||||
 | 
					              other_bases_units[:symbol].is_not_distinct_from(bases_units[:symbol])
 | 
				
			||||||
 | 
					                .and(other_units[:symbol].eq(arel_table[:symbol]))
 | 
				
			||||||
 | 
					                .and(other_units[:user_id].is_distinct_from(arel_table[:user_id]))
 | 
				
			||||||
 | 
					            ).exists
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .select(
 | 
				
			||||||
 | 
					          arel_table[Arel.star],
 | 
				
			||||||
 | 
					          # Decide if Unit can be im-/exported based on existing hierarchy:
 | 
				
			||||||
 | 
					          # * same base unit symbol has to exist
 | 
				
			||||||
 | 
					          # * unit with subunits can only be ported to root
 | 
				
			||||||
 | 
					          arel_table[:base_id].eq(nil).or(
 | 
				
			||||||
 | 
					            (
 | 
				
			||||||
 | 
					              Arel::SelectManager.new.project(1).from(other_units)
 | 
				
			||||||
 | 
					                .join(sub_units).on(other_units[:id].eq(sub_units[:base_id]))
 | 
				
			||||||
 | 
					                .where(
 | 
				
			||||||
 | 
					                  other_units[:symbol].eq(arel_table[:symbol])
 | 
				
			||||||
 | 
					                    .and(other_units[:user_id].is_distinct_from(arel_table[:user_id]))
 | 
				
			||||||
 | 
					                ).exists.not
 | 
				
			||||||
 | 
					            ).and(
 | 
				
			||||||
 | 
					              Arel::SelectManager.new.project(1).from(other_bases_units)
 | 
				
			||||||
 | 
					                .where(
 | 
				
			||||||
 | 
					                  other_bases_units[:symbol].is_not_distinct_from(bases_units[:symbol])
 | 
				
			||||||
 | 
					                    .and(other_bases_units[:user_id].is_distinct_from(bases_units[:user_id]))
 | 
				
			||||||
 | 
					                ).exists
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					          ).as('portable')
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      # Fill base Units to display proper hierarchy. Duplicates will be removed
 | 
				
			||||||
 | 
					      # by final group() - can't be deduplicated with UNION due to 'portable' field.
 | 
				
			||||||
 | 
					      arel_table.join(actionable_units).on(actionable_units[:base_id].eq(arel_table[:id]))
 | 
				
			||||||
 | 
					        .project(arel_table[Arel.star], Arel::Nodes.build_quoted(nil).as('portable'))
 | 
				
			||||||
 | 
					    ]).select(units: [:base_id, :symbol])
 | 
				
			||||||
 | 
					      .select(
 | 
				
			||||||
 | 
					        units[:id].minimum.as('id'), # can be ANY_VALUE()
 | 
				
			||||||
 | 
					        units[:user_id].minimum.as('user_id'), # prefer non-default
 | 
				
			||||||
 | 
					        Arel::Nodes.build_quoted(1).as('multiplier'), # disregard multiplier when sorting
 | 
				
			||||||
 | 
					        units[:portable].minimum.as('portable')
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      .from(units).group(:base_id, :symbol)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
  scope :ordered, ->{
 | 
					  scope :ordered, ->{
 | 
				
			||||||
    parent_symbol = Arel::Nodes::NamedFunction.new(
 | 
					 | 
				
			||||||
                      'COALESCE',
 | 
					 | 
				
			||||||
                      [Arel::Table.new(:bases_units)[:symbol], arel_table[:symbol]]
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
    left_outer_joins(:base)
 | 
					    left_outer_joins(:base)
 | 
				
			||||||
      .order(parent_symbol, arel_table[:base_id].asc.nulls_first, :multiplier, :symbol)
 | 
					      .order(arel_table.coalesce(Arel::Table.new(:bases_units)[:symbol], arel_table[:symbol]),
 | 
				
			||||||
 | 
					             arel_table[:base_id].not_eq(nil), :multiplier, :symbol)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  before_destroy do
 | 
					  before_destroy do
 | 
				
			||||||
@ -28,7 +85,23 @@ class Unit < ApplicationRecord
 | 
				
			|||||||
    nil
 | 
					    nil
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def to_s
 | 
				
			||||||
 | 
					    symbol
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def movable?
 | 
					  def movable?
 | 
				
			||||||
    subunits.empty?
 | 
					    subunits.empty?
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def default?
 | 
				
			||||||
 | 
					    user_id.nil?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Should only by invoked on Units returned from #defaults_diff which are #portable
 | 
				
			||||||
 | 
					  def port!(recipient)
 | 
				
			||||||
 | 
					    recipient_base = base && Unit.find_by!(symbol: base.symbol, user: recipient)
 | 
				
			||||||
 | 
					    params = slice(ATTRIBUTES - [:symbol, :base_id])
 | 
				
			||||||
 | 
					    Unit.find_or_initialize_by(user: recipient, symbol: symbol)
 | 
				
			||||||
 | 
					      .update!(base: recipient_base, **params)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
				
			|||||||
@ -11,7 +11,15 @@ class User < ApplicationRecord
 | 
				
			|||||||
    disabled: 0,    # administratively disallowed to sign in
 | 
					    disabled: 0,    # administratively disallowed to sign in
 | 
				
			||||||
  }, default: :active
 | 
					  }, default: :active
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  has_many :units, -> { ordered }, dependent: :destroy
 | 
					  has_many :units, dependent: :destroy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  validates :email, presence: true, uniqueness: true,
 | 
				
			||||||
 | 
					    length: {maximum: type_for_attribute(:email).limit}
 | 
				
			||||||
 | 
					  validates :unconfirmed_email, length: {maximum: type_for_attribute(:unconfirmed_email).limit}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def to_s
 | 
				
			||||||
 | 
					    email
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def at_least(status)
 | 
					  def at_least(status)
 | 
				
			||||||
    User.statuses[self.status] >= User.statuses[status]
 | 
					    User.statuses[self.status] >= User.statuses[status]
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										23
									
								
								app/views/default/units/_unit.html.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								app/views/default/units/_unit.html.erb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					<%= tag.tr do %>
 | 
				
			||||||
 | 
					  <td class="<%= class_names({subunit: unit.base, grayed: unit.default?}) %>">
 | 
				
			||||||
 | 
					    <%= unit %>
 | 
				
			||||||
 | 
					  </td>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <td class="actions">
 | 
				
			||||||
 | 
					    <% unless unit.portable.nil? %>
 | 
				
			||||||
 | 
					      <% if current_user.at_least(:active) && unit.default? %>
 | 
				
			||||||
 | 
					        <%= image_button_to t('.import'), 'download-outline', import_default_unit_path(unit),
 | 
				
			||||||
 | 
					          disabled_attributes(!unit.portable?) %>
 | 
				
			||||||
 | 
					      <% end %>
 | 
				
			||||||
 | 
					      <% if current_user.at_least(:admin) %>
 | 
				
			||||||
 | 
					        <% if unit.default? %>
 | 
				
			||||||
 | 
					          <%= image_button_to t('.delete'), 'delete-outline', default_unit_path(unit),
 | 
				
			||||||
 | 
					            method: :delete %>
 | 
				
			||||||
 | 
					        <% else %>
 | 
				
			||||||
 | 
					          <%= image_button_to t('.export'), 'upload-outline', export_default_unit_path(unit),
 | 
				
			||||||
 | 
					            disabled_attributes(!unit.portable?) %>
 | 
				
			||||||
 | 
					        <% end %>
 | 
				
			||||||
 | 
					      <% end %>
 | 
				
			||||||
 | 
					    <% end %>
 | 
				
			||||||
 | 
					  </td>
 | 
				
			||||||
 | 
					<% end %>
 | 
				
			||||||
							
								
								
									
										22
									
								
								app/views/default/units/index.html.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								app/views/default/units/index.html.erb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					<div class="rightside buttongrid">
 | 
				
			||||||
 | 
					  <% if current_user.at_least(:active) %>
 | 
				
			||||||
 | 
					    <%# TODO: implement Import all %>
 | 
				
			||||||
 | 
					    <%#= image_button_to t('.import_all'), 'download-multiple-outline',
 | 
				
			||||||
 | 
					      import_all_default_units_path, data: {turbo_stream: true} %>
 | 
				
			||||||
 | 
					  <% end %>
 | 
				
			||||||
 | 
					  <%= image_link_to t('.back'), 'arrow-left-bold-outline', units_path, class: 'tools' %>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<table class="main items">
 | 
				
			||||||
 | 
					  <thead>
 | 
				
			||||||
 | 
					    <tr>
 | 
				
			||||||
 | 
					      <th><%= User.human_attribute_name(:symbol).capitalize %></th>
 | 
				
			||||||
 | 
					      <% if current_user.at_least(:active) %>
 | 
				
			||||||
 | 
					        <th><%= t '.actions' %></th>
 | 
				
			||||||
 | 
					      <% end %>
 | 
				
			||||||
 | 
					    </tr>
 | 
				
			||||||
 | 
					  </thead>
 | 
				
			||||||
 | 
					  <tbody id="units">
 | 
				
			||||||
 | 
					    <%= render(@units) || render_no_items %>
 | 
				
			||||||
 | 
					  </tbody>
 | 
				
			||||||
 | 
					</table>
 | 
				
			||||||
							
								
								
									
										3
									
								
								app/views/default/units/index.turbo_stream.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								app/views/default/units/index.turbo_stream.erb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					<%= turbo_stream.update :units do %>
 | 
				
			||||||
 | 
					  <%= render(@units) || render_no_items  %>
 | 
				
			||||||
 | 
					<% end %>
 | 
				
			||||||
@ -29,7 +29,7 @@
 | 
				
			|||||||
      <%= image_link_to t(".issue_tracker"), "bug-outline", issue_tracker_url,
 | 
					      <%= image_link_to t(".issue_tracker"), "bug-outline", issue_tracker_url,
 | 
				
			||||||
            class: "extendedright" %>
 | 
					            class: "extendedright" %>
 | 
				
			||||||
      <% if user_signed_in? %>
 | 
					      <% if user_signed_in? %>
 | 
				
			||||||
        <%= image_link_to_unless_current(current_user.email, "account-wrench-outline",
 | 
					        <%= image_link_to_unless_current(current_user, "account-wrench-outline",
 | 
				
			||||||
          edit_user_registration_path) {} %>
 | 
					          edit_user_registration_path) {} %>
 | 
				
			||||||
        <% if current_user_disguised? %>
 | 
					        <% if current_user_disguised? %>
 | 
				
			||||||
          <%= image_link_to t(".revert"), "incognito-off", revert_users_path %>
 | 
					          <%= image_link_to t(".revert"), "incognito-off", revert_users_path %>
 | 
				
			||||||
 | 
				
			|||||||
@ -4,24 +4,26 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    <td class="<%= class_names({subunit: @unit.base}) %>">
 | 
					    <td class="<%= class_names({subunit: @unit.base}) %>">
 | 
				
			||||||
      <%= form.text_field :symbol, form: :unit_form, required: true, autofocus: true, size: 12,
 | 
					      <%= form.text_field :symbol, form: :unit_form, required: true, autofocus: true, size: 12,
 | 
				
			||||||
        maxlength: @unit.class.columns_hash['symbol'].limit, autocomplete: "off" %>
 | 
					        maxlength: @unit.class.type_for_attribute(:symbol).limit, autocomplete: "off" %>
 | 
				
			||||||
    </td>
 | 
					    </td>
 | 
				
			||||||
    <td>
 | 
					    <td>
 | 
				
			||||||
      <%= form.text_field :name, form: :unit_form, size: 30,
 | 
					      <%= form.text_area :description, form: :unit_form, cols: 30, rows: 1, escape: false,
 | 
				
			||||||
        maxlength: @unit.class.columns_hash['name'].limit, autocomplete: "off" %>
 | 
					        maxlength: @unit.class.type_for_attribute(:description).limit, autocomplete: "off" %>
 | 
				
			||||||
    </td>
 | 
					    </td>
 | 
				
			||||||
    <td>
 | 
					    <td>
 | 
				
			||||||
      <% unless @unit.base.nil? %>
 | 
					      <% unless @unit.base.nil? %>
 | 
				
			||||||
        <%= form.hidden_field :base_id, form: :unit_form %>
 | 
					        <%= form.hidden_field :base_id, form: :unit_form %>
 | 
				
			||||||
        <%= form.number_field :multiplier, form: :unit_form, required: true, step: "any",
 | 
					        <%= form.number_field :multiplier, form: :unit_form, required: true,
 | 
				
			||||||
          size: 10, autocomplete: "off" %>
 | 
					          size: 10, autocomplete: "off",
 | 
				
			||||||
 | 
					          **number_attributes(@unit.class.type_for_attribute(:multiplier)) %>
 | 
				
			||||||
      <% end %>
 | 
					      <% end %>
 | 
				
			||||||
    </td>
 | 
					    </td>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <td class="actions">
 | 
					    <td class="actions">
 | 
				
			||||||
      <%= form.submit form: :unit_form %>
 | 
					      <%= form.submit form: :unit_form %>
 | 
				
			||||||
      <%= image_link_to t(:cancel), "close-circle-outline", units_path, class: 'dangerous',
 | 
					      <%= image_link_to t(:cancel), "close-outline", units_path, class: 'dangerous',
 | 
				
			||||||
        name: :cancel, onclick: render_turbo_stream('form_close', {link_id: link_id}) %>
 | 
					        name: :cancel, onclick: render_turbo_stream('form_close', {link_id: link_id}) %>
 | 
				
			||||||
    </td>
 | 
					    </td>
 | 
				
			||||||
 | 
					    <td></td>
 | 
				
			||||||
  <% end %>
 | 
					  <% end %>
 | 
				
			||||||
<% end %>
 | 
					<% end %>
 | 
				
			||||||
 | 
				
			|||||||
@ -5,21 +5,21 @@
 | 
				
			|||||||
           data: {drag_path: rebase_unit_path(unit), drop_id: dom_id(unit.base || unit)} do %>
 | 
					           data: {drag_path: rebase_unit_path(unit), drop_id: dom_id(unit.base || unit)} do %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <td class="<%= class_names('link', {subunit: unit.base}) %>">
 | 
					  <td class="<%= class_names('link', {subunit: unit.base}) %>">
 | 
				
			||||||
    <%= link_to unit.symbol, edit_unit_path(unit), id: dom_id(unit, :edit),
 | 
					    <%= link_to unit, edit_unit_path(unit), id: dom_id(unit, :edit),
 | 
				
			||||||
      onclick: 'this.blur();', data: {turbo_stream: true} %>
 | 
					      onclick: 'this.blur();', data: {turbo_stream: true} %>
 | 
				
			||||||
  </td>
 | 
					  </td>
 | 
				
			||||||
  <td><%= unit.name %></td>
 | 
					  <td><%= unit.description %></td>
 | 
				
			||||||
  <td class="number"><%= scientifize(unit.multiplier) %></td>
 | 
					  <td class="number"><%= unit.multiplier.to_html %></td>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <% if current_user.at_least(:active) %>
 | 
					  <% if current_user.at_least(:active) %>
 | 
				
			||||||
    <td class="actions">
 | 
					    <td class="actions">
 | 
				
			||||||
      <% if unit.base.nil? %>
 | 
					      <% if unit.base.nil? %>
 | 
				
			||||||
        <%= image_link_to t(".add_subunit"), "plus-outline", new_unit_path(unit),
 | 
					        <%= image_link_to t('.add_subunit'), 'plus-outline', new_unit_path(unit),
 | 
				
			||||||
          id: dom_id(unit, :add), onclick: 'this.blur();',
 | 
					          id: dom_id(unit, :add), onclick: 'this.blur();',
 | 
				
			||||||
          data: {turbo_stream: true} %>
 | 
					          data: {turbo_stream: true} %>
 | 
				
			||||||
      <% end %>
 | 
					      <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <%= image_button_to t(".delete_unit"), "delete-outline", unit_path(unit),
 | 
					      <%= image_button_to t('.delete_unit'), 'delete-outline', unit_path(unit),
 | 
				
			||||||
        method: :delete %>
 | 
					        method: :delete %>
 | 
				
			||||||
    </td>
 | 
					    </td>
 | 
				
			||||||
    <% if unit.movable? %>
 | 
					    <% if unit.movable? %>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,2 +0,0 @@
 | 
				
			|||||||
<h1>Units::Defaults#index</h1>
 | 
					 | 
				
			||||||
<p>Find me in app/views/units/defaults/index.html.erb</p>
 | 
					 | 
				
			||||||
@ -2,9 +2,8 @@
 | 
				
			|||||||
  <% if current_user.at_least(:active) %>
 | 
					  <% if current_user.at_least(:active) %>
 | 
				
			||||||
    <%= image_link_to t('.add_unit'), 'plus-outline', new_unit_path, id: :add_unit,
 | 
					    <%= image_link_to t('.add_unit'), 'plus-outline', new_unit_path, id: :add_unit,
 | 
				
			||||||
      onclick: 'this.blur();', data: {turbo_stream: true} %>
 | 
					      onclick: 'this.blur();', data: {turbo_stream: true} %>
 | 
				
			||||||
    <%= image_link_to t('.import_units'), 'import', new_unit_path, class: 'tools',
 | 
					 | 
				
			||||||
      data: {turbo_stream: true} %>
 | 
					 | 
				
			||||||
  <% end %>
 | 
					  <% end %>
 | 
				
			||||||
 | 
					  <%= image_link_to t('.import_units'), 'download-outline', default_units_path, class: 'tools' %>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<%= tag.div id: :unit_form %>
 | 
					<%= tag.div id: :unit_form %>
 | 
				
			||||||
@ -13,7 +12,7 @@
 | 
				
			|||||||
  <thead>
 | 
					  <thead>
 | 
				
			||||||
    <tr>
 | 
					    <tr>
 | 
				
			||||||
      <th><%= User.human_attribute_name(:symbol).capitalize %></th>
 | 
					      <th><%= User.human_attribute_name(:symbol).capitalize %></th>
 | 
				
			||||||
      <th><%= User.human_attribute_name(:name).capitalize %></th>
 | 
					      <th><%= User.human_attribute_name(:description).capitalize %></th>
 | 
				
			||||||
      <th><%= User.human_attribute_name(:multiplier).capitalize %></th>
 | 
					      <th><%= User.human_attribute_name(:multiplier).capitalize %></th>
 | 
				
			||||||
      <% if current_user.at_least(:active) %>
 | 
					      <% if current_user.at_least(:active) %>
 | 
				
			||||||
        <th><%= t :actions %></th>
 | 
					        <th><%= t :actions %></th>
 | 
				
			||||||
 | 
				
			|||||||
@ -11,7 +11,7 @@
 | 
				
			|||||||
  <tbody>
 | 
					  <tbody>
 | 
				
			||||||
    <% @users.each do |user| %>
 | 
					    <% @users.each do |user| %>
 | 
				
			||||||
      <tr>
 | 
					      <tr>
 | 
				
			||||||
        <td class="link"><%= link_to user.email, user_path(user) %></td>
 | 
					        <td class="link"><%= link_to user, user_path(user) %></td>
 | 
				
			||||||
        <td>
 | 
					        <td>
 | 
				
			||||||
          <% if user == current_user %>
 | 
					          <% if user == current_user %>
 | 
				
			||||||
            <%= user.status %>
 | 
					            <%= user.status %>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,3 @@
 | 
				
			|||||||
<p>Hello <%= @resource.email %>!</p>
 | 
					<p>Hello <%= @resource %>!</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<p>We're contacting you to notify you that your password has been changed.</p>
 | 
					<p>We're contacting you to notify you that your password has been changed.</p>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
<p>Hello <%= @resource.email %>!</p>
 | 
					<p>Hello <%= @resource %>!</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<p>Someone has requested a link to change your password. You can do this through the link below.</p>
 | 
					<p>Someone has requested a link to change your password. You can do this through the link below.</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
<p>Hello <%= @resource.email %>!</p>
 | 
					<p>Hello <%= @resource %>!</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<p>Your account has been locked due to an excessive number of unsuccessful sign in attempts.</p>
 | 
					<p>Your account has been locked due to an excessive number of unsuccessful sign in attempts.</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
<% content_for :navigation, flush: true do %>
 | 
					<% content_for :navigation, flush: true do %>
 | 
				
			||||||
  <%= image_link_to t(:back), 'arrow-left-bold-outline',
 | 
					  <%= link_to svg_tag("pictograms/arrow-left-bold-outline") + t(:back),
 | 
				
			||||||
    request.referer.present? ? :back : root_path %>
 | 
					    request.referer.present? ? :back : root_path, class: 'tab' %>
 | 
				
			||||||
<% end %>
 | 
					<% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="rightside buttongrid">
 | 
					<div class="rightside buttongrid">
 | 
				
			||||||
 | 
				
			|||||||
@ -23,6 +23,10 @@ module FixinMe
 | 
				
			|||||||
    # Initialize configuration defaults for originally generated Rails version.
 | 
					    # Initialize configuration defaults for originally generated Rails version.
 | 
				
			||||||
    config.load_defaults 7.0
 | 
					    config.load_defaults 7.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Autoload lib/, required e.g. for core library extensions.
 | 
				
			||||||
 | 
					    # https://guides.rubyonrails.org/autoloading_and_reloading_constants.html#config-autoload-lib-ignore.
 | 
				
			||||||
 | 
					    config.autoload_lib(ignore: %w(assets tasks))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Configuration for the application, engines, and railties goes here.
 | 
					    # Configuration for the application, engines, and railties goes here.
 | 
				
			||||||
    #
 | 
					    #
 | 
				
			||||||
    # These settings can be overridden in specific environments using the files
 | 
					    # These settings can be overridden in specific environments using the files
 | 
				
			||||||
@ -32,7 +36,7 @@ module FixinMe
 | 
				
			|||||||
    # config.eager_load_paths << Rails.root.join("extras")
 | 
					    # config.eager_load_paths << Rails.root.join("extras")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    config.action_dispatch.rescue_responses['ApplicationController::AccessForbidden'] = :forbidden
 | 
					    config.action_dispatch.rescue_responses['ApplicationController::AccessForbidden'] = :forbidden
 | 
				
			||||||
    config.action_dispatch.rescue_responses['ArgumentError'] = :bad_request
 | 
					    config.action_dispatch.rescue_responses['ApplicationController::ParameterInvalid'] = :unprocessable_entity
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # SETUP: Below settings need to be updated on a per-installation basis.
 | 
					    # SETUP: Below settings need to be updated on a per-installation basis.
 | 
				
			||||||
    #
 | 
					    #
 | 
				
			||||||
@ -47,5 +51,8 @@ module FixinMe
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    # Email address of admin account
 | 
					    # Email address of admin account
 | 
				
			||||||
    config.admin = 'admin@localhost'
 | 
					    config.admin = 'admin@localhost'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Sender address of account registration-related messages
 | 
				
			||||||
 | 
					    Devise.mailer_sender = 'noreply@localhost'
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										9
									
								
								config/initializers/core_ext.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								config/initializers/core_ext.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					require 'core_ext/big_decimal_scientific_notation'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ActiveSupport.on_load :active_record do
 | 
				
			||||||
 | 
					  ActiveModel::Validations::NumericalityValidator.prepend CoreExt::ActiveModel::Validations::NumericalityValidatesPrecisionAndScale
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ActiveSupport.on_load :action_dispatch_system_test_case do
 | 
				
			||||||
 | 
					  prepend CoreExt::ActionDispatch::SystemTesting::TestHelpers::ScreenshotHelperUniqueId
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@ -24,7 +24,8 @@ Devise.setup do |config|
 | 
				
			|||||||
  # Configure the e-mail address which will be shown in Devise::Mailer,
 | 
					  # Configure the e-mail address which will be shown in Devise::Mailer,
 | 
				
			||||||
  # note that it will be overwritten if you use your own mailer class
 | 
					  # note that it will be overwritten if you use your own mailer class
 | 
				
			||||||
  # with default "from" parameter.
 | 
					  # with default "from" parameter.
 | 
				
			||||||
  config.mailer_sender = 'fixinme@noreply.me'
 | 
					  # This is set in 'config/application.rb'.
 | 
				
			||||||
 | 
					  #config.mailer_sender = 'fixinme@noreply.me'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # Configure the class responsible to send e-mails.
 | 
					  # Configure the class responsible to send e-mails.
 | 
				
			||||||
  # config.mailer = 'Devise::Mailer'
 | 
					  # config.mailer = 'Devise::Mailer'
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +0,0 @@
 | 
				
			|||||||
ActiveSupport.on_load :action_dispatch_system_test_case do
 | 
					 | 
				
			||||||
  prepend CoreExt::ActionDispatch::SystemTesting::TestHelpers::CustomScreenshotHelperUniqueId
 | 
					 | 
				
			||||||
end
 | 
					 | 
				
			||||||
@ -1,4 +1,8 @@
 | 
				
			|||||||
en:
 | 
					en:
 | 
				
			||||||
 | 
					  errors:
 | 
				
			||||||
 | 
					    messages:
 | 
				
			||||||
 | 
					      precision_exceeded: must not exceed %{value} significant digits
 | 
				
			||||||
 | 
					      scale_exceeded: must not exceed %{value} decimal digits
 | 
				
			||||||
  activerecord:
 | 
					  activerecord:
 | 
				
			||||||
    attributes:
 | 
					    attributes:
 | 
				
			||||||
      unit:
 | 
					      unit:
 | 
				
			||||||
@ -33,6 +37,9 @@ en:
 | 
				
			|||||||
        forbidden: >
 | 
					        forbidden: >
 | 
				
			||||||
          You have not been granted access to this action (403 Forbidden).
 | 
					          You have not been granted access to this action (403 Forbidden).
 | 
				
			||||||
          This should not happen, please notify site administrator.
 | 
					          This should not happen, please notify site administrator.
 | 
				
			||||||
 | 
					        not_found: >
 | 
				
			||||||
 | 
					          The record that you requested operation on does not exist (404 Not Found).
 | 
				
			||||||
 | 
					          This should not happen, please notify site administrator.
 | 
				
			||||||
        unprocessable_entity: >
 | 
					        unprocessable_entity: >
 | 
				
			||||||
          The request is semantically incorrect and was rejected (422 Unprocessable Entity).
 | 
					          The request is semantically incorrect and was rejected (422 Unprocessable Entity).
 | 
				
			||||||
          This should not happen, please notify site administrator.
 | 
					          This should not happen, please notify site administrator.
 | 
				
			||||||
@ -54,22 +61,39 @@ en:
 | 
				
			|||||||
      delete_unit: Delete
 | 
					      delete_unit: Delete
 | 
				
			||||||
    index:
 | 
					    index:
 | 
				
			||||||
      add_unit: Add unit
 | 
					      add_unit: Add unit
 | 
				
			||||||
      import_units: Import...
 | 
					      import_units: Import
 | 
				
			||||||
      no_items: There are no configured units. You can try to import some defaults.
 | 
					      no_items: There are no configured units. You can Add some or Import from defaults.
 | 
				
			||||||
      top_level_drop: Drop here to reposition into top-level unit
 | 
					      top_level_drop: Drop here to reposition into top-level unit
 | 
				
			||||||
    new:
 | 
					    new:
 | 
				
			||||||
      none: none
 | 
					      none: none
 | 
				
			||||||
    create:
 | 
					    create:
 | 
				
			||||||
      success: Created new unit
 | 
					      success: Created new unit "%{unit}"
 | 
				
			||||||
    update:
 | 
					    update:
 | 
				
			||||||
      success: Updated unit
 | 
					      success: Updated unit "%{unit}"
 | 
				
			||||||
    rebase:
 | 
					    rebase:
 | 
				
			||||||
      multiplier_reset: Multiplier of "%{symbol}" has been reset to 1, due to repositioning
 | 
					      multiplier_reset: Multiplier of "%{unit}" has been reset to 1, due to repositioning
 | 
				
			||||||
    destroy:
 | 
					    destroy:
 | 
				
			||||||
      success: Deleted unit
 | 
					      success: Deleted unit "%{unit}"
 | 
				
			||||||
 | 
					  default:
 | 
				
			||||||
 | 
					    units:
 | 
				
			||||||
 | 
					      unit:
 | 
				
			||||||
 | 
					        delete: Delete
 | 
				
			||||||
 | 
					        export: Export
 | 
				
			||||||
 | 
					        import: Import
 | 
				
			||||||
 | 
					      index:
 | 
				
			||||||
 | 
					        actions: Actions on defaults
 | 
				
			||||||
 | 
					        back: Back to units
 | 
				
			||||||
 | 
					        import_all: Import all
 | 
				
			||||||
 | 
					        no_items: There are no differences between default and user units.
 | 
				
			||||||
 | 
					      import:
 | 
				
			||||||
 | 
					        success: Imported unit "%{unit}"
 | 
				
			||||||
 | 
					      export:
 | 
				
			||||||
 | 
					        success: Exported unit "%{unit}"
 | 
				
			||||||
 | 
					      destroy:
 | 
				
			||||||
 | 
					        success: Deleted unit "%{unit}"
 | 
				
			||||||
  users:
 | 
					  users:
 | 
				
			||||||
    index:
 | 
					    index:
 | 
				
			||||||
      disguise: View as...
 | 
					      disguise: View as
 | 
				
			||||||
    passwords:
 | 
					    passwords:
 | 
				
			||||||
      edit:
 | 
					      edit:
 | 
				
			||||||
        new_password: New password
 | 
					        new_password: New password
 | 
				
			||||||
 | 
				
			|||||||
@ -3,22 +3,19 @@ Rails.application.routes.draw do
 | 
				
			|||||||
    controllers: {registrations: :registrations}
 | 
					    controllers: {registrations: :registrations}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  resources :units, except: [:show], path_names: {new: '(/:id)/new'} do
 | 
					  resources :units, except: [:show], path_names: {new: '(/:id)/new'} do
 | 
				
			||||||
    member do
 | 
					    member { post :rebase }
 | 
				
			||||||
      post :rebase
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  namespace :units do
 | 
					  namespace :default do
 | 
				
			||||||
    get 'defaults/index'
 | 
					    resources :units, only: [:index, :destroy] do
 | 
				
			||||||
 | 
					      member { post :import, :export }
 | 
				
			||||||
 | 
					      #collection { post :import_all }
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  resources :users, only: [:index, :show, :update] do
 | 
					  resources :users, only: [:index, :show, :update] do
 | 
				
			||||||
    member do
 | 
					    member { get :disguise }
 | 
				
			||||||
      get :disguise
 | 
					    collection { get :revert }
 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
    collection do
 | 
					 | 
				
			||||||
      get :revert
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  devise_scope :user do
 | 
					  devise_scope :user do
 | 
				
			||||||
 | 
				
			|||||||
@ -2,12 +2,12 @@ class CreateUnits < ActiveRecord::Migration[7.0]
 | 
				
			|||||||
  def change
 | 
					  def change
 | 
				
			||||||
    create_table :units do |t|
 | 
					    create_table :units do |t|
 | 
				
			||||||
      t.references :user, foreign_key: true
 | 
					      t.references :user, foreign_key: true
 | 
				
			||||||
      t.string :symbol
 | 
					      t.string :symbol, null: false, limit: 15
 | 
				
			||||||
      t.string :name
 | 
					      t.text :description
 | 
				
			||||||
      t.decimal :multiplier, precision: 30, scale: 15, default: 1.0
 | 
					      t.decimal :multiplier, null: false, precision: 30, scale: 15, default: 1.0
 | 
				
			||||||
      t.references :base
 | 
					      t.references :base
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      t.timestamps
 | 
					      t.timestamps null: false
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
    add_index :units, [:user_id, :symbol], unique: true
 | 
					    add_index :units, [:user_id, :symbol], unique: true
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
				
			|||||||
@ -10,12 +10,12 @@
 | 
				
			|||||||
#
 | 
					#
 | 
				
			||||||
# 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[7.1].define(version: 2023_06_02_185352) do
 | 
					ActiveRecord::Schema[7.2].define(version: 2023_06_02_185352) do
 | 
				
			||||||
  create_table "units", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
 | 
					  create_table "units", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
 | 
				
			||||||
    t.bigint "user_id"
 | 
					    t.bigint "user_id"
 | 
				
			||||||
    t.string "symbol"
 | 
					    t.string "symbol", limit: 15, null: false
 | 
				
			||||||
    t.string "name"
 | 
					    t.text "description"
 | 
				
			||||||
    t.decimal "multiplier", precision: 30, scale: 15, default: "1.0"
 | 
					    t.decimal "multiplier", precision: 30, scale: 15, default: "1.0", null: false
 | 
				
			||||||
    t.bigint "base_id"
 | 
					    t.bigint "base_id"
 | 
				
			||||||
    t.datetime "created_at", null: false
 | 
					    t.datetime "created_at", null: false
 | 
				
			||||||
    t.datetime "updated_at", null: false
 | 
					    t.datetime "updated_at", null: false
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										17
									
								
								db/seeds.rb
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								db/seeds.rb
									
									
									
									
									
								
							@ -20,19 +20,4 @@ end
 | 
				
			|||||||
# Formulas will be deleted as dependent on Quantities
 | 
					# Formulas will be deleted as dependent on Quantities
 | 
				
			||||||
#[Source, Quantity, Unit].each { |model| model.defaults.delete_all }
 | 
					#[Source, Quantity, Unit].each { |model| model.defaults.delete_all }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Unit.transaction do
 | 
					require 'seeds/units.rb'
 | 
				
			||||||
  Unit.defaults.delete_all
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  unit_1 = Unit.create symbol: "1",   name: "dimensionless, one"
 | 
					 | 
				
			||||||
           Unit.create symbol: "%",   base: unit_1, multiplier: 1e-2, name: "percent"
 | 
					 | 
				
			||||||
           Unit.create symbol: "‰",   base: unit_1, multiplier: 1e-3, name: "promille"
 | 
					 | 
				
			||||||
           Unit.create symbol: "‱",   base: unit_1, multiplier: 1e-4, name: "basis point"
 | 
					 | 
				
			||||||
           Unit.create symbol: "ppm", base: unit_1, multiplier: 1e-6, name: "parts per million"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  unit_g = Unit.create symbol: "g",  name: "gram"
 | 
					 | 
				
			||||||
           Unit.create symbol: "ug", base: unit_g, multiplier: 1e-6, name: "microgram"
 | 
					 | 
				
			||||||
           Unit.create symbol: "mg", base: unit_g, multiplier: 1e-3, name: "milligram"
 | 
					 | 
				
			||||||
           Unit.create symbol: "kg", base: unit_g, multiplier: 1e3,  name: "kilogram"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Unit.create symbol: "kcal", name: "kilocalorie"
 | 
					 | 
				
			||||||
end
 | 
					 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										9
									
								
								db/seeds/templates/units.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								db/seeds/templates/units.erb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					Unit.transaction do
 | 
				
			||||||
 | 
					  Unit.defaults.delete_all
 | 
				
			||||||
 | 
					<% Unit.defaults.ordered.each do |unit| %>
 | 
				
			||||||
 | 
					<%= "\n" if unit.base.nil? %>
 | 
				
			||||||
 | 
					  unit_<%= unit.symbol %> =
 | 
				
			||||||
 | 
					    Unit.create symbol: "<%= unit.symbol %>",<% unless unit.base.nil? %> base: unit_<%= unit.base.symbol %>, multiplier: <%= unit.multiplier.to_scientific %>,<% end %>
 | 
				
			||||||
 | 
					                description: "<%= unit.description %>"
 | 
				
			||||||
 | 
					<% end %>
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										36
									
								
								db/seeds/units.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								db/seeds/units.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					Unit.transaction do
 | 
				
			||||||
 | 
					  Unit.defaults.delete_all
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  unit_1 =
 | 
				
			||||||
 | 
					    Unit.create symbol: "1",
 | 
				
			||||||
 | 
					                description: "dimensionless, one"
 | 
				
			||||||
 | 
					  unit_ppm =
 | 
				
			||||||
 | 
					    Unit.create symbol: "ppm", base: unit_1, multiplier: 1e-6,
 | 
				
			||||||
 | 
					                description: "parts per million"
 | 
				
			||||||
 | 
					  unit_‱ =
 | 
				
			||||||
 | 
					    Unit.create symbol: "‱", base: unit_1, multiplier: 1e-4,
 | 
				
			||||||
 | 
					                description: "basis point"
 | 
				
			||||||
 | 
					  unit_‰ =
 | 
				
			||||||
 | 
					    Unit.create symbol: "‰", base: unit_1, multiplier: 1e-3,
 | 
				
			||||||
 | 
					                description: "promille"
 | 
				
			||||||
 | 
					  unit_% =
 | 
				
			||||||
 | 
					    Unit.create symbol: "%", base: unit_1, multiplier: 1e-2,
 | 
				
			||||||
 | 
					                description: "percent"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  unit_g =
 | 
				
			||||||
 | 
					    Unit.create symbol: "g",
 | 
				
			||||||
 | 
					                description: "gram"
 | 
				
			||||||
 | 
					  unit_ug =
 | 
				
			||||||
 | 
					    Unit.create symbol: "ug", base: unit_g, multiplier: 1e-6,
 | 
				
			||||||
 | 
					                description: "microgram"
 | 
				
			||||||
 | 
					  unit_mg =
 | 
				
			||||||
 | 
					    Unit.create symbol: "mg", base: unit_g, multiplier: 1e-3,
 | 
				
			||||||
 | 
					                description: "milligram"
 | 
				
			||||||
 | 
					  unit_kg =
 | 
				
			||||||
 | 
					    Unit.create symbol: "kg", base: unit_g, multiplier: 1e3,
 | 
				
			||||||
 | 
					                description: "kilogram"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  unit_kcal =
 | 
				
			||||||
 | 
					    Unit.create symbol: "kcal",
 | 
				
			||||||
 | 
					                description: "kilocalorie"
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
module CoreExt::ActionDispatch::SystemTesting::TestHelpers::CustomScreenshotHelperUniqueId
 | 
					module CoreExt::ActionDispatch::SystemTesting::TestHelpers::ScreenshotHelperUniqueId
 | 
				
			||||||
  private
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def unique
 | 
					  def unique
 | 
				
			||||||
@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					module CoreExt::ActiveModel::Validations::NumericalityValidatesPrecisionAndScale
 | 
				
			||||||
 | 
					  def validate_each(record, attr_name, value, ...)
 | 
				
			||||||
 | 
					    super(record, attr_name, value, ...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if options[:precision] || options[:scale]
 | 
				
			||||||
 | 
					      attr_type = record.class.type_for_attribute(attr_name)
 | 
				
			||||||
 | 
					      value = BigDecimal(value) unless value.is_a? BigDecimal
 | 
				
			||||||
 | 
					      if options[:precision] && (value.precision > attr_type.precision)
 | 
				
			||||||
 | 
					        record.errors.add(attr_name, :precision_exceeded, **filtered_options(attr_type.precision))
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					      if options[:scale] && (value.scale > attr_type.scale)
 | 
				
			||||||
 | 
					        record.errors.add(attr_name, :scale_exceeded, **filtered_options(attr_type.scale))
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										36
									
								
								lib/core_ext/big_decimal_scientific_notation.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								lib/core_ext/big_decimal_scientific_notation.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					module CoreExt
 | 
				
			||||||
 | 
					  module BigDecimalScientificNotation
 | 
				
			||||||
 | 
					    def to_scientific
 | 
				
			||||||
 | 
					      return 'NaN' unless finite?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      sign, coefficient, base, exponent = split
 | 
				
			||||||
 | 
					      (sign == -1 ? '-' : '') +
 | 
				
			||||||
 | 
					        (coefficient.length > 1 ? coefficient.insert(1, '.') : coefficient) +
 | 
				
			||||||
 | 
					        (exponent != 1 ? "e#{exponent-1}" : '')
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Converts value to HTML formatted scientific notation
 | 
				
			||||||
 | 
					    def to_html
 | 
				
			||||||
 | 
					      sign, coefficient, base, exponent = split
 | 
				
			||||||
 | 
					      return 'NaN' unless sign
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      result = (sign == -1 ? '-' : '')
 | 
				
			||||||
 | 
					      unless coefficient == '1' && sign == 1
 | 
				
			||||||
 | 
					        if coefficient.length > 1
 | 
				
			||||||
 | 
					          result += coefficient.insert(1, '.')
 | 
				
			||||||
 | 
					        elsif
 | 
				
			||||||
 | 
					          result += coefficient
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					        if exponent != 1
 | 
				
			||||||
 | 
					          result += "×"
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					      if exponent != 1
 | 
				
			||||||
 | 
					        result += "10<sup>% d</sup>" % [exponent-1]
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					      result.html_safe
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					BigDecimal.prepend CoreExt::BigDecimalScientificNotation
 | 
				
			||||||
							
								
								
									
										12
									
								
								lib/tasks/db.rake
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								lib/tasks/db.rake
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					namespace :db do
 | 
				
			||||||
 | 
					  namespace :seed do
 | 
				
			||||||
 | 
					    desc "Dump default settings as seed data to db/seeds/*.rb"
 | 
				
			||||||
 | 
					    task export: :environment do
 | 
				
			||||||
 | 
					      seeds_path = Pathname.new(Rails.application.paths["db"].first) / 'seeds'
 | 
				
			||||||
 | 
					      (seeds_path / 'templates').glob('*.erb').each do |template_path|
 | 
				
			||||||
 | 
					        template = ERB.new(template_path.read, trim_mode: '<>')
 | 
				
			||||||
 | 
					        (seeds_path / "#{template_path.basename('.*').to_s}.rb").write(template.result)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@ -1,10 +1,13 @@
 | 
				
			|||||||
require "test_helper"
 | 
					require "test_helper"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
 | 
					class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
 | 
				
			||||||
 | 
					  extend ActionView::Helpers::TranslationHelper
 | 
				
			||||||
  include ActionView::Helpers::UrlHelper
 | 
					  include ActionView::Helpers::UrlHelper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # NOTE: geckodriver installed with Firefox, ignore incompatibility warning
 | 
					  # NOTE: geckodriver installed with Firefox, ignore incompatibility warning
 | 
				
			||||||
  Selenium::WebDriver.logger.ignore(:selenium_manager)
 | 
					  Selenium::WebDriver.logger
 | 
				
			||||||
 | 
					    .ignore(:selenium_manager, :clear_session_storage, :clear_local_storage)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Capybara.configure do |config|
 | 
					  Capybara.configure do |config|
 | 
				
			||||||
    config.save_path = "#{Rails.root}/tmp/screenshots/"
 | 
					    config.save_path = "#{Rails.root}/tmp/screenshots/"
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
@ -30,4 +33,18 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
 | 
				
			|||||||
  #def assert_stale(element)
 | 
					  #def assert_stale(element)
 | 
				
			||||||
  #  assert_raises(Selenium::WebDriver::Error::StaleElementReferenceError) { element.tag_name }
 | 
					  #  assert_raises(Selenium::WebDriver::Error::StaleElementReferenceError) { element.tag_name }
 | 
				
			||||||
  #end
 | 
					  #end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # HTML does not allow [disabled] attribute on <a> tag, so it's not possible to
 | 
				
			||||||
 | 
					  # easily find them using e.g. :link selector
 | 
				
			||||||
 | 
					  #Capybara.add_selector(:disabled_link) do
 | 
				
			||||||
 | 
					  #  label "<a> tag with [disabled] attribute"
 | 
				
			||||||
 | 
					  #end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  test "click disabled link" do
 | 
				
			||||||
 | 
					    # Link should be unclickable
 | 
				
			||||||
 | 
					    # assert_raises(Selenium::WebDriver::Error::ElementClickInterceptedError) do
 | 
				
			||||||
 | 
					    #   # Use custom selector for disabled links
 | 
				
			||||||
 | 
					    #   find('a[disabled]').click
 | 
				
			||||||
 | 
					    # end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
require "test_helper"
 | 
					require "test_helper"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Units::DefaultsControllerTest < ActionDispatch::IntegrationTest
 | 
					class Default::UnitsControllerTest < ActionDispatch::IntegrationTest
 | 
				
			||||||
  test "should get index" do
 | 
					  test "should get index" do
 | 
				
			||||||
    get units_defaults_index_url
 | 
					    get units_defaults_index_url
 | 
				
			||||||
    assert_response :success
 | 
					    assert_response :success
 | 
				
			||||||
							
								
								
									
										16
									
								
								test/fixtures/units.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										16
									
								
								test/fixtures/units.yml
									
									
									
									
										vendored
									
									
								
							@ -1,40 +1,40 @@
 | 
				
			|||||||
g:
 | 
					g:
 | 
				
			||||||
  user: admin
 | 
					  user: admin
 | 
				
			||||||
  symbol: g
 | 
					  symbol: g
 | 
				
			||||||
  name: gram
 | 
					  description: gram
 | 
				
			||||||
kg:
 | 
					kg:
 | 
				
			||||||
  user: admin
 | 
					  user: admin
 | 
				
			||||||
  symbol: kg
 | 
					  symbol: kg
 | 
				
			||||||
  name: kilogram
 | 
					  description: kilogram
 | 
				
			||||||
  multiplier: 1000
 | 
					  multiplier: 1000
 | 
				
			||||||
  base: g
 | 
					  base: g
 | 
				
			||||||
1:
 | 
					1:
 | 
				
			||||||
  user: admin
 | 
					  user: admin
 | 
				
			||||||
  symbol: 1
 | 
					  symbol: 1
 | 
				
			||||||
  name: one
 | 
					  description: one
 | 
				
			||||||
s:
 | 
					s:
 | 
				
			||||||
  user: admin
 | 
					  user: admin
 | 
				
			||||||
  symbol: s
 | 
					  symbol: s
 | 
				
			||||||
  name: second
 | 
					  description: second
 | 
				
			||||||
percent:
 | 
					percent:
 | 
				
			||||||
  user: admin
 | 
					  user: admin
 | 
				
			||||||
  symbol: '%'
 | 
					  symbol: '%'
 | 
				
			||||||
  name: percent
 | 
					  description: percent
 | 
				
			||||||
  multiplier: 0.01
 | 
					  multiplier: 0.01
 | 
				
			||||||
  base: 1
 | 
					  base: 1
 | 
				
			||||||
µg:
 | 
					µg:
 | 
				
			||||||
  user: admin
 | 
					  user: admin
 | 
				
			||||||
  symbol: µg
 | 
					  symbol: µg
 | 
				
			||||||
  name: microgram
 | 
					  description: microgram
 | 
				
			||||||
  multiplier: 0.000001
 | 
					  multiplier: 0.000001
 | 
				
			||||||
  base: g
 | 
					  base: g
 | 
				
			||||||
mg:
 | 
					mg:
 | 
				
			||||||
  user: admin
 | 
					  user: admin
 | 
				
			||||||
  symbol: mg
 | 
					  symbol: mg
 | 
				
			||||||
  name: milligram
 | 
					  description: milligram
 | 
				
			||||||
  multiplier: 0.001
 | 
					  multiplier: 0.001
 | 
				
			||||||
  base: g
 | 
					  base: g
 | 
				
			||||||
g_alice:
 | 
					g_alice:
 | 
				
			||||||
  user: alice
 | 
					  user: alice
 | 
				
			||||||
  symbol: g
 | 
					  symbol: g
 | 
				
			||||||
  name: gram
 | 
					  description: gram
 | 
				
			||||||
 | 
				
			|||||||
@ -1,8 +1,16 @@
 | 
				
			|||||||
require "application_system_test_case"
 | 
					require "application_system_test_case"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UnitsTest < ApplicationSystemTestCase
 | 
					class UnitsTest < ApplicationSystemTestCase
 | 
				
			||||||
 | 
					  LINK_LABELS = {
 | 
				
			||||||
 | 
					    add_unit: t('units.index.add_unit'),
 | 
				
			||||||
 | 
					    add_subunit: t('units.unit.add_subunit'),
 | 
				
			||||||
 | 
					    edit: nil
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  setup do
 | 
					  setup do
 | 
				
			||||||
    @user = sign_in
 | 
					    @user = sign_in
 | 
				
			||||||
 | 
					    LINK_LABELS[:edit] = Regexp.union(@user.units.map(&:symbol))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    visit units_path
 | 
					    visit units_path
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -12,7 +20,8 @@ class UnitsTest < ApplicationSystemTestCase
 | 
				
			|||||||
      assert_selector 'tr', count: @user.units.count
 | 
					      assert_selector 'tr', count: @user.units.count
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Unit.destroy_all
 | 
					    # Cannot #destroy_all due to {dependent: :restrict*} on Unit.subunits association
 | 
				
			||||||
 | 
					    @user.units.delete_all
 | 
				
			||||||
    visit units_path
 | 
					    visit units_path
 | 
				
			||||||
    within 'tbody' do
 | 
					    within 'tbody' do
 | 
				
			||||||
      assert_selector 'tr', count: 1
 | 
					      assert_selector 'tr', count: 1
 | 
				
			||||||
@ -20,16 +29,24 @@ class UnitsTest < ApplicationSystemTestCase
 | 
				
			|||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  test "add unit" do
 | 
					  test "new" do
 | 
				
			||||||
    click_on t('units.index.add_unit')
 | 
					    type, label = LINK_LABELS.assoc([:add_unit, :add_subunit].sample)
 | 
				
			||||||
 | 
					    add_link = all(:link, exact_text: label).sample
 | 
				
			||||||
 | 
					    add_link.click
 | 
				
			||||||
 | 
					    assert_equal 'disabled', add_link[:disabled]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    within 'tbody > tr:has(input[type=text], textarea)' do
 | 
					    within 'tbody > tr:has(input[type=text], textarea)' do
 | 
				
			||||||
      assert_selector ':focus'
 | 
					      assert_selector ':focus'
 | 
				
			||||||
      maxlength = all(:fillable_field).to_h { |f| [f[:name], f[:maxlength].to_i || 1000] }
 | 
					
 | 
				
			||||||
 | 
					      maxlength = all(:fillable_field).to_h { |f| [f[:name], f[:maxlength].to_i || 2**16] }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      fill_in 'unit[symbol]',
 | 
					      fill_in 'unit[symbol]',
 | 
				
			||||||
        with: SecureRandom.random_symbol(rand([1..15, 15..maxlength['unit[symbol]']].sample))
 | 
					        with: random_string(rand([1..3, 4..maxlength['unit[symbol]']].sample))
 | 
				
			||||||
      fill_in 'unit[name]',
 | 
					      fill_in 'unit[description]', with: random_string(rand(0..maxlength['unit[description]']))
 | 
				
			||||||
        with: [nil, SecureRandom.alphanumeric(rand(1..maxlength['unit[name]']))].sample
 | 
					      within :field, 'unit[multiplier]' do |field|
 | 
				
			||||||
 | 
					        fill_in with: random_number(field[:max], field[:step])
 | 
				
			||||||
 | 
					      end if add_link[:text] != t('units.index.add_unit')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      assert_difference ->{ Unit.count }, 1 do
 | 
					      assert_difference ->{ Unit.count }, 1 do
 | 
				
			||||||
        click_on t('helpers.submit.create')
 | 
					        click_on t('helpers.submit.create')
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
@ -39,39 +56,48 @@ class UnitsTest < ApplicationSystemTestCase
 | 
				
			|||||||
      assert_no_selector :fillable_field
 | 
					      assert_no_selector :fillable_field
 | 
				
			||||||
      assert_selector 'tr', count: @user.units.count
 | 
					      assert_selector 'tr', count: @user.units.count
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
    assert_selector '.flash.notice', text: /^#{t('units.create.success')}/
 | 
					    assert_no_selector :element, :a, 'disabled': 'disabled',
 | 
				
			||||||
 | 
					      exact_text: Regexp.union(LINK_LABELS.fetch_values(:add_unit, :add_subunit))
 | 
				
			||||||
 | 
					    assert_selector '.flash.notice', text: t('units.create.success', unit: Unit.all.last.symbol)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  test "add and edit disallow opening multiple forms" do
 | 
					  # TODO: check proper form/button redisplay and flash messages on add/edit
 | 
				
			||||||
    # Once new/edit form is open, other actions on the same page either replace
 | 
					  test "new and edit form on validation error" do
 | 
				
			||||||
    # the form or leave it untouched
 | 
					  end
 | 
				
			||||||
    # TODO: add non-empty form closing warning
 | 
					
 | 
				
			||||||
 | 
					  # TODO: add non-empty form closing warning
 | 
				
			||||||
 | 
					  test "new and edit disallow opening multiple forms" do
 | 
				
			||||||
 | 
					    # Once new/edit form is open, attempt to open another one will close it
 | 
				
			||||||
    links = {}
 | 
					    links = {}
 | 
				
			||||||
    link_labels = {1 => [t('units.index.add_unit'), t('units.unit.add_subunit')],
 | 
					    targets = {}
 | 
				
			||||||
                   0 => units.map(&:symbol)}
 | 
					    LINK_LABELS.each_pair do |type, labels|
 | 
				
			||||||
    link_labels.each_pair do |row_change, labels|
 | 
					      links[type] = all(:link_or_button, exact_text: labels).to_a
 | 
				
			||||||
      all(:link_or_button, exact_text: Regexp.union(labels)).map { |l| links[l] = row_change }
 | 
					      targets[type] = links[type].sample
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
    link, rows = links.assoc(links.keys.sample).tap { |l, _| links.delete(l) }
 | 
					    # Define tr count change depending on link clicked
 | 
				
			||||||
 | 
					    row_change = {add_unit: 1, add_subunit: 1, edit: 0}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    type, link = targets.assoc(targets.keys.sample).tap { |t, _| targets.delete(t) }
 | 
				
			||||||
 | 
					    rows = row_change[type]
 | 
				
			||||||
    assert_difference ->{ all('tbody tr').count }, rows do
 | 
					    assert_difference ->{ all('tbody tr').count }, rows do
 | 
				
			||||||
      link.click
 | 
					      link.click
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
    find 'tbody tr:has(input[type=text]:focus)'
 | 
					    within('tbody tr:has(input[type=text])') { assert_selector ':focus' }
 | 
				
			||||||
 | 
					    if type == :edit
 | 
				
			||||||
    # Link should be now unavailable or unclickable
 | 
					      assert !link.visible?
 | 
				
			||||||
    begin
 | 
					      [:add_subunit, :edit].each do |t|
 | 
				
			||||||
      assert_raises(Selenium::WebDriver::Error::ElementClickInterceptedError) do
 | 
					        assert_difference(->{ links[t].length }, -1) { links[t].select!(&:visible?) }
 | 
				
			||||||
        link.click
 | 
					      end
 | 
				
			||||||
      end if link.visible?
 | 
					    else
 | 
				
			||||||
    rescue Selenium::WebDriver::Error::StaleElementReferenceError
 | 
					      assert link[:disabled]
 | 
				
			||||||
      link = nil
 | 
					 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    link = links.keys.select(&:visible?).sample
 | 
					    targets.merge([:add_subunit, :edit].map { |t| [t, links[t].sample] }.to_h)
 | 
				
			||||||
    assert_difference ->{ all('tbody tr').count }, links[link] - rows do
 | 
					    type, link = targets.assoc(targets.keys.sample)
 | 
				
			||||||
 | 
					    assert_difference ->{ all('tbody tr').count }, row_change[type] - rows do
 | 
				
			||||||
      link.click
 | 
					      link.click
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
    assert_selector 'tbody tr:has(input[type=text]:focus)', count: 1
 | 
					    within('tbody tr:has(input[type=text])') { assert_selector ':focus' }
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # NOTE: extend with any add/edit link
 | 
					  # NOTE: extend with any add/edit link
 | 
				
			||||||
 | 
				
			|||||||
@ -9,15 +9,41 @@ class ActiveSupport::TestCase
 | 
				
			|||||||
  # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
 | 
					  # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
 | 
				
			||||||
  fixtures :all
 | 
					  fixtures :all
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  include AbstractController::Translation
 | 
					 | 
				
			||||||
  include ActionMailer::TestHelper
 | 
					  include ActionMailer::TestHelper
 | 
				
			||||||
 | 
					  include ActionView::Helpers::TranslationHelper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # NOTE: use public #alphanumeric(chars: ...) from Ruby 3.3 onwards
 | 
					  # List of categorized Unicode characters:
 | 
				
			||||||
  SecureRandom.class_eval do
 | 
					  # * http://www.unicode.org/Public/UNIDATA/UnicodeData.txt
 | 
				
			||||||
    def self.random_symbol(n = 10)
 | 
					  # File format: http://www.unicode.org/L2/L1999/UnicodeData.html
 | 
				
			||||||
      # Unicode characters: 32-126, 160-383
 | 
					  # Select from graphic ranges: L, M, N, P, S, Zs
 | 
				
			||||||
      choose([*' '..'~', 160.chr(Encoding::UTF_8), *'¡'..'ſ'], n)
 | 
					  UNICODE_CHARS = {
 | 
				
			||||||
 | 
					    1 => [*"\u0020".."\u007E"],
 | 
				
			||||||
 | 
					    2 => [*"\u00A0".."\u00AC",
 | 
				
			||||||
 | 
					          *"\u00AE".."\u05FF",
 | 
				
			||||||
 | 
					          *"\u0606".."\u061B",
 | 
				
			||||||
 | 
					          *"\u061D".."\u06DC",
 | 
				
			||||||
 | 
					          *"\u06DE".."\u070E",
 | 
				
			||||||
 | 
					          *"\u0710".."\u07FF"]
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  UNICODE_CHARS.default = UNICODE_CHARS[1] + UNICODE_CHARS[2]
 | 
				
			||||||
 | 
					  def random_string(bytes = 10)
 | 
				
			||||||
 | 
					    result = ''
 | 
				
			||||||
 | 
					    while bytes > 0
 | 
				
			||||||
 | 
					      result += UNICODE_CHARS[bytes].sample.tap { |c| bytes -= c.bytesize }
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					    result
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Assumes: max >= step and step = 1e[-]N, both as strings
 | 
				
			||||||
 | 
					  def random_number(max, step)
 | 
				
			||||||
 | 
					    max.delete!('.')
 | 
				
			||||||
 | 
					    precision = max.length
 | 
				
			||||||
 | 
					    start = rand(precision) + 1
 | 
				
			||||||
 | 
					    d = (rand(max.to_i) + 1) % 10**start
 | 
				
			||||||
 | 
					    length = rand([0, 1..4, 4..precision].sample)
 | 
				
			||||||
 | 
					    d = d.truncate(-start + length)
 | 
				
			||||||
 | 
					    d = 10**(start - length) if d.zero?
 | 
				
			||||||
 | 
					    BigDecimal(step) * d
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def randomize_user_password!(user)
 | 
					  def randomize_user_password!(user)
 | 
				
			||||||
@ -25,11 +51,11 @@ class ActiveSupport::TestCase
 | 
				
			|||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def random_password
 | 
					  def random_password
 | 
				
			||||||
    SecureRandom.alphanumeric rand(Rails.configuration.devise.password_length)
 | 
					    Random.alphanumeric rand(Rails.configuration.devise.password_length)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def random_email
 | 
					  def random_email
 | 
				
			||||||
    "%s@%s.%s" % (1..3).map { SecureRandom.alphanumeric(rand(1..20)) }
 | 
					    "%s@%s.%s" % (1..3).map { Random.alphanumeric(rand(1..20)) }
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def with_last_email
 | 
					  def with_last_email
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user