SQLite's Arel visitor wraps CTE branches in extra parentheses, making
the UNION ALL inside recursive CTEs invalid. Also SQLite lacks LPAD()
and CAST(... AS BINARY). Fix by using the existing pathname column for
ordering on SQLite, which already encodes the hierarchical path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace MySQL-specific LPAD() with SQLite's format() for zero-padded
row numbering, and skip CAST(... AS BINARY) on SQLite where string
comparisons are already binary by default.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without this guard, the last admin in the system could delete their own
account, making the application unmanageable. This adds a model method
`User#sole_admin?`, a controller guard in `RegistrationsController#destroy`,
and disables the delete button in the profile edit view when the current
user is the only remaining admin.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Test infrastructure:
- Allow www.example.com host in test env (ActionDispatch::HostAuthorization
was blocking all integration test requests)
- Include Devise::Test::IntegrationHelpers in ActionDispatch::IntegrationTest
so tests can sign in with sign_in(user)
Controller tests:
- Rewrite UsersControllerTest to match actual routes/actions (no new/create/
edit/destroy); sign in as admin; test update-self rejection via turbo_stream
- Fix Default::UnitsControllerTest to sign in before requesting the index
SQLite compatibility in Unit#defaults_diff:
- Hoist the inner "units" CTE to the outer WITH RECURSIVE level (fixes nested
WITH syntax error) — this was the existing TODO in the code
- Use Unit.joins(...) for the recursive part instead of a raw Arel::SelectManager
so the SQLite visitor does not wrap it in parentheses inside UNION ALL
- Drop the named "units" CTE (conflicts with the table name under WITH RECURSIVE
in SQLite); apply the user/defaults scope directly on the base case
- Qualify GROUP BY columns to avoid ambiguity when bases_units is joined
- Qualify ORDER BY :multiplier/:symbol to avoid ambiguity (Unit.ordering)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the CLI-only setup (db:seed + manual application.rb edits)
with a web wizard shown automatically on first visit when no admin
account exists yet.
SetupController (GET/POST /setup) collects the admin e-mail and
password, a "skip e-mail confirmation" toggle, and an option to
seed the built-in default units. Once submitted it creates the
admin User, persists the chosen options as Setting records, and
redirects to the sign-in page.
ApplicationController gains a redirect_to_setup_if_needed
before_action that catches every request (including Devise routes)
when no admin exists, so a fresh installation always lands on the
wizard rather than an empty sign-in form.
A new Setting model provides a lightweight key-value store for
runtime options that were previously hard-coded in application.rb
(e.g. skip_email_confirmation). RegistrationsController now reads
that flag from the database instead of from the application config.
Seeds.rb is kept for headless / automated deployments and skips
admin creation when an admin already exists (idempotent), with a
comment pointing to the web wizard as the preferred path.
Also extends the SQLite nil-limit fix (|| Float::INFINITY) to the
Quantity model, which suffered the same ArgumentError as Unit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduce config.skip_email_confirmation in application.rb.dist.
When set to true, new registrations are automatically confirmed
without requiring email verification — useful for installations
where outgoing email is not configured or for development/testing.
Implemented by calling skip_confirmation! in build_resource before
the record is saved, so no confirmation email is ever sent.
Also fix ArgumentError raised in length validations when
type_for_attribute(:column).limit returns nil, which happens with
SQLite for string columns that have no explicit limit in the
migration. Guard with || Float::INFINITY so the validation is
effectively skipped when the database imposes no limit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Separate scope only provided some optimisation by reducing the count of
records :numbered and should be unnecessary in future once numbering
can be merged with ordering into one recursive query
Base symbol was displayed twice when it existed as default and
non-default and both of them had at least one subunit.
Also: sorting by base_id yielded non-alphabetic order in such case.