Compare commits
16 Commits
9acdc4e6db
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b6e059da6 | ||
|
|
e9f8f03db2 | ||
|
|
b289cdc320 | ||
|
|
8bb410dcfa | ||
|
|
ce67776eec | ||
|
|
9f71fe65e5 | ||
|
|
e15835bda9 | ||
|
|
83320d4c9a | ||
|
|
a2008e2ae3 | ||
|
|
b45a451748 | ||
|
|
441caabb98 | ||
|
|
a139bde102 | ||
|
|
f35a09f07a | ||
|
|
1a10e3c784 | ||
|
|
4fe95ca538 | ||
|
|
fa36305244 |
@@ -0,0 +1 @@
|
|||||||
|
markup: markdown
|
||||||
@@ -18,6 +18,8 @@ gem "turbo-rails"
|
|||||||
gem "stimulus-rails"
|
gem "stimulus-rails"
|
||||||
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
|
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
|
||||||
gem "jbuilder"
|
gem "jbuilder"
|
||||||
|
gem "grape"
|
||||||
|
gem "rswag-ui"
|
||||||
gem "caxlsx"
|
gem "caxlsx"
|
||||||
gem "caxlsx_rails"
|
gem "caxlsx_rails"
|
||||||
|
|
||||||
@@ -44,6 +46,9 @@ gem "thruster", require: false
|
|||||||
# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
|
# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
|
||||||
gem "image_processing", "~> 1.2"
|
gem "image_processing", "~> 1.2"
|
||||||
|
|
||||||
|
# Pagination [https://github.com/ddnexus/pagy]
|
||||||
|
gem "pagy", "~> 8.0"
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
|
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
|
||||||
gem "debug", platforms: %i[ mri windows ], require: "debug/prelude"
|
gem "debug", platforms: %i[ mri windows ], require: "debug/prelude"
|
||||||
@@ -56,6 +61,9 @@ group :development, :test do
|
|||||||
|
|
||||||
# Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/]
|
# Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/]
|
||||||
gem "rubocop-rails-omakase", require: false
|
gem "rubocop-rails-omakase", require: false
|
||||||
|
|
||||||
|
# Parallel test runner [https://github.com/grosser/parallel_tests]
|
||||||
|
gem "parallel_tests", require: false
|
||||||
end
|
end
|
||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
|
|||||||
@@ -84,9 +84,9 @@ GEM
|
|||||||
benchmark (0.5.0)
|
benchmark (0.5.0)
|
||||||
bigdecimal (4.0.1)
|
bigdecimal (4.0.1)
|
||||||
bindex (0.8.1)
|
bindex (0.8.1)
|
||||||
bootsnap (1.21.1)
|
bootsnap (1.23.0)
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
brakeman (8.0.1)
|
brakeman (8.0.2)
|
||||||
racc
|
racc
|
||||||
builder (3.3.0)
|
builder (3.3.0)
|
||||||
bundler-audit (0.9.3)
|
bundler-audit (0.9.3)
|
||||||
@@ -120,6 +120,26 @@ GEM
|
|||||||
docile (1.4.1)
|
docile (1.4.1)
|
||||||
dotenv (3.2.0)
|
dotenv (3.2.0)
|
||||||
drb (2.2.3)
|
drb (2.2.3)
|
||||||
|
dry-configurable (1.3.0)
|
||||||
|
dry-core (~> 1.1)
|
||||||
|
zeitwerk (~> 2.6)
|
||||||
|
dry-core (1.2.0)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
logger
|
||||||
|
zeitwerk (~> 2.6)
|
||||||
|
dry-inflector (1.3.1)
|
||||||
|
dry-logic (1.6.0)
|
||||||
|
bigdecimal
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
dry-core (~> 1.1)
|
||||||
|
zeitwerk (~> 2.6)
|
||||||
|
dry-types (1.9.1)
|
||||||
|
bigdecimal (>= 3.0)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
dry-core (~> 1.0)
|
||||||
|
dry-inflector (~> 1.0)
|
||||||
|
dry-logic (~> 1.4)
|
||||||
|
zeitwerk (~> 2.6)
|
||||||
ed25519 (1.4.0)
|
ed25519 (1.4.0)
|
||||||
erb (6.0.1)
|
erb (6.0.1)
|
||||||
erubi (1.13.1)
|
erubi (1.13.1)
|
||||||
@@ -131,6 +151,13 @@ GEM
|
|||||||
raabro (~> 1.4)
|
raabro (~> 1.4)
|
||||||
globalid (1.3.0)
|
globalid (1.3.0)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
|
grape (3.1.1)
|
||||||
|
activesupport (>= 7.1)
|
||||||
|
dry-configurable
|
||||||
|
dry-types (>= 1.1)
|
||||||
|
mustermann-grape (~> 1.1.0)
|
||||||
|
rack (>= 2)
|
||||||
|
zeitwerk
|
||||||
htmlentities (4.4.2)
|
htmlentities (4.4.2)
|
||||||
i18n (1.14.8)
|
i18n (1.14.8)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
@@ -142,14 +169,15 @@ GEM
|
|||||||
activesupport (>= 6.0.0)
|
activesupport (>= 6.0.0)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
io-console (0.8.2)
|
io-console (0.8.2)
|
||||||
irb (1.16.0)
|
irb (1.17.0)
|
||||||
pp (>= 0.6.0)
|
pp (>= 0.6.0)
|
||||||
|
prism (>= 1.3.0)
|
||||||
rdoc (>= 4.0.0)
|
rdoc (>= 4.0.0)
|
||||||
reline (>= 0.4.2)
|
reline (>= 0.4.2)
|
||||||
jbuilder (2.14.1)
|
jbuilder (2.14.1)
|
||||||
actionview (>= 7.0.0)
|
actionview (>= 7.0.0)
|
||||||
activesupport (>= 7.0.0)
|
activesupport (>= 7.0.0)
|
||||||
json (2.18.0)
|
json (2.18.1)
|
||||||
kamal (2.10.1)
|
kamal (2.10.1)
|
||||||
activesupport (>= 7.0)
|
activesupport (>= 7.0)
|
||||||
base64 (~> 0.2)
|
base64 (~> 0.2)
|
||||||
@@ -181,7 +209,11 @@ GEM
|
|||||||
minitest (6.0.1)
|
minitest (6.0.1)
|
||||||
prism (~> 1.5)
|
prism (~> 1.5)
|
||||||
msgpack (1.8.0)
|
msgpack (1.8.0)
|
||||||
net-imap (0.6.2)
|
mustermann (3.0.4)
|
||||||
|
ruby2_keywords (~> 0.0.1)
|
||||||
|
mustermann-grape (1.1.0)
|
||||||
|
mustermann (>= 1.0.0)
|
||||||
|
net-imap (0.6.3)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
net-pop (0.1.2)
|
net-pop (0.1.2)
|
||||||
@@ -196,11 +228,14 @@ GEM
|
|||||||
net-protocol
|
net-protocol
|
||||||
net-ssh (7.3.0)
|
net-ssh (7.3.0)
|
||||||
nio4r (2.7.5)
|
nio4r (2.7.5)
|
||||||
nokogiri (1.19.0-x86_64-linux-gnu)
|
nokogiri (1.19.1-x86_64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
ostruct (0.6.3)
|
ostruct (0.6.3)
|
||||||
|
pagy (8.6.3)
|
||||||
parallel (1.27.0)
|
parallel (1.27.0)
|
||||||
parser (3.3.10.1)
|
parallel_tests (5.6.0)
|
||||||
|
parallel
|
||||||
|
parser (3.3.10.2)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
pp (0.6.3)
|
pp (0.6.3)
|
||||||
@@ -219,7 +254,7 @@ GEM
|
|||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rack (3.2.4)
|
rack (3.2.5)
|
||||||
rack-session (2.1.1)
|
rack-session (2.1.1)
|
||||||
base64 (>= 0.1.0)
|
base64 (>= 0.1.0)
|
||||||
rack (>= 3.0.0)
|
rack (>= 3.0.0)
|
||||||
@@ -259,7 +294,7 @@ GEM
|
|||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.3.1)
|
rake (13.3.1)
|
||||||
rdoc (7.1.0)
|
rdoc (7.2.0)
|
||||||
erb
|
erb
|
||||||
psych (>= 4.0.0)
|
psych (>= 4.0.0)
|
||||||
tsort
|
tsort
|
||||||
@@ -273,7 +308,10 @@ GEM
|
|||||||
logger (~> 1)
|
logger (~> 1)
|
||||||
nokogiri (~> 1)
|
nokogiri (~> 1)
|
||||||
rubyzip (>= 3.0.0, < 4.0.0)
|
rubyzip (>= 3.0.0, < 4.0.0)
|
||||||
rubocop (1.84.0)
|
rswag-ui (2.17.0)
|
||||||
|
actionpack (>= 5.2, < 8.2)
|
||||||
|
railties (>= 5.2, < 8.2)
|
||||||
|
rubocop (1.84.2)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (~> 3.17.0.2)
|
language_server-protocol (~> 3.17.0.2)
|
||||||
lint_roller (~> 1.1.0)
|
lint_roller (~> 1.1.0)
|
||||||
@@ -305,9 +343,10 @@ GEM
|
|||||||
ruby-vips (2.3.0)
|
ruby-vips (2.3.0)
|
||||||
ffi (~> 1.12)
|
ffi (~> 1.12)
|
||||||
logger
|
logger
|
||||||
|
ruby2_keywords (0.0.5)
|
||||||
rubyzip (3.2.2)
|
rubyzip (3.2.2)
|
||||||
securerandom (0.4.1)
|
securerandom (0.4.1)
|
||||||
selenium-webdriver (4.40.0)
|
selenium-webdriver (4.41.0)
|
||||||
base64 (~> 0.2)
|
base64 (~> 0.2)
|
||||||
logger (~> 1.4)
|
logger (~> 1.4)
|
||||||
rexml (~> 3.2, >= 3.2.5)
|
rexml (~> 3.2, >= 3.2.5)
|
||||||
@@ -328,7 +367,7 @@ GEM
|
|||||||
activejob (>= 7.2)
|
activejob (>= 7.2)
|
||||||
activerecord (>= 7.2)
|
activerecord (>= 7.2)
|
||||||
railties (>= 7.2)
|
railties (>= 7.2)
|
||||||
solid_queue (1.3.1)
|
solid_queue (1.3.2)
|
||||||
activejob (>= 7.1)
|
activejob (>= 7.1)
|
||||||
activerecord (>= 7.1)
|
activerecord (>= 7.1)
|
||||||
concurrent-ruby (>= 1.3.1)
|
concurrent-ruby (>= 1.3.1)
|
||||||
@@ -347,7 +386,7 @@ GEM
|
|||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
stringio (3.2.0)
|
stringio (3.2.0)
|
||||||
thor (1.5.0)
|
thor (1.5.0)
|
||||||
thruster (0.1.17-x86_64-linux)
|
thruster (0.1.18-x86_64-linux)
|
||||||
timeout (0.6.0)
|
timeout (0.6.0)
|
||||||
tsort (0.2.0)
|
tsort (0.2.0)
|
||||||
turbo-rails (2.0.23)
|
turbo-rails (2.0.23)
|
||||||
@@ -372,7 +411,7 @@ GEM
|
|||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
xpath (3.2.0)
|
xpath (3.2.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
zeitwerk (2.7.4)
|
zeitwerk (2.7.5)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
x86_64-linux-gnu
|
x86_64-linux-gnu
|
||||||
@@ -387,14 +426,18 @@ DEPENDENCIES
|
|||||||
caxlsx
|
caxlsx
|
||||||
caxlsx_rails
|
caxlsx_rails
|
||||||
debug
|
debug
|
||||||
|
grape
|
||||||
image_processing (~> 1.2)
|
image_processing (~> 1.2)
|
||||||
importmap-rails
|
importmap-rails
|
||||||
jbuilder
|
jbuilder
|
||||||
kamal
|
kamal
|
||||||
|
pagy (~> 8.0)
|
||||||
|
parallel_tests
|
||||||
propshaft
|
propshaft
|
||||||
puma (>= 5.0)
|
puma (>= 5.0)
|
||||||
rails (~> 8.1.2)
|
rails (~> 8.1.2)
|
||||||
roo (~> 3.0)
|
roo (~> 3.0)
|
||||||
|
rswag-ui
|
||||||
rubocop-rails-omakase
|
rubocop-rails-omakase
|
||||||
selenium-webdriver
|
selenium-webdriver
|
||||||
simplecov
|
simplecov
|
||||||
@@ -429,8 +472,8 @@ CHECKSUMS
|
|||||||
benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c
|
benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c
|
||||||
bigdecimal (4.0.1) sha256=8b07d3d065a9f921c80ceaea7c9d4ae596697295b584c296fe599dd0ad01c4a7
|
bigdecimal (4.0.1) sha256=8b07d3d065a9f921c80ceaea7c9d4ae596697295b584c296fe599dd0ad01c4a7
|
||||||
bindex (0.8.1) sha256=7b1ecc9dc539ed8bccfc8cb4d2732046227b09d6f37582ff12e50a5047ceb17e
|
bindex (0.8.1) sha256=7b1ecc9dc539ed8bccfc8cb4d2732046227b09d6f37582ff12e50a5047ceb17e
|
||||||
bootsnap (1.21.1) sha256=9373acfe732da35846623c337d3481af8ce77c7b3a927fb50e9aa92b46dbc4c4
|
bootsnap (1.23.0) sha256=c1254f458d58558b58be0f8eb8f6eec2821456785b7cdd1e16248e2020d3f214
|
||||||
brakeman (8.0.1) sha256=c68ce0ac35a6295027c4eab8b4ac597d2a0bfc82f0d62dcd334bbf944d352f70
|
brakeman (8.0.2) sha256=7b02065ce8b1de93949cefd3f2ad78e8eb370e644b95c8556a32a912a782426a
|
||||||
builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
|
builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
|
||||||
bundler-audit (0.9.3) sha256=81c8766c71e47d0d28a0f98c7eed028539f21a6ea3cd8f685eb6f42333c9b4e9
|
bundler-audit (0.9.3) sha256=81c8766c71e47d0d28a0f98c7eed028539f21a6ea3cd8f685eb6f42333c9b4e9
|
||||||
capybara (3.40.0) sha256=42dba720578ea1ca65fd7a41d163dd368502c191804558f6e0f71b391054aeef
|
capybara (3.40.0) sha256=42dba720578ea1ca65fd7a41d163dd368502c191804558f6e0f71b391054aeef
|
||||||
@@ -445,6 +488,11 @@ CHECKSUMS
|
|||||||
docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
|
docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
|
||||||
dotenv (3.2.0) sha256=e375b83121ea7ca4ce20f214740076129ab8514cd81378161f11c03853fe619d
|
dotenv (3.2.0) sha256=e375b83121ea7ca4ce20f214740076129ab8514cd81378161f11c03853fe619d
|
||||||
drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
|
drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
|
||||||
|
dry-configurable (1.3.0) sha256=882d862858567fc1210d2549d4c090f34370fc1bb7c5c1933de3fe792e18afa8
|
||||||
|
dry-core (1.2.0) sha256=0cc5a7da88df397f153947eeeae42e876e999c1e30900f3c536fb173854e96a1
|
||||||
|
dry-inflector (1.3.1) sha256=7fb0c2bb04f67638f25c52e7ba39ab435d922a3a5c3cd196120f63accb682dcc
|
||||||
|
dry-logic (1.6.0) sha256=da6fedbc0f90fc41f9b0cc7e6f05f5d529d1efaef6c8dcc8e0733f685745cea2
|
||||||
|
dry-types (1.9.1) sha256=baebeecdb9f8395d6c9d227b62011279440943e3ef2468fe8ccc1ba11467f178
|
||||||
ed25519 (1.4.0) sha256=16e97f5198689a154247169f3453ef4cfd3f7a47481fde0ae33206cdfdcac506
|
ed25519 (1.4.0) sha256=16e97f5198689a154247169f3453ef4cfd3f7a47481fde0ae33206cdfdcac506
|
||||||
erb (6.0.1) sha256=28ecdd99c5472aebd5674d6061e3c6b0a45c049578b071e5a52c2a7f13c197e5
|
erb (6.0.1) sha256=28ecdd99c5472aebd5674d6061e3c6b0a45c049578b071e5a52c2a7f13c197e5
|
||||||
erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9
|
erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9
|
||||||
@@ -452,14 +500,15 @@ CHECKSUMS
|
|||||||
ffi (1.17.3-x86_64-linux-gnu) sha256=3746b01f677aae7b16dc1acb7cb3cc17b3e35bdae7676a3f568153fb0e2c887f
|
ffi (1.17.3-x86_64-linux-gnu) sha256=3746b01f677aae7b16dc1acb7cb3cc17b3e35bdae7676a3f568153fb0e2c887f
|
||||||
fugit (1.12.1) sha256=5898f478ede9b415f0804e42b8f3fd53f814bd85eebffceebdbc34e1107aaf68
|
fugit (1.12.1) sha256=5898f478ede9b415f0804e42b8f3fd53f814bd85eebffceebdbc34e1107aaf68
|
||||||
globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11
|
globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11
|
||||||
|
grape (3.1.1) sha256=774f16782d917a90e69de0499dfaab571e5ad967569ac066a2b0b918af12de69
|
||||||
htmlentities (4.4.2) sha256=bbafbdf69f2eca9262be4efef7e43e6a1de54c95eb600f26984f71d2fe96c5c3
|
htmlentities (4.4.2) sha256=bbafbdf69f2eca9262be4efef7e43e6a1de54c95eb600f26984f71d2fe96c5c3
|
||||||
i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
|
i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
|
||||||
image_processing (1.14.0) sha256=754cc169c9c262980889bec6bfd325ed1dafad34f85242b5a07b60af004742fb
|
image_processing (1.14.0) sha256=754cc169c9c262980889bec6bfd325ed1dafad34f85242b5a07b60af004742fb
|
||||||
importmap-rails (2.2.3) sha256=7101be2a4dc97cf1558fb8f573a718404c5f6bcfe94f304bf1f39e444feeb16a
|
importmap-rails (2.2.3) sha256=7101be2a4dc97cf1558fb8f573a718404c5f6bcfe94f304bf1f39e444feeb16a
|
||||||
io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
|
io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
|
||||||
irb (1.16.0) sha256=2abe56c9ac947cdcb2f150572904ba798c1e93c890c256f8429981a7675b0806
|
irb (1.17.0) sha256=168c4ddb93d8a361a045c41d92b2952c7a118fa73f23fe14e55609eb7a863aae
|
||||||
jbuilder (2.14.1) sha256=4eb26376ff60ef100cb4fd6fd7533cd271f9998327e86adf20fd8c0e69fabb42
|
jbuilder (2.14.1) sha256=4eb26376ff60ef100cb4fd6fd7533cd271f9998327e86adf20fd8c0e69fabb42
|
||||||
json (2.18.0) sha256=b10506aee4183f5cf49e0efc48073d7b75843ce3782c68dbeb763351c08fd505
|
json (2.18.1) sha256=fe112755501b8d0466b5ada6cf50c8c3f41e897fa128ac5d263ec09eedc9f986
|
||||||
kamal (2.10.1) sha256=53b7ecb4c33dd83b1aedfc7aacd1c059f835993258a552d70d584c6ce32b6340
|
kamal (2.10.1) sha256=53b7ecb4c33dd83b1aedfc7aacd1c059f835993258a552d70d584c6ce32b6340
|
||||||
language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
|
language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
|
||||||
lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
|
lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
|
||||||
@@ -472,7 +521,9 @@ CHECKSUMS
|
|||||||
mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
|
mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
|
||||||
minitest (6.0.1) sha256=7854c74f48e2e975969062833adc4013f249a4b212f5e7b9d5c040bf838d54bb
|
minitest (6.0.1) sha256=7854c74f48e2e975969062833adc4013f249a4b212f5e7b9d5c040bf838d54bb
|
||||||
msgpack (1.8.0) sha256=e64ce0212000d016809f5048b48eb3a65ffb169db22238fb4b72472fecb2d732
|
msgpack (1.8.0) sha256=e64ce0212000d016809f5048b48eb3a65ffb169db22238fb4b72472fecb2d732
|
||||||
net-imap (0.6.2) sha256=08caacad486853c61676cca0c0c47df93db02abc4a8239a8b67eb0981428acc6
|
mustermann (3.0.4) sha256=85fadcb6b3c6493a8b511b42426f904b7f27b282835502233dd154daab13aa22
|
||||||
|
mustermann-grape (1.1.0) sha256=8d258a986004c8f01ce4c023c0b037c168a9ed889cf5778068ad54398fa458c5
|
||||||
|
net-imap (0.6.3) sha256=9bab75f876596d09ee7bf911a291da478e0cd6badc54dfb82874855ccc82f2ad
|
||||||
net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3
|
net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3
|
||||||
net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8
|
net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8
|
||||||
net-scp (4.1.0) sha256=a99b0b92a1e5d360b0de4ffbf2dc0c91531502d3d4f56c28b0139a7c093d1a5d
|
net-scp (4.1.0) sha256=a99b0b92a1e5d360b0de4ffbf2dc0c91531502d3d4f56c28b0139a7c093d1a5d
|
||||||
@@ -480,10 +531,12 @@ CHECKSUMS
|
|||||||
net-smtp (0.5.1) sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736
|
net-smtp (0.5.1) sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736
|
||||||
net-ssh (7.3.0) sha256=172076c4b30ce56fb25a03961b0c4da14e1246426401b0f89cba1a3b54bf3ef0
|
net-ssh (7.3.0) sha256=172076c4b30ce56fb25a03961b0c4da14e1246426401b0f89cba1a3b54bf3ef0
|
||||||
nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1
|
nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1
|
||||||
nokogiri (1.19.0-x86_64-linux-gnu) sha256=f482b95c713d60031d48c44ce14562f8d2ce31e3a9e8dd0ccb131e9e5a68b58c
|
nokogiri (1.19.1-x86_64-linux-gnu) sha256=1a4902842a186b4f901078e692d12257678e6133858d0566152fe29cdb98456a
|
||||||
ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912
|
ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912
|
||||||
|
pagy (8.6.3) sha256=537b2ee3119f237dd6c4a0d0a35c67a77b9d91ebb9d4f85e31407c2686774fb2
|
||||||
parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130
|
parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130
|
||||||
parser (3.3.10.1) sha256=06f6a725d2cd91e5e7f2b7c32ba143631e1f7c8ae2fb918fc4cebec187e6a688
|
parallel_tests (5.6.0) sha256=b2d7af382d1c0289daba65308d9143b3ad82d817d500c0ad1b4bde468beb13af
|
||||||
|
parser (3.3.10.2) sha256=6f60c84aa4bdcedb6d1a2434b738fe8a8136807b6adc8f7f53b97da9bc4e9357
|
||||||
pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6
|
pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6
|
||||||
prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
|
prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
|
||||||
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
|
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
|
||||||
@@ -493,7 +546,7 @@ CHECKSUMS
|
|||||||
puma (7.2.0) sha256=bf8ef4ab514a4e6d4554cb4326b2004eba5036ae05cf765cfe51aba9706a72a8
|
puma (7.2.0) sha256=bf8ef4ab514a4e6d4554cb4326b2004eba5036ae05cf765cfe51aba9706a72a8
|
||||||
raabro (1.4.0) sha256=d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882
|
raabro (1.4.0) sha256=d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882
|
||||||
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
|
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
|
||||||
rack (3.2.4) sha256=5d74b6f75082a643f43c1e76b419c40f0e5527fcfee1e669ac1e6b73c0ccb6f6
|
rack (3.2.5) sha256=4cbd0974c0b79f7a139b4812004a62e4c60b145cba76422e288ee670601ed6d3
|
||||||
rack-session (2.1.1) sha256=0b6dc07dea7e4b583f58a48e8b806d4c9f1c6c9214ebc202ec94562cbea2e4e9
|
rack-session (2.1.1) sha256=0b6dc07dea7e4b583f58a48e8b806d4c9f1c6c9214ebc202ec94562cbea2e4e9
|
||||||
rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463
|
rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463
|
||||||
rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868
|
rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868
|
||||||
@@ -503,33 +556,35 @@ CHECKSUMS
|
|||||||
railties (8.1.2) sha256=1289ece76b4f7668fc46d07e55cc992b5b8751f2ad85548b7da351b8c59f8055
|
railties (8.1.2) sha256=1289ece76b4f7668fc46d07e55cc992b5b8751f2ad85548b7da351b8c59f8055
|
||||||
rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
|
rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
|
||||||
rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
|
rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
|
||||||
rdoc (7.1.0) sha256=494899df0706c178596ca6e1d50f1b7eb285a9b2aae715be5abd742734f17363
|
rdoc (7.2.0) sha256=8650f76cd4009c3b54955eb5d7e3a075c60a57276766ebf36f9085e8c9f23192
|
||||||
regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4
|
regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4
|
||||||
reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
|
reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
|
||||||
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
|
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
|
||||||
roo (3.0.0) sha256=6fdd7a9158d657c69768b4168754ff2110cc21fdc01a1bec1010820cb05c91b1
|
roo (3.0.0) sha256=6fdd7a9158d657c69768b4168754ff2110cc21fdc01a1bec1010820cb05c91b1
|
||||||
rubocop (1.84.0) sha256=88dec310153bb685a879f5a7cdb601f6287b8f0ee675d9dc63a17c7204c4190a
|
rswag-ui (2.17.0) sha256=5f707b9b5e8171ddf9f519f6e401e79e419bd1d07387508603e76124f2443212
|
||||||
|
rubocop (1.84.2) sha256=5692cea54168f3dc8cb79a6fe95c5424b7ea893c707ad7a4307b0585e88dbf5f
|
||||||
rubocop-ast (1.49.0) sha256=49c3676d3123a0923d333e20c6c2dbaaae2d2287b475273fddee0c61da9f71fd
|
rubocop-ast (1.49.0) sha256=49c3676d3123a0923d333e20c6c2dbaaae2d2287b475273fddee0c61da9f71fd
|
||||||
rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834
|
rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834
|
||||||
rubocop-rails (2.34.3) sha256=10d37989024865ecda8199f311f3faca990143fbac967de943f88aca11eb9ad2
|
rubocop-rails (2.34.3) sha256=10d37989024865ecda8199f311f3faca990143fbac967de943f88aca11eb9ad2
|
||||||
rubocop-rails-omakase (1.1.0) sha256=2af73ac8ee5852de2919abbd2618af9c15c19b512c4cfc1f9a5d3b6ef009109d
|
rubocop-rails-omakase (1.1.0) sha256=2af73ac8ee5852de2919abbd2618af9c15c19b512c4cfc1f9a5d3b6ef009109d
|
||||||
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
|
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
|
||||||
ruby-vips (2.3.0) sha256=e685ec02c13969912debbd98019e50492e12989282da5f37d05f5471442f5374
|
ruby-vips (2.3.0) sha256=e685ec02c13969912debbd98019e50492e12989282da5f37d05f5471442f5374
|
||||||
|
ruby2_keywords (0.0.5) sha256=ffd13740c573b7301cf7a2e61fc857b2a8e3d3aff32545d6f8300d8bae10e3ef
|
||||||
rubyzip (3.2.2) sha256=c0ed99385f0625415c8f05bcae33fe649ed2952894a95ff8b08f26ca57ea5b3c
|
rubyzip (3.2.2) sha256=c0ed99385f0625415c8f05bcae33fe649ed2952894a95ff8b08f26ca57ea5b3c
|
||||||
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
|
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
|
||||||
selenium-webdriver (4.40.0) sha256=16ef7aa9853c1d4b9d52eac45aafa916e3934c5c83cb4facb03f250adfd15e5b
|
selenium-webdriver (4.41.0) sha256=cdc1173cd55cf186022cea83156cc2d0bec06d337e039b02ad25d94e41bedd22
|
||||||
simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5
|
simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5
|
||||||
simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246
|
simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246
|
||||||
simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428
|
simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428
|
||||||
solid_cable (3.0.12) sha256=a168a54731a455d5627af48d8441ea3b554b8c1f6e6cd6074109de493e6b0460
|
solid_cable (3.0.12) sha256=a168a54731a455d5627af48d8441ea3b554b8c1f6e6cd6074109de493e6b0460
|
||||||
solid_cache (1.0.10) sha256=bc05a2fb3ac78a6f43cbb5946679cf9db67dd30d22939ededc385cb93e120d41
|
solid_cache (1.0.10) sha256=bc05a2fb3ac78a6f43cbb5946679cf9db67dd30d22939ededc385cb93e120d41
|
||||||
solid_queue (1.3.1) sha256=d9580111180c339804ff1a810a7768f69f5dc694d31e86cf1535ff2cd7a87428
|
solid_queue (1.3.2) sha256=44a53047be4255f616ff13fa5d35980e7b3eee6e31d957eadb88fbe8e0db4509
|
||||||
sqlite3 (2.9.0-x86_64-linux-gnu) sha256=72fff9bd750070ba3af695511ba5f0e0a2d8a9206f84869640b3e99dfaf3d5a5
|
sqlite3 (2.9.0-x86_64-linux-gnu) sha256=72fff9bd750070ba3af695511ba5f0e0a2d8a9206f84869640b3e99dfaf3d5a5
|
||||||
sshkit (1.25.0) sha256=c8c6543cdb60f91f1d277306d585dd11b6a064cb44eab0972827e4311ff96744
|
sshkit (1.25.0) sha256=c8c6543cdb60f91f1d277306d585dd11b6a064cb44eab0972827e4311ff96744
|
||||||
stimulus-rails (1.3.4) sha256=765676ffa1f33af64ce026d26b48e8ffb2e0b94e0f50e9119e11d6107d67cb06
|
stimulus-rails (1.3.4) sha256=765676ffa1f33af64ce026d26b48e8ffb2e0b94e0f50e9119e11d6107d67cb06
|
||||||
stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1
|
stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1
|
||||||
thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73
|
thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73
|
||||||
thruster (0.1.17-x86_64-linux) sha256=77b8f335075bd4ece7631dc84a19a710a1e6e7102cbce147b165b45851bdfcd3
|
thruster (0.1.18-x86_64-linux) sha256=0ec1ff5f12289c1ac10cf8e28ce6b5266f4e73416b34a664b79d037c7d955c40
|
||||||
timeout (0.6.0) sha256=6d722ad619f96ee383a0c557ec6eb8c4ecb08af3af62098a0be5057bf00de1af
|
timeout (0.6.0) sha256=6d722ad619f96ee383a0c557ec6eb8c4ecb08af3af62098a0be5057bf00de1af
|
||||||
tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
|
tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
|
||||||
turbo-rails (2.0.23) sha256=ee0d90733aafff056cf51ff11e803d65e43cae258cc55f6492020ec1f9f9315f
|
turbo-rails (2.0.23) sha256=ee0d90733aafff056cf51ff11e803d65e43cae258cc55f6492020ec1f9f9315f
|
||||||
@@ -543,7 +598,7 @@ CHECKSUMS
|
|||||||
websocket-driver (0.8.0) sha256=ed0dba4b943c22f17f9a734817e808bc84cdce6a7e22045f5315aa57676d4962
|
websocket-driver (0.8.0) sha256=ed0dba4b943c22f17f9a734817e808bc84cdce6a7e22045f5315aa57676d4962
|
||||||
websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241
|
websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241
|
||||||
xpath (3.2.0) sha256=6dfda79d91bb3b949b947ecc5919f042ef2f399b904013eb3ef6d20dd3a4082e
|
xpath (3.2.0) sha256=6dfda79d91bb3b949b947ecc5919f042ef2f399b904013eb3ef6d20dd3a4082e
|
||||||
zeitwerk (2.7.4) sha256=2bef90f356bdafe9a6c2bd32bcd804f83a4f9b8bc27f3600fff051eb3edcec8b
|
zeitwerk (2.7.5) sha256=d8da92128c09ea6ec62c949011b00ed4a20242b255293dd66bf41545398f73dd
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
4.0.4
|
4.0.4
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
"Sanasto Wiki" is a web-based dictionary application for simultaneous translators in the living Christianity. The application provides publicly accessible translations while restricting editing and commenting to invited contributors.
|
"Sanasto Wiki" is a web-based glossary for simultaneous translators in the living Christianity. The application provides publicly accessible translations while restricting editing and commenting to invited contributors.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -188,15 +188,25 @@ See 'public/Kristillisyyden sanasto ver 23.5.2013.xlsx'
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## API (Optional Future)
|
## API (Public Sync)
|
||||||
|
|
||||||
REST API for potential mobile app or integration:
|
Public JSON endpoint for syncing entries:
|
||||||
```
|
```
|
||||||
GET /api/entries
|
GET /api/entries
|
||||||
GET /api/entries/:id
|
GET /api/entries?since=2026-01-01T12:00:00Z
|
||||||
GET /api/entries/search?q=:query&lang=:code
|
```
|
||||||
POST /api/entries (authenticated)
|
|
||||||
PATCH /api/entries/:id (authenticated)
|
Responses include all language columns, category and `updated_at`. The optional `since`
|
||||||
|
parameter filters by `updated_at` (ISO8601).
|
||||||
|
|
||||||
|
Swagger docs:
|
||||||
|
```
|
||||||
|
GET /api/swagger
|
||||||
|
```
|
||||||
|
|
||||||
|
Swagger UI:
|
||||||
|
```
|
||||||
|
GET /api
|
||||||
```
|
```
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
require "grape"
|
||||||
|
require "ostruct"
|
||||||
|
|
||||||
|
class Api::Base < Grape::API
|
||||||
|
format :json
|
||||||
|
default_format :json
|
||||||
|
content_type :json, "application/json"
|
||||||
|
|
||||||
|
helpers do
|
||||||
|
def parse_since_param(raw_since)
|
||||||
|
return nil if raw_since.blank?
|
||||||
|
|
||||||
|
Time.iso8601(raw_since)
|
||||||
|
rescue ArgumentError
|
||||||
|
error!({ error: "Invalid since parameter. Use ISO8601 timestamp." }, 400)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
resource :entries do
|
||||||
|
desc "Return public entries in all languages",
|
||||||
|
attributes: OpenStruct.new(success: nil, produces: nil)
|
||||||
|
params do
|
||||||
|
optional :since,
|
||||||
|
type: String,
|
||||||
|
desc: "ISO8601 timestamp. Returns entries updated after this time."
|
||||||
|
end
|
||||||
|
get do
|
||||||
|
since_time = parse_since_param(params[:since])
|
||||||
|
|
||||||
|
entries_scope = Entry.active_entries
|
||||||
|
entries_scope = entries_scope.where("updated_at > ?", since_time) if since_time
|
||||||
|
|
||||||
|
entries_scope
|
||||||
|
.order(:updated_at, :id)
|
||||||
|
.select(
|
||||||
|
:id,
|
||||||
|
:category,
|
||||||
|
:fi,
|
||||||
|
:en,
|
||||||
|
:sv,
|
||||||
|
:no,
|
||||||
|
:ru,
|
||||||
|
:de,
|
||||||
|
:updated_at
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
# config/routes.rb
|
||||||
|
|
||||||
|
# app/controllers/api/swagger_controller.rb
|
||||||
|
module Api
|
||||||
|
class SwaggerController < ApplicationController
|
||||||
|
def index
|
||||||
|
render json: {
|
||||||
|
openapi: "3.0.0",
|
||||||
|
info: {
|
||||||
|
title: "Sanasto Wiki API",
|
||||||
|
description: "Public sync API for Sanasto Wiki glossary entries.",
|
||||||
|
version: "1.0.0"
|
||||||
|
},
|
||||||
|
servers: [
|
||||||
|
{
|
||||||
|
url: "https://#{request.host}",
|
||||||
|
description: "Production server"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
paths: {
|
||||||
|
"/api/entries": {
|
||||||
|
get: {
|
||||||
|
summary: "Return public entries in all languages",
|
||||||
|
description: "Retrieve all active glossary entries with optional filtering by update timestamp",
|
||||||
|
tags: [ "Entries" ],
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: "since",
|
||||||
|
in: "query",
|
||||||
|
description: "ISO8601 timestamp. Returns entries updated after this time.",
|
||||||
|
required: false,
|
||||||
|
schema: {
|
||||||
|
type: "string",
|
||||||
|
format: "date-time",
|
||||||
|
example: "2024-01-01T00:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
responses: {
|
||||||
|
"200": {
|
||||||
|
description: "List of entries",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: {
|
||||||
|
type: "array",
|
||||||
|
items: {
|
||||||
|
"$ref": "#/components/schemas/Entry"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
description: "Invalid since parameter",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
error: { type: "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
schemas: {
|
||||||
|
Entry: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: "integer",
|
||||||
|
description: "Entry ID"
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
type: "string",
|
||||||
|
description: "Entry category"
|
||||||
|
},
|
||||||
|
fi: {
|
||||||
|
type: "string",
|
||||||
|
description: "Finnish translation"
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
type: "string",
|
||||||
|
description: "English translation"
|
||||||
|
},
|
||||||
|
sv: {
|
||||||
|
type: "string",
|
||||||
|
description: "Swedish translation"
|
||||||
|
},
|
||||||
|
no: {
|
||||||
|
type: "string",
|
||||||
|
description: "Norwegian translation"
|
||||||
|
},
|
||||||
|
ru: {
|
||||||
|
type: "string",
|
||||||
|
description: "Russian translation"
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
type: "string",
|
||||||
|
description: "German translation"
|
||||||
|
},
|
||||||
|
updated_at: {
|
||||||
|
type: "string",
|
||||||
|
format: "date-time",
|
||||||
|
description: "Last update timestamp (ISO8601)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
class ApplicationController < ActionController::Base
|
class ApplicationController < ActionController::Base
|
||||||
include BotBlocker
|
include BotBlocker
|
||||||
|
include Pagy::Backend
|
||||||
|
|
||||||
# Changes to the importmap will invalidate the etag for HTML responses
|
# Changes to the importmap will invalidate the etag for HTML responses
|
||||||
stale_when_importmap_changes
|
stale_when_importmap_changes
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ class EntriesController < ApplicationController
|
|||||||
@category = params[:category].presence
|
@category = params[:category].presence
|
||||||
@query = params[:q].to_s.strip
|
@query = params[:q].to_s.strip
|
||||||
@starts_with = params[:starts_with].presence
|
@starts_with = params[:starts_with].presence
|
||||||
@page = [ params[:page].to_i, 1 ].max
|
|
||||||
@per_page = 25
|
|
||||||
|
|
||||||
entries_scope = Entry.active_entries
|
entries_scope = Entry.active_entries
|
||||||
entries_scope = entries_scope.with_category(@category)
|
entries_scope = entries_scope.with_category(@category)
|
||||||
@@ -16,9 +14,8 @@ class EntriesController < ApplicationController
|
|||||||
entries_scope = entries_scope.alphabetical_for(@language_code) if @query.blank? && @starts_with.blank? && @language_code.present?
|
entries_scope = entries_scope.alphabetical_for(@language_code) if @query.blank? && @starts_with.blank? && @language_code.present?
|
||||||
entries_scope = entries_scope.order(created_at: :desc) if entries_scope.order_values.empty?
|
entries_scope = entries_scope.order(created_at: :desc) if entries_scope.order_values.empty?
|
||||||
|
|
||||||
@total_entries = entries_scope.count
|
@pagy, @entries = pagy(entries_scope, items: 25)
|
||||||
@total_pages = (@total_entries.to_f / @per_page).ceil
|
@total_entries = @pagy.count
|
||||||
@entries = entries_scope.offset((@page - 1) * @per_page).limit(@per_page)
|
|
||||||
|
|
||||||
@entry_count = Entry.active_entries.count
|
@entry_count = Entry.active_entries.count
|
||||||
@requested_count = Entry.requested.count
|
@requested_count = Entry.requested.count
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
module ApplicationHelper
|
module ApplicationHelper
|
||||||
|
include Pagy::Frontend
|
||||||
|
|
||||||
def language_name(code)
|
def language_name(code)
|
||||||
supported_languages.find { |l| l.code == code }&.name
|
supported_languages.find { |l| l.code == code }&.name
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -86,17 +86,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-between mt-4 text-sm text-slate-600">
|
<div class="flex items-center justify-between mt-4 text-sm text-slate-600">
|
||||||
<div>Page <%= @page %> of <%= [@total_pages, 1].max %></div>
|
<div><%= pagy_info(@pagy).html_safe %></div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<% previous_page = @page > 1 ? @page - 1 : nil %>
|
<%
|
||||||
<% next_page = @page < @total_pages ? @page + 1 : nil %>
|
pagination_params = { q: @query.presence, category: @category.presence, language: @language_code.presence, starts_with: @starts_with.presence }.compact
|
||||||
<% pagination_params = { q: @query.presence, category: @category.presence, language: @language_code.presence, starts_with: @starts_with.presence }.compact %>
|
prev_url = @pagy.prev ? entries_path(pagination_params.merge(page: @pagy.prev)) : nil
|
||||||
<%= link_to "Previous", previous_page ? entries_path(pagination_params.merge(page: previous_page)) : "#",
|
next_url = @pagy.next ? entries_path(pagination_params.merge(page: @pagy.next)) : nil
|
||||||
class: "px-3 py-1.5 rounded-md border border-slate-200 #{previous_page ? 'hover:border-indigo-300' : 'text-slate-300 cursor-not-allowed'}",
|
%>
|
||||||
data: { turbo_stream: true } %>
|
<%= link_to "Previous", prev_url || "#",
|
||||||
<%= link_to "Next", next_page ? entries_path(pagination_params.merge(page: next_page)) : "#",
|
class: "px-3 py-1.5 rounded-md border border-slate-200 #{'opacity-50 pointer-events-none' unless prev_url}",
|
||||||
class: "px-3 py-1.5 rounded-md border border-slate-200 #{next_page ? 'hover:border-indigo-300' : 'text-slate-300 cursor-not-allowed'}",
|
data: (prev_url ? { turbo_stream: true } : {}) %>
|
||||||
data: { turbo_stream: true } %>
|
<%= link_to "Next", next_url || "#",
|
||||||
|
class: "px-3 py-1.5 rounded-md border border-slate-200 #{'opacity-50 pointer-events-none' unless next_url}",
|
||||||
|
data: (next_url ? { turbo_stream: true } : {}) %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,10 +14,10 @@
|
|||||||
<%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
|
<%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
|
||||||
<%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
|
<%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
|
||||||
|
|
||||||
<link rel="icon" href="/favicon.ico" type="image/x-icon">
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<link rel="icon" href="/icon.svg" type="image/svg+xml">
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" />
|
||||||
<link rel="icon" href="/icon.png" type="image/png">
|
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||||
<link rel="apple-touch-icon" href="/icon.png">
|
<link rel="apple-touch-icon" href="/icon-512.png" />
|
||||||
|
|
||||||
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
||||||
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
|
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
"name": "SanastoWiki",
|
"name": "SanastoWiki",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/icon.png",
|
"src": "/icon-512.png",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"sizes": "512x512"
|
"sizes": "512x512"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/icon.png",
|
"src": "/icon-512.png",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"purpose": "maskable"
|
"purpose": "maskable"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
require_relative "boot"
|
require_relative "boot"
|
||||||
|
|
||||||
require "rails/all"
|
require "rails/all"
|
||||||
|
require_relative "../lib/middleware/sanasto_cors"
|
||||||
|
|
||||||
# Require the gems listed in Gemfile, including any gems
|
# Require the gems listed in Gemfile, including any gems
|
||||||
# you've limited to :test, :development, or :production.
|
# you've limited to :test, :development, or :production.
|
||||||
@@ -24,5 +25,7 @@ module SanastoWiki
|
|||||||
# config.time_zone = "Central Time (US & Canada)"
|
# config.time_zone = "Central Time (US & Canada)"
|
||||||
# config.eager_load_paths << Rails.root.join("extras")
|
# config.eager_load_paths << Rails.root.join("extras")
|
||||||
config.active_record.schema_format = :sql
|
config.active_record.schema_format = :sql
|
||||||
|
|
||||||
|
config.middleware.insert_before 0, Middleware::SanastoCors
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ development:
|
|||||||
# Do not set this db to the same as development or production.
|
# Do not set this db to the same as development or production.
|
||||||
test:
|
test:
|
||||||
<<: *default
|
<<: *default
|
||||||
database: storage/test.sqlite3
|
database: storage/test<%= ENV["TEST_ENV_NUMBER"] %>.sqlite3
|
||||||
|
|
||||||
# Store production database in the storage/ directory, which by default
|
# Store production database in the storage/ directory, which by default
|
||||||
# is mounted as a persistent Docker volume in config/deploy.yml.
|
# is mounted as a persistent Docker volume in config/deploy.yml.
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ servers:
|
|||||||
# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
|
# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
|
||||||
proxy:
|
proxy:
|
||||||
ssl: true
|
ssl: true
|
||||||
host: sanasto.rin.no
|
hosts:
|
||||||
|
- sanasto.rin.no
|
||||||
|
- sanasto.wiki
|
||||||
# Kamal proxy will forward to your app on port 3000
|
# Kamal proxy will forward to your app on port 3000
|
||||||
|
|
||||||
# Credentials for your image host.
|
# Credentials for your image host.
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ Rails.application.configure do
|
|||||||
# Make template changes take effect immediately.
|
# Make template changes take effect immediately.
|
||||||
config.action_mailer.perform_caching = false
|
config.action_mailer.perform_caching = false
|
||||||
|
|
||||||
|
# You can use Mailpit for SMTP in development - https://github.com/axllent/mailpit
|
||||||
config.action_mailer.delivery_method = :smtp
|
config.action_mailer.delivery_method = :smtp
|
||||||
config.action_mailer.smtp_settings = {
|
config.action_mailer.smtp_settings = {
|
||||||
address: "localhost",
|
address: "localhost",
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ Rails.application.configure do
|
|||||||
# config.action_mailer.raise_delivery_errors = false
|
# config.action_mailer.raise_delivery_errors = false
|
||||||
|
|
||||||
# Set host to be used by links generated in mailer templates.
|
# Set host to be used by links generated in mailer templates.
|
||||||
config.action_mailer.default_url_options = { host: "sanasto.rin.no" }
|
config.action_mailer.default_url_options = { host: "sanasto.wiki" }
|
||||||
|
|
||||||
# Specify outgoing SMTP server. Remember to add smtp/* credentials via bin/rails credentials:edit.
|
# Specify outgoing SMTP server. Remember to add smtp/* credentials via bin/rails credentials:edit.
|
||||||
config.action_mailer.smtp_settings = {
|
config.action_mailer.smtp_settings = {
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Pagy Configuration
|
||||||
|
require "pagy/extras/overflow"
|
||||||
|
|
||||||
|
Pagy::DEFAULT[:items] = 25 # Match current 25 items per page
|
||||||
|
Pagy::DEFAULT[:page_param] = :page
|
||||||
|
Pagy::DEFAULT[:overflow] = :last_page
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
Rswag::Ui.configure do |config|
|
||||||
|
config.swagger_endpoint "/api/swagger", "Sanasto Wiki API"
|
||||||
|
end
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
Rails.application.routes.draw do
|
Rails.application.routes.draw do
|
||||||
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
|
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
|
||||||
|
|
||||||
|
mount Api::Base => "/api"
|
||||||
|
get "/api/swagger", to: "api/swagger#index"
|
||||||
|
mount Rswag::Ui::Engine => "/api"
|
||||||
|
|
||||||
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
|
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
|
||||||
# Can be used by load balancers and uptime monitors to verify that the app is live.
|
# Can be used by load balancers and uptime monitors to verify that the app is live.
|
||||||
get "up" => "rails/health#show", as: :rails_health_check
|
get "up" => "rails/health#show", as: :rails_health_check
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# Deployment (Kamal)
|
||||||
|
|
||||||
|
We deploy with Kamal. You do NOT need to manually set up web servers or Docker
|
||||||
|
on the VM. `kamal setup` provisions everything.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Local machine
|
||||||
|
- Ruby + Bundler
|
||||||
|
- Docker (for building images)
|
||||||
|
- SSH key access to the VM
|
||||||
|
- Registry credentials (see `config/deploy.yml`)
|
||||||
|
|
||||||
|
### Remote VM
|
||||||
|
- Bare VM with SSH access
|
||||||
|
- Open ports: 22, 80, 443
|
||||||
|
- A domain name pointing at the VM (for SSL)
|
||||||
|
|
||||||
|
## Configure
|
||||||
|
|
||||||
|
Update `config/deploy.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
service: sanasto-wiki
|
||||||
|
image: your-registry/sanasto-wiki
|
||||||
|
|
||||||
|
servers:
|
||||||
|
web:
|
||||||
|
- your-server-ip
|
||||||
|
|
||||||
|
proxy:
|
||||||
|
ssl: true
|
||||||
|
host: sanasto.example.com
|
||||||
|
|
||||||
|
registry:
|
||||||
|
server: ghcr.io
|
||||||
|
username: your-github-username
|
||||||
|
|
||||||
|
ssh:
|
||||||
|
user: deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
Set the registry password:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export KAMAL_REGISTRY_PASSWORD="your-token"
|
||||||
|
```
|
||||||
|
|
||||||
|
Make sure `config/master.key` is present locally.
|
||||||
|
|
||||||
|
## First deploy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bundle exec kamal setup
|
||||||
|
```
|
||||||
|
|
||||||
|
## Regular deploys
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bundle exec kamal deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Useful commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bundle exec kamal app logs --follow
|
||||||
|
bundle exec kamal app details
|
||||||
|
bundle exec kamal app exec --interactive --reuse "bin/rails console"
|
||||||
|
bundle exec kamal rollback
|
||||||
|
```
|
||||||
|
|
||||||
|
## Initial app setup
|
||||||
|
|
||||||
|
Visit `https://your-domain/setup` once after the first deploy to create the
|
||||||
|
admin account.
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
module Middleware
|
||||||
|
class SanastoCors
|
||||||
|
ALLOWED_APP_ID = ENV.fetch("SANASTO_APP_ID", "app.sanasto").freeze
|
||||||
|
APP_ID_HEADER = "HTTP_X_SANASTO_APP"
|
||||||
|
|
||||||
|
def initialize(app)
|
||||||
|
@app = app
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(env)
|
||||||
|
if allow_cors_for?(env)
|
||||||
|
if env["REQUEST_METHOD"] == "OPTIONS"
|
||||||
|
return preflight_response(env["HTTP_ORIGIN"], allowed_request_headers(env))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
status, headers, body = @app.call(env)
|
||||||
|
if allow_cors_for?(env)
|
||||||
|
apply_cors_headers(headers, env["HTTP_ORIGIN"])
|
||||||
|
end
|
||||||
|
[ status, headers, body ]
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def allow_cors_for?(env)
|
||||||
|
origin = env["HTTP_ORIGIN"].to_s
|
||||||
|
return false if origin.empty?
|
||||||
|
|
||||||
|
if env["REQUEST_METHOD"] == "OPTIONS"
|
||||||
|
return preflight_includes_app_id_header?(env)
|
||||||
|
end
|
||||||
|
|
||||||
|
app_id = env[APP_ID_HEADER].to_s
|
||||||
|
return false if app_id.empty?
|
||||||
|
|
||||||
|
app_id == ALLOWED_APP_ID
|
||||||
|
end
|
||||||
|
|
||||||
|
def preflight_includes_app_id_header?(env)
|
||||||
|
access_control_headers = env["HTTP_ACCESS_CONTROL_REQUEST_HEADERS"].to_s
|
||||||
|
return false if access_control_headers.empty?
|
||||||
|
|
||||||
|
access_control_headers
|
||||||
|
.split(",")
|
||||||
|
.map { |header_name| header_name.strip.downcase }
|
||||||
|
.include?("x-sanasto-app")
|
||||||
|
end
|
||||||
|
|
||||||
|
def allowed_request_headers(env)
|
||||||
|
access_control_headers = env["HTTP_ACCESS_CONTROL_REQUEST_HEADERS"].to_s
|
||||||
|
return default_allowed_headers if access_control_headers.empty?
|
||||||
|
|
||||||
|
sanitized = access_control_headers
|
||||||
|
.split(",")
|
||||||
|
.map { |header_name| header_name.strip }
|
||||||
|
.reject(&:empty?)
|
||||||
|
.join(", ")
|
||||||
|
|
||||||
|
sanitized.empty? ? default_allowed_headers : sanitized
|
||||||
|
end
|
||||||
|
|
||||||
|
def preflight_response(origin, allowed_headers)
|
||||||
|
headers = {}
|
||||||
|
apply_cors_headers(headers, origin, allowed_headers)
|
||||||
|
headers["Access-Control-Max-Age"] = "86400"
|
||||||
|
headers["Vary"] = [
|
||||||
|
headers["Vary"],
|
||||||
|
"Access-Control-Request-Headers",
|
||||||
|
"Access-Control-Request-Method"
|
||||||
|
].compact.join(", ")
|
||||||
|
[ 204, headers, [] ]
|
||||||
|
end
|
||||||
|
|
||||||
|
def apply_cors_headers(headers, origin, allowed_headers = default_allowed_headers)
|
||||||
|
headers["Access-Control-Allow-Origin"] = origin
|
||||||
|
headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, PATCH, DELETE, OPTIONS"
|
||||||
|
headers["Access-Control-Allow-Headers"] = allowed_headers
|
||||||
|
headers["Vary"] = [ headers["Vary"], "Origin, X-Sanasto-App" ].compact.join(", ")
|
||||||
|
end
|
||||||
|
|
||||||
|
def default_allowed_headers
|
||||||
|
"Origin, Content-Type, Accept, Authorization, X-Sanasto-App"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 23 KiB |
@@ -41,7 +41,8 @@ class EntriesControllerTest < ActionDispatch::IntegrationTest
|
|||||||
test "should paginate results" do
|
test "should paginate results" do
|
||||||
get entries_url, params: { page: 2 }
|
get entries_url, params: { page: 2 }
|
||||||
assert_response :success
|
assert_response :success
|
||||||
assert_select "div", text: /Page 2 of/i
|
assert_select "a", text: "Previous"
|
||||||
|
assert_select "a", text: "Next"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "should handle invalid language code" do
|
test "should handle invalid language code" do
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class ApiEntriesTest < ActionDispatch::IntegrationTest
|
||||||
|
test "returns active entries with language fields" do
|
||||||
|
entry = entries(:one)
|
||||||
|
|
||||||
|
get "/api/entries"
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
payload = JSON.parse(response.body)
|
||||||
|
entry_payload = payload.find { |item| item["id"] == entry.id }
|
||||||
|
|
||||||
|
assert_not_nil entry_payload
|
||||||
|
assert_equal entry.fi, entry_payload["fi"]
|
||||||
|
assert_equal entry.en, entry_payload["en"]
|
||||||
|
assert_equal entry.sv, entry_payload["sv"]
|
||||||
|
assert_equal entry.no, entry_payload["no"]
|
||||||
|
assert_equal entry.ru, entry_payload["ru"]
|
||||||
|
assert_equal entry.de, entry_payload["de"]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "filters by updated_at when since param is provided" do
|
||||||
|
older_entry = Entry.create!(
|
||||||
|
fi: "Older Entry",
|
||||||
|
category: :word,
|
||||||
|
status: :active
|
||||||
|
)
|
||||||
|
older_entry.update_column(:updated_at, 2.days.ago)
|
||||||
|
|
||||||
|
newer_entry = Entry.create!(
|
||||||
|
fi: "Newer Entry",
|
||||||
|
category: :word,
|
||||||
|
status: :active
|
||||||
|
)
|
||||||
|
newer_entry.update_column(:updated_at, 1.hour.ago)
|
||||||
|
|
||||||
|
get "/api/entries", params: { since: 1.day.ago.iso8601 }
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
payload = JSON.parse(response.body)
|
||||||
|
returned_ids = payload.map { |item| item["id"] }
|
||||||
|
|
||||||
|
assert_includes returned_ids, newer_entry.id
|
||||||
|
assert_not_includes returned_ids, older_entry.id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns bad request for invalid since param" do
|
||||||
|
get "/api/entries", params: { since: "not-a-time" }
|
||||||
|
|
||||||
|
assert_response :bad_request
|
||||||
|
payload = JSON.parse(response.body)
|
||||||
|
assert_equal "Invalid since parameter. Use ISO8601 timestamp.", payload["error"]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "exposes swagger docs" do
|
||||||
|
get "/api/swagger"
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
payload = JSON.parse(response.body)
|
||||||
|
assert_equal "Sanasto Wiki API", payload.dig("info", "title")
|
||||||
|
assert_includes payload.keys, "paths"
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class CorsPreflightTest < ActionDispatch::IntegrationTest
|
||||||
|
setup do
|
||||||
|
@origin = "http://localhost:5173"
|
||||||
|
@app_id = "app.sanasto"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "options preflight returns cors headers when app header is requested" do
|
||||||
|
options "/api/entries", headers: {
|
||||||
|
"Origin" => @origin,
|
||||||
|
"Access-Control-Request-Method" => "GET",
|
||||||
|
"Access-Control-Request-Headers" => "x-sanasto-app"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_response :no_content
|
||||||
|
assert_equal @origin, response.headers["Access-Control-Allow-Origin"]
|
||||||
|
assert_includes response.headers["Access-Control-Allow-Headers"], "x-sanasto-app"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "get includes cors headers when app id is provided" do
|
||||||
|
get "/api/entries", headers: {
|
||||||
|
"Origin" => @origin,
|
||||||
|
"X-Sanasto-App" => @app_id
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
assert_equal @origin, response.headers["Access-Control-Allow-Origin"]
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class SanastoCorsTest < ActiveSupport::TestCase
|
||||||
|
def setup
|
||||||
|
@app = ->(_env) { [ 200, {}, [ "ok" ] ] }
|
||||||
|
@middleware = Middleware::SanastoCors.new(@app)
|
||||||
|
@origin = "http://localhost:5173"
|
||||||
|
@app_id = "app.sanasto"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "adds cors headers for allowed get requests" do
|
||||||
|
env = Rack::MockRequest.env_for(
|
||||||
|
"/api/entries",
|
||||||
|
method: "GET",
|
||||||
|
"HTTP_ORIGIN" => @origin,
|
||||||
|
"HTTP_X_SANASTO_APP" => @app_id
|
||||||
|
)
|
||||||
|
|
||||||
|
status, headers, _body = @middleware.call(env)
|
||||||
|
|
||||||
|
assert_equal 200, status
|
||||||
|
assert_equal @origin, headers["Access-Control-Allow-Origin"]
|
||||||
|
assert_includes headers["Access-Control-Allow-Headers"], "X-Sanasto-App"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns preflight response with cors headers when app header is requested" do
|
||||||
|
env = Rack::MockRequest.env_for(
|
||||||
|
"/api/entries",
|
||||||
|
method: "OPTIONS",
|
||||||
|
"HTTP_ORIGIN" => @origin,
|
||||||
|
"HTTP_ACCESS_CONTROL_REQUEST_METHOD" => "GET",
|
||||||
|
"HTTP_ACCESS_CONTROL_REQUEST_HEADERS" => "x-sanasto-app"
|
||||||
|
)
|
||||||
|
|
||||||
|
status, headers, _body = @middleware.call(env)
|
||||||
|
|
||||||
|
assert_equal 204, status
|
||||||
|
assert_equal @origin, headers["Access-Control-Allow-Origin"]
|
||||||
|
assert_includes headers["Access-Control-Allow-Headers"], "x-sanasto-app"
|
||||||
|
assert_includes headers["Vary"], "Access-Control-Request-Headers"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not add cors headers when app id is missing" do
|
||||||
|
env = Rack::MockRequest.env_for(
|
||||||
|
"/api/entries",
|
||||||
|
method: "GET",
|
||||||
|
"HTTP_ORIGIN" => @origin
|
||||||
|
)
|
||||||
|
|
||||||
|
status, headers, _body = @middleware.call(env)
|
||||||
|
|
||||||
|
assert_equal 200, status
|
||||||
|
assert_nil headers["Access-Control-Allow-Origin"]
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -102,7 +102,7 @@ class PublicBrowsingTest < ApplicationSystemTestCase
|
|||||||
visit root_path
|
visit root_path
|
||||||
|
|
||||||
assert_selector ".entry-row", count: 25
|
assert_selector ".entry-row", count: 25
|
||||||
assert_link "2"
|
assert_link "Next"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "visitor sees entry statistics" do
|
test "visitor sees entry statistics" do
|
||||||
|
|||||||