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"
|
||||
ruby file: ".ruby-version"
|
||||
|
||||
gem "rails", "~> 7.1.2"
|
||||
gem "rails", "~> 7.2.2"
|
||||
gem "sprockets-rails"
|
||||
gem "mysql2", "~> 0.5"
|
||||
gem "puma", "~> 6.0"
|
||||
|
245
Gemfile.lock
245
Gemfile.lock
@ -1,124 +1,122 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (7.1.3)
|
||||
actionpack (= 7.1.3)
|
||||
activesupport (= 7.1.3)
|
||||
actioncable (7.2.2)
|
||||
actionpack (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
zeitwerk (~> 2.6)
|
||||
actionmailbox (7.1.3)
|
||||
actionpack (= 7.1.3)
|
||||
activejob (= 7.1.3)
|
||||
activerecord (= 7.1.3)
|
||||
activestorage (= 7.1.3)
|
||||
activesupport (= 7.1.3)
|
||||
mail (>= 2.7.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
actionmailer (7.1.3)
|
||||
actionpack (= 7.1.3)
|
||||
actionview (= 7.1.3)
|
||||
activejob (= 7.1.3)
|
||||
activesupport (= 7.1.3)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
actionmailbox (7.2.2)
|
||||
actionpack (= 7.2.2)
|
||||
activejob (= 7.2.2)
|
||||
activerecord (= 7.2.2)
|
||||
activestorage (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
mail (>= 2.8.0)
|
||||
actionmailer (7.2.2)
|
||||
actionpack (= 7.2.2)
|
||||
actionview (= 7.2.2)
|
||||
activejob (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
mail (>= 2.8.0)
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (7.1.3)
|
||||
actionview (= 7.1.3)
|
||||
activesupport (= 7.1.3)
|
||||
actionpack (7.2.2)
|
||||
actionview (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
nokogiri (>= 1.8.5)
|
||||
racc
|
||||
rack (>= 2.2.4)
|
||||
rack (>= 2.2.4, < 3.2)
|
||||
rack-session (>= 1.0.1)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
actiontext (7.1.3)
|
||||
actionpack (= 7.1.3)
|
||||
activerecord (= 7.1.3)
|
||||
activestorage (= 7.1.3)
|
||||
activesupport (= 7.1.3)
|
||||
useragent (~> 0.16)
|
||||
actiontext (7.2.2)
|
||||
actionpack (= 7.2.2)
|
||||
activerecord (= 7.2.2)
|
||||
activestorage (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (7.1.3)
|
||||
activesupport (= 7.1.3)
|
||||
actionview (7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
activejob (7.1.3)
|
||||
activesupport (= 7.1.3)
|
||||
activejob (7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.1.3)
|
||||
activesupport (= 7.1.3)
|
||||
activerecord (7.1.3)
|
||||
activemodel (= 7.1.3)
|
||||
activesupport (= 7.1.3)
|
||||
activemodel (7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
activerecord (7.2.2)
|
||||
activemodel (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
timeout (>= 0.4.0)
|
||||
activestorage (7.1.3)
|
||||
actionpack (= 7.1.3)
|
||||
activejob (= 7.1.3)
|
||||
activerecord (= 7.1.3)
|
||||
activesupport (= 7.1.3)
|
||||
activestorage (7.2.2)
|
||||
actionpack (= 7.2.2)
|
||||
activejob (= 7.2.2)
|
||||
activerecord (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
marcel (~> 1.0)
|
||||
activesupport (7.1.3)
|
||||
activesupport (7.2.2)
|
||||
base64
|
||||
benchmark (>= 0.3)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||
connection_pool (>= 2.2.5)
|
||||
drb
|
||||
i18n (>= 1.6, < 2)
|
||||
logger (>= 1.4.2)
|
||||
minitest (>= 5.1)
|
||||
mutex_m
|
||||
tzinfo (~> 2.0)
|
||||
addressable (2.8.6)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
securerandom (>= 0.3)
|
||||
tzinfo (~> 2.0, >= 2.0.5)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
base64 (0.2.0)
|
||||
bcrypt (3.1.20)
|
||||
bigdecimal (3.1.6)
|
||||
benchmark (0.4.0)
|
||||
bigdecimal (3.1.8)
|
||||
bindex (0.8.1)
|
||||
builder (3.2.4)
|
||||
builder (3.3.0)
|
||||
byebug (11.1.3)
|
||||
capybara (3.39.2)
|
||||
capybara (3.40.0)
|
||||
addressable
|
||||
matrix
|
||||
mini_mime (>= 0.1.3)
|
||||
nokogiri (~> 1.8)
|
||||
nokogiri (~> 1.11)
|
||||
rack (>= 1.6.0)
|
||||
rack-test (>= 0.6.3)
|
||||
regexp_parser (>= 1.5, < 3.0)
|
||||
xpath (~> 3.2)
|
||||
concurrent-ruby (1.2.3)
|
||||
concurrent-ruby (1.3.4)
|
||||
connection_pool (2.4.1)
|
||||
crass (1.0.6)
|
||||
date (3.3.4)
|
||||
devise (4.9.3)
|
||||
date (3.4.1)
|
||||
devise (4.9.4)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0)
|
||||
responders
|
||||
warden (~> 1.2.3)
|
||||
drb (2.2.0)
|
||||
ruby2_keywords
|
||||
erubi (1.12.0)
|
||||
ffi (1.16.3)
|
||||
drb (2.2.1)
|
||||
erubi (1.13.0)
|
||||
ffi (1.17.0-x86_64-linux-gnu)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
i18n (1.14.1)
|
||||
i18n (1.14.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
importmap-rails (2.0.1)
|
||||
importmap-rails (2.0.3)
|
||||
actionpack (>= 6.0.0)
|
||||
activesupport (>= 6.0.0)
|
||||
railties (>= 6.0.0)
|
||||
io-console (0.7.2)
|
||||
irb (1.11.1)
|
||||
rdoc
|
||||
io-console (0.8.0)
|
||||
irb (1.14.1)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
loofah (2.22.0)
|
||||
logger (1.6.2)
|
||||
loofah (2.23.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
mail (2.8.1)
|
||||
@ -126,79 +124,77 @@ GEM
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
marcel (1.0.2)
|
||||
marcel (1.0.4)
|
||||
matrix (0.4.2)
|
||||
mini_mime (1.1.5)
|
||||
minitest (5.21.2)
|
||||
mutex_m (0.2.0)
|
||||
mysql2 (0.5.5)
|
||||
net-imap (0.4.9.1)
|
||||
minitest (5.25.4)
|
||||
mysql2 (0.5.6)
|
||||
net-imap (0.5.1)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
net-protocol
|
||||
net-protocol (0.2.2)
|
||||
timeout
|
||||
net-smtp (0.4.0.1)
|
||||
net-smtp (0.5.0)
|
||||
net-protocol
|
||||
nio4r (2.7.0)
|
||||
nokogiri (1.16.0-x86_64-linux)
|
||||
nio4r (2.7.4)
|
||||
nokogiri (1.16.8-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
orm_adapter (0.5.0)
|
||||
psych (5.1.2)
|
||||
psych (5.2.1)
|
||||
date
|
||||
stringio
|
||||
public_suffix (5.0.4)
|
||||
puma (6.4.2)
|
||||
public_suffix (6.0.1)
|
||||
puma (6.5.0)
|
||||
nio4r (~> 2.0)
|
||||
racc (1.7.3)
|
||||
rack (3.0.8)
|
||||
racc (1.8.1)
|
||||
rack (3.1.8)
|
||||
rack-session (2.0.0)
|
||||
rack (>= 3.0.0)
|
||||
rack-test (2.1.0)
|
||||
rack (>= 1.3)
|
||||
rackup (2.1.0)
|
||||
rackup (2.2.1)
|
||||
rack (>= 3)
|
||||
webrick (~> 1.8)
|
||||
rails (7.1.3)
|
||||
actioncable (= 7.1.3)
|
||||
actionmailbox (= 7.1.3)
|
||||
actionmailer (= 7.1.3)
|
||||
actionpack (= 7.1.3)
|
||||
actiontext (= 7.1.3)
|
||||
actionview (= 7.1.3)
|
||||
activejob (= 7.1.3)
|
||||
activemodel (= 7.1.3)
|
||||
activerecord (= 7.1.3)
|
||||
activestorage (= 7.1.3)
|
||||
activesupport (= 7.1.3)
|
||||
rails (7.2.2)
|
||||
actioncable (= 7.2.2)
|
||||
actionmailbox (= 7.2.2)
|
||||
actionmailer (= 7.2.2)
|
||||
actionpack (= 7.2.2)
|
||||
actiontext (= 7.2.2)
|
||||
actionview (= 7.2.2)
|
||||
activejob (= 7.2.2)
|
||||
activemodel (= 7.2.2)
|
||||
activerecord (= 7.2.2)
|
||||
activestorage (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 7.1.3)
|
||||
railties (= 7.2.2)
|
||||
rails-dom-testing (2.2.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.6.0)
|
||||
rails-html-sanitizer (1.6.1)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (~> 1.14)
|
||||
railties (7.1.3)
|
||||
actionpack (= 7.1.3)
|
||||
activesupport (= 7.1.3)
|
||||
irb
|
||||
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.2.2)
|
||||
actionpack (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
irb (~> 1.13)
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rake (13.1.0)
|
||||
rdoc (6.6.2)
|
||||
rake (13.2.1)
|
||||
rdoc (6.8.1)
|
||||
psych (>= 4.0.0)
|
||||
regexp_parser (2.9.0)
|
||||
reline (0.4.2)
|
||||
regexp_parser (2.9.3)
|
||||
reline (0.5.12)
|
||||
io-console (~> 0.5)
|
||||
responders (3.1.1)
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
rexml (3.2.6)
|
||||
ruby2_keywords (0.0.5)
|
||||
rexml (3.3.9)
|
||||
rubyzip (2.3.2)
|
||||
sassc (2.4.0)
|
||||
ffi (~> 1.9)
|
||||
@ -208,27 +204,30 @@ GEM
|
||||
sprockets (> 3.0)
|
||||
sprockets-rails
|
||||
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)
|
||||
rubyzip (>= 1.2.2, < 3.0)
|
||||
websocket (~> 1.0)
|
||||
sprockets (4.2.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (>= 2.2.4, < 4)
|
||||
sprockets-rails (3.4.2)
|
||||
actionpack (>= 5.2)
|
||||
activesupport (>= 5.2)
|
||||
sprockets-rails (3.5.2)
|
||||
actionpack (>= 6.1)
|
||||
activesupport (>= 6.1)
|
||||
sprockets (>= 3.0.0)
|
||||
stringio (3.1.0)
|
||||
thor (1.3.0)
|
||||
tilt (2.3.0)
|
||||
timeout (0.4.1)
|
||||
turbo-rails (2.0.0.pre.beta.3)
|
||||
stringio (3.1.2)
|
||||
thor (1.3.2)
|
||||
tilt (2.4.0)
|
||||
timeout (0.4.2)
|
||||
turbo-rails (2.0.11)
|
||||
actionpack (>= 6.0.0)
|
||||
activejob (>= 6.0.0)
|
||||
railties (>= 6.0.0)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
useragent (0.16.11)
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
web-console (4.2.1)
|
||||
@ -236,14 +235,13 @@ GEM
|
||||
activemodel (>= 6.0.0)
|
||||
bindex (>= 0.4.0)
|
||||
railties (>= 6.0.0)
|
||||
webrick (1.8.1)
|
||||
websocket (1.2.10)
|
||||
websocket (1.2.11)
|
||||
websocket-driver (0.7.6)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
zeitwerk (2.6.12)
|
||||
zeitwerk (2.7.1)
|
||||
|
||||
PLATFORMS
|
||||
x86_64-linux
|
||||
@ -255,7 +253,7 @@ DEPENDENCIES
|
||||
importmap-rails
|
||||
mysql2 (~> 0.5)
|
||||
puma (~> 6.0)
|
||||
rails (~> 7.1.2)
|
||||
rails (~> 7.2.2)
|
||||
sassc-rails
|
||||
selenium-webdriver
|
||||
sprockets-rails
|
||||
@ -263,5 +261,8 @@ DEPENDENCIES
|
||||
tzinfo-data
|
||||
web-console
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.3.0p0
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.3
|
||||
|
17
README.md
17
README.md
@ -9,8 +9,11 @@ Software requirements
|
||||
|
||||
* Server side:
|
||||
* Ruby version: developed on Ruby 3.x
|
||||
* database with recursive Common Table Expressions (CTE) support, e.g.
|
||||
* database with:
|
||||
* 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
|
||||
* Client side:
|
||||
* 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
|
||||
|
||||
|
||||
### 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;
|
||||
}
|
||||
input:not([type=submit]):not([type=checkbox]),
|
||||
select {
|
||||
select,
|
||||
textarea {
|
||||
padding: 0.2em 0.4em;
|
||||
}
|
||||
.button,
|
||||
button,
|
||||
input,
|
||||
select {
|
||||
select,
|
||||
textarea {
|
||||
border: solid 1px var(--color-gray);
|
||||
border-radius: 0.25em;
|
||||
}
|
||||
textarea {
|
||||
margin: 0
|
||||
}
|
||||
.button > svg,
|
||||
.tab > svg,
|
||||
button > svg {
|
||||
@ -151,7 +156,8 @@ input[type=checkbox]:checked {
|
||||
-webkit-appearance: checkbox;
|
||||
}
|
||||
input:hover,
|
||||
select:hover {
|
||||
select:hover,
|
||||
textarea:hover {
|
||||
border-color: #009ade;
|
||||
outline: solid 1px #009ade;
|
||||
}
|
||||
@ -160,11 +166,13 @@ select:hover {
|
||||
}
|
||||
input:focus-visible,
|
||||
select:focus-within,
|
||||
select:focus-visible {
|
||||
select:focus-visible,
|
||||
textarea:focus-visible {
|
||||
accent-color: #006c9b;
|
||||
background-color: var(--color-focus-gray);
|
||||
}
|
||||
input[type=text]:read-only {
|
||||
input[type=text]:read-only,
|
||||
textarea:read-only {
|
||||
border: none;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
@ -336,7 +344,7 @@ table.items th,
|
||||
table.items td {
|
||||
padding-inline: 1em 0;
|
||||
}
|
||||
table.items td:has(input) {
|
||||
table.items td:has(input, textarea) {
|
||||
padding-inline-start: calc(0.6em - 0.9px);
|
||||
}
|
||||
table.items th:last-child {
|
||||
@ -367,7 +375,7 @@ table.items td.link a::after {
|
||||
table.items td.subunit {
|
||||
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);
|
||||
}
|
||||
table.items td.actions {
|
||||
@ -390,6 +398,9 @@ table.items tr.dropzone::after {
|
||||
table.items td.handle {
|
||||
cursor: move;
|
||||
}
|
||||
table.items tr.form td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* TODO: replace :hover:focus-visible combos with proper LOVE stye order */
|
||||
/* TODO: Update styling, including rem removal. */
|
||||
@ -410,7 +421,8 @@ table.items td.link a:hover:focus-visible {
|
||||
color: #006c9b;
|
||||
}
|
||||
|
||||
table.items td:not(:first-child) {
|
||||
table.items td:not(:first-child),
|
||||
.grayed {
|
||||
color: 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 input:not(:hover),
|
||||
table.items select:not(:hover) {
|
||||
table.items select:not(:hover),
|
||||
table.items textarea:not(:hover) {
|
||||
border-color: var(--color-border-gray);
|
||||
}
|
||||
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
|
||||
before_action only: [:new] do
|
||||
before_action only: :new do
|
||||
find_unit if params[:id].present?
|
||||
end
|
||||
before_action :find_unit, only: [:edit, :update, :rebase, :destroy]
|
||||
@ -9,7 +9,7 @@ class UnitsController < ApplicationController
|
||||
end
|
||||
|
||||
def index
|
||||
@units = current_user.units.includes(:subunits)
|
||||
@units = current_user.units.includes(:subunits).ordered
|
||||
end
|
||||
|
||||
def new
|
||||
@ -19,7 +19,7 @@ class UnitsController < ApplicationController
|
||||
def create
|
||||
@unit = current_user.units.new(unit_params)
|
||||
if @unit.save
|
||||
flash.now[:notice] = t(".success")
|
||||
flash.now[:notice] = t('.success', unit: @unit)
|
||||
run_and_render :index
|
||||
else
|
||||
render :new
|
||||
@ -31,7 +31,7 @@ class UnitsController < ApplicationController
|
||||
|
||||
def update
|
||||
if @unit.update(unit_params.except(:base_id))
|
||||
flash.now[:notice] = t(".success")
|
||||
flash.now[:notice] = t('.success', unit: @unit)
|
||||
run_and_render :index
|
||||
else
|
||||
render :edit
|
||||
@ -40,25 +40,28 @@ class UnitsController < ApplicationController
|
||||
|
||||
def rebase
|
||||
permitted = params.require(:unit).permit(:base_id)
|
||||
if permitted[:base_id].blank? && @unit.multiplier != 1
|
||||
permitted.merge!(multiplier: 1)
|
||||
flash.now[:notice] = t(".multiplier_reset", symbol: @unit.symbol)
|
||||
end
|
||||
permitted.merge!(multiplier: 1) if permitted[:base_id].blank? && @unit.multiplier != 1
|
||||
|
||||
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
|
||||
|
||||
def destroy
|
||||
if @unit.destroy
|
||||
flash.now[:notice] = t(".success")
|
||||
end
|
||||
@unit.destroy!
|
||||
flash.now[:notice] = t('.success', unit: @unit)
|
||||
ensure
|
||||
run_and_render :index
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def unit_params
|
||||
params.require(:unit).permit(:symbol, :name, :base_id, :multiplier)
|
||||
params.require(:unit).permit(Unit::ATTRIBUTES)
|
||||
end
|
||||
|
||||
def find_unit
|
||||
|
@ -2,12 +2,13 @@ class UsersController < ApplicationController
|
||||
helper_method :allow_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
|
||||
raise AccessForbidden unless current_user_disguised?
|
||||
end
|
||||
before_action except: :revert do
|
||||
raise AccessForbidden unless current_user.at_least(:admin)
|
||||
end
|
||||
|
||||
def index
|
||||
@users = User.all
|
||||
|
@ -84,6 +84,7 @@ module ApplicationHelper
|
||||
[:button_to, :link_to, :link_to_unless_current].each do |method_name|
|
||||
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
||||
def image_#{method_name}(name, image = nil, options = nil, html_options = {}, &block)
|
||||
name = name.to_s
|
||||
name = svg_tag("pictograms/\#{image}") + name if image
|
||||
|
||||
html_options[:class] = class_names(
|
||||
@ -95,6 +96,11 @@ module ApplicationHelper
|
||||
html_options[:onclick] = "return confirm('\#{html_options[:onclick][:confirm]}');"
|
||||
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
|
||||
end
|
||||
RUBY_EVAL
|
||||
@ -123,27 +129,13 @@ module ApplicationHelper
|
||||
"Turbo.renderStreamMessage('#{j(render partial: partial, locals: locals)}'); return false;"
|
||||
end
|
||||
|
||||
private
|
||||
def disabled_attributes(disabled)
|
||||
disabled ? {disabled: true, aria: {disabled: true}, tabindex: -1} : {}
|
||||
end
|
||||
|
||||
# Converts value to HTML formatted scientific notation
|
||||
def scientifize(d)
|
||||
sign, coefficient, base, exponent = d.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
|
||||
def number_attributes(type)
|
||||
step = BigDecimal(10).power(-type.scale)
|
||||
max = BigDecimal(10).power(type.precision - type.scale) - step
|
||||
{min: -max, max: max, step: step}
|
||||
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
|
||||
ATTRIBUTES = [:symbol, :description, :multiplier, :base_id]
|
||||
|
||||
belongs_to :user, optional: true
|
||||
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
|
||||
errors.add(:base, :user_mismatch) unless user == base.user
|
||||
errors.add(:base, :multilevel_nesting) if base.base.present?
|
||||
end
|
||||
validates :symbol, presence: true, uniqueness: {scope: :user_id},
|
||||
length: {maximum: columns_hash['symbol'].limit}
|
||||
validates :name, length: {maximum: columns_hash['name'].limit}
|
||||
length: {maximum: type_for_attribute(:symbol).limit}
|
||||
validates :description, length: {maximum: type_for_attribute(:description).limit}
|
||||
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 :ordered, ->{
|
||||
parent_symbol = Arel::Nodes::NamedFunction.new(
|
||||
'COALESCE',
|
||||
[Arel::Table.new(:bases_units)[:symbol], arel_table[:symbol]]
|
||||
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, ->{
|
||||
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
|
||||
@ -28,7 +85,23 @@ class Unit < ApplicationRecord
|
||||
nil
|
||||
end
|
||||
|
||||
def to_s
|
||||
symbol
|
||||
end
|
||||
|
||||
def movable?
|
||||
subunits.empty?
|
||||
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
|
||||
|
@ -11,7 +11,15 @@ class User < ApplicationRecord
|
||||
disabled: 0, # administratively disallowed to sign in
|
||||
}, 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)
|
||||
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,
|
||||
class: "extendedright" %>
|
||||
<% 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) {} %>
|
||||
<% if current_user_disguised? %>
|
||||
<%= image_link_to t(".revert"), "incognito-off", revert_users_path %>
|
||||
|
@ -4,24 +4,26 @@
|
||||
|
||||
<td class="<%= class_names({subunit: @unit.base}) %>">
|
||||
<%= 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>
|
||||
<%= form.text_field :name, form: :unit_form, size: 30,
|
||||
maxlength: @unit.class.columns_hash['name'].limit, autocomplete: "off" %>
|
||||
<%= form.text_area :description, form: :unit_form, cols: 30, rows: 1, escape: false,
|
||||
maxlength: @unit.class.type_for_attribute(:description).limit, autocomplete: "off" %>
|
||||
</td>
|
||||
<td>
|
||||
<% unless @unit.base.nil? %>
|
||||
<%= form.hidden_field :base_id, form: :unit_form %>
|
||||
<%= form.number_field :multiplier, form: :unit_form, required: true, step: "any",
|
||||
size: 10, autocomplete: "off" %>
|
||||
<%= form.number_field :multiplier, form: :unit_form, required: true,
|
||||
size: 10, autocomplete: "off",
|
||||
**number_attributes(@unit.class.type_for_attribute(:multiplier)) %>
|
||||
<% end %>
|
||||
</td>
|
||||
|
||||
<td class="actions">
|
||||
<%= 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}) %>
|
||||
</td>
|
||||
<td></td>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
@ -5,21 +5,21 @@
|
||||
data: {drag_path: rebase_unit_path(unit), drop_id: dom_id(unit.base || unit)} do %>
|
||||
|
||||
<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} %>
|
||||
</td>
|
||||
<td><%= unit.name %></td>
|
||||
<td class="number"><%= scientifize(unit.multiplier) %></td>
|
||||
<td><%= unit.description %></td>
|
||||
<td class="number"><%= unit.multiplier.to_html %></td>
|
||||
|
||||
<% if current_user.at_least(:active) %>
|
||||
<td class="actions">
|
||||
<% 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();',
|
||||
data: {turbo_stream: true} %>
|
||||
<% 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 %>
|
||||
</td>
|
||||
<% 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) %>
|
||||
<%= image_link_to t('.add_unit'), 'plus-outline', new_unit_path, id: :add_unit,
|
||||
onclick: 'this.blur();', data: {turbo_stream: true} %>
|
||||
<%= image_link_to t('.import_units'), 'import', new_unit_path, class: 'tools',
|
||||
data: {turbo_stream: true} %>
|
||||
<% end %>
|
||||
<%= image_link_to t('.import_units'), 'download-outline', default_units_path, class: 'tools' %>
|
||||
</div>
|
||||
|
||||
<%= tag.div id: :unit_form %>
|
||||
@ -13,7 +12,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<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>
|
||||
<% if current_user.at_least(:active) %>
|
||||
<th><%= t :actions %></th>
|
||||
|
@ -11,7 +11,7 @@
|
||||
<tbody>
|
||||
<% @users.each do |user| %>
|
||||
<tr>
|
||||
<td class="link"><%= link_to user.email, user_path(user) %></td>
|
||||
<td class="link"><%= link_to user, user_path(user) %></td>
|
||||
<td>
|
||||
<% if user == current_user %>
|
||||
<%= 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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
<% content_for :navigation, flush: true do %>
|
||||
<%= image_link_to t(:back), 'arrow-left-bold-outline',
|
||||
request.referer.present? ? :back : root_path %>
|
||||
<%= link_to svg_tag("pictograms/arrow-left-bold-outline") + t(:back),
|
||||
request.referer.present? ? :back : root_path, class: 'tab' %>
|
||||
<% end %>
|
||||
|
||||
<div class="rightside buttongrid">
|
||||
|
@ -23,6 +23,10 @@ module FixinMe
|
||||
# Initialize configuration defaults for originally generated Rails version.
|
||||
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.
|
||||
#
|
||||
# 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.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.
|
||||
#
|
||||
@ -47,5 +51,8 @@ module FixinMe
|
||||
|
||||
# Email address of admin account
|
||||
config.admin = 'admin@localhost'
|
||||
|
||||
# Sender address of account registration-related messages
|
||||
Devise.mailer_sender = 'noreply@localhost'
|
||||
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,
|
||||
# note that it will be overwritten if you use your own mailer class
|
||||
# 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.
|
||||
# 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:
|
||||
errors:
|
||||
messages:
|
||||
precision_exceeded: must not exceed %{value} significant digits
|
||||
scale_exceeded: must not exceed %{value} decimal digits
|
||||
activerecord:
|
||||
attributes:
|
||||
unit:
|
||||
@ -33,6 +37,9 @@ en:
|
||||
forbidden: >
|
||||
You have not been granted access to this action (403 Forbidden).
|
||||
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: >
|
||||
The request is semantically incorrect and was rejected (422 Unprocessable Entity).
|
||||
This should not happen, please notify site administrator.
|
||||
@ -54,22 +61,39 @@ en:
|
||||
delete_unit: Delete
|
||||
index:
|
||||
add_unit: Add unit
|
||||
import_units: Import...
|
||||
no_items: There are no configured units. You can try to import some defaults.
|
||||
import_units: Import
|
||||
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
|
||||
new:
|
||||
none: none
|
||||
create:
|
||||
success: Created new unit
|
||||
success: Created new unit "%{unit}"
|
||||
update:
|
||||
success: Updated unit
|
||||
success: Updated unit "%{unit}"
|
||||
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:
|
||||
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:
|
||||
index:
|
||||
disguise: View as...
|
||||
disguise: View as
|
||||
passwords:
|
||||
edit:
|
||||
new_password: New password
|
||||
|
@ -3,22 +3,19 @@ Rails.application.routes.draw do
|
||||
controllers: {registrations: :registrations}
|
||||
|
||||
resources :units, except: [:show], path_names: {new: '(/:id)/new'} do
|
||||
member do
|
||||
post :rebase
|
||||
end
|
||||
member { post :rebase }
|
||||
end
|
||||
|
||||
namespace :units do
|
||||
get 'defaults/index'
|
||||
namespace :default do
|
||||
resources :units, only: [:index, :destroy] do
|
||||
member { post :import, :export }
|
||||
#collection { post :import_all }
|
||||
end
|
||||
end
|
||||
|
||||
resources :users, only: [:index, :show, :update] do
|
||||
member do
|
||||
get :disguise
|
||||
end
|
||||
collection do
|
||||
get :revert
|
||||
end
|
||||
member { get :disguise }
|
||||
collection { get :revert }
|
||||
end
|
||||
|
||||
devise_scope :user do
|
||||
|
@ -2,12 +2,12 @@ class CreateUnits < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
create_table :units do |t|
|
||||
t.references :user, foreign_key: true
|
||||
t.string :symbol
|
||||
t.string :name
|
||||
t.decimal :multiplier, precision: 30, scale: 15, default: 1.0
|
||||
t.string :symbol, null: false, limit: 15
|
||||
t.text :description
|
||||
t.decimal :multiplier, null: false, precision: 30, scale: 15, default: 1.0
|
||||
t.references :base
|
||||
|
||||
t.timestamps
|
||||
t.timestamps null: false
|
||||
end
|
||||
add_index :units, [:user_id, :symbol], unique: true
|
||||
end
|
||||
|
@ -10,12 +10,12 @@
|
||||
#
|
||||
# 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|
|
||||
t.bigint "user_id"
|
||||
t.string "symbol"
|
||||
t.string "name"
|
||||
t.decimal "multiplier", precision: 30, scale: 15, default: "1.0"
|
||||
t.string "symbol", limit: 15, null: false
|
||||
t.text "description"
|
||||
t.decimal "multiplier", precision: 30, scale: 15, default: "1.0", null: false
|
||||
t.bigint "base_id"
|
||||
t.datetime "created_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
|
||||
#[Source, Quantity, Unit].each { |model| model.defaults.delete_all }
|
||||
|
||||
Unit.transaction do
|
||||
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
|
||||
require 'seeds/units.rb'
|
||||
|
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
|
||||
|
||||
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"
|
||||
|
||||
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
|
||||
extend ActionView::Helpers::TranslationHelper
|
||||
include ActionView::Helpers::UrlHelper
|
||||
|
||||
# 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|
|
||||
config.save_path = "#{Rails.root}/tmp/screenshots/"
|
||||
end
|
||||
@ -30,4 +33,18 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
|
||||
#def assert_stale(element)
|
||||
# assert_raises(Selenium::WebDriver::Error::StaleElementReferenceError) { element.tag_name }
|
||||
#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
|
||||
|
@ -1,6 +1,6 @@
|
||||
require "test_helper"
|
||||
|
||||
class Units::DefaultsControllerTest < ActionDispatch::IntegrationTest
|
||||
class Default::UnitsControllerTest < ActionDispatch::IntegrationTest
|
||||
test "should get index" do
|
||||
get units_defaults_index_url
|
||||
assert_response :success
|
16
test/fixtures/units.yml
vendored
16
test/fixtures/units.yml
vendored
@ -1,40 +1,40 @@
|
||||
g:
|
||||
user: admin
|
||||
symbol: g
|
||||
name: gram
|
||||
description: gram
|
||||
kg:
|
||||
user: admin
|
||||
symbol: kg
|
||||
name: kilogram
|
||||
description: kilogram
|
||||
multiplier: 1000
|
||||
base: g
|
||||
1:
|
||||
user: admin
|
||||
symbol: 1
|
||||
name: one
|
||||
description: one
|
||||
s:
|
||||
user: admin
|
||||
symbol: s
|
||||
name: second
|
||||
description: second
|
||||
percent:
|
||||
user: admin
|
||||
symbol: '%'
|
||||
name: percent
|
||||
description: percent
|
||||
multiplier: 0.01
|
||||
base: 1
|
||||
µg:
|
||||
user: admin
|
||||
symbol: µg
|
||||
name: microgram
|
||||
description: microgram
|
||||
multiplier: 0.000001
|
||||
base: g
|
||||
mg:
|
||||
user: admin
|
||||
symbol: mg
|
||||
name: milligram
|
||||
description: milligram
|
||||
multiplier: 0.001
|
||||
base: g
|
||||
g_alice:
|
||||
user: alice
|
||||
symbol: g
|
||||
name: gram
|
||||
description: gram
|
||||
|
@ -1,8 +1,16 @@
|
||||
require "application_system_test_case"
|
||||
|
||||
class UnitsTest < ApplicationSystemTestCase
|
||||
LINK_LABELS = {
|
||||
add_unit: t('units.index.add_unit'),
|
||||
add_subunit: t('units.unit.add_subunit'),
|
||||
edit: nil
|
||||
}
|
||||
|
||||
setup do
|
||||
@user = sign_in
|
||||
LINK_LABELS[:edit] = Regexp.union(@user.units.map(&:symbol))
|
||||
|
||||
visit units_path
|
||||
end
|
||||
|
||||
@ -12,7 +20,8 @@ class UnitsTest < ApplicationSystemTestCase
|
||||
assert_selector 'tr', count: @user.units.count
|
||||
end
|
||||
|
||||
Unit.destroy_all
|
||||
# Cannot #destroy_all due to {dependent: :restrict*} on Unit.subunits association
|
||||
@user.units.delete_all
|
||||
visit units_path
|
||||
within 'tbody' do
|
||||
assert_selector 'tr', count: 1
|
||||
@ -20,16 +29,24 @@ class UnitsTest < ApplicationSystemTestCase
|
||||
end
|
||||
end
|
||||
|
||||
test "add unit" do
|
||||
click_on t('units.index.add_unit')
|
||||
test "new" do
|
||||
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
|
||||
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]',
|
||||
with: SecureRandom.random_symbol(rand([1..15, 15..maxlength['unit[symbol]']].sample))
|
||||
fill_in 'unit[name]',
|
||||
with: [nil, SecureRandom.alphanumeric(rand(1..maxlength['unit[name]']))].sample
|
||||
with: random_string(rand([1..3, 4..maxlength['unit[symbol]']].sample))
|
||||
fill_in 'unit[description]', with: random_string(rand(0..maxlength['unit[description]']))
|
||||
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
|
||||
click_on t('helpers.submit.create')
|
||||
end
|
||||
@ -39,39 +56,48 @@ class UnitsTest < ApplicationSystemTestCase
|
||||
assert_no_selector :fillable_field
|
||||
assert_selector 'tr', count: @user.units.count
|
||||
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
|
||||
|
||||
test "add and edit disallow opening multiple forms" do
|
||||
# Once new/edit form is open, other actions on the same page either replace
|
||||
# the form or leave it untouched
|
||||
# TODO: add non-empty form closing warning
|
||||
links = {}
|
||||
link_labels = {1 => [t('units.index.add_unit'), t('units.unit.add_subunit')],
|
||||
0 => units.map(&:symbol)}
|
||||
link_labels.each_pair do |row_change, labels|
|
||||
all(:link_or_button, exact_text: Regexp.union(labels)).map { |l| links[l] = row_change }
|
||||
# TODO: check proper form/button redisplay and flash messages on add/edit
|
||||
test "new and edit form on validation error" do
|
||||
end
|
||||
link, rows = links.assoc(links.keys.sample).tap { |l, _| links.delete(l) }
|
||||
|
||||
# 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 = {}
|
||||
targets = {}
|
||||
LINK_LABELS.each_pair do |type, labels|
|
||||
links[type] = all(:link_or_button, exact_text: labels).to_a
|
||||
targets[type] = links[type].sample
|
||||
end
|
||||
# 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
|
||||
link.click
|
||||
end
|
||||
find 'tbody tr:has(input[type=text]:focus)'
|
||||
|
||||
# Link should be now unavailable or unclickable
|
||||
begin
|
||||
assert_raises(Selenium::WebDriver::Error::ElementClickInterceptedError) do
|
||||
link.click
|
||||
end if link.visible?
|
||||
rescue Selenium::WebDriver::Error::StaleElementReferenceError
|
||||
link = nil
|
||||
within('tbody tr:has(input[type=text])') { assert_selector ':focus' }
|
||||
if type == :edit
|
||||
assert !link.visible?
|
||||
[:add_subunit, :edit].each do |t|
|
||||
assert_difference(->{ links[t].length }, -1) { links[t].select!(&:visible?) }
|
||||
end
|
||||
else
|
||||
assert link[:disabled]
|
||||
end
|
||||
|
||||
link = links.keys.select(&:visible?).sample
|
||||
assert_difference ->{ all('tbody tr').count }, links[link] - rows do
|
||||
targets.merge([:add_subunit, :edit].map { |t| [t, links[t].sample] }.to_h)
|
||||
type, link = targets.assoc(targets.keys.sample)
|
||||
assert_difference ->{ all('tbody tr').count }, row_change[type] - rows do
|
||||
link.click
|
||||
end
|
||||
assert_selector 'tbody tr:has(input[type=text]:focus)', count: 1
|
||||
within('tbody tr:has(input[type=text])') { assert_selector ':focus' }
|
||||
end
|
||||
|
||||
# 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.
|
||||
fixtures :all
|
||||
|
||||
include AbstractController::Translation
|
||||
include ActionMailer::TestHelper
|
||||
include ActionView::Helpers::TranslationHelper
|
||||
|
||||
# NOTE: use public #alphanumeric(chars: ...) from Ruby 3.3 onwards
|
||||
SecureRandom.class_eval do
|
||||
def self.random_symbol(n = 10)
|
||||
# Unicode characters: 32-126, 160-383
|
||||
choose([*' '..'~', 160.chr(Encoding::UTF_8), *'¡'..'ſ'], n)
|
||||
# List of categorized Unicode characters:
|
||||
# * http://www.unicode.org/Public/UNIDATA/UnicodeData.txt
|
||||
# File format: http://www.unicode.org/L2/L1999/UnicodeData.html
|
||||
# Select from graphic ranges: L, M, N, P, S, Zs
|
||||
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
|
||||
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
|
||||
|
||||
def randomize_user_password!(user)
|
||||
@ -25,11 +51,11 @@ class ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
def random_password
|
||||
SecureRandom.alphanumeric rand(Rails.configuration.devise.password_length)
|
||||
Random.alphanumeric rand(Rails.configuration.devise.password_length)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def with_last_email
|
||||
|
Loading…
x
Reference in New Issue
Block a user