From a6ac17b30e720bb1f3e3cdac3f421d9b7c26247b Mon Sep 17 00:00:00 2001 From: Albert Stefanov Date: Sat, 10 Jan 2026 08:57:13 +0200 Subject: [PATCH 01/19] Remove capistrano --- Gemfile | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Gemfile b/Gemfile index 4e6060f..dcdba69 100644 --- a/Gemfile +++ b/Gemfile @@ -86,13 +86,6 @@ gem "matrix" gem "rack-cors" group :development do - # Use Capistrano for deployment - gem "capistrano" - gem "capistrano-rails" - gem "capistrano-bundler" - gem "capistrano-rbenv" - gem "net-ssh" - gem "pry" gem "pry-rails" gem "pry-doc" From 385cf5e0b9cde8dd85e1d5ffd4163fc7a72775aa Mon Sep 17 00:00:00 2001 From: Albert Stefanov Date: Sat, 10 Jan 2026 09:30:08 +0200 Subject: [PATCH 02/19] Remove GPG signing from emails This brings out a lot of infrastructure for pretty much no benefit. No current members use the GPG signing stuff, and it has a lot of native dependencies. Also note that 19b1860 for some reason didn't update the db/schema.rb, which broke migrations on new apps... --- .github/workflows/rspec.yml | 6 - Gemfile | 3 - app/controllers/application_controller.rb | 1 - app/mailers/authentication_mailer.rb | 3 - app/models/user.rb | 8 -- app/views/devise/registrations/edit.html.slim | 1 - config/locales/bg.yml | 3 - config/locales/en.yml | 3 - config/locales/simple_form.bg.yml | 1 - config/locales/simple_form.en.yml | 1 - ...72625_remove_gpg_fingerprint_from_users.rb | 5 + db/schema.rb | 119 +++++++++--------- spec/factories/users.rb | 1 - spec/models/user_spec.rb | 20 --- 14 files changed, 64 insertions(+), 111 deletions(-) create mode 100644 db/migrate/20260110072625_remove_gpg_fingerprint_from_users.rb diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index d848722..f9ffa6f 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -44,20 +44,14 @@ jobs: env: RAILS_ENV: test run: bundle exec bin/rails db:schema:load - - name: Set up GPG - env: - GNUPGHOME: tmp/fauna_fake_gpg_home - run: mkdir tmp/fauna_fake_gpg_home && gpg -K && gpg --gen-key --batch spec/support/test_gpg_key_generation.batch - name: Set up test user env: RAILS_ENV: test - GNUPGHOME: tmp/fauna_fake_gpg_home run: bundle exec bin/setup -u admin -e admin@example.com -n admin -p foobarbaz - name: Build and test with rspec env: - GNUPGHOME: tmp/fauna_fake_gpg_home RAILS_ENV: test run: bundle exec rspec diff --git a/Gemfile b/Gemfile index dcdba69..0ce51ba 100644 --- a/Gemfile +++ b/Gemfile @@ -61,9 +61,6 @@ gem "kaminari" gem "rolify" gem "pundit" -# GPG Signing -gem "mail-gpg" - # Asynchronous job execution gem "delayed_job" gem "delayed_job_active_record" diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 6f91d87..26221f0 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -37,7 +37,6 @@ def configure_permitted_parameters :email, :url, :locale, :twitter, :announce_my_presence, :github, :jabber, - :gpg_fingerprint, :pin, :pin_confirmation, phone_numbers_attributes: [ :_destroy, diff --git a/app/mailers/authentication_mailer.rb b/app/mailers/authentication_mailer.rb index 378aa32..1283a3b 100644 --- a/app/mailers/authentication_mailer.rb +++ b/app/mailers/authentication_mailer.rb @@ -1,5 +1,2 @@ class AuthenticationMailer < Devise::Mailer - def mail(headers = {}, &block) - super(headers.merge(gpg: {sign: true}), &block) - end end diff --git a/app/models/user.rb b/app/models/user.rb index 7750056..ec9913b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -20,13 +20,11 @@ class User < ApplicationRecord validates :last_name, presence: true validates :github, format: {with: /\A[a-z0-9][a-z0-9-]{,38}\z/i}, allow_blank: true validates :jabber, format: {with: /\A[^@]+@[^@]+\z/}, allow_blank: true - validates :gpg_fingerprint, format: {with: /\A[0-9a-f]{4}( ?)([0-9a-f]{4}\1){4}\1{0,2}([0-9a-f]{4}\1){4}[0-9a-f]{4}\z/i}, allow_blank: true validates :pin, numericality: true, length: {minimum: 6}, allow_blank: true, confirmation: true validates :locale, presence: true, inclusion: {in: I18n.available_locales.map(&:to_s)} accepts_nested_attributes_for :phone_numbers, update_only: true, allow_destroy: true, reject_if: :all_blank attr_accessor :login - after_validation :normalize_gpg_fingerprint def self.find_for_database_authentication(warden_conditions) conditions = warden_conditions.dup @@ -64,10 +62,4 @@ def pin=(pin) def to_s "User(id: #{id}, email: #{email}, name: #{name})" end - - private - - def normalize_gpg_fingerprint - self.gpg_fingerprint = gpg_fingerprint.delete(" ").upcase.gsub(/([0-9a-f]{4})/i, '\1 ').strip if gpg_fingerprint.present? - end end diff --git a/app/views/devise/registrations/edit.html.slim b/app/views/devise/registrations/edit.html.slim index f6775f4..f2bf9bb 100644 --- a/app/views/devise/registrations/edit.html.slim +++ b/app/views/devise/registrations/edit.html.slim @@ -44,7 +44,6 @@ section.container = f.input :twitter = f.input :url = f.input :jabber - = f.input :gpg_fingerprint = f.input :announce_my_presence, hint: t('views.registrations.announce_my_presence_explanation') = f.input :current_password, hint: t('views.registrations.we_need_your_current_password'), required: true, wrapper_html: {class: 'has-warning'} diff --git a/config/locales/bg.yml b/config/locales/bg.yml index 7c01bad..b2d865f 100644 --- a/config/locales/bg.yml +++ b/config/locales/bg.yml @@ -14,8 +14,6 @@ bg: invalid: трябва да съдържа „@“ точно веднъж github: invalid: трябва да съдържа по-малко от 40 латински букви, цифри или „-“ и да не започва с „-“ - gpg_fingerprint: - invalid: трябва да съдържа точно 40 цифри или латински букви от „A“ до „F“ network_device: attributes: mac_address: @@ -51,7 +49,6 @@ bg: password_confirmation: Потвърждение на паролата current_password: Текуща парола announce_my_presence: Показвай името ми, когато съм в Лаба и когато отварям вратата - gpg_fingerprint: Отпечатък на GPG ключ jabber: Jabber акаунт picture: Снимка github: Github акаунт diff --git a/config/locales/en.yml b/config/locales/en.yml index 82f339d..b3565c0 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -14,8 +14,6 @@ en: invalid: must contain "@" exactly once github: invalid: must contain less than 40 latin letters, numbers, or "-", and not begin with a "-" - gpg_fingerprint: - invalid: must to contain exactly 40 digits or the latin leters from "A" to "F" network_device: attributes: mac_address: @@ -45,7 +43,6 @@ en: password_confirmation: Password confirmation current_password: Current password announce_my_presence: Show my name when I'm at the Lab and when I open the doors - gpg_fingerprint: GPG key fingerprint jabber: Jabber/XMPP account picture: Photo github: Github account diff --git a/config/locales/simple_form.bg.yml b/config/locales/simple_form.bg.yml index d4754b1..36d7bd0 100644 --- a/config/locales/simple_form.bg.yml +++ b/config/locales/simple_form.bg.yml @@ -16,7 +16,6 @@ bg: twitter: потребителско име с или без „@“ url: URL, започващ с „http://“ или „https://“ jabber: Jabber акаунт във формат „username@domain.com“ - gpg_fingerprint: Отпечатък на GPG ключ с или без интервали pin: използва се за отключване на вратата и трябва да е число с поне 6 цифри; оставете празно, ако не искате да го променяте phone_numbers: phone_number: телефонен номер с или без код за държава diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index acfbf84..49ca2ec 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -13,7 +13,6 @@ en: twitter: username with optional "@" url: URL beginning with http:// or https:// jabber: Jabber account in the "username@domain.com" format - gpg_fingerprint: GPG fingerprint with or without spaces pin: it is used to lock/unlock the doors and must be a number with at least 6 digits; leave empty if you do not want to change it phone_numbers: phone_number: phone number with optional country code diff --git a/db/migrate/20260110072625_remove_gpg_fingerprint_from_users.rb b/db/migrate/20260110072625_remove_gpg_fingerprint_from_users.rb new file mode 100644 index 0000000..af0be00 --- /dev/null +++ b/db/migrate/20260110072625_remove_gpg_fingerprint_from_users.rb @@ -0,0 +1,5 @@ +class RemoveGpgFingerprintFromUsers < ActiveRecord::Migration[7.0] + def change + remove_column :users, :gpg_fingerprint, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index aad0e9e..0c5e408 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,164 +10,163 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_01_21_190249) do +ActiveRecord::Schema[7.0].define(version: 2026_01_10_072625) do create_table "audit_log_entries", force: :cascade do |t| - t.string "type" - t.integer "user_id" - t.json "payload" t.datetime "created_at", null: false + t.json "payload" + t.string "type" t.datetime "updated_at", null: false + t.integer "user_id" t.index ["type"], name: "index_audit_log_entries_on_type" t.index ["user_id"], name: "index_audit_log_entries_on_user_id" end create_table "delayed_jobs", force: :cascade do |t| - t.integer "priority", default: 0, null: false t.integer "attempts", default: 0, null: false + t.datetime "created_at", precision: nil + t.datetime "failed_at", precision: nil t.text "handler", null: false t.text "last_error" - t.datetime "run_at", precision: nil t.datetime "locked_at", precision: nil - t.datetime "failed_at", precision: nil t.string "locked_by" + t.integer "priority", default: 0, null: false t.string "queue" - t.datetime "created_at", precision: nil + t.datetime "run_at", precision: nil t.datetime "updated_at", precision: nil t.index ["priority", "run_at"], name: "delayed_jobs_priority" end create_table "door_actions", force: :cascade do |t| - t.string "type" + t.datetime "created_at", precision: nil, null: false + t.boolean "execution_succeeded" t.integer "initiator_id" t.text "origin_information" - t.datetime "created_at", precision: nil, null: false + t.string "type" t.datetime "updated_at", precision: nil, null: false - t.boolean "execution_succeeded" t.index ["initiator_id"], name: "index_door_actions_on_initiator_id" t.index ["type"], name: "index_door_actions_on_type" end create_table "door_status_notifications", force: :cascade do |t| + t.datetime "created_at", precision: nil, null: false t.string "door" t.string "latch" - t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false end create_table "log_entries", force: :cascade do |t| + t.datetime "created_at", precision: nil, null: false t.integer "loggable_id", null: false t.string "loggable_type", null: false - t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false end create_table "network_devices", force: :cascade do |t| - t.integer "owner_id", null: false - t.string "mac_address" t.datetime "created_at", precision: nil, null: false + t.string "description" + t.string "mac_address" + t.integer "owner_id", null: false t.datetime "updated_at", precision: nil, null: false t.boolean "use_for_presence", default: true, null: false - t.string "description" t.index ["mac_address"], name: "index_network_devices_on_mac_address" t.index ["owner_id"], name: "index_network_devices_on_owner_id" end create_table "oauth_access_grants", force: :cascade do |t| - t.integer "resource_owner_id", null: false t.integer "application_id", null: false - t.string "token", null: false + t.string "code_challenge" + t.string "code_challenge_method" + t.datetime "created_at", precision: nil, null: false t.integer "expires_in", null: false t.text "redirect_uri", null: false - t.datetime "created_at", precision: nil, null: false + t.integer "resource_owner_id", null: false t.datetime "revoked_at", precision: nil t.string "scopes" - t.string "code_challenge" - t.string "code_challenge_method" + t.string "token", null: false t.index ["token"], name: "index_oauth_access_grants_on_token", unique: true end create_table "oauth_access_tokens", force: :cascade do |t| - t.integer "resource_owner_id" t.integer "application_id" - t.string "token", null: false - t.string "refresh_token" + t.datetime "created_at", precision: nil, null: false t.integer "expires_in" + t.string "previous_refresh_token", default: "", null: false + t.string "refresh_token" + t.integer "resource_owner_id" t.datetime "revoked_at", precision: nil - t.datetime "created_at", precision: nil, null: false t.string "scopes" - t.string "previous_refresh_token", default: "", null: false + t.string "token", null: false t.index ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true t.index ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id" t.index ["token"], name: "index_oauth_access_tokens_on_token", unique: true end create_table "oauth_applications", force: :cascade do |t| + t.boolean "confidential", default: true, null: false + t.datetime "created_at", precision: nil, null: false t.string "name", null: false - t.string "uid", null: false - t.string "secret", null: false + t.integer "owner_id" + t.string "owner_type" t.text "redirect_uri", null: false t.string "scopes", default: "", null: false - t.datetime "created_at", precision: nil, null: false + t.string "secret", null: false + t.string "uid", null: false t.datetime "updated_at", precision: nil, null: false - t.integer "owner_id" - t.string "owner_type" - t.boolean "confidential", default: true, null: false t.index ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type" t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true end create_table "phone_numbers", force: :cascade do |t| + t.datetime "created_at", precision: nil, null: false t.integer "owner_id", null: false t.string "phone_number" - t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false t.index ["owner_id"], name: "index_phone_numbers_on_owner_id" t.index ["phone_number"], name: "index_phone_numbers_on_phone_number" end create_table "roles", force: :cascade do |t| + t.datetime "created_at", precision: nil, null: false t.string "name" - t.string "resource_type" t.integer "resource_id" - t.datetime "created_at", precision: nil, null: false + t.string "resource_type" t.datetime "updated_at", precision: nil, null: false t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id" t.index ["name"], name: "index_roles_on_name" end create_table "users", force: :cascade do |t| - t.string "email", default: "", null: false - t.string "encrypted_password", default: "", null: false - t.string "reset_password_token" - t.datetime "reset_password_sent_at", precision: nil - t.datetime "remember_created_at", precision: nil - t.integer "sign_in_count", default: 0, null: false - t.datetime "current_sign_in_at", precision: nil - t.datetime "last_sign_in_at", precision: nil - t.string "current_sign_in_ip" - t.string "last_sign_in_ip" - t.string "first_name" - t.string "last_name" - t.string "url" - t.string "twitter" t.boolean "announce_my_presence", default: false, null: false - t.datetime "created_at", precision: nil, null: false - t.datetime "updated_at", precision: nil, null: false t.boolean "can_unlock_door", default: false, null: false - t.string "username" + t.datetime "confirmation_sent_at", precision: nil t.string "confirmation_token" t.datetime "confirmed_at", precision: nil - t.datetime "confirmation_sent_at", precision: nil - t.string "unconfirmed_email" + t.datetime "created_at", precision: nil, null: false + t.datetime "current_sign_in_at", precision: nil + t.string "current_sign_in_ip" + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false + t.string "encrypted_pin" t.integer "failed_attempts", default: 0, null: false - t.string "unlock_token" - t.datetime "locked_at", precision: nil + t.string "first_name" t.string "github" t.string "jabber" - t.string "gpg_fingerprint" - t.string "picture" - t.string "encrypted_pin" + t.string "last_name" + t.datetime "last_sign_in_at", precision: nil + t.string "last_sign_in_ip" t.string "locale" + t.datetime "locked_at", precision: nil + t.string "picture" + t.datetime "remember_created_at", precision: nil + t.datetime "reset_password_sent_at", precision: nil + t.string "reset_password_token" + t.integer "sign_in_count", default: 0, null: false + t.string "twitter" + t.string "unconfirmed_email" + t.string "unlock_token" + t.datetime "updated_at", precision: nil, null: false + t.string "url" + t.string "username" t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["email"], name: "index_users_on_email", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true @@ -176,8 +175,8 @@ end create_table "users_roles", id: false, force: :cascade do |t| - t.integer "user_id" t.integer "role_id" + t.integer "user_id" t.index ["user_id", "role_id"], name: "index_users_roles_on_user_id_and_role_id" end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index d9e8ae0..65c319d 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -14,7 +14,6 @@ confirmed_at { Time.now } github { "foobar" } jabber { "foo@bar.com" } - gpg_fingerprint { "AAAA AAAA AAAA AAAA AAAA AAAA AAAA AAAA AAAA AAAA" } factory :board_member do after(:create) { |user| user.add_role(:board_member) } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 09b9fa9..fd015ad 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -196,26 +196,6 @@ end end - describe "GPG key fingerprint" do - it "can be nil" do - expect(build(:user, gpg_fingerprint: nil)).to_not have_error_on :gpg_fingerprint - end - - it "must contain 40 hex case insensitive digits separated with any number of spaces" do - expect(build(:user, gpg_fingerprint: "a" * 40)).to_not have_error_on :gpg_fingerprint - expect(build(:user, gpg_fingerprint: "A" * 40)).to_not have_error_on :gpg_fingerprint - expect(build(:user, gpg_fingerprint: "aaaa aaaa aaaa aaaa aaaa aaaa aaaa aaaa aaaa aaaa")).to_not have_error_on :gpg_fingerprint - expect(build(:user, gpg_fingerprint: "AAAA AAAA AAAA AAAA AAAA AAAA AAAA AAAA AAAA AAAA")).to_not have_error_on :gpg_fingerprint - - expect(build(:user, gpg_fingerprint: "z" * 40)).to have_error_on :gpg_fingerprint - expect(build(:user, gpg_fingerprint: "a" * 39)).to have_error_on :gpg_fingerprint - end - - it "stores the gpg fingerprint upcased and properly delimited with spaces" do - expect(create(:user, gpg_fingerprint: "a" * 40).gpg_fingerprint).to eq "AAAA AAAA AAAA AAAA AAAA AAAA AAAA AAAA AAAA AAAA" - end - end - describe "#pin" do it "returns the pin" do expect(build(:user, pin: 123456).pin).to eq 123456 From afa4990e443762f90da8fb500fb5b5b459a4cbb8 Mon Sep 17 00:00:00 2001 From: Albert Stefanov Date: Sun, 11 Jan 2026 12:22:36 +0200 Subject: [PATCH 03/19] Update to Rails 8 and FontAwesome 6 --- .gitignore | 3 + Gemfile | 23 +- Gemfile.lock | 420 ++++++++---------- app/assets/stylesheets/application.scss | 1 - .../applications/_application.html.erb | 4 +- .../_application.html.slim | 2 +- .../_role_assignments.html.slim | 6 +- app/views/layouts/_navigation.html.slim | 11 +- app/views/network_devices/index.html.slim | 2 +- app/views/oauth/applications/index.html.slim | 2 +- app/views/oauth/applications/show.html.slim | 4 +- config/application.rb | 2 +- db/seeds.rb | 13 + 13 files changed, 220 insertions(+), 273 deletions(-) diff --git a/.gitignore b/.gitignore index ac000aa..4e0e83d 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,6 @@ # Ignore Ruby installations /vendor/bundle/ruby/ + +# Ignore precompiled assets +/public/assets/ diff --git a/Gemfile b/Gemfile index 0ce51ba..7ab2e2f 100644 --- a/Gemfile +++ b/Gemfile @@ -2,12 +2,12 @@ source "https://rubygems.org" git_source(:github) { |repo| "https://github.com/#{repo}.git" } # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' -gem "rails", "~> 7.0.8" +gem "rails", "~> 8.1.1" gem "bootsnap" # Use SCSS for stylesheets -gem "sass-rails" gem "sprockets" +gem 'dartsass-sprockets' # Use Uglifier as compressor for JavaScript assets gem "uglifier" @@ -43,7 +43,7 @@ gem "slim-rails" # Use the Bootstrap CSS framework and the FA icon font gem "bootstrap-sass" -gem "font-awesome-sass", "~> 4.0" +gem "font-awesome-sass", :github => 'sunbirddcim/font-awesome-sass' # Gravatar helper gem "gravatar-ultimate" @@ -83,23 +83,6 @@ gem "matrix" gem "rack-cors" group :development do - gem "pry" - gem "pry-rails" - gem "pry-doc" - - # Continuous testing with Guard - gem "guard-rspec" - - # Deploy to a puma server - gem "capistrano3-puma", "~> 5" - - # Goodies for prettier printing of records in the console - gem "awesome_print" - gem "hirb" - - gem "better_errors" - gem "binding_of_caller" - gem "web-console" end diff --git a/Gemfile.lock b/Gemfile.lock index e92e62a..e70ef28 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,87 +1,96 @@ +GIT + remote: https://github.com/sunbirddcim/font-awesome-sass.git + revision: d23c4173812366b719768bd071d19dd7f1dfed6d + specs: + font-awesome-sass (6.5.2.pre.sunbird.pre.1) + GEM remote: https://rubygems.org/ specs: - actioncable (7.0.8.1) - actionpack (= 7.0.8.1) - activesupport (= 7.0.8.1) + action_text-trix (2.1.16) + railties + actioncable (8.1.2) + actionpack (= 8.1.2) + activesupport (= 8.1.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.8.1) - actionpack (= 7.0.8.1) - activejob (= 7.0.8.1) - activerecord (= 7.0.8.1) - activestorage (= 7.0.8.1) - activesupport (= 7.0.8.1) - mail (>= 2.7.1) - net-imap - net-pop - net-smtp - actionmailer (7.0.8.1) - actionpack (= 7.0.8.1) - actionview (= 7.0.8.1) - activejob (= 7.0.8.1) - activesupport (= 7.0.8.1) - mail (~> 2.5, >= 2.5.4) - net-imap - net-pop - net-smtp - rails-dom-testing (~> 2.0) - actionpack (7.0.8.1) - actionview (= 7.0.8.1) - activesupport (= 7.0.8.1) - rack (~> 2.0, >= 2.2.4) + zeitwerk (~> 2.6) + actionmailbox (8.1.2) + actionpack (= 8.1.2) + activejob (= 8.1.2) + activerecord (= 8.1.2) + activestorage (= 8.1.2) + activesupport (= 8.1.2) + mail (>= 2.8.0) + actionmailer (8.1.2) + actionpack (= 8.1.2) + actionview (= 8.1.2) + activejob (= 8.1.2) + activesupport (= 8.1.2) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (8.1.2) + actionview (= 8.1.2) + activesupport (= 8.1.2) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.8.1) - actionpack (= 7.0.8.1) - activerecord (= 7.0.8.1) - activestorage (= 7.0.8.1) - activesupport (= 7.0.8.1) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (8.1.2) + action_text-trix (~> 2.1.15) + actionpack (= 8.1.2) + activerecord (= 8.1.2) + activestorage (= 8.1.2) + activesupport (= 8.1.2) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.8.1) - activesupport (= 7.0.8.1) + actionview (8.1.2) + activesupport (= 8.1.2) builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (7.0.8.1) - activesupport (= 7.0.8.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (8.1.2) + activesupport (= 8.1.2) globalid (>= 0.3.6) - activemodel (7.0.8.1) - activesupport (= 7.0.8.1) - activerecord (7.0.8.1) - activemodel (= 7.0.8.1) - activesupport (= 7.0.8.1) - activestorage (7.0.8.1) - actionpack (= 7.0.8.1) - activejob (= 7.0.8.1) - activerecord (= 7.0.8.1) - activesupport (= 7.0.8.1) + activemodel (8.1.2) + activesupport (= 8.1.2) + activerecord (8.1.2) + activemodel (= 8.1.2) + activesupport (= 8.1.2) + timeout (>= 0.4.0) + activestorage (8.1.2) + actionpack (= 8.1.2) + activejob (= 8.1.2) + activerecord (= 8.1.2) + activesupport (= 8.1.2) marcel (~> 1.0) - mini_mime (>= 1.1.0) - activesupport (7.0.8.1) - concurrent-ruby (~> 1.0, >= 1.0.2) + activesupport (8.1.2) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) + json + logger (>= 1.4.2) minitest (>= 5.1) - tzinfo (~> 2.0) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) - airbrussh (1.5.1) - sshkit (>= 1.6.1, != 1.7.0) ast (2.4.2) - autoprefixer-rails (10.4.16.0) + autoprefixer-rails (10.4.21.0) execjs (~> 2) - awesome_print (1.9.2) + base64 (0.3.0) bcrypt (3.1.20) - better_errors (2.10.1) - erubi (>= 1.0.0) - rack (>= 0.9.0) - rouge (>= 1.0.0) + benchmark (0.5.0) + bigdecimal (4.0.1) bindex (0.8.1) - binding_of_caller (1.0.0) - debug_inspector (>= 0.0.1) bootsnap (1.17.1) msgpack (~> 1.2) bootstrap-sass (3.4.1) @@ -94,28 +103,11 @@ GEM bundler (>= 1.2.0, < 3) thor (~> 1.0) byebug (11.1.3) - capistrano (3.18.0) - airbrussh (>= 1.0.0) - i18n - rake (>= 10.0.0) - sshkit (>= 1.9.0) - capistrano-bundler (2.1.0) - capistrano (~> 3.1) - capistrano-rails (1.6.3) - capistrano (~> 3.1) - capistrano-bundler (>= 1.1, < 3) - capistrano-rbenv (2.2.0) - capistrano (~> 3.1) - sshkit (~> 1.3) - capistrano3-puma (5.2.0) - capistrano (~> 3.7) - capistrano-bundler - puma (>= 4.0, < 6.0) - 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) @@ -124,7 +116,6 @@ GEM capybara (>= 1.0, < 4) launchy cocoon (1.2.15) - coderay (1.1.3) coffee-rails (5.0.0) coffee-script (>= 2.2.0) railties (>= 5.2.0) @@ -132,30 +123,40 @@ GEM coffee-script-source execjs coffee-script-source (1.12.2) - concurrent-ruby (1.2.3) + concurrent-ruby (1.3.6) + connection_pool (3.0.2) crass (1.0.6) daemons (1.4.1) - date (3.4.1) - debug_inspector (1.2.0) - delayed_job (4.1.11) - activesupport (>= 3.0, < 8.0) - delayed_job_active_record (4.1.8) - activerecord (>= 3.0, < 8.0) + dartsass-sprockets (3.2.1) + railties (>= 4.0.0) + sassc-embedded (~> 1.80.1) + sprockets (> 3.0) + sprockets-rails + tilt + date (3.5.1) + delayed_job (4.2.0) + activesupport (>= 3.0, < 9.0) + benchmark + logger + delayed_job_active_record (4.1.11) + activerecord (>= 3.0, < 9.0) delayed_job (>= 3.0, < 5) - devise (4.9.3) + devise (4.9.4) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) responders warden (~> 1.2.3) - devise-i18n (1.12.0) + devise-i18n (1.15.0) devise (>= 4.9.0) + rails-i18n diff-lcs (1.5.0) docile (1.4.0) - doorkeeper (5.6.8) + doorkeeper (5.8.2) railties (>= 5) doorkeeper-i18n (5.2.7) doorkeeper (>= 5.2) + drb (2.2.3) erubi (1.12.0) execjs (2.9.1) factory_bot (6.4.5) @@ -167,37 +168,25 @@ GEM faker (3.2.3) i18n (>= 1.8.11, < 2) ffi (1.16.3) - font-awesome-sass (4.7.0) - sass (>= 3.2) - formatador (1.1.0) - globalid (1.2.1) + globalid (1.3.0) activesupport (>= 6.1) - gpgme (2.0.23) - mini_portile2 (~> 2.7) + google-protobuf (4.33.3-x86_64-linux-gnu) + bigdecimal + rake (>= 13) gravatar-ultimate (2.0.0) activesupport (>= 2.3.14) rack - guard (2.18.1) - formatador (>= 0.2.4) - listen (>= 2.7, < 4.0) - lumberjack (>= 1.0.12, < 2.0) - nenv (~> 0.1) - notiffany (~> 0.0) - pry (>= 0.13.0) - shellany (~> 0.0) - thor (>= 0.18.1) - guard-compat (1.2.1) - guard-rspec (4.7.3) - guard (~> 2.1) - guard-compat (~> 1.1) - rspec (>= 2.99.0, < 4.0) - hirb (0.7.3) i18n (1.14.4) concurrent-ruby (~> 1.0) - jbuilder (2.11.5) - actionview (>= 5.0.0) - activesupport (>= 5.0.0) - jquery-rails (4.6.0) + io-console (0.8.2) + irb (1.16.0) + pp (>= 0.6.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + jbuilder (2.14.1) + actionview (>= 7.0.0) + activesupport (>= 7.0.0) + jquery-rails (4.6.1) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) @@ -218,50 +207,33 @@ GEM launchy (2.5.2) addressable (~> 2.8) lint_roller (1.1.0) - listen (3.8.0) - rb-fsevent (~> 0.10, >= 0.10.3) - rb-inotify (~> 0.9, >= 0.9.10) + logger (1.7.0) loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) - lumberjack (1.2.10) - mail (2.8.1) + mail (2.9.0) + logger mini_mime (>= 0.1.1) net-imap net-pop net-smtp - mail-gpg (0.4.4) - gpgme (~> 2.0, >= 2.0.2) - mail (~> 2.5, >= 2.5.3) - marcel (1.0.4) + marcel (1.1.0) matrix (0.4.2) - method_source (1.0.0) mini_mime (1.1.5) - mini_portile2 (2.8.5) minitest (5.22.3) msgpack (1.7.2) - mutex_m (0.2.0) - nenv (0.3.0) - net-imap (0.4.20) + net-imap (0.6.2) date net-protocol net-pop (0.1.2) net-protocol net-protocol (0.2.2) timeout - net-scp (4.0.0) - net-ssh (>= 2.6.5, < 8.0.0) - net-sftp (4.0.0) - net-ssh (>= 5.0.0, < 8.0.0) net-smtp (0.4.0.1) net-protocol - net-ssh (7.2.1) nio4r (2.7.4) - nokogiri (1.16.3-x86_64-linux) + nokogiri (1.19.0-x86_64-linux-gnu) racc (~> 1.4) - notiffany (0.1.3) - nenv (~> 0.1) - shellany (~> 0.0) orm_adapter (0.5.0) parallel (1.24.0) parser (3.3.0.4) @@ -272,14 +244,9 @@ GEM phony_rails (0.15.0) activesupport (>= 3.0) phony (>= 2.18.12) - pry (0.14.2) - coderay (~> 1.1) - method_source (~> 1.0) - pry-doc (1.5.0) - pry (~> 0.11) - yard (~> 0.9.11) - pry-rails (0.3.9) - pry (>= 0.10.4) + pp (0.6.3) + prettyprint + prettyprint (0.2.0) psych (5.1.2) stringio public_suffix (5.0.4) @@ -288,61 +255,63 @@ GEM pundit (2.3.1) activesupport (>= 3.0.0) racc (1.7.3) - rack (2.2.9) - rack-cors (2.0.2) - rack (>= 2.0.0) - rack-test (2.1.0) + rack (3.2.4) + rack-cors (3.0.0) + logger + rack (>= 3.0.14) + rack-session (2.1.1) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) rack (>= 1.3) - rails (7.0.8.1) - actioncable (= 7.0.8.1) - actionmailbox (= 7.0.8.1) - actionmailer (= 7.0.8.1) - actionpack (= 7.0.8.1) - actiontext (= 7.0.8.1) - actionview (= 7.0.8.1) - activejob (= 7.0.8.1) - activemodel (= 7.0.8.1) - activerecord (= 7.0.8.1) - activestorage (= 7.0.8.1) - activesupport (= 7.0.8.1) + rackup (2.3.1) + rack (>= 3) + rails (8.1.2) + actioncable (= 8.1.2) + actionmailbox (= 8.1.2) + actionmailer (= 8.1.2) + actionpack (= 8.1.2) + actiontext (= 8.1.2) + actionview (= 8.1.2) + activejob (= 8.1.2) + activemodel (= 8.1.2) + activerecord (= 8.1.2) + activestorage (= 8.1.2) + activesupport (= 8.1.2) bundler (>= 1.15.0) - railties (= 7.0.8.1) - rails-dom-testing (2.2.0) + railties (= 8.1.2) + rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) - rails-i18n (7.0.8) + rails-i18n (8.1.0) i18n (>= 0.7, < 2) - railties (>= 6.0.0, < 8) - railties (7.0.8.1) - actionpack (= 7.0.8.1) - activesupport (= 7.0.8.1) - method_source + railties (>= 8.0.0, < 9) + railties (8.1.2) + actionpack (= 8.1.2) + activesupport (= 8.1.2) + irb (~> 1.13) + rackup (>= 1.0.0) rake (>= 12.2) - thor (~> 1.0) - zeitwerk (~> 2.5) + thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) + zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.1.0) - rb-fsevent (0.11.2) - rb-inotify (0.10.1) - ffi (~> 1.0) rdoc (6.6.3.1) psych (>= 4.0.0) regexp_parser (2.9.0) - responders (3.1.1) - actionpack (>= 5.2) - railties (>= 5.2) + reline (0.6.3) + io-console (~> 0.5) + responders (3.2.0) + actionpack (>= 7.0) + railties (>= 7.0) rexml (3.3.9) rolify (6.0.1) - rouge (4.2.0) - rspec (3.12.0) - rspec-core (~> 3.12.0) - rspec-expectations (~> 3.12.0) - rspec-mocks (~> 3.12.0) - rspec-core (3.12.2) + rspec-core (3.12.3) rspec-support (~> 3.12.0) rspec-expectations (3.12.3) diff-lcs (>= 1.2.0, < 2.0) @@ -350,7 +319,7 @@ GEM rspec-mocks (3.12.6) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-rails (6.1.0) + rspec-rails (6.1.1) actionpack (>= 6.1) activesupport (>= 6.1) railties (>= 6.1) @@ -377,31 +346,22 @@ GEM rubocop-ast (>= 1.30.0, < 2.0) ruby-progressbar (1.13.0) rubyzip (2.3.2) - sass (3.7.4) - sass-listen (~> 4.0.0) - sass-listen (4.0.0) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - sass-rails (6.0.0) - sassc-rails (~> 2.1, >= 2.1.1) + sass-embedded (1.97.2-x86_64-linux-gnu) + google-protobuf (~> 4.31) sassc (2.4.0) ffi (~> 1.9) - sassc-rails (2.1.2) - railties (>= 4.0.0) - sassc (>= 2.0) - sprockets (> 3.0) - sprockets-rails - tilt + sassc-embedded (1.80.8) + sass-embedded (~> 1.80) sdoc (2.6.1) rdoc (>= 5.0) + securerandom (0.4.1) selenium-webdriver (4.16.0) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) - shellany (0.0.1) - simple_form (5.3.0) - actionpack (>= 5.2) - activemodel (>= 5.2) + simple_form (5.4.1) + actionpack (>= 7.0) + activemodel (>= 7.0) simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) @@ -411,7 +371,7 @@ GEM slim (5.2.0) temple (~> 0.10.0) tilt (>= 2.1.0) - slim-rails (3.6.3) + slim-rails (4.0.0) actionpack (>= 3.1) railties (>= 3.1) slim (>= 3.0, < 6.0, != 5.0.0) @@ -419,19 +379,15 @@ GEM spring (4.1.3) spring-commands-rspec (1.0.4) spring (>= 0.9.1) - sprockets (4.2.1) + sprockets (4.2.2) concurrent-ruby (~> 1.0) + logger 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) - sqlite3 (1.7.0-x86_64-linux) - sshkit (1.22.0) - mutex_m - net-scp (>= 1.1.2) - net-sftp (>= 2.1.2) - net-ssh (>= 2.8.0) + sqlite3 (2.9.0-x86_64-linux-gnu) standard (1.33.0) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.0) @@ -449,11 +405,14 @@ GEM thor (1.3.1) tilt (2.3.0) timeout (0.4.3) + tsort (0.2.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) uglifier (4.2.0) execjs (>= 0.3.0, < 3) unicode-display_width (2.5.0) + uri (1.1.1) + useragent (0.16.11) warden (1.2.9) rack (>= 2.0.9) web-console (4.2.1) @@ -463,7 +422,8 @@ GEM railties (>= 6.0.0) webrick (1.8.2) websocket (1.2.10) - websocket-driver (0.7.6) + websocket-driver (0.8.0) + base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) xmlrpc (0.3.3) @@ -473,31 +433,23 @@ GEM yaml_db (0.7.0) rails (>= 3.0) rake (>= 0.8.7) - yard (0.9.36) zeitwerk (2.6.13) PLATFORMS x86_64-linux DEPENDENCIES - awesome_print - better_errors - binding_of_caller bootsnap bootstrap-sass brakeman bundler-audit byebug - capistrano - capistrano-bundler - capistrano-rails - capistrano-rbenv - capistrano3-puma (~> 5) capybara capybara-screenshot cocoon coffee-rails daemons + dartsass-sprockets delayed_job delayed_job_active_record devise @@ -507,30 +459,22 @@ DEPENDENCIES factory_bot_rails fakefs faker - font-awesome-sass (~> 4.0) + font-awesome-sass! gravatar-ultimate - guard-rspec - hirb jbuilder jquery-rails kaminari - mail-gpg matrix net-smtp - net-ssh pg phony_rails - pry - pry-doc - pry-rails puma pundit rack-cors - rails (~> 7.0.8) + rails (~> 8.1.1) rails-i18n rolify rspec-rails - sass-rails sdoc selenium-webdriver simple_form diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 85a4b22..af1469d 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -1,4 +1,3 @@ -@import 'font-awesome-sprockets'; @import 'font-awesome'; @import 'paper_variables'; @import 'bootstrap-sprockets'; diff --git a/app/views/doorkeeper/applications/_application.html.erb b/app/views/doorkeeper/applications/_application.html.erb index 0c11e08..3e9ac8b 100644 --- a/app/views/doorkeeper/applications/_application.html.erb +++ b/app/views/doorkeeper/applications/_application.html.erb @@ -3,8 +3,8 @@ <%= application.redirect_uri %>
- <%= link_to icon(:pencil), edit_oauth_application_path(application), class: 'edit', alt: t('doorkeeper.applications.buttons.edit') %> - <%= link_to icon(:times), oauth_application_path(application), method: :delete, class: 'delete', data: {confirm: t('doorkeeper.applications.confirmations.destroy')} %> + <%= link_to icon('fa-solid', :pencil), edit_oauth_application_path(application), class: 'edit', alt: t('doorkeeper.applications.buttons.edit') %> + <%= link_to icon('fa-solid', :times), oauth_application_path(application), method: :delete, class: 'delete', data: {confirm: t('doorkeeper.applications.confirmations.destroy')} %>
diff --git a/app/views/doorkeeper/authorized_applications/_application.html.slim b/app/views/doorkeeper/authorized_applications/_application.html.slim index a5c14d0..c72d42d 100644 --- a/app/views/doorkeeper/authorized_applications/_application.html.slim +++ b/app/views/doorkeeper/authorized_applications/_application.html.slim @@ -3,4 +3,4 @@ tr td = l application.created_at, format: :long td.text-right .record-actions - = link_to icon(:times), oauth_authorized_application_path(application), method: :delete, class: 'delete', data: {confirm: t('doorkeeper.authorized_applications.confirmations.revoke')}, title: t('doorkeeper.authorized_applications.buttons.revoke') + = link_to icon('fa-solid', :times), oauth_authorized_application_path(application), method: :delete, class: 'delete', data: {confirm: t('doorkeeper.authorized_applications.confirmations.revoke')}, title: t('doorkeeper.authorized_applications.buttons.revoke') diff --git a/app/views/fauna/role_assignments/_role_assignments.html.slim b/app/views/fauna/role_assignments/_role_assignments.html.slim index aa0db60..7b647db 100644 --- a/app/views/fauna/role_assignments/_role_assignments.html.slim +++ b/app/views/fauna/role_assignments/_role_assignments.html.slim @@ -2,7 +2,9 @@ - Role.predefined.each do |predefined_role| - if user.has_role? predefined_role.name = link_to role_assignment_fauna_user_role_assignments_path(user.id, predefined_role.name), method: :delete, remote: true, class: 'btn btn-sm btn-warning', data: {disable: true, confirm: t('views.roles.are_you_sure')} do - = icon 'check-square-o', predefined_role.localized_name + => icon('fa-solid', 'check-square-o') + = predefined_role.localized_name - else = link_to fauna_user_role_assignments_path(user.id, role: {name: predefined_role.name}), method: :post, remote: true, class: 'btn btn-sm btn-default', data: {disable: true, confirm: t('views.roles.are_you_sure')} do - = icon 'square-o', predefined_role.localized_name + => icon('fa-solid', 'square-o') + = predefined_role.localized_name diff --git a/app/views/layouts/_navigation.html.slim b/app/views/layouts/_navigation.html.slim index 0af4ceb..9ee13df 100644 --- a/app/views/layouts/_navigation.html.slim +++ b/app/views/layouts/_navigation.html.slim @@ -12,12 +12,13 @@ nav class="navbar navbar-static-top #{content_for?(:nav_color) ? yield(:nav_colo - if user_signed_in? and UserPolicy.new(current_user, nil).index? li class="#{'active' if current_page?(fauna_users_path)}" = link_to fauna_users_path do - = icon 'users', t('views.navigation.labbers') + => icon('fa-solid', :users) + = t('views.navigation.labbers') ul.nav.navbar-nav.navbar-right - unless user_signed_in? li.dropdown = link_to '#', class: 'dropdown-toggle', data: {toggle: 'dropdown'} do - => icon 'flag' + => icon('fa-solid', :flag) = {bg: "БГ", en: "EN"}[I18n.locale] span.caret< ul.dropdown-menu role="menu" @@ -27,7 +28,8 @@ nav class="navbar navbar-static-top #{content_for?(:nav_color) ? yield(:nav_colo - if user_signed_in? li.dropdown class="#{'active' if current_page?(edit_user_registration_path) || current_page?(user_network_devices_path)}" a.dropdown-toggle data-toggle="dropdown" href="#" - = icon 'user', t('views.navigation.account') + => icon('fa-solid', :user) + = t('views.navigation.account') b.caret< ul.dropdown-menu li @@ -44,4 +46,5 @@ nav class="navbar navbar-static-top #{content_for?(:nav_color) ? yield(:nav_colo - else li class="#{'active' if controller_name == 'sessions'}" = link_to new_user_session_path do - = icon 'sign-in', t('views.navigation.sign_in') + => icon('fa-solid', 'sign-in') + = t('views.navigation.sign_in') diff --git a/app/views/network_devices/index.html.slim b/app/views/network_devices/index.html.slim index e62602f..875cdf1 100644 --- a/app/views/network_devices/index.html.slim +++ b/app/views/network_devices/index.html.slim @@ -1,7 +1,7 @@ - content_for(:title) { t 'views.network_devices.network_devices' } - content_for(:page_actions) do = link_to new_user_network_device_path, class: ['btn', 'btn-primary'] do - => icon :plus + => icon('fa-solid', :plus) = t('.new_network_device') section.container diff --git a/app/views/oauth/applications/index.html.slim b/app/views/oauth/applications/index.html.slim index c6009d2..e6bcd14 100644 --- a/app/views/oauth/applications/index.html.slim +++ b/app/views/oauth/applications/index.html.slim @@ -1,7 +1,7 @@ - content_for(:title) { t 'doorkeeper.applications.index.title' } - content_for(:page_actions) do = link_to new_oauth_application_path, class: ['btn', 'btn-primary'] do - => icon :plus + => icon('fa-solid', :plus) = t('doorkeeper.applications.index.new') section.container diff --git a/app/views/oauth/applications/show.html.slim b/app/views/oauth/applications/show.html.slim index 9cff02f..248d778 100644 --- a/app/views/oauth/applications/show.html.slim +++ b/app/views/oauth/applications/show.html.slim @@ -1,10 +1,10 @@ - content_for(:title) { t('doorkeeper.applications.show.title', name: @application.name) } - content_for(:page_actions) do = link_to edit_oauth_application_path(@application), class: ['btn', 'btn-primary'] do - => icon(:pencil) + => icon('fa-solid', :pencil) = t('doorkeeper.applications.buttons.edit') = link_to oauth_application_path(@application), method: :delete, class: ['btn', 'btn-danger'], data: {confirm: t('doorkeeper.applications.confirmations.destroy')} do - => icon(:times) + => icon('fa-solid', :times) = t('doorkeeper.applications.buttons.destroy') section.container diff --git a/config/application.rb b/config/application.rb index f623aad..cc82a74 100644 --- a/config/application.rb +++ b/config/application.rb @@ -9,7 +9,7 @@ module Fauna class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 5.2 + config.load_defaults 8.1 # Configuration for the application, engines, and railties goes here. # diff --git a/db/seeds.rb b/db/seeds.rb index 4edb1e8..78fd80b 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -5,3 +5,16 @@ # # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) # Mayor.create(name: 'Emanuel', city: cities.first) + +roles = [ + :board_member, + :trusted_member, + :member, + :tenant, + :landlord, + :infra +] + +roles.each do |role| + Role.find_or_create_by(name: role) +end From b925d292075692254c50bdb337ca9b3376451d26 Mon Sep 17 00:00:00 2001 From: Albert Stefanov Date: Sun, 11 Jan 2026 12:24:31 +0200 Subject: [PATCH 04/19] Add Devise .bg locale Fixes the issue with error messages in Bulgarian. src: https://gist.github.com/mitio/5487275 --- config/locales/devise.bg.yml | 60 ++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 config/locales/devise.bg.yml diff --git a/config/locales/devise.bg.yml b/config/locales/devise.bg.yml new file mode 100644 index 0000000..3921ab7 --- /dev/null +++ b/config/locales/devise.bg.yml @@ -0,0 +1,60 @@ +# Additional translations at https://github.com/plataformatec/devise/wiki/I18n + +bg: + devise: + confirmations: + confirmed: "Вашият профил беше успешно потвърден. Влязохте успешно в него." + send_instructions: "Ще получите писмо с инструкции как да потвърдите вашия профил до няколко минути." + send_paranoid_instructions: "Ако вашият имейл адрес съществува в базата ни, ще получите там инструкции как да потвърдите вашия профил." + failure: + already_authenticated: "Вече сте влязъл в профила си." + inactive: "Профилът ви все още не е активиран." + invalid: "Невалиден имейл адрес или парола." + invalid_token: "Невалиден идентификационен ключ." + last_attempt: "Разполагате с още един опит преди профилът ви да бъде заключен." + locked: "Профилът ви е заключен." + not_found_in_database: "Невалиден имейл адрес или парола." + timeout: "Сесията ви изтече, моля влезте отново, за да продължите." + unauthenticated: "Преди да продължите, трябва да влезете в профила си или да се регистрирате." + unconfirmed: "Преди да продължите, трябва да потвърдите регистрацията си." + mailer: + confirmation_instructions: + subject: "Инструкции за потвърждаване" + reset_password_instructions: + subject: "Инструкции за смяна на паролата" + unlock_instructions: + subject: "Инструкции за отключване" + omniauth_callbacks: + failure: "Не успяхме да ви оторизираме чрез %{kind}, защото \"%{reason}\"." + success: "Успешно оторизиран чрез %{kind} профил." + passwords: + no_token: "Може да достъпвате тази страница само от имейл за промяна на паролата. Ако сте отворили тази страница от такъв имейл, уверете се, чеизползвате целия URL-адрес, който сме ви изпратили." + send_instructions: "Ще получите писмо с инструкции как да промените паролата си до няколко минути." + send_paranoid_instructions: "Ако вашият имейл адрес съществува в базата ни, ще получите там инструкции за промяна на вашата парола." + updated: "Паролата ви беше променена успешно. Влязохте успешно в профила си." + updated_not_active: "Паролата ви беше променена успешно." + registrations: + destroyed: "Довиждане! Вашият профил беше успешно изтрит. Надяваме се скоро да ви видим отново." + signed_up: "Добре дошли! Вие се регистрирахте успешно." + signed_up_but_inactive: "Вие се регистирахте успешно. Въпреки това, не можете да влезете в профила си, защото той все още не е потвърден." + signed_up_but_locked: "Вие се регистрирахте успешно. Въпреки това, не можете да влезете в профила си, защото той е заключен." + signed_up_but_unconfirmed: "Писмо с връзка за потвърждаване на профила ви беше изпратено на вашия имейл адрес. Моля, отворете връзката, за даактивирате вашия профил." + update_needs_confirmation: "Вие променихте профила си успешно, но ние трябва да проверим вашия нов имейл адрес. Моля, проверете пощата си и отворете връзката за потвърждаване на новия адрес." + updated: "Вие променихте профила си успешно." + sessions: + signed_in: "Влязохте успешно." + signed_out: "Излязохте успешно." + unlocks: + send_instructions: "Ще получите писмо с инструкции как да отключите профила си до няколко минути." + send_paranoid_instructions: "Ако вашият профил съществува в базата ни, на вашия имейл адрес ще получите инструкции за отключването му до няколко минути." + unlocked: "Вашият профил беще отключен успешно. За да продължите, влезте в него." + errors: + messages: + already_confirmed: "е вече потвърден, моля опитайте да влезете в профила си с него" + confirmation_period_expired: "трябва да се потвърди в рамките на %{period}, моля направете нова заявка за потвърждение" + expired: "е изтекъл, моля заявете нов" + not_found: "не е намерен" + not_locked: "не бе заключен" + not_saved: + one: "Една грешка попречи този %{resource} да бъде записан:" + other: "%{count} грешки попречиха този %{resource} да бъде записан:" From 8eb4596c130bd71e110bb0083438cd925be52d8b Mon Sep 17 00:00:00 2001 From: Albert Stefanov Date: Sun, 11 Jan 2026 14:47:53 +0200 Subject: [PATCH 05/19] Remove Rolify and allow temporary roles The frontend is not currently implemented, but this allows us to add expiring or scheduled access. --- Gemfile | 1 - Gemfile.lock | 2 -- app/models/role.rb | 12 ++----- app/models/user.rb | 35 ++++++++++++++----- app/models/user_role.rb | 9 +++++ config/initializers/rolify.rb | 7 ---- ...20260111102636_add_times_to_users_roles.rb | 15 ++++++++ db/schema.rb | 13 ++++--- 8 files changed, 63 insertions(+), 31 deletions(-) create mode 100644 app/models/user_role.rb delete mode 100644 config/initializers/rolify.rb create mode 100644 db/migrate/20260111102636_add_times_to_users_roles.rb diff --git a/Gemfile b/Gemfile index 7ab2e2f..9a8105a 100644 --- a/Gemfile +++ b/Gemfile @@ -58,7 +58,6 @@ gem "cocoon" gem "kaminari" # Use rolify and pundit for authorization -gem "rolify" gem "pundit" # Asynchronous job execution diff --git a/Gemfile.lock b/Gemfile.lock index e70ef28..ceb163b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -310,7 +310,6 @@ GEM actionpack (>= 7.0) railties (>= 7.0) rexml (3.3.9) - rolify (6.0.1) rspec-core (3.12.3) rspec-support (~> 3.12.0) rspec-expectations (3.12.3) @@ -473,7 +472,6 @@ DEPENDENCIES rack-cors rails (~> 8.1.1) rails-i18n - rolify rspec-rails sdoc selenium-webdriver diff --git a/app/models/role.rb b/app/models/role.rb index 5583a59..215b7c2 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -1,12 +1,8 @@ class Role < ApplicationRecord - PREDEFINED_ROLES = [:board_member, :trusted_member, :member, :infra, :tenant, :landlord].freeze + PREDEFINED_ROLES = %i[board_member trusted_member member infra tenant landlord].freeze - has_and_belongs_to_many :users, join_table: :users_roles - belongs_to :resource, polymorphic: true, optional: true - - validates :resource_type, - inclusion: {in: Rolify.resource_types}, - allow_nil: true + has_many :user_roles + has_many :users, through: :user_roles validates :name, inclusion: {in: Role::PREDEFINED_ROLES.map(&:to_s)} @@ -17,6 +13,4 @@ def localized_name def self.predefined PREDEFINED_ROLES.map { |name| Role.find_or_initialize_by name: name } end - - scopify end diff --git a/app/models/user.rb b/app/models/user.rb index ec9913b..8641256 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,17 +1,18 @@ -require "digest/md5" +require 'digest/md5' class User < ApplicationRecord - rolify - # Include default devise modules. Others available are: # :timeoutable and :omniauthable devise :database_authenticatable, :registerable, :confirmable, :lockable, :recoverable, :rememberable, :trackable, :validatable, :doorkeeper, authentication_keys: [:login] + has_many :user_roles + has_many :roles, through: :user_roles + has_many :network_devices, foreign_key: :owner_id, dependent: :destroy has_many :phone_numbers, foreign_key: :owner_id, dependent: :destroy - has_many :oauth_applications, class_name: "Doorkeeper::Application", as: :owner + has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner validates :username, uniqueness: {case_sensitive: false}, presence: true validates :twitter, format: {with: /\A[A-Za-z0-9_]{1,15}\z/}, allow_blank: true @@ -30,24 +31,42 @@ def self.find_for_database_authentication(warden_conditions) conditions = warden_conditions.dup if (login = conditions.delete(:login)) - where(conditions).where(["lower(username) = :value OR lower(email) = :value", {value: login.downcase.strip}]).first + where(conditions).where(['lower(username) = :value OR lower(email) = :value', {value: login.downcase.strip}]).first end end + def has_role?(role) + self.roles.exists?(name: role) + end + + def add_role(role_name) + role = Role.find_by(name: role_name) + + self.user_roles.find_or_create_by(role_id: role.id, end_time: nil) + self.roles.reload + end + + def remove_role(role_name) + role = Role.find_by(name: role_name) + + self.user_roles.find_by(role_id: role.id).update(end_time: Time.current) + self.roles.reload + end + def email_md5 Digest::MD5.hexdigest email end def name - [first_name, last_name].compact.join(" ").strip + [first_name, last_name].compact.join(' ').strip end def twitter=(handle) - write_attribute :twitter, handle.gsub(/\A@/, "") if handle + write_attribute :twitter, handle.gsub(/\A@/, '') if handle end def picture(size = 128) - Gravatar.new(email).image_url ssl: true, s: size, d: "retro" + Gravatar.new(email).image_url ssl: true, s: size, d: 'retro' end def pin=(pin) diff --git a/app/models/user_role.rb b/app/models/user_role.rb new file mode 100644 index 0000000..2d51509 --- /dev/null +++ b/app/models/user_role.rb @@ -0,0 +1,9 @@ +class UserRole < ApplicationRecord + self.table_name = :users_roles + + belongs_to :role + belongs_to :user + + scope :active_at, ->(time) { where('(start_time < ?) and (end_time is null or end_time > ?)', time, time) } + default_scope { where('(start_time < ?) and (end_time is null or end_time > ?)', Time.current, Time.current) } +end diff --git a/config/initializers/rolify.rb b/config/initializers/rolify.rb deleted file mode 100644 index 4a17233..0000000 --- a/config/initializers/rolify.rb +++ /dev/null @@ -1,7 +0,0 @@ -Rolify.configure do |config| - # By default ORM adapter is ActiveRecord. uncomment to use mongoid - # config.use_mongoid - - # Dynamic shortcuts for User class (user.is_admin? like methods). Default is: false - # config.use_dynamic_shortcuts -end diff --git a/db/migrate/20260111102636_add_times_to_users_roles.rb b/db/migrate/20260111102636_add_times_to_users_roles.rb new file mode 100644 index 0000000..78ca420 --- /dev/null +++ b/db/migrate/20260111102636_add_times_to_users_roles.rb @@ -0,0 +1,15 @@ +class AddTimesToUsersRoles < ActiveRecord::Migration[8.1] + def up + add_column :users_roles, :start_time, :datetime, null: false, default: -> { 'CURRENT_TIMESTAMP' } + add_column :users_roles, :end_time, :datetime, null: true + + execute('ALTER TABLE users_roles ADD PRIMARY KEY (user_id, role_id, start_time)') + end + + def down + remove_column :users_roles, :start_time + remove_column :users_roles, :end_time + + execute('ALTER TABLE users_roles DROP CONSTRAINT users_roles_pkey') + end +end diff --git a/db/schema.rb b/db/schema.rb index 0c5e408..50cf2c8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,10 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2026_01_10_072625) do +ActiveRecord::Schema[8.1].define(version: 2026_01_11_102636) do + # These are extensions that must be enabled in order to support this database + enable_extension "pg_catalog.plpgsql" + create_table "audit_log_entries", force: :cascade do |t| t.datetime "created_at", null: false t.json "payload" @@ -174,9 +177,11 @@ t.index ["username"], name: "index_users_on_username", unique: true end - create_table "users_roles", id: false, force: :cascade do |t| - t.integer "role_id" - t.integer "user_id" + create_table "users_roles", primary_key: ["user_id", "role_id", "start_time"], force: :cascade do |t| + t.datetime "end_time" + t.integer "role_id", null: false + t.datetime "start_time", default: -> { "CURRENT_TIMESTAMP" }, null: false + t.integer "user_id", null: false t.index ["user_id", "role_id"], name: "index_users_roles_on_user_id_and_role_id" end From 5774d762bb04567cab6e9975c83c8a8724d67f6e Mon Sep 17 00:00:00 2001 From: Albert Stefanov Date: Sun, 8 Feb 2026 10:22:56 +0200 Subject: [PATCH 06/19] migrate to bootsrap 5 --- Gemfile | 2 +- Gemfile.lock | 13 +- app/assets/javascripts/application.js | 6 +- app/assets/stylesheets/application.scss | 43 +-- .../components/_flash_message.scss | 9 +- .../stylesheets/components/_footer.scss | 34 +- .../stylesheets/components/_header.scss | 35 +- .../components/_large_buttons.scss | 16 +- app/assets/stylesheets/components/_login.scss | 19 +- .../components/_nested_fields.scss | 7 +- .../components/_network_devices.scss | 5 + .../stylesheets/components/_page_actions.scss | 13 +- .../components/_record_actions.scss | 7 +- .../stylesheets/components/_record_table.scss | 5 + .../components/_role_assignments.scss | 5 + app/helpers/bootstrap_flash_helper.rb | 6 +- .../_role_assignments.html.slim | 2 + .../_temporary_roles.html.slim | 12 + app/views/fauna/users/_user.html.slim | 2 + app/views/fauna/users/index.html.slim | 1 + app/views/layouts/_navigation.html.slim | 62 ++- app/views/layouts/application.html.slim | 7 +- .../network_devices/_network_device.html.slim | 4 +- .../oauth/applications/_application.html.slim | 4 +- config/application.rb | 2 + config/initializers/simple_form.rb | 40 +- config/initializers/simple_form_bootstrap.rb | 362 +++++++++++++++--- config/locales/simple_form.en.yml | 46 ++- lib/templates/slim/scaffold/_form.html.slim | 1 + 29 files changed, 512 insertions(+), 258 deletions(-) create mode 100644 app/views/fauna/role_assignments/_temporary_roles.html.slim diff --git a/Gemfile b/Gemfile index 9a8105a..6f48d2f 100644 --- a/Gemfile +++ b/Gemfile @@ -42,7 +42,7 @@ gem "phony_rails" gem "slim-rails" # Use the Bootstrap CSS framework and the FA icon font -gem "bootstrap-sass" +gem "bootstrap" gem "font-awesome-sass", :github => 'sunbirddcim/font-awesome-sass' # Gravatar helper diff --git a/Gemfile.lock b/Gemfile.lock index ceb163b..68d5995 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -84,8 +84,6 @@ GEM addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) ast (2.4.2) - autoprefixer-rails (10.4.21.0) - execjs (~> 2) base64 (0.3.0) bcrypt (3.1.20) benchmark (0.5.0) @@ -93,9 +91,8 @@ GEM bindex (0.8.1) bootsnap (1.17.1) msgpack (~> 1.2) - bootstrap-sass (3.4.1) - autoprefixer-rails (>= 5.2.1) - sassc (>= 2.0.0) + bootstrap (5.3.8) + popper_js (>= 2.11.8, < 3) brakeman (6.1.1) racc builder (3.2.4) @@ -167,7 +164,6 @@ GEM fakefs (2.5.0) faker (3.2.3) i18n (>= 1.8.11, < 2) - ffi (1.16.3) globalid (1.3.0) activesupport (>= 6.1) google-protobuf (4.33.3-x86_64-linux-gnu) @@ -244,6 +240,7 @@ GEM phony_rails (0.15.0) activesupport (>= 3.0) phony (>= 2.18.12) + popper_js (2.11.8) pp (0.6.3) prettyprint prettyprint (0.2.0) @@ -347,8 +344,6 @@ GEM rubyzip (2.3.2) sass-embedded (1.97.2-x86_64-linux-gnu) google-protobuf (~> 4.31) - sassc (2.4.0) - ffi (~> 1.9) sassc-embedded (1.80.8) sass-embedded (~> 1.80) sdoc (2.6.1) @@ -439,7 +434,7 @@ PLATFORMS DEPENDENCIES bootsnap - bootstrap-sass + bootstrap brakeman bundler-audit byebug diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 645a1a5..2534c3a 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -12,10 +12,8 @@ // //= require jquery3 //= require rails-ujs -//= require bootstrap/dropdown -//= require bootstrap/collapse -//= require bootstrap/alert -//= require bootstrap/transition +//= require popper +//= require bootstrap //= require cocoon //= require_tree . diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index af1469d..147f298 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -1,43 +1,10 @@ -@import 'font-awesome'; -@import 'paper_variables'; -@import 'bootstrap-sprockets'; - -// Core variables and mixins -@import "bootstrap/variables"; -@import "bootstrap/mixins"; - -// Reset and dependencies -@import "bootstrap/normalize"; +@import 'bootstrap/functions'; +@import 'bootstrap/variables'; +@import 'bootstrap/mixins'; +@import 'bootstrap'; -// Core CSS -@import "bootstrap/scaffolding"; -@import "bootstrap/type"; -@import "bootstrap/code"; -@import "bootstrap/grid"; -@import "bootstrap/tables"; -@import "bootstrap/forms"; -@import "bootstrap/buttons"; - -// Components -@import "bootstrap/component-animations"; -@import "bootstrap/dropdowns"; -@import "bootstrap/button-groups"; -@import "bootstrap/input-groups"; -@import "bootstrap/navs"; -@import "bootstrap/navbar"; -@import "bootstrap/pagination"; -@import "bootstrap/labels"; -@import "bootstrap/badges"; -@import "bootstrap/thumbnails"; -@import "bootstrap/alerts"; -@import "bootstrap/panels"; -@import "bootstrap/close"; - -// Utility classes -@import "bootstrap/utilities"; -@import "bootstrap/responsive-utilities"; +@import 'font-awesome'; -@import 'paper'; @import 'roboto'; @import 'open_sans'; diff --git a/app/assets/stylesheets/components/_flash_message.scss b/app/assets/stylesheets/components/_flash_message.scss index 0a367b3..9e79b2f 100644 --- a/app/assets/stylesheets/components/_flash_message.scss +++ b/app/assets/stylesheets/components/_flash_message.scss @@ -1,6 +1,11 @@ +@import 'bootstrap/functions'; +@import 'bootstrap/variables'; +@import 'bootstrap/mixins'; +@import 'bootstrap'; + .flash_message { @extend .col-md-4; - @extend .col-md-offset-4; + @extend .offset-md-4; @extend .col-sm-6; - @extend .col-sm-offset-3; + @extend .offset-sm-3; } \ No newline at end of file diff --git a/app/assets/stylesheets/components/_footer.scss b/app/assets/stylesheets/components/_footer.scss index f2fb605..ee60207 100644 --- a/app/assets/stylesheets/components/_footer.scss +++ b/app/assets/stylesheets/components/_footer.scss @@ -1,37 +1,17 @@ -html { - position: relative; - min-height: 100%; -} - -body { - margin-bottom: 64px; - - @media (max-width: $screen-xs-max) { - margin-bottom: 128px; - } -} - -.footer { - position: absolute; - bottom: 0; - width: 100%; - height: 64px; - background-color: #f5f5f5; - - @media (max-width: $screen-xs-max) { - height: 128px; - } +@import 'bootstrap/functions'; +@import 'bootstrap/variables'; +@import 'bootstrap/mixins'; +@import 'bootstrap/grid'; +footer { p { - margin: 20px 0; - @extend .text-muted; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(sm) { text-align: center; } } p.right { - @media (min-width: $screen-xs-max) { + @include media-breakpoint-up(sm) { text-align: right; } } diff --git a/app/assets/stylesheets/components/_header.scss b/app/assets/stylesheets/components/_header.scss index 4b896ad..ececd2a 100644 --- a/app/assets/stylesheets/components/_header.scss +++ b/app/assets/stylesheets/components/_header.scss @@ -1,16 +1,19 @@ -.navbar-brand { - margin: 0 !important; - padding: 0; - - img { - padding: 5px 0px; - @media (max-width: $screen-sm-min) { - padding: 5px; - } - - } - - &.active { - background-color: #0c7cd5; - } -} +//@import 'bootstrap/functions'; +//@import 'bootstrap/variables'; +//@import 'bootstrap/mixins'; +// +//.navbar-brand { +// margin: 0 !important; +// padding: 0; +// +// img { +// padding: 5px 0; +// @include media-breakpoint-up(sm) { +// padding: 5px; +// } +// } +// +// &.active { +// background-color: #0c7cd5; +// } +//} diff --git a/app/assets/stylesheets/components/_large_buttons.scss b/app/assets/stylesheets/components/_large_buttons.scss index 3ab6c1c..03b11e9 100644 --- a/app/assets/stylesheets/components/_large_buttons.scss +++ b/app/assets/stylesheets/components/_large_buttons.scss @@ -1,5 +1,11 @@ +@use 'sass:map'; +@import 'bootstrap/functions'; +@import 'bootstrap/variables'; +@import 'bootstrap/mixins'; + + .current_status_indicator { - @extend .thumbnail; + //@extend .thumbnail; @extend .text-center; width: 350px; font-size: 2em; @@ -7,16 +13,16 @@ margin: 20px auto 0 auto; i.fa { font-size: 72pt; } - @media (max-width: $screen-sm-min) { + @include media-breakpoint-only(xs) { width: 250px; } - @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) { + @include media-breakpoint-only(sm) { width: 350px; i.fa { font-size: 36pt; } } - @media (min-width: $screen-lg-min) { + @include media-breakpoint-up(lg) { width: 200px; height: 200px; i.fa { font-size: 72pt; } @@ -29,7 +35,7 @@ margin: 0 auto; padding: 20px; - @media (min-width: $screen-md-max) { + @include media-breakpoint-up(lg) { form { display: inline-block; } } } diff --git a/app/assets/stylesheets/components/_login.scss b/app/assets/stylesheets/components/_login.scss index fb578c4..b9da22e 100644 --- a/app/assets/stylesheets/components/_login.scss +++ b/app/assets/stylesheets/components/_login.scss @@ -1,28 +1,33 @@ +@import 'bootstrap/functions'; +@import 'bootstrap/variables'; +@import 'bootstrap/mixins'; +@import 'bootstrap'; + .centered_form { @extend .col-md-4; - @extend .col-md-offset-4; + @extend .offset-md-4; @extend .col-sm-6; - @extend .col-sm-offset-3; + @extend .offset-sm-3; } input[type=submit] { @extend .btn; @extend .btn-primary; @extend .btn-lg; - @extend .btn-block; + //@extend .btn-block; } .account_form { @extend .col-md-10; - @extend .col-md-offset-1; + @extend .offset-md-1; @extend .col-lg-8; - @extend .col-lg-offset-2; + @extend .offset-lg-2; } .form-horizontal { div.boolean { padding: 10px; - @extend .col-sm-offset-3; + @extend .offset-sm-3; } } @@ -32,7 +37,7 @@ input[type=submit] { } .edit_gravatar { - @extend .img-thumbnail; + //@extend .img-thumbnail; position: relative; diff --git a/app/assets/stylesheets/components/_nested_fields.scss b/app/assets/stylesheets/components/_nested_fields.scss index dc1b232..de775bf 100644 --- a/app/assets/stylesheets/components/_nested_fields.scss +++ b/app/assets/stylesheets/components/_nested_fields.scss @@ -1,9 +1,14 @@ +@import 'bootstrap/functions'; +@import 'bootstrap/variables'; +@import 'bootstrap/mixins'; +@import 'bootstrap'; + .add_nested_fields_container { @extend .text-center; } .form-horizontal .add_fields { - @extend .col-sm-offset-3; + @extend .offset-sm-3; @extend .btn; @extend .btn-primary; @extend .btn-sm; diff --git a/app/assets/stylesheets/components/_network_devices.scss b/app/assets/stylesheets/components/_network_devices.scss index c3fd4f4..053a1f3 100644 --- a/app/assets/stylesheets/components/_network_devices.scss +++ b/app/assets/stylesheets/components/_network_devices.scss @@ -1,3 +1,8 @@ +@import 'bootstrap/functions'; +@import 'bootstrap/variables'; +@import 'bootstrap/mixins'; +@import 'bootstrap'; + .new_network_device_container, .network_device_container { @extend .col-sm-4; diff --git a/app/assets/stylesheets/components/_page_actions.scss b/app/assets/stylesheets/components/_page_actions.scss index b1576fc..8e77560 100644 --- a/app/assets/stylesheets/components/_page_actions.scss +++ b/app/assets/stylesheets/components/_page_actions.scss @@ -1,13 +1,18 @@ +@import 'bootstrap/functions'; +@import 'bootstrap/variables'; +@import 'bootstrap/mixins'; +@import 'bootstrap'; + .page-actions { float: right; - @extend .hidden-xs; + @extend .d-none; + @extend .d-sm-block; } .mobile-page-actions { - @extend .hidden-sm; - @extend .visible-xs; + @extend .d-block; + @extend .d-sm-none; a { @extend .btn-lg; - @extend .btn-block; } } \ No newline at end of file diff --git a/app/assets/stylesheets/components/_record_actions.scss b/app/assets/stylesheets/components/_record_actions.scss index 9bb389c..6ec074b 100644 --- a/app/assets/stylesheets/components/_record_actions.scss +++ b/app/assets/stylesheets/components/_record_actions.scss @@ -1,3 +1,8 @@ +@import 'bootstrap/functions'; +@import 'bootstrap/variables'; +@import 'bootstrap/mixins'; +@import 'bootstrap'; + .record-actions { @extend .btn-group; a { @extend .btn; } @@ -7,7 +12,7 @@ } td .record-actions { - @extend .hidden-print; + @extend .d-print-none; justify-content: flex-end; display: flex; } diff --git a/app/assets/stylesheets/components/_record_table.scss b/app/assets/stylesheets/components/_record_table.scss index 4e0a47b..703b1f8 100644 --- a/app/assets/stylesheets/components/_record_table.scss +++ b/app/assets/stylesheets/components/_record_table.scss @@ -1,3 +1,8 @@ +@import 'bootstrap/functions'; +@import 'bootstrap/variables'; +@import 'bootstrap/mixins'; +@import 'bootstrap'; + .record-table { @extend .table; @extend .table-striped; diff --git a/app/assets/stylesheets/components/_role_assignments.scss b/app/assets/stylesheets/components/_role_assignments.scss index 13e0216..d0ebe7b 100644 --- a/app/assets/stylesheets/components/_role_assignments.scss +++ b/app/assets/stylesheets/components/_role_assignments.scss @@ -1,3 +1,8 @@ +@import 'bootstrap/functions'; +@import 'bootstrap/variables'; +@import 'bootstrap/mixins'; +@import 'bootstrap'; + .role-assignments { @extend .btn-group; white-space: nowrap; diff --git a/app/helpers/bootstrap_flash_helper.rb b/app/helpers/bootstrap_flash_helper.rb index 012c155..0d6255a 100644 --- a/app/helpers/bootstrap_flash_helper.rb +++ b/app/helpers/bootstrap_flash_helper.rb @@ -16,10 +16,8 @@ def bootstrap_flash end def flash_container(type, message) - content_tag :div, class: "alert alert-#{type} alert-dismissable" do - button_tag type: "button", class: "close", data: {dismiss: "alert"} do - "×".html_safe - end.safe_concat(message) + content_tag :div, class: "alert alert-#{type}" do + safe_concat(message) end end end diff --git a/app/views/fauna/role_assignments/_role_assignments.html.slim b/app/views/fauna/role_assignments/_role_assignments.html.slim index 7b647db..e722d5f 100644 --- a/app/views/fauna/role_assignments/_role_assignments.html.slim +++ b/app/views/fauna/role_assignments/_role_assignments.html.slim @@ -4,6 +4,8 @@ = link_to role_assignment_fauna_user_role_assignments_path(user.id, predefined_role.name), method: :delete, remote: true, class: 'btn btn-sm btn-warning', data: {disable: true, confirm: t('views.roles.are_you_sure')} do => icon('fa-solid', 'check-square-o') = predefined_role.localized_name + - if user.user_roles.find_by(role: predefined_role.id).end_time != nil + =< icon('fa-solid', 'calendar', html_options: {title: user.user_roles.find_by(role: predefined_role.id).end_time }) - else = link_to fauna_user_role_assignments_path(user.id, role: {name: predefined_role.name}), method: :post, remote: true, class: 'btn btn-sm btn-default', data: {disable: true, confirm: t('views.roles.are_you_sure')} do => icon('fa-solid', 'square-o') diff --git a/app/views/fauna/role_assignments/_temporary_roles.html.slim b/app/views/fauna/role_assignments/_temporary_roles.html.slim new file mode 100644 index 0000000..488a68a --- /dev/null +++ b/app/views/fauna/role_assignments/_temporary_roles.html.slim @@ -0,0 +1,12 @@ +.timed-roles id="timed-roles-#{user.id}" + button.btn.btn-sm.btn-primary[type='button' data-bs-toggle='modal' data-bs-target="#modal-timed-roles-#{user.id}"] + = icon('fa-solid', 'calendar') + + .modal.fade[tabindex="-1" id="modal-timed-roles-#{user.id}" role="dialog" aria-hidden="true"] + .modal-dialog[role="document"] + .modal-content + .modal-header + h5.modal-title = "#{User.human_attribute_name(:roles)} - #{user.name}" + button.btn-close[data-bs-dismiss="modal" aria-bs-label="Close"] + .modal-body + p kur \ No newline at end of file diff --git a/app/views/fauna/users/_user.html.slim b/app/views/fauna/users/_user.html.slim index 79e5104..205df69 100644 --- a/app/views/fauna/users/_user.html.slim +++ b/app/views/fauna/users/_user.html.slim @@ -4,3 +4,5 @@ tr td = user.email td.roles_column = render partial: '/fauna/role_assignments/role_assignments', locals: {user: user} + td.temporary_roles + = render partial: '/fauna/role_assignments/temporary_roles', locals: {user: user} diff --git a/app/views/fauna/users/index.html.slim b/app/views/fauna/users/index.html.slim index 9f125a9..344e6cf 100644 --- a/app/views/fauna/users/index.html.slim +++ b/app/views/fauna/users/index.html.slim @@ -14,5 +14,6 @@ section.container th = User.human_attribute_name(:username) th = User.human_attribute_name(:email) th.roles_column = User.human_attribute_name(:roles) + th.roles_column = User.human_attribute_name(:roles) tbody#user_table = render @users diff --git a/app/views/layouts/_navigation.html.slim b/app/views/layouts/_navigation.html.slim index 9ee13df..6b410fa 100644 --- a/app/views/layouts/_navigation.html.slim +++ b/app/views/layouts/_navigation.html.slim @@ -1,50 +1,44 @@ -nav class="navbar navbar-static-top #{content_for?(:nav_color) ? yield(:nav_color) : 'navbar-inverse'}" +nav class="py-0 navbar navbar-expand-lg #{content_for?(:nav_color) ? yield(:nav_color) : 'navbar-dark bg-primary'}" .container - .navbar-header - button.navbar-toggle data-target=".navbar-inverse-collapse" data-toggle="collapse" type="button" - span.icon-bar - span.icon-bar - span.icon-bar - = link_to root_url, class: "navbar-brand #{'active' if current_page?(root_path)}" do - = image_tag 'logo.svg', class: 'navbar-brand', alt: 'Fauna' - .navbar-collapse.collapse.navbar-inverse-collapse - ul.nav.navbar-nav + = link_to root_url, class: "navbar-brand #{'active' if current_page?(root_path)}" do + = image_tag 'logo.svg', class: 'logo', height: '47px', alt: 'Fauna' + button.navbar-toggler data-target="#navbar-collapse" data-toggle="collapse" type="button" + span.navbar-toggler-icon + + .navbar-collapse.collapse#navbar-collapse + .navbar-nav.flex-grow-1 - if user_signed_in? and UserPolicy.new(current_user, nil).index? - li class="#{'active' if current_page?(fauna_users_path)}" - = link_to fauna_users_path do + a.nav-link + = link_to fauna_users_path, class: "nav-link #{'active' if current_page?(fauna_users_path)}" do => icon('fa-solid', :users) = t('views.navigation.labbers') - ul.nav.navbar-nav.navbar-right + .navbar-nav.navbar-right - unless user_signed_in? - li.dropdown - = link_to '#', class: 'dropdown-toggle', data: {toggle: 'dropdown'} do + .nav-item.dropdown + button.dropdown-toggle.nav-link#langdropdown type='button' data-bs-toggle='dropdown' aria-haspopup='true' aria-expanded='false' => icon('fa-solid', :flag) = {bg: "БГ", en: "EN"}[I18n.locale] span.caret< - ul.dropdown-menu role="menu" + .dropdown-menu aria-labelledby="langdropdown" - I18n.available_locales.each do |locale| - li.text-center - = link_to({bg: "БГ", en: "EN"}[locale], "?locale=#{locale}") + = link_to({bg: "БГ", en: "EN"}[locale], "?locale=#{locale}", class: 'dropdown-item') - if user_signed_in? - li.dropdown class="#{'active' if current_page?(edit_user_registration_path) || current_page?(user_network_devices_path)}" - a.dropdown-toggle data-toggle="dropdown" href="#" + .nav-item.dropdown + / a.nav-link.dropdown class="#{'active' if current_page?(edit_user_registration_path) || current_page?(user_network_devices_path)}" + button.dropdown-toggle.nav-link#accountdropdown type='button' data-bs-toggle='dropdown' aria-haspopup='true' aria-expanded='false' + / a.dropdown-toggle data-toggle="dropdown" href="#" => icon('fa-solid', :user) = t('views.navigation.account') b.caret< - ul.dropdown-menu - li - = link_to t('views.navigation.view_edit'), edit_user_registration_path - li - = link_to t('views.navigation.network_devices'), user_network_devices_path - li - = link_to t('views.navigation.oauth_application_management'), oauth_applications_path - li - = link_to t('views.navigation.oauth_token_management'), oauth_authorized_applications_path - li.divider - li - = link_to t('views.navigation.sign_out'), destroy_user_session_path, method: :delete + .dropdown-menu aria-labelledby="accountdropdown" + = link_to t('views.navigation.view_edit'), edit_user_registration_path, class: 'dropdown-item' + = link_to t('views.navigation.network_devices'), user_network_devices_path, class: 'dropdown-item' + = link_to t('views.navigation.oauth_application_management'), oauth_applications_path, class: 'dropdown-item' + = link_to t('views.navigation.oauth_token_management'), oauth_authorized_applications_path, class: 'dropdown-item' + hr.dropdown-divider role="separator" + = link_to t('views.navigation.sign_out'), destroy_user_session_path, method: :delete, class: 'dropdown-item' - else - li class="#{'active' if controller_name == 'sessions'}" - = link_to new_user_session_path do + a.nav-link + = link_to new_user_session_path, class: "nav-link #{'active' if controller_name == 'sessions'}" do => icon('fa-solid', 'sign-in') = t('views.navigation.sign_in') diff --git a/app/views/layouts/application.html.slim b/app/views/layouts/application.html.slim index 017fdb5..e56508c 100644 --- a/app/views/layouts/application.html.slim +++ b/app/views/layouts/application.html.slim @@ -21,8 +21,9 @@ html lang="#{I18n.locale}" link rel="manifest" href="/manifest.json" body - = render partial: 'layouts/navigation' - = render partial: 'layouts/flash' - main + header + = render partial: 'layouts/navigation' + main.mt-4 + = render partial: 'layouts/flash' == yield = render partial: 'layouts/footer' diff --git a/app/views/network_devices/_network_device.html.slim b/app/views/network_devices/_network_device.html.slim index ae563ea..e4536b8 100644 --- a/app/views/network_devices/_network_device.html.slim +++ b/app/views/network_devices/_network_device.html.slim @@ -3,5 +3,5 @@ tr td = network_device.mac_address td.text-right .record-actions - => link_to icon(:pencil), edit_user_network_device_path(network_device), class: 'edit' - = link_to icon(:times), user_network_device_path(network_device), method: :delete, class: 'delete', data: {confirm: t('views.network_devices.are_you_sure')} + => link_to icon('fa-solid', :pencil), edit_user_network_device_path(network_device), class: 'edit' + = link_to icon('fa-solid', :times), user_network_device_path(network_device), method: :delete, class: 'delete', data: {confirm: t('views.network_devices.are_you_sure')} diff --git a/app/views/oauth/applications/_application.html.slim b/app/views/oauth/applications/_application.html.slim index a96f4af..8d7f5ce 100644 --- a/app/views/oauth/applications/_application.html.slim +++ b/app/views/oauth/applications/_application.html.slim @@ -5,5 +5,5 @@ tr id="application_#{application.id}" = application.redirect_uri td.text-right .record-actions - => link_to icon(:pencil), edit_oauth_application_path(application), class: 'edit', title: t('doorkeeper.applications.buttons.edit') - = link_to icon(:times), oauth_application_path(application), method: :delete, class: 'delete', data: {confirm: t('doorkeeper.applications.confirmations.destroy')}, title: t('doorkeeper.applications.buttons.destroy') + => link_to icon('fa-solid', :pencil), edit_oauth_application_path(application), class: 'edit', title: t('doorkeeper.applications.buttons.edit') + = link_to icon('fa-solid', :times), oauth_application_path(application), method: :delete, class: 'delete', data: {confirm: t('doorkeeper.applications.confirmations.destroy')}, title: t('doorkeeper.applications.buttons.destroy') diff --git a/config/application.rb b/config/application.rb index cc82a74..f73839e 100644 --- a/config/application.rb +++ b/config/application.rb @@ -46,5 +46,7 @@ class Application < Rails::Application end config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :forbidden + + config.sass.quiet_deps = true end end diff --git a/config/initializers/simple_form.rb b/config/initializers/simple_form.rb index 92935e9..d268784 100644 --- a/config/initializers/simple_form.rb +++ b/config/initializers/simple_form.rb @@ -1,3 +1,11 @@ +# frozen_string_literal: true +# +# Uncomment this and change the path if necessary to include your own +# components. +# See https://github.com/heartcombo/simple_form#custom-components to know +# more about custom components. +# Dir[Rails.root.join('lib/components/**/*.rb')].each { |f| require f } +# # Use this setup block to configure all options available in SimpleForm. SimpleForm.setup do |config| # Wrappers are used by the form builder to generate a @@ -6,7 +14,7 @@ # stack. The options given below are used to wrap the # whole input. config.wrappers :default, class: :input, - hint_class: :field_with_hint, error_class: :field_with_errors do |b| + hint_class: :field_with_hint, error_class: :field_with_errors, valid_class: :field_without_errors do |b| ## Extensions enabled by default # Any of these extensions can be disabled for a # given input by passing: `f.input EXTENSION_NAME => false`. @@ -28,8 +36,12 @@ # extensions by default, you can change `b.optional` to `b.use`. # Calculates maxlength from length validations for string inputs + # and/or database column lengths b.optional :maxlength + # Calculate minlength from length validations for string inputs + b.optional :minlength + # Calculates pattern from format validations for string inputs b.optional :pattern @@ -40,9 +52,10 @@ b.optional :readonly ## Inputs + # b.use :input, class: 'input', error_class: 'is-invalid', valid_class: 'is-valid' b.use :label_input - b.use :hint, wrap_with: {tag: :span, class: :hint} - b.use :error, wrap_with: {tag: :span, class: :error} + b.use :hint, wrap_with: { tag: :span, class: :hint } + b.use :error, wrap_with: { tag: :span, class: :error } ## full_messages_for # If you want to display the full error message for the attribute, you can @@ -61,7 +74,7 @@ config.boolean_style = :nested # Default class for buttons - config.button_class = "btn" + config.button_class = 'btn' # Method used to tidy up errors. Specify any Rails Array method. # :first lists the first message for each field. @@ -72,10 +85,7 @@ config.error_notification_tag = :div # CSS class to add for error notification helper. - config.error_notification_class = "error_notification" - - # ID to add for error notification helper. - # config.error_notification_id = nil + config.error_notification_class = 'error_notification' # Series of attempts to detect a default label method for collection. # config.collection_label_methods = [ :to_label, :name, :title, :to_s ] @@ -90,8 +100,7 @@ # config.collection_wrapper_class = nil # You can wrap each item in a collection of radio/check boxes with a tag, - # defaulting to :span. Please note that when using :boolean_style = :nested, - # SimpleForm will force this option to be a label. + # defaulting to :span. # config.item_wrapper_tag = :span # You can define a class to use in all item wrappers. Defaulting to none. @@ -103,7 +112,7 @@ # You can define the class to use on all labels. Default is nil. # config.label_class = nil - # You can define the default class to be used on forms. Can be overriden + # You can define the default class to be used on forms. Can be overridden # with `html: { :class }`. Defaulting to none. # config.default_form_class = nil @@ -120,9 +129,6 @@ # change this configuration to true. config.browser_validations = false - # Collection of methods to detect if a file type was given. - # config.file_methods = [ :mounted_as, :file?, :public_filename ] - # Custom mappings for input types. This should be a hash containing a regexp # to match as key, and the input type that will be used when the field name # matches the regexp as value. @@ -155,7 +161,7 @@ # config.input_class = nil # Define the default class of the input wrapper of the boolean input. - config.boolean_label_class = "checkbox" + config.boolean_label_class = 'checkbox' # Defines if the default input wrapper class should be included in radio # collection wrappers. @@ -163,4 +169,8 @@ # Defines which i18n scope will be used in Simple Form. # config.i18n_scope = 'simple_form' + + # Defines validation classes to the input_field. By default it's nil. + # config.input_field_valid_class = 'is-valid' + # config.input_field_error_class = 'is-invalid' end diff --git a/config/initializers/simple_form_bootstrap.rb b/config/initializers/simple_form_bootstrap.rb index e0fca33..7ec2ec6 100644 --- a/config/initializers/simple_form_bootstrap.rb +++ b/config/initializers/simple_form_bootstrap.rb @@ -1,136 +1,372 @@ +# frozen_string_literal: true + +# These defaults are defined and maintained by the community at +# https://github.com/heartcombo/simple_form-bootstrap +# Please submit feedback, changes and tests only there. + +# Uncomment this and change the path if necessary to include your own +# components. +# See https://github.com/heartcombo/simple_form#custom-components +# to know more about custom components. +# Dir[Rails.root.join('lib/components/**/*.rb')].each { |f| require f } + # Use this setup block to configure all options available in SimpleForm. SimpleForm.setup do |config| - config.error_notification_class = "alert alert-danger" - config.button_class = "btn btn-default" - config.boolean_label_class = nil + # Default class for buttons + config.button_class = 'btn' + + # Define the default class of the input wrapper of the boolean input. + config.boolean_label_class = 'form-check-label' + + # How the label text should be generated altogether with the required text. + config.label_text = lambda { |label, required, explicit_label| "#{label} #{required}" } + + # Define the way to render check boxes / radio buttons with labels. + config.boolean_style = :inline + + # You can wrap each item in a collection of radio/check boxes with a tag + config.item_wrapper_tag = :div + + # Defines if the default input wrapper class should be included in radio + # collection wrappers. + config.include_default_input_wrapper_class = false + + # CSS class to add for error notification helper. + config.error_notification_class = 'alert alert-danger' + + # Method used to tidy up errors. Specify any Rails Array method. + # :first lists the first message for each field. + # :to_sentence to list all errors for each field. + config.error_method = :to_sentence + + # add validation classes to `input_field` + config.input_field_error_class = 'is-invalid' + config.input_field_valid_class = 'is-valid' - config.wrappers :vertical_form, tag: "div", class: "form-group", error_class: "has-error" do |b| + + # vertical forms + # + # vertical default_wrapper + config.wrappers :vertical_form, class: 'mb-3' do |b| b.use :html5 b.use :placeholder b.optional :maxlength + b.optional :minlength b.optional :pattern b.optional :min_max b.optional :readonly - b.use :label, class: "control-label" + b.use :label, class: 'form-label' + b.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' + b.use :full_error, wrap_with: { class: 'invalid-feedback' } + b.use :hint, wrap_with: { class: 'form-text' } + end + + # vertical input for boolean + config.wrappers :vertical_boolean, tag: 'fieldset', class: 'mb-3' do |b| + b.use :html5 + b.optional :readonly + b.wrapper :form_check_wrapper, class: 'form-check' do |bb| + bb.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' + bb.use :label, class: 'form-check-label' + bb.use :full_error, wrap_with: { class: 'invalid-feedback' } + bb.use :hint, wrap_with: { class: 'form-text' } + end + end + + # vertical input for radio buttons and check boxes + config.wrappers :vertical_collection, item_wrapper_class: 'form-check', item_label_class: 'form-check-label', tag: 'fieldset', class: 'mb-3' do |b| + b.use :html5 + b.optional :readonly + b.wrapper :legend_tag, tag: 'legend', class: 'col-form-label pt-0' do |ba| + ba.use :label_text + end + b.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' + b.use :full_error, wrap_with: { class: 'invalid-feedback d-block' } + b.use :hint, wrap_with: { class: 'form-text' } + end - b.use :input, class: "form-control" - b.use :error, wrap_with: {tag: "span", class: "help-block"} - b.use :hint, wrap_with: {tag: "p", class: "help-block"} + # vertical input for inline radio buttons and check boxes + config.wrappers :vertical_collection_inline, item_wrapper_class: 'form-check form-check-inline', item_label_class: 'form-check-label', tag: 'fieldset', class: 'mb-3' do |b| + b.use :html5 + b.optional :readonly + b.wrapper :legend_tag, tag: 'legend', class: 'col-form-label pt-0' do |ba| + ba.use :label_text + end + b.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' + b.use :full_error, wrap_with: { class: 'invalid-feedback d-block' } + b.use :hint, wrap_with: { class: 'form-text' } end - config.wrappers :vertical_file_input, tag: "div", class: "form-group", error_class: "has-error" do |b| + # vertical file input + config.wrappers :vertical_file, class: 'mb-3' do |b| b.use :html5 b.use :placeholder b.optional :maxlength + b.optional :minlength b.optional :readonly - b.use :label, class: "control-label" - - b.use :input - b.use :error, wrap_with: {tag: "span", class: "help-block"} - b.use :hint, wrap_with: {tag: "p", class: "help-block"} + b.use :label, class: 'form-label' + b.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' + b.use :full_error, wrap_with: { class: 'invalid-feedback' } + b.use :hint, wrap_with: { class: 'form-text' } end - config.wrappers :vertical_boolean, tag: "div", class: "form-group", error_class: "has-error" do |b| + # vertical select input + config.wrappers :vertical_select, class: 'mb-3' do |b| b.use :html5 b.optional :readonly + b.use :label, class: 'form-label' + b.use :input, class: 'form-select', error_class: 'is-invalid', valid_class: 'is-valid' + b.use :full_error, wrap_with: { class: 'invalid-feedback' } + b.use :hint, wrap_with: { class: 'form-text' } + end - b.wrapper tag: "div", class: "checkbox" do |ba| - ba.use :label_input + # vertical multi select + config.wrappers :vertical_multi_select, class: 'mb-3' do |b| + b.use :html5 + b.optional :readonly + b.use :label, class: 'form-label' + b.wrapper class: 'd-flex flex-row justify-content-between align-items-center' do |ba| + ba.use :input, class: 'form-select mx-1', error_class: 'is-invalid', valid_class: 'is-valid' end - - b.use :error, wrap_with: {tag: "span", class: "help-block"} - b.use :hint, wrap_with: {tag: "p", class: "help-block"} + b.use :full_error, wrap_with: { class: 'invalid-feedback d-block' } + b.use :hint, wrap_with: { class: 'form-text' } end - config.wrappers :vertical_radio_and_checkboxes, tag: "div", class: "form-group", error_class: "has-error" do |b| + # vertical range input + config.wrappers :vertical_range, class: 'mb-3' do |b| b.use :html5 + b.use :placeholder b.optional :readonly - b.use :label, class: "control-label" - b.use :input - b.use :error, wrap_with: {tag: "span", class: "help-block"} - b.use :hint, wrap_with: {tag: "p", class: "help-block"} + b.optional :step + b.use :label, class: 'form-label' + b.use :input, class: 'form-range', error_class: 'is-invalid', valid_class: 'is-valid' + b.use :full_error, wrap_with: { class: 'invalid-feedback' } + b.use :hint, wrap_with: { class: 'form-text' } end - config.wrappers :horizontal_form, tag: "div", class: "form-group", error_class: "has-error" do |b| + + # horizontal forms + # + # horizontal default_wrapper + config.wrappers :horizontal_form, class: 'row mb-3' do |b| b.use :html5 b.use :placeholder b.optional :maxlength + b.optional :minlength b.optional :pattern b.optional :min_max b.optional :readonly - b.use :label, class: "col-sm-3 control-label" + b.use :label, class: 'col-sm-3 col-form-label' + b.wrapper :grid_wrapper, class: 'col-sm-9' do |ba| + ba.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' + ba.use :full_error, wrap_with: { class: 'invalid-feedback' } + ba.use :hint, wrap_with: { class: 'form-text' } + end + end + + # horizontal input for boolean + config.wrappers :horizontal_boolean, class: 'row mb-3' do |b| + b.use :html5 + b.optional :readonly + b.wrapper :grid_wrapper, class: 'col-sm-9 offset-sm-3' do |wr| + wr.wrapper :form_check_wrapper, class: 'form-check' do |bb| + bb.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' + bb.use :label, class: 'form-check-label' + bb.use :full_error, wrap_with: { class: 'invalid-feedback' } + bb.use :hint, wrap_with: { class: 'form-text' } + end + end + end + + # horizontal input for radio buttons and check boxes + config.wrappers :horizontal_collection, item_wrapper_class: 'form-check', item_label_class: 'form-check-label', class: 'row mb-3' do |b| + b.use :html5 + b.optional :readonly + b.use :label, class: 'col-sm-3 col-form-label pt-0' + b.wrapper :grid_wrapper, class: 'col-sm-9' do |ba| + ba.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' + ba.use :full_error, wrap_with: { class: 'invalid-feedback d-block' } + ba.use :hint, wrap_with: { class: 'form-text' } + end + end - b.wrapper tag: "div", class: "col-sm-9" do |ba| - ba.use :input, class: "form-control" - ba.use :error, wrap_with: {tag: "span", class: "help-block"} - ba.use :hint, wrap_with: {tag: "p", class: "help-block"} + # horizontal input for inline radio buttons and check boxes + config.wrappers :horizontal_collection_inline, item_wrapper_class: 'form-check form-check-inline', item_label_class: 'form-check-label', class: 'row mb-3' do |b| + b.use :html5 + b.optional :readonly + b.use :label, class: 'col-sm-3 col-form-label pt-0' + b.wrapper :grid_wrapper, class: 'col-sm-9' do |ba| + ba.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' + ba.use :full_error, wrap_with: { class: 'invalid-feedback d-block' } + ba.use :hint, wrap_with: { class: 'form-text' } end end - config.wrappers :horizontal_file_input, tag: "div", class: "form-group", error_class: "has-error" do |b| + # horizontal file input + config.wrappers :horizontal_file, class: 'row mb-3' do |b| b.use :html5 b.use :placeholder b.optional :maxlength + b.optional :minlength b.optional :readonly - b.use :label, class: "col-sm-3 control-label" - - b.wrapper tag: "div", class: "col-sm-9" do |ba| - ba.use :input - ba.use :error, wrap_with: {tag: "span", class: "help-block"} - ba.use :hint, wrap_with: {tag: "p", class: "help-block"} + b.use :label, class: 'col-sm-3 col-form-label' + b.wrapper :grid_wrapper, class: 'col-sm-9' do |ba| + ba.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' + ba.use :full_error, wrap_with: { class: 'invalid-feedback' } + ba.use :hint, wrap_with: { class: 'form-text' } end end - config.wrappers :horizontal_boolean, tag: "div", class: "form-group", error_class: "has-error" do |b| + # horizontal select input + config.wrappers :horizontal_select, class: 'row mb-3' do |b| b.use :html5 b.optional :readonly + b.use :label, class: 'col-sm-3 col-form-label' + b.wrapper :grid_wrapper, class: 'col-sm-9' do |ba| + ba.use :input, class: 'form-select', error_class: 'is-invalid', valid_class: 'is-valid' + ba.use :full_error, wrap_with: { class: 'invalid-feedback' } + ba.use :hint, wrap_with: { class: 'form-text' } + end + end - b.wrapper tag: "div", class: "col-sm-offset-3 col-sm-9" do |wr| - wr.wrapper tag: "div", class: "checkbox" do |ba| - ba.use :label_input, class: "col-sm-9" + # horizontal multi select + config.wrappers :horizontal_multi_select, class: 'row mb-3' do |b| + b.use :html5 + b.optional :readonly + b.use :label, class: 'col-sm-3 col-form-label' + b.wrapper :grid_wrapper, class: 'col-sm-9' do |ba| + ba.wrapper class: 'd-flex flex-row justify-content-between align-items-center' do |bb| + bb.use :input, class: 'form-select mx-1', error_class: 'is-invalid', valid_class: 'is-valid' end + ba.use :full_error, wrap_with: { class: 'invalid-feedback d-block' } + ba.use :hint, wrap_with: { class: 'form-text' } + end + end + + # horizontal range input + config.wrappers :horizontal_range, class: 'row mb-3' do |b| + b.use :html5 + b.use :placeholder + b.optional :readonly + b.optional :step + b.use :label, class: 'col-sm-3 col-form-label pt-0' + b.wrapper :grid_wrapper, class: 'col-sm-9' do |ba| + ba.use :input, class: 'form-range', error_class: 'is-invalid', valid_class: 'is-valid' + ba.use :full_error, wrap_with: { class: 'invalid-feedback' } + ba.use :hint, wrap_with: { class: 'form-text' } + end + end + + + # inline forms + # + # inline default_wrapper + config.wrappers :inline_form, class: 'col-12' do |b| + b.use :html5 + b.use :placeholder + b.optional :maxlength + b.optional :minlength + b.optional :pattern + b.optional :min_max + b.optional :readonly + b.use :label, class: 'visually-hidden' - wr.use :error, wrap_with: {tag: "span", class: "help-block"} - wr.use :hint, wrap_with: {tag: "p", class: "help-block"} + b.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' + b.use :error, wrap_with: { class: 'invalid-feedback' } + b.optional :hint, wrap_with: { class: 'form-text' } + end + + # inline input for boolean + config.wrappers :inline_boolean, class: 'col-12' do |b| + b.use :html5 + b.optional :readonly + b.wrapper :form_check_wrapper, class: 'form-check' do |bb| + bb.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' + bb.use :label, class: 'form-check-label' + bb.use :error, wrap_with: { class: 'invalid-feedback' } + bb.optional :hint, wrap_with: { class: 'form-text' } end end - config.wrappers :horizontal_radio_and_checkboxes, tag: "div", class: "form-group", error_class: "has-error" do |b| + + # bootstrap custom forms + # + # custom input switch for boolean + config.wrappers :custom_boolean_switch, class: 'mb-3' do |b| b.use :html5 b.optional :readonly + b.wrapper :form_check_wrapper, tag: 'div', class: 'form-check form-switch' do |bb| + bb.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' + bb.use :label, class: 'form-check-label' + bb.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback' } + bb.use :hint, wrap_with: { class: 'form-text' } + end + end - b.use :label, class: "col-sm-3 control-label" - b.wrapper tag: "div", class: "col-sm-9" do |ba| - ba.use :input - ba.use :error, wrap_with: {tag: "span", class: "help-block"} - ba.use :hint, wrap_with: {tag: "p", class: "help-block"} + # Input Group - custom component + # see example app and config at https://github.com/heartcombo/simple_form-bootstrap + config.wrappers :input_group, class: 'mb-3' do |b| + b.use :html5 + b.use :placeholder + b.optional :maxlength + b.optional :minlength + b.optional :pattern + b.optional :min_max + b.optional :readonly + b.use :label, class: 'form-label' + b.wrapper :input_group_tag, class: 'input-group' do |ba| + ba.optional :prepend + ba.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' + ba.optional :append + ba.use :full_error, wrap_with: { class: 'invalid-feedback' } end + b.use :hint, wrap_with: { class: 'form-text' } end - config.wrappers :inline_form, tag: "div", class: "form-group", error_class: "has-error" do |b| + + # Floating Labels form + # + # floating labels default_wrapper + config.wrappers :floating_labels_form, class: 'form-floating mb-3' do |b| b.use :html5 b.use :placeholder b.optional :maxlength + b.optional :minlength b.optional :pattern b.optional :min_max b.optional :readonly - b.use :label, class: "sr-only" + b.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' + b.use :label + b.use :full_error, wrap_with: { class: 'invalid-feedback' } + b.use :hint, wrap_with: { class: 'form-text' } + end - b.use :input, class: "form-control" - b.use :error, wrap_with: {tag: "span", class: "help-block"} - b.use :hint, wrap_with: {tag: "p", class: "help-block"} + # custom multi select + config.wrappers :floating_labels_select, class: 'form-floating mb-3' do |b| + b.use :html5 + b.optional :readonly + b.use :input, class: 'form-select', error_class: 'is-invalid', valid_class: 'is-valid' + b.use :label + b.use :full_error, wrap_with: { class: 'invalid-feedback' } + b.use :hint, wrap_with: { class: 'form-text' } end - # Wrappers for forms and inputs using the Bootstrap toolkit. - # Check the Bootstrap docs (http://getbootstrap.com) - # to learn about the different styles for forms and inputs, - # buttons and other elements. + + # The default wrapper to be used by the FormBuilder. config.default_wrapper = :vertical_form + + # Custom wrappers for input types. This should be a hash containing an input + # type as key and the wrapper that will be used for all inputs with specified type. config.wrapper_mappings = { - check_boxes: :vertical_radio_and_checkboxes, - radio_buttons: :vertical_radio_and_checkboxes, - file: :vertical_file_input, - boolean: :vertical_boolean + boolean: :vertical_boolean, + check_boxes: :vertical_collection, + date: :vertical_multi_select, + datetime: :vertical_multi_select, + file: :vertical_file, + radio_buttons: :vertical_collection, + range: :vertical_range, + time: :vertical_multi_select, + select: :vertical_select } end diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 49ca2ec..2374383 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -1,25 +1,31 @@ en: simple_form: - 'yes': 'Yes' - 'no': 'No' + "yes": 'Yes' + "no": 'No' required: - text: 'mandatory field' + text: 'required' mark: '*' + # You can uncomment the line below if you need to overwrite the whole required html. + # When using html, text and mark won't be used. + # html: '*' error_notification: - default_message: 'Please take a look at the indicated mistakes in the form:' - hints: - user: - github: username - twitter: username with optional "@" - url: URL beginning with http:// or https:// - jabber: Jabber account in the "username@domain.com" format - pin: it is used to lock/unlock the doors and must be a number with at least 6 digits; leave empty if you do not want to change it - phone_numbers: - phone_number: phone number with optional country code - network_device: - description: hostname or short hint - helpers: - submit: - network_device: - create: "Create" - update: "Change" + default_message: "Please review the problems below:" + # Examples + # labels: + # defaults: + # password: 'Password' + # user: + # new: + # email: 'E-mail to sign in.' + # edit: + # email: 'E-mail.' + # hints: + # defaults: + # username: 'User name to sign in.' + # password: 'No special characters, please.' + # include_blanks: + # defaults: + # age: 'Rather not say' + # prompts: + # defaults: + # age: 'Select your age' diff --git a/lib/templates/slim/scaffold/_form.html.slim b/lib/templates/slim/scaffold/_form.html.slim index a2ff775..3427928 100644 --- a/lib/templates/slim/scaffold/_form.html.slim +++ b/lib/templates/slim/scaffold/_form.html.slim @@ -1,5 +1,6 @@ = simple_form_for(@<%= singular_table_name %>) do |f| = f.error_notification + = f.error_notification message: f.object.errors[:base].to_sentence if f.object.errors[:base].present? .form-inputs <%- attributes.each do |attribute| -%> From 913f1a19fecbe6635ab474725a3a62a9f1cb8e2c Mon Sep 17 00:00:00 2001 From: Albert Stefanov Date: Sun, 8 Feb 2026 10:55:02 +0200 Subject: [PATCH 07/19] frontend: use the same bootstrap theme as space.initlab.org --- Gemfile | 1 + Gemfile.lock | 7 +++++++ app/assets/stylesheets/application.scss | 2 ++ app/helpers/bootstrap_flash_helper.rb | 13 ++++++++++--- app/views/layouts/_deprecated.html.slim | 8 +++++--- app/views/layouts/_flash.html.slim | 6 +++--- app/views/layouts/_footer.html.slim | 4 ++-- app/views/layouts/_navigation.html.slim | 2 +- config/initializers/bootswatch.rb | 1 + 9 files changed, 32 insertions(+), 12 deletions(-) create mode 100644 config/initializers/bootswatch.rb diff --git a/Gemfile b/Gemfile index 6f48d2f..50747a2 100644 --- a/Gemfile +++ b/Gemfile @@ -44,6 +44,7 @@ gem "slim-rails" # Use the Bootstrap CSS framework and the FA icon font gem "bootstrap" gem "font-awesome-sass", :github => 'sunbirddcim/font-awesome-sass' +gem "bootswatch", :github => "thomaspark/bootswatch" # Gravatar helper gem "gravatar-ultimate" diff --git a/Gemfile.lock b/Gemfile.lock index 68d5995..3f3d6d8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,6 +4,12 @@ GIT specs: font-awesome-sass (6.5.2.pre.sunbird.pre.1) +GIT + remote: https://github.com/thomaspark/bootswatch.git + revision: 149996ddaa93db10c6fc05145e3ab98872718c5b + specs: + bootswatch (5.3.8) + GEM remote: https://rubygems.org/ specs: @@ -435,6 +441,7 @@ PLATFORMS DEPENDENCIES bootsnap bootstrap + bootswatch! brakeman bundler-audit byebug diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 147f298..c193ac5 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -1,7 +1,9 @@ +@import "materia/variables"; @import 'bootstrap/functions'; @import 'bootstrap/variables'; @import 'bootstrap/mixins'; @import 'bootstrap'; +@import "materia/bootswatch"; @import 'font-awesome'; diff --git a/app/helpers/bootstrap_flash_helper.rb b/app/helpers/bootstrap_flash_helper.rb index 0d6255a..f036e5d 100644 --- a/app/helpers/bootstrap_flash_helper.rb +++ b/app/helpers/bootstrap_flash_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module BootstrapFlashHelper ALERT_TYPES_MAP = { notice: :success, @@ -5,19 +7,24 @@ module BootstrapFlashHelper error: :danger, info: :info, warning: :warning - } + }.freeze def bootstrap_flash safe_join(flash.each_with_object([]) do |(type, message), messages| next if message.blank? || !message.respond_to?(:to_str) + type = ALERT_TYPES_MAP.fetch(type.to_sym, type) messages << flash_container(type, message) end, "\n").presence end def flash_container(type, message) - content_tag :div, class: "alert alert-#{type}" do - safe_concat(message) + content_tag :div, class: "card border-#{type}" do + content_tag :div, class: 'card-body' do + content_tag :div, class: 'card-text' do + safe_concat(message) + end + end end end end diff --git a/app/views/layouts/_deprecated.html.slim b/app/views/layouts/_deprecated.html.slim index cc306c5..5731a0b 100644 --- a/app/views/layouts/_deprecated.html.slim +++ b/app/views/layouts/_deprecated.html.slim @@ -1,3 +1,5 @@ -.alert.alert-info - = t 'views.deprecated.message' - =< link_to 'https://space.initlab.org/', 'https://space.initlab.org/' +.card.border-info + .card-body + .card-text + = t 'views.deprecated.message' + =< link_to 'https://space.initlab.org/', 'https://space.initlab.org/' diff --git a/app/views/layouts/_flash.html.slim b/app/views/layouts/_flash.html.slim index 0e44ab1..2d23d4f 100644 --- a/app/views/layouts/_flash.html.slim +++ b/app/views/layouts/_flash.html.slim @@ -1,4 +1,4 @@ -.container - .row - .flash_message +- if !flash.empty? + .container + .row.my-4 = bootstrap_flash diff --git a/app/views/layouts/_footer.html.slim b/app/views/layouts/_footer.html.slim index 0c623b4..0128740 100644 --- a/app/views/layouts/_footer.html.slim +++ b/app/views/layouts/_footer.html.slim @@ -2,10 +2,10 @@ footer.footer .container .row div.col-xs-12.col-sm-6 - p + p.my-4 = t('views.footer.license') =< link_to 'MIT License', 'https://github.com/initLab/fauna/blob/master/LICENSE' div.col-xs-12.col-sm-6 - p.right + p.right.my-4 = t('views.footer.source_code') =< link_to 'Github', 'https://github.com/initLab/fauna/' diff --git a/app/views/layouts/_navigation.html.slim b/app/views/layouts/_navigation.html.slim index 6b410fa..876b265 100644 --- a/app/views/layouts/_navigation.html.slim +++ b/app/views/layouts/_navigation.html.slim @@ -1,6 +1,6 @@ nav class="py-0 navbar navbar-expand-lg #{content_for?(:nav_color) ? yield(:nav_color) : 'navbar-dark bg-primary'}" .container - = link_to root_url, class: "navbar-brand #{'active' if current_page?(root_path)}" do + = link_to root_url, class: "navbar-brand", active: false do = image_tag 'logo.svg', class: 'logo', height: '47px', alt: 'Fauna' button.navbar-toggler data-target="#navbar-collapse" data-toggle="collapse" type="button" span.navbar-toggler-icon diff --git a/config/initializers/bootswatch.rb b/config/initializers/bootswatch.rb new file mode 100644 index 0000000..7e94884 --- /dev/null +++ b/config/initializers/bootswatch.rb @@ -0,0 +1 @@ +Rails.application.config.assets.paths += Gem.loaded_specs["bootswatch"].load_paths From 72542d6e03135775c699e14f9126476906b90676 Mon Sep 17 00:00:00 2001 From: Albert Stefanov Date: Sat, 14 Feb 2026 10:10:18 +0200 Subject: [PATCH 08/19] Role Assignments UI Prototype This is not wired in at all. --- .../fauna/role_assignments_controller.rb | 8 ++ app/models/user_role.rb | 3 + .../fauna/role_assignments/_form.html.slim | 11 +++ .../_role_assignments.html.slim | 2 + .../_temporary_roles.html.slim | 76 ++++++++++++++++++- .../fauna/role_assignments/index.html.slim | 65 ++++++++++++++++ .../fauna/role_assignments/new.html.slim | 4 + app/views/fauna/users/_user.html.slim | 6 +- config/locales/bg.yml | 13 ++++ config/locales/en.yml | 7 ++ config/routes.rb | 2 +- 11 files changed, 193 insertions(+), 4 deletions(-) create mode 100644 app/views/fauna/role_assignments/_form.html.slim create mode 100644 app/views/fauna/role_assignments/index.html.slim create mode 100644 app/views/fauna/role_assignments/new.html.slim diff --git a/app/controllers/fauna/role_assignments_controller.rb b/app/controllers/fauna/role_assignments_controller.rb index b29f45c..051eab4 100644 --- a/app/controllers/fauna/role_assignments_controller.rb +++ b/app/controllers/fauna/role_assignments_controller.rb @@ -4,6 +4,14 @@ class RoleAssignmentsController < ApplicationController before_action :assign_user + def index + authorize :role_assignment + end + + def new + authorize :role_assignment + end + def create authorize :role_assignment diff --git a/app/models/user_role.rb b/app/models/user_role.rb index 2d51509..749cb96 100644 --- a/app/models/user_role.rb +++ b/app/models/user_role.rb @@ -4,6 +4,9 @@ class UserRole < ApplicationRecord belongs_to :role belongs_to :user + scope :expired, -> { where('(end_time is not null and end_time < ?)', Time.current) } + scope :future, -> { where('(start_time is not null and start_time > ?)', Time.current) } + scope :active_at, ->(time) { where('(start_time < ?) and (end_time is null or end_time > ?)', time, time) } default_scope { where('(start_time < ?) and (end_time is null or end_time > ?)', Time.current, Time.current) } end diff --git a/app/views/fauna/role_assignments/_form.html.slim b/app/views/fauna/role_assignments/_form.html.slim new file mode 100644 index 0000000..3fd6691 --- /dev/null +++ b/app/views/fauna/role_assignments/_form.html.slim @@ -0,0 +1,11 @@ +.centered_form + = simple_form_for @user.user_roles.new, url: fauna_user_role_assignments_path do |f| + header + legend + h3 = Role.model_name.human + .form-inputs + = f.input :start_time, as: :date, html5: true + = f.input :end_time, as: :date, html5: true + = f.input :role, collection: Role.all.map { |r| [r.localized_name, r.id] } + .form-actions + = f.button :submit diff --git a/app/views/fauna/role_assignments/_role_assignments.html.slim b/app/views/fauna/role_assignments/_role_assignments.html.slim index e722d5f..fceab90 100644 --- a/app/views/fauna/role_assignments/_role_assignments.html.slim +++ b/app/views/fauna/role_assignments/_role_assignments.html.slim @@ -10,3 +10,5 @@ = link_to fauna_user_role_assignments_path(user.id, role: {name: predefined_role.name}), method: :post, remote: true, class: 'btn btn-sm btn-default', data: {disable: true, confirm: t('views.roles.are_you_sure')} do => icon('fa-solid', 'square-o') = predefined_role.localized_name + - if user.user_roles.unscoped.future.find_by(role: predefined_role.id) + =< icon('fa-solid', 'calendar', html_options: { title: user.user_roles.unscoped.future.find_by(role: predefined_role.id).end_time }) diff --git a/app/views/fauna/role_assignments/_temporary_roles.html.slim b/app/views/fauna/role_assignments/_temporary_roles.html.slim index 488a68a..a404460 100644 --- a/app/views/fauna/role_assignments/_temporary_roles.html.slim +++ b/app/views/fauna/role_assignments/_temporary_roles.html.slim @@ -2,11 +2,83 @@ button.btn.btn-sm.btn-primary[type='button' data-bs-toggle='modal' data-bs-target="#modal-timed-roles-#{user.id}"] = icon('fa-solid', 'calendar') - .modal.fade[tabindex="-1" id="modal-timed-roles-#{user.id}" role="dialog" aria-hidden="true"] + .modal.modal-lg.fade[tabindex="-1" id="modal-timed-roles-#{user.id}" role="dialog" aria-hidden="true"] .modal-dialog[role="document"] .modal-content .modal-header h5.modal-title = "#{User.human_attribute_name(:roles)} - #{user.name}" + div.px-4 + button.btn.btn-success + = t('views.roles.add', model: Role.model_name.human) button.btn-close[data-bs-dismiss="modal" aria-bs-label="Close"] .modal-body - p kur \ No newline at end of file + + + .fw-bold = t('views.roles.current') + div.d-table + div.d-table-row + div.d-table-cell.px-2.font-weight-bold + = UserRole.human_attribute_name(:role) + div.d-table-cell.px-2.font-weight-bold + = UserRole.human_attribute_name(:start_time) + div.d-table-cell.px-2.font-weight-bold + = UserRole.human_attribute_name(:end_time) + + - user.user_roles.order(end_time: :desc).each do |ur| + div.d-table-row + div.d-table-cell.px-2 + = ur.role.localized_name + div.d-table-cell.px-2 + - if !ur.start_time.nil? + = ur.start_time + div.d-table-cell.px-2 + - if !ur.end_time.nil? + = ur.end_time + div.d-table-cell.px-2 + button.btn.btn-danger.btn-sm + = icon('fa-solid', :close) + hr + .fw-bold = t('views.roles.future') + div.d-table + div.d-table-row + div.d-table-cell.px-2.font-weight-bold + = UserRole.human_attribute_name(:role) + div.d-table-cell.px-2.font-weight-bold + = UserRole.human_attribute_name(:start_time) + div.d-table-cell.px-2.font-weight-bold + = UserRole.human_attribute_name(:end_time) + + - user.user_roles.unscoped.future.order(end_time: :desc).each do |ur| + div.d-table-row + div.d-table-cell.px-2 + = ur.role.localized_name + div.d-table-cell.px-2 + - if !ur.start_time.nil? + = ur.start_time + div.d-table-cell.px-2 + - if !ur.end_time.nil? + = ur.end_time + div.d-table-cell.px-2 + button.btn.btn-danger.btn-sm + = icon('fa-solid', :close) + hr + .fw-bold = t('views.roles.expired') + div.d-table + div.d-table-row + div.d-table-cell.px-2.font-weight-bold + = UserRole.human_attribute_name(:role) + div.d-table-cell.px-2.font-weight-bold + = UserRole.human_attribute_name(:start_time) + div.d-table-cell.px-2.font-weight-bold + = UserRole.human_attribute_name(:end_time) + + - user.user_roles.unscoped.expired.order(end_time: :desc).each do |ur| + div.d-table-row + div.d-table-cell.px-2 + = ur.role.localized_name + div.d-table-cell.px-2 + - if !ur.start_time.nil? + = ur.start_time + div.d-table-cell.px-2 + - if !ur.end_time.nil? + = ur.end_time diff --git a/app/views/fauna/role_assignments/index.html.slim b/app/views/fauna/role_assignments/index.html.slim new file mode 100644 index 0000000..5589949 --- /dev/null +++ b/app/views/fauna/role_assignments/index.html.slim @@ -0,0 +1,65 @@ +- content_for(:title) { "#{User.human_attribute_name(:roles)} - #{@user.name}" } +- content_for(:page_actions) do + = link_to new_fauna_user_role_assignment_path, class: ['btn', 'btn-success'] do + => icon('fa-solid', :plus) + = t('views.roles.add') + +section.container + header.row + .col-sm-12 + header.page-header + .page-actions + = yield :page_actions + h2 = "#{User.human_attribute_name(:roles)} - #{@user.name}" + .row + .col-sm-12 + h4 = t('views.roles.current') + table.table.table-hover.table-sm.m-2 + thead + tr + th[scope='col'] = UserRole.human_attribute_name(:role) + th[scope='col'] = UserRole.human_attribute_name(:start_time) + th[scope='col'] = UserRole.human_attribute_name(:end_time) + th + + tbody + - @user.user_roles.order(end_time: :desc).each do |ur| + tr + td = ur.role.localized_name + td = ur.start_time + td = ur.end_time + td + button.btn.btn-danger.btn-sm.m-0 + = t('views.roles.deactivate') + hr + h4 = t 'views.roles.future' + table.table.table-hover.table-sm.m-2 + thead + tr + th = UserRole.human_attribute_name(:role) + th = UserRole.human_attribute_name(:start_time) + th = UserRole.human_attribute_name(:end_time) + th + tbody + - @user.user_roles.unscoped.future.order(end_time: :desc).each do |ur| + tr + td = ur.role.localized_name + td = ur.start_time + td = ur.end_time + td + button.btn.btn-danger.btn-sm + = t('views.roles.cancel') + hr + h4 = t('views.roles.expired') + table.table.table-hover.table-sm.m-2 + thead + tr + th = UserRole.human_attribute_name(:role) + th = UserRole.human_attribute_name(:start_time) + th = UserRole.human_attribute_name(:end_time) + + - @user.user_roles.unscoped.expired.order(end_time: :desc).each do |ur| + tr + td = ur.role.localized_name + td = ur.start_time + td = ur.end_time diff --git a/app/views/fauna/role_assignments/new.html.slim b/app/views/fauna/role_assignments/new.html.slim new file mode 100644 index 0000000..96a05be --- /dev/null +++ b/app/views/fauna/role_assignments/new.html.slim @@ -0,0 +1,4 @@ +- content_for(:title) { t 'views.roles.add' } +section.container + .row + = render partial: 'form' diff --git a/app/views/fauna/users/_user.html.slim b/app/views/fauna/users/_user.html.slim index 205df69..b6d334d 100644 --- a/app/views/fauna/users/_user.html.slim +++ b/app/views/fauna/users/_user.html.slim @@ -5,4 +5,8 @@ tr td.roles_column = render partial: '/fauna/role_assignments/role_assignments', locals: {user: user} td.temporary_roles - = render partial: '/fauna/role_assignments/temporary_roles', locals: {user: user} + = link_to fauna_user_role_assignments_path(user.id), :class => 'btn btn-sm btn-primary' do + = icon('fa-solid', 'calendar') + / button.btn.btn-sm.btn-primary[type='button' data-bs-toggle='modal' data-bs-target="#modal-timed-roles-#{user.id}"] + = icon('fa-solid', 'calendar') + / = render partial: '/fauna/role_assignments/temporary_roles', locals: {user: user} diff --git a/config/locales/bg.yml b/config/locales/bg.yml index b2d865f..7134c0d 100644 --- a/config/locales/bg.yml +++ b/config/locales/bg.yml @@ -63,6 +63,11 @@ bg: use_for_presence: Използвай за определяне кога съм в Лаба phone_number: phone_number: Телефонен номер + user_role: + user: Потребител + role: Роля + start_time: Начало + end_time: Край mailers: unlocks: hello: "Здравейте, %{name}!" @@ -153,6 +158,14 @@ bg: new_network_device: Ново мрежово устройство roles: are_you_sure: 'Сигурни ли сте?' + add: 'Добави достъп' + active: 'Активна' + inactive: 'Неактивна' + expired: 'Изтекли' + current: 'Активни' + future: 'Бъдещи' + deactivate: 'Деактивирай' + cancel: 'Откажи' footer: license: 'init Lab Fauna е лицензирана под' source_code: 'Изходният код е достъпен в' diff --git a/config/locales/en.yml b/config/locales/en.yml index b3565c0..84d3e6c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -57,6 +57,11 @@ en: use_for_presence: Use to determine when I'm in the Lab phone_number: phone_number: Phone number + user_role: + user: User + role: Role + start_time: From + end_time: Until mailers: unlocks: hello: "Hello, %{name}!" @@ -146,6 +151,8 @@ en: new_network_device: New network device roles: are_you_sure: 'Are you shnur?' + active: 'Active' + inactive: 'Inactive' footer: license: 'init Lab Fauna is licensed under' source_code: 'The source code is available in' diff --git a/config/routes.rb b/config/routes.rb index f43bed4..fbaedaa 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -15,7 +15,7 @@ namespace :fauna do resources :users, only: [:index, :edit, :update, :show, :destroy] do - resources :role_assignments, only: [:create] do + resources :role_assignments, only: [:index, :new, :create] do collection do delete ":role_name", action: "destroy", as: "role_assignment" end From a6c0007de11dd4b305d8f8ff2e2f8c2abab15d37 Mon Sep 17 00:00:00 2001 From: Albert Stefanov Date: Sun, 15 Feb 2026 11:11:27 +0200 Subject: [PATCH 09/19] Roles: add controller actions Now access can actually be added and removed via the new UI. Note - deleting roles from the labbers page is broken --- .../components/{_login.scss => _forms.scss} | 0 .../components/_role_assignments.scss | 13 ----- .../fauna/role_assignments_controller.rb | 31 +++++++++-- app/models/user.rb | 55 ++++++++++--------- app/models/user_role.rb | 7 ++- .../_role_assignments.html.slim | 9 ++- .../fauna/role_assignments/index.html.slim | 22 ++++---- config/routes.rb | 34 ++++++------ 8 files changed, 94 insertions(+), 77 deletions(-) rename app/assets/stylesheets/components/{_login.scss => _forms.scss} (100%) delete mode 100644 app/assets/stylesheets/components/_role_assignments.scss diff --git a/app/assets/stylesheets/components/_login.scss b/app/assets/stylesheets/components/_forms.scss similarity index 100% rename from app/assets/stylesheets/components/_login.scss rename to app/assets/stylesheets/components/_forms.scss diff --git a/app/assets/stylesheets/components/_role_assignments.scss b/app/assets/stylesheets/components/_role_assignments.scss deleted file mode 100644 index d0ebe7b..0000000 --- a/app/assets/stylesheets/components/_role_assignments.scss +++ /dev/null @@ -1,13 +0,0 @@ -@import 'bootstrap/functions'; -@import 'bootstrap/variables'; -@import 'bootstrap/mixins'; -@import 'bootstrap'; - -.role-assignments { - @extend .btn-group; - white-space: nowrap; - .btn { - float: none; - display: inline-block; - } -} \ No newline at end of file diff --git a/app/controllers/fauna/role_assignments_controller.rb b/app/controllers/fauna/role_assignments_controller.rb index 051eab4..8b432fb 100644 --- a/app/controllers/fauna/role_assignments_controller.rb +++ b/app/controllers/fauna/role_assignments_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module Fauna class RoleAssignmentsController < ApplicationController before_action :authenticate_user! @@ -14,10 +15,21 @@ def new def create authorize :role_assignment + role = Role.find(role_params[:role]) + start_time = role_params[:start_time] + end_time = role_params[:end_time] respond_to do |format| + format.html do + if @user.add_role(role, start_time, end_time) + redirect_to fauna_user_role_assignments_path + else + render :new, status: :unprocessable_content + end + end + format.js do - if @user.add_role(role_params[:name]).persisted? + if @user.add_role(role, start_time, end_time) render :refresh, status: :created else head :unprocessable_entity @@ -30,13 +42,22 @@ def create def destroy authorize :role_assignment + role = Role.find(params[:id]) + start_time = params[:start_time] respond_to do |format| - format.js do - if @user.remove_role(params[:role_name]).empty? - head :unprocessable_entity + format.html do + if @user.remove_role(role, start_time) + redirect_to fauna_user_role_assignments_path else + redirect_back fallback_location: fauna_user_role_assignments_path, status: :unprocessable_entity + end + end + format.js do + if @user.remove_role(role, start_time) render :refresh + else + head :unprocessable_entity end end end @@ -51,7 +72,7 @@ def assign_user end def role_params - params.require(:role).permit(:name) + params.require(:user_role).permit(%i[role start_time end_time]) end end end diff --git a/app/models/user.rb b/app/models/user.rb index 8641256..e76f961 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,11 +1,13 @@ +# frozen_string_literal: true + require 'digest/md5' class User < ApplicationRecord # Include default devise modules. Others available are: # :timeoutable and :omniauthable devise :database_authenticatable, :registerable, :confirmable, :lockable, - :recoverable, :rememberable, :trackable, :validatable, :doorkeeper, - authentication_keys: [:login] + :recoverable, :rememberable, :trackable, :validatable, :doorkeeper, + authentication_keys: [:login] has_many :user_roles has_many :roles, through: :user_roles @@ -14,15 +16,16 @@ class User < ApplicationRecord has_many :phone_numbers, foreign_key: :owner_id, dependent: :destroy has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner - validates :username, uniqueness: {case_sensitive: false}, presence: true - validates :twitter, format: {with: /\A[A-Za-z0-9_]{1,15}\z/}, allow_blank: true - validates :url, format: {with: /\A(http|https):\/\/[a-z0-9]+([-.]{1}[a-z0-9]+)*\.[a-z-]{2,63}(:[0-9]{1,5})?(\/.*)?\z/ix}, allow_blank: true + validates :username, uniqueness: { case_sensitive: false }, presence: true + validates :twitter, format: { with: /\A[A-Za-z0-9_]{1,15}\z/ }, allow_blank: true + validates :url, + format: { with: %r{\A(http|https)://[a-z0-9]+([-.]{1}[a-z0-9]+)*\.[a-z-]{2,63}(:[0-9]{1,5})?(/.*)?\z}ix }, allow_blank: true validates :first_name, presence: true validates :last_name, presence: true - validates :github, format: {with: /\A[a-z0-9][a-z0-9-]{,38}\z/i}, allow_blank: true - validates :jabber, format: {with: /\A[^@]+@[^@]+\z/}, allow_blank: true - validates :pin, numericality: true, length: {minimum: 6}, allow_blank: true, confirmation: true - validates :locale, presence: true, inclusion: {in: I18n.available_locales.map(&:to_s)} + validates :github, format: { with: /\A[a-z0-9][a-z0-9-]{,38}\z/i }, allow_blank: true + validates :jabber, format: { with: /\A[^@]+@[^@]+\z/ }, allow_blank: true + validates :pin, numericality: true, length: { minimum: 6 }, allow_blank: true, confirmation: true + validates :locale, presence: true, inclusion: { in: I18n.available_locales.map(&:to_s) } accepts_nested_attributes_for :phone_numbers, update_only: true, allow_destroy: true, reject_if: :all_blank attr_accessor :login @@ -30,27 +33,25 @@ class User < ApplicationRecord def self.find_for_database_authentication(warden_conditions) conditions = warden_conditions.dup - if (login = conditions.delete(:login)) - where(conditions).where(['lower(username) = :value OR lower(email) = :value', {value: login.downcase.strip}]).first - end + return unless (login = conditions.delete(:login)) + + where(conditions).where(['lower(username) = :value OR lower(email) = :value', + { value: login.downcase.strip }]).first end def has_role?(role) - self.roles.exists?(name: role) + roles.exists?(name: role) end - def add_role(role_name) - role = Role.find_by(name: role_name) - - self.user_roles.find_or_create_by(role_id: role.id, end_time: nil) - self.roles.reload + def add_role(role, start_time = nil, end_time = nil) + user_role = UserRole.new(user_id: id, role_id: role.id, start_time: start_time, end_time: end_time) + user_role.save end - def remove_role(role_name) - role = Role.find_by(name: role_name) - - self.user_roles.find_by(role_id: role.id).update(end_time: Time.current) - self.roles.reload + def remove_role(role, start_time = nil) + user_role = UserRole.unscoped.find_by(user_id: id, role_id: role.id, start_time: start_time) + user_role ||= UserRole.find_by(user_id: id, role_id: role.id) + user_role.deactivate end def email_md5 @@ -70,10 +71,10 @@ def picture(size = 128) end def pin=(pin) - if pin.present? - @pin = pin - self.encrypted_pin = BCrypt::Password.create pin - end + return unless pin.present? + + @pin = pin + self.encrypted_pin = BCrypt::Password.create pin end attr_reader :pin diff --git a/app/models/user_role.rb b/app/models/user_role.rb index 749cb96..f992543 100644 --- a/app/models/user_role.rb +++ b/app/models/user_role.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class UserRole < ApplicationRecord self.table_name = :users_roles @@ -5,8 +6,12 @@ class UserRole < ApplicationRecord belongs_to :user scope :expired, -> { where('(end_time is not null and end_time < ?)', Time.current) } - scope :future, -> { where('(start_time is not null and start_time > ?)', Time.current) } + scope :future, -> { where('(start_time is not null and start_time > ? and end_time > ?)', Time.current, Time.current) } scope :active_at, ->(time) { where('(start_time < ?) and (end_time is null or end_time > ?)', time, time) } default_scope { where('(start_time < ?) and (end_time is null or end_time > ?)', Time.current, Time.current) } + + def deactivate + update(end_time: Time.current) + end end diff --git a/app/views/fauna/role_assignments/_role_assignments.html.slim b/app/views/fauna/role_assignments/_role_assignments.html.slim index fceab90..6856ef9 100644 --- a/app/views/fauna/role_assignments/_role_assignments.html.slim +++ b/app/views/fauna/role_assignments/_role_assignments.html.slim @@ -1,13 +1,16 @@ -.role-assignments id="role-assignments-#{user.id}" +.role-assignments.btn-group id="role-assignments-#{user.id}" role="group" - Role.predefined.each do |predefined_role| - if user.has_role? predefined_role.name - = link_to role_assignment_fauna_user_role_assignments_path(user.id, predefined_role.name), method: :delete, remote: true, class: 'btn btn-sm btn-warning', data: {disable: true, confirm: t('views.roles.are_you_sure')} do + / TODO: hidden form here + / = button_to fauna_user_role_assignments_path(user_id: user.id, id: predefined_role.id), method: :delete, remote: true, class: 'btn btn-sm btn-warning', data: {disable: true, confirm: t('views.roles.are_you_sure')} do + button.btn.btn-sm.btn-warning => icon('fa-solid', 'check-square-o') = predefined_role.localized_name - if user.user_roles.find_by(role: predefined_role.id).end_time != nil =< icon('fa-solid', 'calendar', html_options: {title: user.user_roles.find_by(role: predefined_role.id).end_time }) - else - = link_to fauna_user_role_assignments_path(user.id, role: {name: predefined_role.name}), method: :post, remote: true, class: 'btn btn-sm btn-default', data: {disable: true, confirm: t('views.roles.are_you_sure')} do + / = link_to fauna_user_role_assignments_path(user.id, role: {name: predefined_role.name}), method: :post, remote: true, class: 'btn btn-sm btn-default', data: {disable: true, confirm: t('views.roles.are_you_sure')} do + button.btn.btn-sm.btn-default.disabled => icon('fa-solid', 'square-o') = predefined_role.localized_name - if user.user_roles.unscoped.future.find_by(role: predefined_role.id) diff --git a/app/views/fauna/role_assignments/index.html.slim b/app/views/fauna/role_assignments/index.html.slim index 5589949..ab6bf21 100644 --- a/app/views/fauna/role_assignments/index.html.slim +++ b/app/views/fauna/role_assignments/index.html.slim @@ -26,11 +26,12 @@ section.container - @user.user_roles.order(end_time: :desc).each do |ur| tr td = ur.role.localized_name - td = ur.start_time - td = ur.end_time + td = ur.start_time.localtime + td = ur.end_time&.localtime td - button.btn.btn-danger.btn-sm.m-0 - = t('views.roles.deactivate') + = form_tag fauna_user_role_assignment_path(id: ur.role_id), method: :delete do + = hidden_field_tag :start_time, value: ur.start_time + = submit_tag t('views.roles.deactivate'), class: 'btn btn-danger btn-sm m-0' hr h4 = t 'views.roles.future' table.table.table-hover.table-sm.m-2 @@ -44,11 +45,12 @@ section.container - @user.user_roles.unscoped.future.order(end_time: :desc).each do |ur| tr td = ur.role.localized_name - td = ur.start_time - td = ur.end_time + td = ur.start_time.localtime + td = ur.end_time&.localtime td - button.btn.btn-danger.btn-sm - = t('views.roles.cancel') + = form_tag fauna_user_role_assignment_path(id: ur.role_id), method: :delete do + = hidden_field_tag :start_time, value: ur.start_time + = submit_tag t('views.roles.cancel'), class: 'btn btn-danger btn-sm m-0' hr h4 = t('views.roles.expired') table.table.table-hover.table-sm.m-2 @@ -61,5 +63,5 @@ section.container - @user.user_roles.unscoped.expired.order(end_time: :desc).each do |ur| tr td = ur.role.localized_name - td = ur.start_time - td = ur.end_time + td = ur.start_time.localtime + td = ur.end_time.localtime diff --git a/config/routes.rb b/config/routes.rb index fbaedaa..cbc8ae5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,7 @@ +# frozen_string_literal: true Rails.application.routes.draw do use_doorkeeper do - controllers applications: "oauth/applications" + controllers applications: 'oauth/applications' end resource :user, only: [] do @@ -9,45 +10,42 @@ authenticated do devise_scope :user do - get "users/edit" => "devise/registrations#edit", :as => :user_root + get 'users/edit' => 'devise/registrations#edit', :as => :user_root end end namespace :fauna do - resources :users, only: [:index, :edit, :update, :show, :destroy] do - resources :role_assignments, only: [:index, :new, :create] do - collection do - delete ":role_name", action: "destroy", as: "role_assignment" - end + resources :users, only: %i[index edit update show destroy] do + resources :role_assignments, only: %i[index new create destroy] do end end end - namespace :api, defaults: {format: "json"} do + namespace :api, defaults: {format: 'json'} do resources :users, only: [] do collection do - get "present" + get 'present' end end resource :current_user, only: :show - resource :phone_access, only: [], controller: "phone_access" do + resource :phone_access, only: [], controller: 'phone_access' do collection do - post "phone_number_token" - post "verify_pin" + post 'phone_number_token' + post 'verify_pin' end end end - get "spaceapi/status", to: "space_api#status" - get "spaceapi/oauth_status", to: "space_api#oauth_status" + get 'spaceapi/status', to: 'space_api#status' + get 'spaceapi/oauth_status', to: 'space_api#oauth_status' - get "manifest", to: "web_app_manifest#manifest" + get 'manifest', to: 'web_app_manifest#manifest' - devise_for :users, controllers: {registrations: "registrations"} - get "dashboard/index" - root "dashboard#index" + devise_for :users, controllers: {registrations: 'registrations'} + get 'dashboard/index' + root 'dashboard#index' # The priority is based upon order of creation: first created -> highest priority. # See how all your routes lay out with "rake routes". From 73e084aaa56e7b4f58771fb3fa0acd0c12dab8b4 Mon Sep 17 00:00:00 2001 From: Albert Stefanov Date: Wed, 8 Apr 2026 10:46:58 +0300 Subject: [PATCH 10/19] Upgrade dependencies & rails --- .gitignore | 4 + Gemfile | 3 + Gemfile.lock | 366 +++++++++--------- .../api/phone_access_controller.rb | 14 +- config/environments/production.rb | 8 +- config/space_api.yml | 1 + 6 files changed, 212 insertions(+), 184 deletions(-) diff --git a/.gitignore b/.gitignore index 4e0e83d..59056db 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,7 @@ # Ignore precompiled assets /public/assets/ + +# Ignore key files for decrypting credentials and more. +/config/*.key + diff --git a/Gemfile b/Gemfile index 50747a2..7e2739b 100644 --- a/Gemfile +++ b/Gemfile @@ -51,6 +51,9 @@ gem "gravatar-ultimate" # Add Ruby 2.4.0 support gem "xmlrpc" +# SpaceApi +gem 'ostruct' + # Use simple form for form building gem "simple_form" gem "cocoon" diff --git a/Gemfile.lock b/Gemfile.lock index 3f3d6d8..4c221a5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -6,38 +6,38 @@ GIT GIT remote: https://github.com/thomaspark/bootswatch.git - revision: 149996ddaa93db10c6fc05145e3ab98872718c5b + revision: 3eb431f4a24ac7a7a6b95ce280d8d3dd3628f3de specs: bootswatch (5.3.8) GEM remote: https://rubygems.org/ specs: - action_text-trix (2.1.16) + action_text-trix (2.1.18) railties - actioncable (8.1.2) - actionpack (= 8.1.2) - activesupport (= 8.1.2) + actioncable (8.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (8.1.2) - actionpack (= 8.1.2) - activejob (= 8.1.2) - activerecord (= 8.1.2) - activestorage (= 8.1.2) - activesupport (= 8.1.2) + actionmailbox (8.1.3) + actionpack (= 8.1.3) + activejob (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) mail (>= 2.8.0) - actionmailer (8.1.2) - actionpack (= 8.1.2) - actionview (= 8.1.2) - activejob (= 8.1.2) - activesupport (= 8.1.2) + actionmailer (8.1.3) + actionpack (= 8.1.3) + actionview (= 8.1.3) + activejob (= 8.1.3) + activesupport (= 8.1.3) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (8.1.2) - actionview (= 8.1.2) - activesupport (= 8.1.2) + actionpack (8.1.3) + actionview (= 8.1.3) + activesupport (= 8.1.3) nokogiri (>= 1.8.5) rack (>= 2.2.4) rack-session (>= 1.0.1) @@ -45,36 +45,36 @@ GEM rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (8.1.2) + actiontext (8.1.3) action_text-trix (~> 2.1.15) - actionpack (= 8.1.2) - activerecord (= 8.1.2) - activestorage (= 8.1.2) - activesupport (= 8.1.2) + actionpack (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (8.1.2) - activesupport (= 8.1.2) + actionview (8.1.3) + activesupport (= 8.1.3) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (8.1.2) - activesupport (= 8.1.2) + activejob (8.1.3) + activesupport (= 8.1.3) globalid (>= 0.3.6) - activemodel (8.1.2) - activesupport (= 8.1.2) - activerecord (8.1.2) - activemodel (= 8.1.2) - activesupport (= 8.1.2) + activemodel (8.1.3) + activesupport (= 8.1.3) + activerecord (8.1.3) + activemodel (= 8.1.3) + activesupport (= 8.1.3) timeout (>= 0.4.0) - activestorage (8.1.2) - actionpack (= 8.1.2) - activejob (= 8.1.2) - activerecord (= 8.1.2) - activesupport (= 8.1.2) + activestorage (8.1.3) + actionpack (= 8.1.3) + activejob (= 8.1.3) + activerecord (= 8.1.3) + activesupport (= 8.1.3) marcel (~> 1.0) - activesupport (8.1.2) + activesupport (8.1.3) base64 bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) @@ -87,25 +87,26 @@ GEM securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) uri (>= 0.13.1) - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) - ast (2.4.2) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) + ast (2.4.3) base64 (0.3.0) - bcrypt (3.1.20) + bcrypt (3.1.22) benchmark (0.5.0) - bigdecimal (4.0.1) + bigdecimal (4.1.1) bindex (0.8.1) - bootsnap (1.17.1) + bootsnap (1.23.0) msgpack (~> 1.2) bootstrap (5.3.8) popper_js (>= 2.11.8, < 3) - brakeman (6.1.1) + brakeman (8.0.4) racc - builder (3.2.4) - bundler-audit (0.9.1) - bundler (>= 1.2.0, < 3) + builder (3.3.0) + bundler-audit (0.9.3) + bundler (>= 1.2.0) thor (~> 1.0) - byebug (11.1.3) + byebug (13.0.0) + reline (>= 0.6.0) capybara (3.40.0) addressable matrix @@ -115,9 +116,11 @@ GEM rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) - capybara-screenshot (1.0.26) + capybara-screenshot (1.0.27) capybara (>= 1.0, < 4) launchy + childprocess (5.1.0) + logger (~> 1.5) cocoon (1.2.15) coffee-rails (5.0.0) coffee-script (>= 2.2.0) @@ -144,45 +147,47 @@ GEM delayed_job_active_record (4.1.11) activerecord (>= 3.0, < 9.0) delayed_job (>= 3.0, < 5) - devise (4.9.4) + devise (5.0.3) bcrypt (~> 3.0) orm_adapter (~> 0.1) - railties (>= 4.1.0) + railties (>= 7.0) responders warden (~> 1.2.3) - devise-i18n (1.15.0) - devise (>= 4.9.0) + devise-i18n (1.16.0) + devise (>= 5.0.0) rails-i18n - diff-lcs (1.5.0) - docile (1.4.0) - doorkeeper (5.8.2) + diff-lcs (1.6.2) + docile (1.4.1) + doorkeeper (5.9.0) railties (>= 5) - doorkeeper-i18n (5.2.7) + doorkeeper-i18n (5.2.8) doorkeeper (>= 5.2) drb (2.2.3) - erubi (1.12.0) - execjs (2.9.1) - factory_bot (6.4.5) - activesupport (>= 5.0.0) - factory_bot_rails (6.4.3) - factory_bot (~> 6.4) - railties (>= 5.0.0) - fakefs (2.5.0) - faker (3.2.3) + erb (6.0.2) + erubi (1.13.1) + execjs (2.10.1) + factory_bot (6.5.6) + activesupport (>= 6.1.0) + factory_bot_rails (6.5.1) + factory_bot (~> 6.5) + railties (>= 6.1.0) + fakefs (3.2.1) + faker (3.6.1) i18n (>= 1.8.11, < 2) globalid (1.3.0) activesupport (>= 6.1) - google-protobuf (4.33.3-x86_64-linux-gnu) + google-protobuf (4.34.1-x86_64-linux-gnu) bigdecimal - rake (>= 13) + rake (~> 13.3) gravatar-ultimate (2.0.0) activesupport (>= 2.3.14) rack - i18n (1.14.4) + i18n (1.14.8) concurrent-ruby (~> 1.0) io-console (0.8.2) - irb (1.16.0) + irb (1.17.0) pp (>= 0.6.0) + prism (>= 1.3.0) rdoc (>= 4.0.0) reline (>= 0.4.2) jbuilder (2.14.1) @@ -192,7 +197,7 @@ GEM rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - json (2.7.1) + json (2.19.3) kaminari (1.2.2) activesupport (>= 4.1.0) kaminari-actionview (= 1.2.2) @@ -205,12 +210,14 @@ GEM activerecord kaminari-core (= 1.2.2) kaminari-core (1.2.2) - language_server-protocol (3.17.0.3) - launchy (2.5.2) + language_server-protocol (3.17.0.5) + launchy (3.1.1) addressable (~> 2.8) + childprocess (~> 5.0) + logger (~> 1.6) lint_roller (1.1.0) logger (1.7.0) - loofah (2.22.0) + loofah (2.25.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.9.0) @@ -220,29 +227,32 @@ GEM net-pop net-smtp marcel (1.1.0) - matrix (0.4.2) + matrix (0.4.3) mini_mime (1.1.5) - minitest (5.22.3) - msgpack (1.7.2) - net-imap (0.6.2) + minitest (6.0.3) + drb (~> 2.0) + prism (~> 1.5) + msgpack (1.8.0) + net-imap (0.6.3) 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.1) net-protocol - nio4r (2.7.4) - nokogiri (1.19.0-x86_64-linux-gnu) + nio4r (2.7.5) + nokogiri (1.19.2-x86_64-linux-gnu) racc (~> 1.4) orm_adapter (0.5.0) - parallel (1.24.0) - parser (3.3.0.4) + ostruct (0.6.3) + parallel (1.28.0) + parser (3.3.11.1) ast (~> 2.4.1) racc - pg (1.5.4) - phony (2.20.11) + pg (1.6.3-x86_64-linux) + phony (3.0.5) phony_rails (0.15.0) activesupport (>= 3.0) phony (>= 2.18.12) @@ -250,52 +260,54 @@ GEM pp (0.6.3) prettyprint prettyprint (0.2.0) - psych (5.1.2) + prism (1.9.0) + psych (5.3.1) + date stringio - public_suffix (5.0.4) - puma (5.6.9) + public_suffix (7.0.5) + puma (7.2.0) nio4r (~> 2.0) - pundit (2.3.1) + pundit (2.5.2) activesupport (>= 3.0.0) - racc (1.7.3) - rack (3.2.4) + racc (1.8.1) + rack (3.2.6) rack-cors (3.0.0) logger rack (>= 3.0.14) - rack-session (2.1.1) + rack-session (2.1.2) base64 (>= 0.1.0) rack (>= 3.0.0) rack-test (2.2.0) rack (>= 1.3) rackup (2.3.1) rack (>= 3) - rails (8.1.2) - actioncable (= 8.1.2) - actionmailbox (= 8.1.2) - actionmailer (= 8.1.2) - actionpack (= 8.1.2) - actiontext (= 8.1.2) - actionview (= 8.1.2) - activejob (= 8.1.2) - activemodel (= 8.1.2) - activerecord (= 8.1.2) - activestorage (= 8.1.2) - activesupport (= 8.1.2) + rails (8.1.3) + actioncable (= 8.1.3) + actionmailbox (= 8.1.3) + actionmailer (= 8.1.3) + actionpack (= 8.1.3) + actiontext (= 8.1.3) + actionview (= 8.1.3) + activejob (= 8.1.3) + activemodel (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) bundler (>= 1.15.0) - railties (= 8.1.2) + railties (= 8.1.3) rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.6.0) - loofah (~> 2.21) - nokogiri (~> 1.14) + rails-html-sanitizer (1.7.0) + loofah (~> 2.25) + 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) rails-i18n (8.1.0) i18n (>= 0.7, < 2) railties (>= 8.0.0, < 9) - railties (8.1.2) - actionpack (= 8.1.2) - activesupport (= 8.1.2) + railties (8.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) @@ -303,61 +315,67 @@ GEM tsort (>= 0.2) zeitwerk (~> 2.6) rainbow (3.1.1) - rake (13.1.0) - rdoc (6.6.3.1) + rake (13.3.1) + rdoc (7.2.0) + erb psych (>= 4.0.0) - regexp_parser (2.9.0) + tsort + regexp_parser (2.12.0) reline (0.6.3) io-console (~> 0.5) responders (3.2.0) actionpack (>= 7.0) railties (>= 7.0) - rexml (3.3.9) - rspec-core (3.12.3) - rspec-support (~> 3.12.0) - rspec-expectations (3.12.3) + rexml (3.4.4) + rspec-core (3.13.6) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-mocks (3.12.6) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.8) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-rails (6.1.1) - actionpack (>= 6.1) - activesupport (>= 6.1) - railties (>= 6.1) - rspec-core (~> 3.12) - rspec-expectations (~> 3.12) - rspec-mocks (~> 3.12) - rspec-support (~> 3.12) - rspec-support (3.12.1) - rubocop (1.59.0) + rspec-support (~> 3.13.0) + rspec-rails (8.0.4) + actionpack (>= 7.2) + activesupport (>= 7.2) + railties (>= 7.2) + rspec-core (>= 3.13.0, < 5.0.0) + rspec-expectations (>= 3.13.0, < 5.0.0) + rspec-mocks (>= 3.13.0, < 5.0.0) + rspec-support (>= 3.13.0, < 5.0.0) + rspec-support (3.13.7) + rubocop (1.84.2) json (~> 2.3) - language_server-protocol (>= 3.17.0) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) parallel (~> 1.10) - parser (>= 3.2.2.4) + parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) - rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.30.0, < 2.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.49.0, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.30.0) - parser (>= 3.2.1.0) - rubocop-performance (1.20.2) - rubocop (>= 1.48.1, < 2.0) - rubocop-ast (>= 1.30.0, < 2.0) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.49.1) + parser (>= 3.3.7.2) + prism (~> 1.7) + rubocop-performance (1.26.1) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.47.1, < 2.0) ruby-progressbar (1.13.0) - rubyzip (2.3.2) - sass-embedded (1.97.2-x86_64-linux-gnu) + rubyzip (3.2.2) + sass-embedded (1.99.0-x86_64-linux-gnu) google-protobuf (~> 4.31) sassc-embedded (1.80.8) sass-embedded (~> 1.80) - sdoc (2.6.1) + sdoc (2.6.5) rdoc (>= 5.0) securerandom (0.4.1) - selenium-webdriver (4.16.0) + selenium-webdriver (4.41.0) + base64 (~> 0.2) + logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) - rubyzip (>= 1.2.2, < 3.0) + rubyzip (>= 1.2.2, < 4.0) websocket (~> 1.0) simple_form (5.4.1) actionpack (>= 7.0) @@ -366,17 +384,17 @@ GEM docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) - simplecov-html (0.12.3) + simplecov-html (0.13.2) simplecov_json_formatter (0.1.4) - slim (5.2.0) + slim (5.2.1) temple (~> 0.10.0) tilt (>= 2.1.0) slim-rails (4.0.0) actionpack (>= 3.1) railties (>= 3.1) slim (>= 3.0, < 6.0, != 5.0.0) - snmp (1.3.2) - spring (4.1.3) + snmp (1.3.4) + spring (4.4.2) spring-commands-rspec (1.0.4) spring (>= 0.9.1) sprockets (4.2.2) @@ -387,41 +405,42 @@ GEM actionpack (>= 6.1) activesupport (>= 6.1) sprockets (>= 3.0.0) - sqlite3 (2.9.0-x86_64-linux-gnu) - standard (1.33.0) + sqlite3 (2.9.2-x86_64-linux-gnu) + standard (1.54.0) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.0) - rubocop (~> 1.59.0) + rubocop (~> 1.84.0) standard-custom (~> 1.0.0) - standard-performance (~> 1.3) + standard-performance (~> 1.8) standard-custom (1.0.2) lint_roller (~> 1.0) rubocop (~> 1.50) - standard-performance (1.3.1) + standard-performance (1.9.0) lint_roller (~> 1.1) - rubocop-performance (~> 1.20.2) - stringio (3.1.0) - temple (0.10.3) - thor (1.3.1) - tilt (2.3.0) - timeout (0.4.3) + rubocop-performance (~> 1.26.0) + stringio (3.2.0) + temple (0.10.4) + thor (1.5.0) + tilt (2.7.0) + timeout (0.6.1) tsort (0.2.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - uglifier (4.2.0) + uglifier (4.2.1) execjs (>= 0.3.0, < 3) - unicode-display_width (2.5.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) uri (1.1.1) useragent (0.16.11) warden (1.2.9) rack (>= 2.0.9) - web-console (4.2.1) - actionview (>= 6.0.0) - activemodel (>= 6.0.0) + web-console (4.3.0) + actionview (>= 8.0.0) bindex (>= 0.4.0) - railties (>= 6.0.0) - webrick (1.8.2) - websocket (1.2.10) + railties (>= 8.0.0) + webrick (1.9.2) + websocket (1.2.11) websocket-driver (0.8.0) base64 websocket-extensions (>= 0.1.0) @@ -433,7 +452,7 @@ GEM yaml_db (0.7.0) rails (>= 3.0) rake (>= 0.8.7) - zeitwerk (2.6.13) + zeitwerk (2.7.5) PLATFORMS x86_64-linux @@ -467,6 +486,7 @@ DEPENDENCIES kaminari matrix net-smtp + ostruct pg phony_rails puma diff --git a/app/controllers/api/phone_access_controller.rb b/app/controllers/api/phone_access_controller.rb index 0808347..737ad3f 100644 --- a/app/controllers/api/phone_access_controller.rb +++ b/app/controllers/api/phone_access_controller.rb @@ -5,15 +5,15 @@ class Api::PhoneAccessController < Api::ApplicationController before_action -> { doorkeeper_authorize! :account_data_read }, only: [:verify_pin] def phone_number_token - secret = Rails.application.secrets.PHONE_TOKEN_SECRET - return head :internal_server_error if !secret + secret = Rails.application.credentials.phone.token_secret + return head :internal_server_error unless secret - phone_app_id = Rails.application.secrets.PHONE_OAUTH_APP_ID - return head :internal_server_error if !phone_app_id + phone_app_id = Rails.application.credentials.phone.app_id + return head :internal_server_error unless phone_app_id - return head :bad_request if !params[:phone_number] + return head :bad_request unless params[:phone_number] - return head :forbidden if !ActiveSupport::SecurityUtils.secure_compare(params[:secret], secret) + return head :forbidden unless ActiveSupport::SecurityUtils.secure_compare(params[:secret], secret) # TODO: the default country code should be taken from the model @user = User.joins(:phone_numbers).find_by phone_numbers: {phone_number: PhonyRails.normalize_number(params[:phone_number], default_country_code: "BG")} @@ -29,7 +29,7 @@ def phone_number_token end def verify_pin - return head :bad_request if !params[:pin] + return head :bad_request unless params[:pin] @valid = if current_resource_owner.encrypted_pin? (BCrypt::Password.new(current_resource_owner.encrypted_pin) == params[:pin]) ? "valid" : "invalid" diff --git a/config/environments/production.rb b/config/environments/production.rb index 285ee8b..5866bd3 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -44,7 +44,7 @@ # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. - config.force_ssl = true +# config.force_ssl = true # Include generic and useful information about system operation, but avoid logging too much # information to avoid inadvertent exposure of personally identifiable information (PII). @@ -69,10 +69,10 @@ # SMTP is default # config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = { - address: Rails.application.secrets.SMTP_ADDRESS, + address: Rails.application.credentials.smtp.host, port: 465, - user_name: Rails.application.secrets.SMTP_USER_NAME, - password: Rails.application.secrets.SMTP_PASSWORD, + user_name: Rails.application.credentials.smtp.user, + password: Rails.application.credentials.smtp.password, ssl: true, } diff --git a/config/space_api.yml b/config/space_api.yml index 6b16022..0efe7e4 100644 --- a/config/space_api.yml +++ b/config/space_api.yml @@ -1,3 +1,4 @@ +--- space: init Lab logo: initlab-logo.png url: https://initlab.org From ad3551fd3491d01e8e49387572acfc76784a5efc Mon Sep 17 00:00:00 2001 From: Albert Stefanov Date: Wed, 8 Apr 2026 10:47:16 +0300 Subject: [PATCH 11/19] RadWho: don't spam the log if file nonexistent --- app/models/rad_who.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/rad_who.rb b/app/models/rad_who.rb index e010c0d..f6b7307 100644 --- a/app/models/rad_who.rb +++ b/app/models/rad_who.rb @@ -8,7 +8,7 @@ def initialize end def self.radwho - if Rails.env.production? + if Rails.env.production? and File.file?('/var/log/freeradius/radutmp') `radwho -i -r -F /var/log/freeradius/radutmp`.gsub(RAW_ENTRY_REGEXP, '\k-\k') else "" From 1bd5d2df8ba914bb0c87722d1e2d3cd10a66c34a Mon Sep 17 00:00:00 2001 From: Albert Stefanov Date: Wed, 8 Apr 2026 10:47:44 +0300 Subject: [PATCH 12/19] auth: fix future roles filter --- app/models/user.rb | 2 +- app/models/user_role.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index e76f961..3dee6ee 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -51,7 +51,7 @@ def add_role(role, start_time = nil, end_time = nil) def remove_role(role, start_time = nil) user_role = UserRole.unscoped.find_by(user_id: id, role_id: role.id, start_time: start_time) user_role ||= UserRole.find_by(user_id: id, role_id: role.id) - user_role.deactivate + user_role&.deactivate end def email_md5 diff --git a/app/models/user_role.rb b/app/models/user_role.rb index f992543..74e488c 100644 --- a/app/models/user_role.rb +++ b/app/models/user_role.rb @@ -6,7 +6,7 @@ class UserRole < ApplicationRecord belongs_to :user scope :expired, -> { where('(end_time is not null and end_time < ?)', Time.current) } - scope :future, -> { where('(start_time is not null and start_time > ? and end_time > ?)', Time.current, Time.current) } + scope :future, -> { where('(start_time is not null and start_time > ? and (end_time is null or end_time > ?))', Time.current, Time.current) } scope :active_at, ->(time) { where('(start_time < ?) and (end_time is null or end_time > ?)', time, time) } default_scope { where('(start_time < ?) and (end_time is null or end_time > ?)', Time.current, Time.current) } From a265f329972a6cd416a6063d192842e9ce56c433 Mon Sep 17 00:00:00 2001 From: Albert Stefanov Date: Sun, 19 Apr 2026 11:10:46 +0300 Subject: [PATCH 13/19] BUGBUG: Fix user-role scopes Turns out calling `unscope` without specific where-fields removes all the JOIN conditions... This fixes the scopes, so they actually match the user :) --- app/models/user.rb | 9 +++------ app/models/user_role.rb | 9 +++++---- .../fauna/role_assignments/_role_assignments.html.slim | 8 ++++---- .../fauna/role_assignments/_temporary_roles.html.slim | 4 ++-- app/views/fauna/role_assignments/index.html.slim | 10 +++++----- 5 files changed, 19 insertions(+), 21 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 3dee6ee..11c9b78 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -19,7 +19,8 @@ class User < ApplicationRecord validates :username, uniqueness: { case_sensitive: false }, presence: true validates :twitter, format: { with: /\A[A-Za-z0-9_]{1,15}\z/ }, allow_blank: true validates :url, - format: { with: %r{\A(http|https)://[a-z0-9]+([-.]{1}[a-z0-9]+)*\.[a-z-]{2,63}(:[0-9]{1,5})?(/.*)?\z}ix }, allow_blank: true + format: { with: %r{\A(http|https)://[a-z0-9]+([-.]{1}[a-z0-9]+)*\.[a-z-]{2,63}(:[0-9]{1,5})?(/.*)?\z}ix }, + allow_blank: true validates :first_name, presence: true validates :last_name, presence: true validates :github, format: { with: /\A[a-z0-9][a-z0-9-]{,38}\z/i }, allow_blank: true @@ -39,7 +40,7 @@ def self.find_for_database_authentication(warden_conditions) { value: login.downcase.strip }]).first end - def has_role?(role) + def role?(role) roles.exists?(name: role) end @@ -54,10 +55,6 @@ def remove_role(role, start_time = nil) user_role&.deactivate end - def email_md5 - Digest::MD5.hexdigest email - end - def name [first_name, last_name].compact.join(' ').strip end diff --git a/app/models/user_role.rb b/app/models/user_role.rb index 74e488c..05b64c8 100644 --- a/app/models/user_role.rb +++ b/app/models/user_role.rb @@ -5,11 +5,12 @@ class UserRole < ApplicationRecord belongs_to :role belongs_to :user - scope :expired, -> { where('(end_time is not null and end_time < ?)', Time.current) } - scope :future, -> { where('(start_time is not null and start_time > ? and (end_time is null or end_time > ?))', Time.current, Time.current) } + scope :expired, -> { unscope(where: %i[start_time end_time]).where('(end_time is not null and end_time < ?)', Time.current) } + scope :future, -> { unscope(where: %i[start_time end_time]).where('(start_time is not null and start_time > ? and (end_time is null or end_time > ?))', Time.current, Time.current) } - scope :active_at, ->(time) { where('(start_time < ?) and (end_time is null or end_time > ?)', time, time) } - default_scope { where('(start_time < ?) and (end_time is null or end_time > ?)', Time.current, Time.current) } + scope :active_at, ->(time) { unscope(where: %i[start_time end_time]).where('(start_time < ?) and (end_time is null or end_time > ?)', time, time) } + + default_scope { where(start_time: ...Time.current).and(where(end_time: nil).or(where(end_time: Time.current...))) } def deactivate update(end_time: Time.current) diff --git a/app/views/fauna/role_assignments/_role_assignments.html.slim b/app/views/fauna/role_assignments/_role_assignments.html.slim index 6856ef9..982099c 100644 --- a/app/views/fauna/role_assignments/_role_assignments.html.slim +++ b/app/views/fauna/role_assignments/_role_assignments.html.slim @@ -1,17 +1,17 @@ .role-assignments.btn-group id="role-assignments-#{user.id}" role="group" - Role.predefined.each do |predefined_role| - - if user.has_role? predefined_role.name + - if user.role? predefined_role.name / TODO: hidden form here / = button_to fauna_user_role_assignments_path(user_id: user.id, id: predefined_role.id), method: :delete, remote: true, class: 'btn btn-sm btn-warning', data: {disable: true, confirm: t('views.roles.are_you_sure')} do button.btn.btn-sm.btn-warning => icon('fa-solid', 'check-square-o') = predefined_role.localized_name - - if user.user_roles.find_by(role: predefined_role.id).end_time != nil - =< icon('fa-solid', 'calendar', html_options: {title: user.user_roles.find_by(role: predefined_role.id).end_time }) + - unless user.user_roles.find_by(role: predefined_role.id)&.end_time.nil? + =< icon('fa-solid', 'calendar', html_options: {title: user.user_roles.find_by(role: predefined_role.id)&.end_time }) - else / = link_to fauna_user_role_assignments_path(user.id, role: {name: predefined_role.name}), method: :post, remote: true, class: 'btn btn-sm btn-default', data: {disable: true, confirm: t('views.roles.are_you_sure')} do button.btn.btn-sm.btn-default.disabled => icon('fa-solid', 'square-o') = predefined_role.localized_name - - if user.user_roles.unscoped.future.find_by(role: predefined_role.id) + - if user.user_roles.future.find_by(role: predefined_role.id) =< icon('fa-solid', 'calendar', html_options: { title: user.user_roles.unscoped.future.find_by(role: predefined_role.id).end_time }) diff --git a/app/views/fauna/role_assignments/_temporary_roles.html.slim b/app/views/fauna/role_assignments/_temporary_roles.html.slim index a404460..f93e85d 100644 --- a/app/views/fauna/role_assignments/_temporary_roles.html.slim +++ b/app/views/fauna/role_assignments/_temporary_roles.html.slim @@ -48,7 +48,7 @@ div.d-table-cell.px-2.font-weight-bold = UserRole.human_attribute_name(:end_time) - - user.user_roles.unscoped.future.order(end_time: :desc).each do |ur| + - user.user_roles.future.order(end_time: :desc).each do |ur| div.d-table-row div.d-table-cell.px-2 = ur.role.localized_name @@ -72,7 +72,7 @@ div.d-table-cell.px-2.font-weight-bold = UserRole.human_attribute_name(:end_time) - - user.user_roles.unscoped.expired.order(end_time: :desc).each do |ur| + - user.user_roles.expired.order(end_time: :desc).each do |ur| div.d-table-row div.d-table-cell.px-2 = ur.role.localized_name diff --git a/app/views/fauna/role_assignments/index.html.slim b/app/views/fauna/role_assignments/index.html.slim index ab6bf21..1c7a9f8 100644 --- a/app/views/fauna/role_assignments/index.html.slim +++ b/app/views/fauna/role_assignments/index.html.slim @@ -1,6 +1,6 @@ - content_for(:title) { "#{User.human_attribute_name(:roles)} - #{@user.name}" } - content_for(:page_actions) do - = link_to new_fauna_user_role_assignment_path, class: ['btn', 'btn-success'] do + = link_to new_fauna_user_role_assignment_path, class: %w[btn btn-success] do => icon('fa-solid', :plus) = t('views.roles.add') @@ -31,7 +31,7 @@ section.container td = form_tag fauna_user_role_assignment_path(id: ur.role_id), method: :delete do = hidden_field_tag :start_time, value: ur.start_time - = submit_tag t('views.roles.deactivate'), class: 'btn btn-danger btn-sm m-0' + = button_tag icon('fa-solid', :close, t('views.roles.deactivate')), class: 'btn btn-danger btn-sm m-0' hr h4 = t 'views.roles.future' table.table.table-hover.table-sm.m-2 @@ -42,7 +42,7 @@ section.container th = UserRole.human_attribute_name(:end_time) th tbody - - @user.user_roles.unscoped.future.order(end_time: :desc).each do |ur| + - @user.user_roles.future.order(end_time: :desc).each do |ur| tr td = ur.role.localized_name td = ur.start_time.localtime @@ -50,7 +50,7 @@ section.container td = form_tag fauna_user_role_assignment_path(id: ur.role_id), method: :delete do = hidden_field_tag :start_time, value: ur.start_time - = submit_tag t('views.roles.cancel'), class: 'btn btn-danger btn-sm m-0' + = button_tag icon('fa-solid', :trash, t('views.roles.cancel')), class: 'btn btn-danger btn-sm m-0' hr h4 = t('views.roles.expired') table.table.table-hover.table-sm.m-2 @@ -60,7 +60,7 @@ section.container th = UserRole.human_attribute_name(:start_time) th = UserRole.human_attribute_name(:end_time) - - @user.user_roles.unscoped.expired.order(end_time: :desc).each do |ur| + - @user.user_roles.expired.order(end_time: :desc).each do |ur| tr td = ur.role.localized_name td = ur.start_time.localtime From a674edaa2940748e373773614bf719b53deb4cbc Mon Sep 17 00:00:00 2001 From: Albert Stefanov Date: Sun, 19 Apr 2026 11:48:59 +0300 Subject: [PATCH 14/19] design: match space.initlab.org --- app/assets/images/initlab-logo.svg | 393 +----------------------- app/views/layouts/_navigation.html.slim | 9 +- 2 files changed, 14 insertions(+), 388 deletions(-) diff --git a/app/assets/images/initlab-logo.svg b/app/assets/images/initlab-logo.svg index 61d6ea1..ff17f18 100644 --- a/app/assets/images/initlab-logo.svg +++ b/app/assets/images/initlab-logo.svg @@ -1,384 +1,11 @@ - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + diff --git a/app/views/layouts/_navigation.html.slim b/app/views/layouts/_navigation.html.slim index 876b265..cfb01fa 100644 --- a/app/views/layouts/_navigation.html.slim +++ b/app/views/layouts/_navigation.html.slim @@ -1,17 +1,16 @@ nav class="py-0 navbar navbar-expand-lg #{content_for?(:nav_color) ? yield(:nav_color) : 'navbar-dark bg-primary'}" .container = link_to root_url, class: "navbar-brand", active: false do - = image_tag 'logo.svg', class: 'logo', height: '47px', alt: 'Fauna' + = image_tag 'initlab-logo.svg', class: 'logo', height: '47px', alt: 'Fauna' button.navbar-toggler data-target="#navbar-collapse" data-toggle="collapse" type="button" span.navbar-toggler-icon .navbar-collapse.collapse#navbar-collapse .navbar-nav.flex-grow-1 - if user_signed_in? and UserPolicy.new(current_user, nil).index? - a.nav-link - = link_to fauna_users_path, class: "nav-link #{'active' if current_page?(fauna_users_path)}" do - => icon('fa-solid', :users) - = t('views.navigation.labbers') + = link_to fauna_users_path, class: "nav-link #{'active' if current_page?(fauna_users_path)}" do + => icon('fa-solid', :users) + = t('views.navigation.labbers') .navbar-nav.navbar-right - unless user_signed_in? .nav-item.dropdown From 2621b2c3d6b3141944e4dd3055e3844cf39833c0 Mon Sep 17 00:00:00 2001 From: Albert Stefanov Date: Sun, 19 Apr 2026 11:49:19 +0300 Subject: [PATCH 15/19] Redirect homepage to space --- app/controllers/dashboard_controller.rb | 3 +++ app/controllers/registrations_controller.rb | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index e1192e4..36f3a2f 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -1,2 +1,5 @@ class DashboardController < ApplicationController + def index + redirect_to 'https://space.initlab.org', allow_other_host: true + end end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 655a4d0..000e6ce 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -22,4 +22,13 @@ def create respond_with resource end end + + def after_sign_up_path_for(_resource) + user_network_devices_path + end + + def after_inactive_sign_up_path_for(_resource) + new_user_session_path + end + end From 647b420bc7fe05fd9c0a7ddc30c694196d97ca93 Mon Sep 17 00:00:00 2001 From: Albert Stefanov Date: Sun, 19 Apr 2026 16:35:42 +0300 Subject: [PATCH 16/19] Role assignments: UI redesign --- app/controllers/api/users_controller.rb | 4 +- .../fauna/role_assignments_controller.rb | 44 ++-------- app/views/devise/sessions/new.html.slim | 3 +- .../fauna/role_assignments/_form.html.slim | 23 ++--- .../_role_assignments.html.slim | 17 ---- .../fauna/role_assignments/index.html.slim | 85 ++++++++++--------- .../fauna/role_assignments/refresh.js.erb | 1 - app/views/fauna/users/_user.html.slim | 33 +++++-- app/views/fauna/users/index.html.slim | 3 +- app/views/layouts/application.html.slim | 4 +- config/application.rb | 2 +- config/initializers/simple_form_bootstrap.rb | 4 +- config/locales/devise.bg.yml | 2 +- config/locales/simple_form.bg.yml | 2 + 14 files changed, 109 insertions(+), 118 deletions(-) delete mode 100644 app/views/fauna/role_assignments/_role_assignments.html.slim delete mode 100644 app/views/fauna/role_assignments/refresh.js.erb diff --git a/app/controllers/api/users_controller.rb b/app/controllers/api/users_controller.rb index d37363b..42c1965 100644 --- a/app/controllers/api/users_controller.rb +++ b/app/controllers/api/users_controller.rb @@ -1,8 +1,10 @@ -class Api::UsersController < Api::ApplicationController +module Api + class UsersController < Api::ApplicationController wrap_parameters format: [:json] include Api::PublicApiExposingController def present @users = Presence.present_users end + end end diff --git a/app/controllers/fauna/role_assignments_controller.rb b/app/controllers/fauna/role_assignments_controller.rb index 8b432fb..af87ce4 100644 --- a/app/controllers/fauna/role_assignments_controller.rb +++ b/app/controllers/fauna/role_assignments_controller.rb @@ -9,33 +9,18 @@ def index authorize :role_assignment end - def new - authorize :role_assignment - end - def create authorize :role_assignment role = Role.find(role_params[:role]) start_time = role_params[:start_time] end_time = role_params[:end_time] - respond_to do |format| - format.html do - if @user.add_role(role, start_time, end_time) - redirect_to fauna_user_role_assignments_path - else - render :new, status: :unprocessable_content - end - end - - format.js do - if @user.add_role(role, start_time, end_time) - render :refresh, status: :created - else - head :unprocessable_entity - end - end + if @user.add_role(role, start_time, end_time) + redirect_to fauna_user_role_assignments_path + else + render :new, status: :unprocessable_content end + rescue Pundit::NotAuthorizedError head :forbidden end @@ -45,21 +30,10 @@ def destroy role = Role.find(params[:id]) start_time = params[:start_time] - respond_to do |format| - format.html do - if @user.remove_role(role, start_time) - redirect_to fauna_user_role_assignments_path - else - redirect_back fallback_location: fauna_user_role_assignments_path, status: :unprocessable_entity - end - end - format.js do - if @user.remove_role(role, start_time) - render :refresh - else - head :unprocessable_entity - end - end + if @user.remove_role(role, start_time) + redirect_to fauna_user_role_assignments_path + else + redirect_back fallback_location: fauna_user_role_assignments_path, status: :unprocessable_entity end rescue Pundit::NotAuthorizedError head :forbidden diff --git a/app/views/devise/sessions/new.html.slim b/app/views/devise/sessions/new.html.slim index 5616b32..903f40a 100644 --- a/app/views/devise/sessions/new.html.slim +++ b/app/views/devise/sessions/new.html.slim @@ -1,6 +1,6 @@ - content_for(:title) { t 'views.sessions.sign_in_heading' } section.container - .row + .row.standalone-form .centered_form = simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| header @@ -14,3 +14,4 @@ section.container .form-actions = f.button :submit, t('views.sessions.sign_in') = render "devise/shared/links" +f \ No newline at end of file diff --git a/app/views/fauna/role_assignments/_form.html.slim b/app/views/fauna/role_assignments/_form.html.slim index 3fd6691..9597409 100644 --- a/app/views/fauna/role_assignments/_form.html.slim +++ b/app/views/fauna/role_assignments/_form.html.slim @@ -1,11 +1,12 @@ -.centered_form - = simple_form_for @user.user_roles.new, url: fauna_user_role_assignments_path do |f| - header - legend - h3 = Role.model_name.human - .form-inputs - = f.input :start_time, as: :date, html5: true - = f.input :end_time, as: :date, html5: true - = f.input :role, collection: Role.all.map { |r| [r.localized_name, r.id] } - .form-actions - = f.button :submit += form_for @user.user_roles.new, url: fauna_user_role_assignments_path, html: { class: 'row m-0 p-0 row-cols-xs-auto g-3 align-items-end' } do |f| + .col-2 + = f.label :role, class: 'form-label fw-bold' + = f.select :role, Role.all.map { |r| [r.localized_name.to_s, r.id] }, {}, { required: true, selected: nil, class: 'form-select' } + .col-4 + = f.label :start_time, class: 'form-label fw-bold' + = f.datetime_field :start_time, as: :date, html5: true, class: 'form-control' + .col-4 + = f.label :end_time, class: 'form-label fw-bold' + = f.datetime_field :end_time, as: :date, html5: true, class: 'form-control' + .col-2.d-grid + = f.button :submit, class: 'btn btn-success' \ No newline at end of file diff --git a/app/views/fauna/role_assignments/_role_assignments.html.slim b/app/views/fauna/role_assignments/_role_assignments.html.slim deleted file mode 100644 index 982099c..0000000 --- a/app/views/fauna/role_assignments/_role_assignments.html.slim +++ /dev/null @@ -1,17 +0,0 @@ -.role-assignments.btn-group id="role-assignments-#{user.id}" role="group" - - Role.predefined.each do |predefined_role| - - if user.role? predefined_role.name - / TODO: hidden form here - / = button_to fauna_user_role_assignments_path(user_id: user.id, id: predefined_role.id), method: :delete, remote: true, class: 'btn btn-sm btn-warning', data: {disable: true, confirm: t('views.roles.are_you_sure')} do - button.btn.btn-sm.btn-warning - => icon('fa-solid', 'check-square-o') - = predefined_role.localized_name - - unless user.user_roles.find_by(role: predefined_role.id)&.end_time.nil? - =< icon('fa-solid', 'calendar', html_options: {title: user.user_roles.find_by(role: predefined_role.id)&.end_time }) - - else - / = link_to fauna_user_role_assignments_path(user.id, role: {name: predefined_role.name}), method: :post, remote: true, class: 'btn btn-sm btn-default', data: {disable: true, confirm: t('views.roles.are_you_sure')} do - button.btn.btn-sm.btn-default.disabled - => icon('fa-solid', 'square-o') - = predefined_role.localized_name - - if user.user_roles.future.find_by(role: predefined_role.id) - =< icon('fa-solid', 'calendar', html_options: { title: user.user_roles.unscoped.future.find_by(role: predefined_role.id).end_time }) diff --git a/app/views/fauna/role_assignments/index.html.slim b/app/views/fauna/role_assignments/index.html.slim index 1c7a9f8..fc02ca2 100644 --- a/app/views/fauna/role_assignments/index.html.slim +++ b/app/views/fauna/role_assignments/index.html.slim @@ -1,27 +1,31 @@ - content_for(:title) { "#{User.human_attribute_name(:roles)} - #{@user.name}" } -- content_for(:page_actions) do - = link_to new_fauna_user_role_assignment_path, class: %w[btn btn-success] do - => icon('fa-solid', :plus) - = t('views.roles.add') +/ - content_for(:page_actions) do +/ = link_to new_fauna_user_role_assignment_path, class: %w[btn btn-success] do +/ => icon('fa-solid', :plus) +/ = t 'views.roles.add' section.container header.row - .col-sm-12 + .col header.page-header .page-actions = yield :page_actions h2 = "#{User.human_attribute_name(:roles)} - #{@user.name}" - .row - .col-sm-12 - h4 = t('views.roles.current') - table.table.table-hover.table-sm.m-2 + + .row.my-3 + .col.pe-0 + h4 = t 'views.roles.add' + = render partial: 'form' + .row.my-3 + .col + h4 = t 'views.roles.current' + table.table.table-hover.table-sm.m-2.table-responsive thead tr - th[scope='col'] = UserRole.human_attribute_name(:role) - th[scope='col'] = UserRole.human_attribute_name(:start_time) - th[scope='col'] = UserRole.human_attribute_name(:end_time) - th - + th.col-2[scope='col'] = UserRole.human_attribute_name :role + th.col-4[scope='col'] = UserRole.human_attribute_name :start_time + th.col-4[scope='col'] = UserRole.human_attribute_name :end_time + th.col-2 tbody - @user.user_roles.order(end_time: :desc).each do |ur| tr @@ -29,39 +33,42 @@ section.container td = ur.start_time.localtime td = ur.end_time&.localtime td - = form_tag fauna_user_role_assignment_path(id: ur.role_id), method: :delete do + = form_tag fauna_user_role_assignment_path(id: ur.role_id), method: :delete, class: 'd-grid' do = hidden_field_tag :start_time, value: ur.start_time = button_tag icon('fa-solid', :close, t('views.roles.deactivate')), class: 'btn btn-danger btn-sm m-0' hr h4 = t 'views.roles.future' - table.table.table-hover.table-sm.m-2 + table.table.table-hover.table-sm.m-2.table-responsive thead tr - th = UserRole.human_attribute_name(:role) - th = UserRole.human_attribute_name(:start_time) - th = UserRole.human_attribute_name(:end_time) - th + th.col-2[scope='col'] = UserRole.human_attribute_name :role + th.col-4[scope='col'] = UserRole.human_attribute_name :start_time + th.col-4[scope='col'] = UserRole.human_attribute_name :end_time + th.col-2 tbody - - @user.user_roles.future.order(end_time: :desc).each do |ur| - tr - td = ur.role.localized_name - td = ur.start_time.localtime - td = ur.end_time&.localtime - td - = form_tag fauna_user_role_assignment_path(id: ur.role_id), method: :delete do - = hidden_field_tag :start_time, value: ur.start_time - = button_tag icon('fa-solid', :trash, t('views.roles.cancel')), class: 'btn btn-danger btn-sm m-0' + - @user.user_roles.future.order(end_time: :desc).each do |ur| + tr + td = ur.role.localized_name + td = ur.start_time.localtime + td = ur.end_time&.localtime + td + = form_tag fauna_user_role_assignment_path(id: ur.role_id), method: :delete, class: 'd-grid' do + = hidden_field_tag :start_time, value: ur.start_time + = button_tag icon('fa-solid', :trash, t('views.roles.cancel')), class: 'btn btn-danger btn-sm m-0' hr - h4 = t('views.roles.expired') - table.table.table-hover.table-sm.m-2 + h4 = t 'views.roles.expired' + table.table.table-hover.table-sm.m-2.table-responsive thead tr - th = UserRole.human_attribute_name(:role) - th = UserRole.human_attribute_name(:start_time) - th = UserRole.human_attribute_name(:end_time) + th.col-2[scope='col'] = UserRole.human_attribute_name :role + th.col-4[scope='col'] = UserRole.human_attribute_name :start_time + th.col-4[scope='col'] = UserRole.human_attribute_name :end_time + th.col-2 - - @user.user_roles.expired.order(end_time: :desc).each do |ur| - tr - td = ur.role.localized_name - td = ur.start_time.localtime - td = ur.end_time.localtime + tbody + - @user.user_roles.expired.order(end_time: :desc).each do |ur| + tr + td = ur.role.localized_name + td = ur.start_time.localtime + td = ur.end_time&.localtime + td \ No newline at end of file diff --git a/app/views/fauna/role_assignments/refresh.js.erb b/app/views/fauna/role_assignments/refresh.js.erb deleted file mode 100644 index a6382ed..0000000 --- a/app/views/fauna/role_assignments/refresh.js.erb +++ /dev/null @@ -1 +0,0 @@ -$("#role-assignments-<%= @user.id %>").replaceWith("<%= j render partial: 'role_assignments', locals: {user: @user} %>"); diff --git a/app/views/fauna/users/_user.html.slim b/app/views/fauna/users/_user.html.slim index b6d334d..b4063f9 100644 --- a/app/views/fauna/users/_user.html.slim +++ b/app/views/fauna/users/_user.html.slim @@ -2,11 +2,34 @@ tr td = user.name td = user.username td = user.email - td.roles_column - = render partial: '/fauna/role_assignments/role_assignments', locals: {user: user} - td.temporary_roles - = link_to fauna_user_role_assignments_path(user.id), :class => 'btn btn-sm btn-primary' do - = icon('fa-solid', 'calendar') + td.d-flex + ul.list-group.list-group-horizontal-md.align-items-center.border-primary + - Role.predefined.each do |predefined_role| + - if user.role? predefined_role.name + / TODO: hidden form here + / = button_to fauna_user_role_assignments_path(user_id: user.id, id: predefined_role.id), method: :delete, remote: true, class: 'btn btn-sm btn-warning', data: {disable: true, confirm: t('views.roles.are_you_sure')} do + / button.btn.btn-sm.btn-warning.disabled + li.list-group-item.list-group-item-success.px-2 + - if user.user_roles.find_by(role: predefined_role.id)&.end_time.nil? + => icon('fa-solid', 'square-check') + - else + => icon('fa-solid', 'calendar', html_options: { title: user.user_roles.find_by(role: predefined_role.id)&.end_time }) + = predefined_role.localized_name + + - elsif user.user_roles.future.find_by(role: predefined_role.id) + / = link_to fauna_user_role_assignments_path(user.id, role: {name: predefined_role.name}), method: :post, remote: true, class: 'btn btn-sm btn-default', data: {disable: true, confirm: t('views.roles.are_you_sure')} do + / button.btn.btn-sm.btn-default.disabled + li.list-group-item.list-group-item-warning.px-2 + => icon('fa-regular', 'calendar', html_options: { title: user.user_roles.unscoped.future.find_by(role: predefined_role.id).end_time }) + = predefined_role.localized_name + - else + li.list-group-item.px-2 + => icon('fa-regular', 'square-minus') + = predefined_role.localized_name + .d-inline.m-2 + = link_to fauna_user_role_assignments_path(user.id), :class => 'btn btn-primary btn-sm' do + => icon('fa-solid', 'calendar') + = t 'views.navigation.view_edit' / button.btn.btn-sm.btn-primary[type='button' data-bs-toggle='modal' data-bs-target="#modal-timed-roles-#{user.id}"] = icon('fa-solid', 'calendar') / = render partial: '/fauna/role_assignments/temporary_roles', locals: {user: user} diff --git a/app/views/fauna/users/index.html.slim b/app/views/fauna/users/index.html.slim index 344e6cf..9c01b5b 100644 --- a/app/views/fauna/users/index.html.slim +++ b/app/views/fauna/users/index.html.slim @@ -13,7 +13,6 @@ section.container th = User.human_attribute_name(:name) th = User.human_attribute_name(:username) th = User.human_attribute_name(:email) - th.roles_column = User.human_attribute_name(:roles) - th.roles_column = User.human_attribute_name(:roles) + th = User.human_attribute_name(:roles) tbody#user_table = render @users diff --git a/app/views/layouts/application.html.slim b/app/views/layouts/application.html.slim index e56508c..8b6a217 100644 --- a/app/views/layouts/application.html.slim +++ b/app/views/layouts/application.html.slim @@ -16,8 +16,8 @@ html lang="#{I18n.locale}" = csrf_meta_tags = csp_meta_tag - = stylesheet_link_tag 'application', :media => 'all', 'data-turbolinks-track' => 'reload', :nonce => content_security_policy_nonce, :integrity => true - = javascript_include_tag 'application', 'data-turbolinks-track' => 'reload', :nonce => content_security_policy_nonce, :integrity => true + = stylesheet_link_tag 'application', :media => 'all', :nonce => content_security_policy_nonce, :integrity => true + = javascript_include_tag 'application', :nonce => content_security_policy_nonce, :integrity => true link rel="manifest" href="/manifest.json" body diff --git a/config/application.rb b/config/application.rb index f73839e..c054224 100644 --- a/config/application.rb +++ b/config/application.rb @@ -25,7 +25,7 @@ class Application < Rails::Application config.i18n.enforce_available_locales = true config.i18n.default_locale = :bg - config.action_view.field_error_proc = proc do |html_tag, instance| + config.action_view.field_error_proc = proc do |html_tag, _instance| html_tag end diff --git a/config/initializers/simple_form_bootstrap.rb b/config/initializers/simple_form_bootstrap.rb index 7ec2ec6..97f813d 100644 --- a/config/initializers/simple_form_bootstrap.rb +++ b/config/initializers/simple_form_bootstrap.rb @@ -19,7 +19,7 @@ config.boolean_label_class = 'form-check-label' # How the label text should be generated altogether with the required text. - config.label_text = lambda { |label, required, explicit_label| "#{label} #{required}" } + config.label_text = ->(label, required, _explicit_label) { "#{label} #{required}" } # Define the way to render check boxes / radio buttons with labels. config.boolean_style = :inline @@ -32,7 +32,7 @@ config.include_default_input_wrapper_class = false # CSS class to add for error notification helper. - config.error_notification_class = 'alert alert-danger' + config.error_notification_class = 'card card-body border-danger text-danger md-6 mb-1' # Method used to tidy up errors. Specify any Rails Array method. # :first lists the first message for each field. diff --git a/config/locales/devise.bg.yml b/config/locales/devise.bg.yml index 3921ab7..9c99819 100644 --- a/config/locales/devise.bg.yml +++ b/config/locales/devise.bg.yml @@ -38,7 +38,7 @@ bg: signed_up: "Добре дошли! Вие се регистрирахте успешно." signed_up_but_inactive: "Вие се регистирахте успешно. Въпреки това, не можете да влезете в профила си, защото той все още не е потвърден." signed_up_but_locked: "Вие се регистрирахте успешно. Въпреки това, не можете да влезете в профила си, защото той е заключен." - signed_up_but_unconfirmed: "Писмо с връзка за потвърждаване на профила ви беше изпратено на вашия имейл адрес. Моля, отворете връзката, за даактивирате вашия профил." + signed_up_but_unconfirmed: "Писмо с връзка за потвърждаване на профила ви беше изпратено на вашия имейл адрес. Моля, отворете връзката, за да активирате вашия профил." update_needs_confirmation: "Вие променихте профила си успешно, но ние трябва да проверим вашия нов имейл адрес. Моля, проверете пощата си и отворете връзката за потвърждаване на новия адрес." updated: "Вие променихте профила си успешно." sessions: diff --git a/config/locales/simple_form.bg.yml b/config/locales/simple_form.bg.yml index 36d7bd0..519c7df 100644 --- a/config/locales/simple_form.bg.yml +++ b/config/locales/simple_form.bg.yml @@ -26,6 +26,8 @@ bg: network_device: create: "Създай" update: "Промени" + user_role: + create: "Добави достъп" # Examples # labels: # defaults: From b3fa8ed99ed100348015549239f8b9aea42329e4 Mon Sep 17 00:00:00 2001 From: Albert Stefanov Date: Mon, 20 Apr 2026 08:51:00 +0300 Subject: [PATCH 17/19] Deployment fixes Turns out I missed some catch-22 stuff, and borked the css :) --- Gemfile | 2 ++ Gemfile.lock | 2 ++ app/assets/stylesheets/application.scss | 2 -- app/controllers/application_controller.rb | 2 +- app/policies/application_policy.rb | 2 +- config/environments/production.rb | 6 +++--- config/initializers/devise.rb | 4 ++-- 7 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Gemfile b/Gemfile index 7e2739b..dac7599 100644 --- a/Gemfile +++ b/Gemfile @@ -131,4 +131,6 @@ group :production do gem "pg" gem "puma" + + gem "sd_notify" end diff --git a/Gemfile.lock b/Gemfile.lock index 4c221a5..d0701f3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -368,6 +368,7 @@ GEM google-protobuf (~> 4.31) sassc-embedded (1.80.8) sass-embedded (~> 1.80) + sd_notify (0.1.1) sdoc (2.6.5) rdoc (>= 5.0) securerandom (0.4.1) @@ -495,6 +496,7 @@ DEPENDENCIES rails (~> 8.1.1) rails-i18n rspec-rails + sd_notify sdoc selenium-webdriver simple_form diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index c193ac5..595603d 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -12,10 +12,8 @@ @import 'components/flash_message'; @import 'components/footer'; -@import 'components/login'; @import 'components/nested_fields'; @import 'components/network_devices'; -@import 'components/role_assignments'; @import 'components/large_buttons'; @import 'fauna/user_list'; @import 'components/header'; diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 26221f0..cc327e8 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -32,7 +32,7 @@ def set_locale def configure_permitted_parameters devise_parameter_sanitizer.permit(:sign_up, keys: [:first_name, :last_name, :username, :email]) - devise_parameter_sanitizer.permit(:sign_in, keys: [:username]) + devise_parameter_sanitizer.permit(:sign_in, keys: [:username, :email]) devise_parameter_sanitizer.permit(:account_update, keys: [:first_name, :last_name, :username, :email, :url, :locale, :twitter, :announce_my_presence, diff --git a/app/policies/application_policy.rb b/app/policies/application_policy.rb index 8980019..afe3d19 100644 --- a/app/policies/application_policy.rb +++ b/app/policies/application_policy.rb @@ -9,7 +9,7 @@ def initialize(user, record) end def index? - user&.has_role?(:board_member) + user&.role?(:board_member) end def show? diff --git a/config/environments/production.rb b/config/environments/production.rb index 5866bd3..7d3837f 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -69,10 +69,10 @@ # SMTP is default # config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = { - address: Rails.application.credentials.smtp.host, + address: Rails.application.credentials&.smtp&.host, port: 465, - user_name: Rails.application.credentials.smtp.user, - password: Rails.application.credentials.smtp.password, + user_name: Rails.application.credentials&.smtp&.user, + password: Rails.application.credentials&.smtp&.password, ssl: true, } diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 83ff64c..cc5cfca 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -41,12 +41,12 @@ # Configure which authentication keys should be case-insensitive. # These keys will be downcased upon creating or modifying a user and when used # to authenticate or find a user. Default is :email. - config.case_insensitive_keys = [:email] + config.case_insensitive_keys = [:email, :username] # Configure which authentication keys should have whitespace stripped. # These keys will have whitespace before and after removed upon creating or # modifying a user and when used to authenticate or find a user. Default is :email. - config.strip_whitespace_keys = [:email] + config.strip_whitespace_keys = [:email, :username] # Tell if authentication through request.params is enabled. True by default. # It can be set to an array that will enable params authentication only for the From 62f59ad919d33131914c012a08c6b798ad022cfd Mon Sep 17 00:00:00 2001 From: Albert Stefanov Date: Wed, 29 Apr 2026 12:36:35 +0300 Subject: [PATCH 18/19] Fix theming - redesign registration/profile forms - remove materia theme, and bootswatch in general - update simple_form components for BS5 --- Gemfile | 1 - Gemfile.lock | 7 - app/assets/stylesheets/application.scss | 2 - .../components/_nested_fields.scss | 6 - .../_phone_number_fields.html.slim | 2 +- app/views/devise/registrations/edit.html.slim | 123 ++++++++++++------ app/views/devise/registrations/new.html.slim | 17 ++- .../fauna/role_assignments/_form.html.slim | 4 +- app/views/fauna/users/_user.html.slim | 10 +- app/views/fauna/users/index.html.slim | 10 +- config/initializers/bootswatch.rb | 1 - config/initializers/simple_form_bootstrap.rb | 32 ++--- config/locales/bg.yml | 3 + config/locales/en.yml | 3 + 14 files changed, 126 insertions(+), 95 deletions(-) delete mode 100644 config/initializers/bootswatch.rb diff --git a/Gemfile b/Gemfile index dac7599..c4acb7a 100644 --- a/Gemfile +++ b/Gemfile @@ -44,7 +44,6 @@ gem "slim-rails" # Use the Bootstrap CSS framework and the FA icon font gem "bootstrap" gem "font-awesome-sass", :github => 'sunbirddcim/font-awesome-sass' -gem "bootswatch", :github => "thomaspark/bootswatch" # Gravatar helper gem "gravatar-ultimate" diff --git a/Gemfile.lock b/Gemfile.lock index d0701f3..9762a67 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,12 +4,6 @@ GIT specs: font-awesome-sass (6.5.2.pre.sunbird.pre.1) -GIT - remote: https://github.com/thomaspark/bootswatch.git - revision: 3eb431f4a24ac7a7a6b95ce280d8d3dd3628f3de - specs: - bootswatch (5.3.8) - GEM remote: https://rubygems.org/ specs: @@ -461,7 +455,6 @@ PLATFORMS DEPENDENCIES bootsnap bootstrap - bootswatch! brakeman bundler-audit byebug diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 595603d..f2dd4f4 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -1,9 +1,7 @@ -@import "materia/variables"; @import 'bootstrap/functions'; @import 'bootstrap/variables'; @import 'bootstrap/mixins'; @import 'bootstrap'; -@import "materia/bootswatch"; @import 'font-awesome'; diff --git a/app/assets/stylesheets/components/_nested_fields.scss b/app/assets/stylesheets/components/_nested_fields.scss index de775bf..2ed1525 100644 --- a/app/assets/stylesheets/components/_nested_fields.scss +++ b/app/assets/stylesheets/components/_nested_fields.scss @@ -7,9 +7,3 @@ @extend .text-center; } -.form-horizontal .add_fields { - @extend .offset-sm-3; - @extend .btn; - @extend .btn-primary; - @extend .btn-sm; -} diff --git a/app/views/devise/registrations/_phone_number_fields.html.slim b/app/views/devise/registrations/_phone_number_fields.html.slim index a1532ca..f107375 100644 --- a/app/views/devise/registrations/_phone_number_fields.html.slim +++ b/app/views/devise/registrations/_phone_number_fields.html.slim @@ -1,4 +1,4 @@ -.nested-fields +.nested-fields.mb-3 = f.input :phone_number do .input-group = f.input_field :phone_number, class: 'form-control', value: f.object.phone_number.try(:phony_formatted, format: :international) diff --git a/app/views/devise/registrations/edit.html.slim b/app/views/devise/registrations/edit.html.slim index f2bf9bb..818f18d 100644 --- a/app/views/devise/registrations/edit.html.slim +++ b/app/views/devise/registrations/edit.html.slim @@ -1,56 +1,95 @@ - content_for(:title) { t 'views.registrations.edit_account' } section.container - .row - .account_form - = simple_form_for(resource, as: resource_name, url: registration_path(resource_name), wrapper: :horizontal_form, html: { method: :put, class: 'form-horizontal' }) do |f| + .row.justify-content-center + .col-12 + = simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, class: 'form-horizontal' }) do |f| header legend h3 = t('views.registrations.edit_account') = f.error_notification - .form-inputs - .avatar_box - .edit_gravatar - = image_tag resource.picture(128) - span.overlay - | Avatar by - =< link_to 'Gravatar', 'https://gravatar.com' - - = f.input :first_name, required: true - = f.input :last_name, required: true - = f.input :username, required: true - = f.input :email, required: true - = f.input :locale, collection: I18n.available_locales, required: true - - if devise_mapping.confirmable? && resource.pending_reconfirmation? - p - | Currently waiting confirmation for: - =< resource.unconfirmed_email - - = f.input :password, autocomplete: "off", hint: t('views.registrations.leave_blank_if_you_do_not_want_to_change'), required: false - = f.input :password_confirmation, required: false - - = f.input :pin, as: :password, autocomplete: "off", required: false - = f.input :pin_confirmation, as: :password, required: false - - #phone_numbers + h4 = t('views.registrations.personal_data') + .row + .col-6 + .row + .col-12 + .col-6 + = f.input :first_name, required: true + .col-6 + = f.input :last_name, required: true + .col-12 + = f.input :username, required: true + .col-12 + = f.input :email, required: true + - if devise_mapping.confirmable? && resource.pending_reconfirmation? + p + | Currently waiting confirmation for: + =< resource.unconfirmed_email + .col-md-3 + .col-6.col-md-3 + .row + = f.input :locale, collection: I18n.available_locales, required: true, class: '' + .row + .row.edit_gravatar + .col.d-flex.justify-content-center + = image_tag resource.picture(192) + .row + .col.d-flex.justify-content-center + span.overlay + | Avatar by + =< link_to 'Gravatar', 'https://gravatar.com' + + .row + .col + = f.input :announce_my_presence, hint: t('views.registrations.announce_my_presence_explanation') + + .row + .col-6 + = f.input :password, autocomplete: "off", hint: t('views.registrations.leave_blank_if_you_do_not_want_to_change'), required: false + .col-6 + = f.input :password_confirmation, required: false + + hr + h4 = t('views.registrations.phones') + .row + .col-6 + .row + = f.input :pin, as: :password, autocomplete: "off", required: false + = f.input :pin_confirmation, as: :password, required: false + .col-12.d-grid + = link_to_add_association t('views.registrations.add_phone_number'), f, :phone_numbers, role: 'button', class: 'btn btn-primary', data: {'association-insertion-node' => '#phone_numbers', 'association-insertion-method' => 'append'} + + .col-6#phone_numbers = f.simple_fields_for :phone_numbers do |phone_numbers_form| = render 'phone_number_fields', f: phone_numbers_form - .add_nested_fields_container - = link_to_add_association t('views.registrations.add_phone_number'), f, :phone_numbers, role: 'button', data: {'association-insertion-node' => '#phone_numbers', 'association-insertion-method' => 'append'} + .add_nested_fields_container.mb-3 + + hr + h4 = t('views.registrations.socials') + + .row + .col-3 + = f.input :github + .col-3 + = f.input :url + .col-3 + = f.input :twitter + .col-3 + = f.input :jabber - = f.input :github - = f.input :twitter - = f.input :url - = f.input :jabber - = f.input :announce_my_presence, hint: t('views.registrations.announce_my_presence_explanation') + hr - = f.input :current_password, hint: t('views.registrations.we_need_your_current_password'), required: true, wrapper_html: {class: 'has-warning'} + .row.align-items-end + .col-6 + = f.input :current_password, hint: t('views.registrations.we_need_your_current_password'), required: true, wrapper_html: {class: 'has-warning'} + .form-actions.d-grid.g-2 + = f.button :submit, t('views.registrations.update') + .col-3 + .col-3 + .row + = t('views.registrations.unhappy') + = link_to t('views.registrations.cancel_my_account'), registration_path(resource_name), data: { confirm: t('views.registrations.are_you_sure') }, method: :delete, class: 'btn btn-danger' + .clearfix - .form-actions - = f.button :submit, t('views.registrations.update') - .clearfix - h4 - = t('views.registrations.unhappy') - = link_to t('views.registrations.cancel_my_account'), registration_path(resource_name), data: { confirm: t('views.registrations.are_you_sure') }, method: :delete, class: 'btn btn-danger pull-right' diff --git a/app/views/devise/registrations/new.html.slim b/app/views/devise/registrations/new.html.slim index bbcf31f..b6f8969 100644 --- a/app/views/devise/registrations/new.html.slim +++ b/app/views/devise/registrations/new.html.slim @@ -2,18 +2,21 @@ section.container .row .centered_form - = simple_form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| + = simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { class: 'form-horizontal' }) do |f| header legend h3 = t 'views.registrations.sign_up' = f.error_notification .form-inputs - = f.input :username, required: true, autofocus: true - = f.input :first_name, required: true, autofocus: true - = f.input :last_name, required: true, autofocus: true - = f.input :email, required: true, autofocus: true - = f.input :password, required: true, hint: (t("views.registrations.minimum_characters", minimum: @minimum_password_length) if @validatable) - = f.input :password_confirmation, required: true + .row + .col-6 = f.input :email, required: true, autofocus: true + .col-6 = f.input :username, required: true, autofocus: true + .row + .col-6 = f.input :first_name, required: true, autofocus: true + .col-6 = f.input :last_name, required: true, autofocus: true + .row + .col-6 = f.input :password, required: true, hint: (t("views.registrations.minimum_characters", minimum: @minimum_password_length) if @validatable) + .col-6 = f.input :password_confirmation, required: true .form-actions = f.button :submit, t('views.registrations.sign_me_up') = render "devise/shared/links" diff --git a/app/views/fauna/role_assignments/_form.html.slim b/app/views/fauna/role_assignments/_form.html.slim index 9597409..6e9f1e7 100644 --- a/app/views/fauna/role_assignments/_form.html.slim +++ b/app/views/fauna/role_assignments/_form.html.slim @@ -4,9 +4,9 @@ = f.select :role, Role.all.map { |r| [r.localized_name.to_s, r.id] }, {}, { required: true, selected: nil, class: 'form-select' } .col-4 = f.label :start_time, class: 'form-label fw-bold' - = f.datetime_field :start_time, as: :date, html5: true, class: 'form-control' + = f.datetime_field :start_time, as: :date, html5: true, class: 'form-control', value: Time.current .col-4 = f.label :end_time, class: 'form-label fw-bold' = f.datetime_field :end_time, as: :date, html5: true, class: 'form-control' .col-2.d-grid - = f.button :submit, class: 'btn btn-success' \ No newline at end of file + = f.button :submit, class: 'btn btn-success' diff --git a/app/views/fauna/users/_user.html.slim b/app/views/fauna/users/_user.html.slim index b4063f9..b2e92a0 100644 --- a/app/views/fauna/users/_user.html.slim +++ b/app/views/fauna/users/_user.html.slim @@ -3,13 +3,13 @@ tr td = user.username td = user.email td.d-flex - ul.list-group.list-group-horizontal-md.align-items-center.border-primary + ul.list-group.list-group-horizontal-md.align-items-center.border-primary.small - Role.predefined.each do |predefined_role| - if user.role? predefined_role.name / TODO: hidden form here / = button_to fauna_user_role_assignments_path(user_id: user.id, id: predefined_role.id), method: :delete, remote: true, class: 'btn btn-sm btn-warning', data: {disable: true, confirm: t('views.roles.are_you_sure')} do / button.btn.btn-sm.btn-warning.disabled - li.list-group-item.list-group-item-success.px-2 + li.list-group-item.list-group-item-success.px-1 - if user.user_roles.find_by(role: predefined_role.id)&.end_time.nil? => icon('fa-solid', 'square-check') - else @@ -19,17 +19,17 @@ tr - elsif user.user_roles.future.find_by(role: predefined_role.id) / = link_to fauna_user_role_assignments_path(user.id, role: {name: predefined_role.name}), method: :post, remote: true, class: 'btn btn-sm btn-default', data: {disable: true, confirm: t('views.roles.are_you_sure')} do / button.btn.btn-sm.btn-default.disabled - li.list-group-item.list-group-item-warning.px-2 + li.list-group-item.list-group-item-warning.px-1 => icon('fa-regular', 'calendar', html_options: { title: user.user_roles.unscoped.future.find_by(role: predefined_role.id).end_time }) = predefined_role.localized_name - else - li.list-group-item.px-2 + li.list-group-item.px-1 => icon('fa-regular', 'square-minus') = predefined_role.localized_name .d-inline.m-2 = link_to fauna_user_role_assignments_path(user.id), :class => 'btn btn-primary btn-sm' do => icon('fa-solid', 'calendar') - = t 'views.navigation.view_edit' + / = t 'views.navigation.view_edit' / button.btn.btn-sm.btn-primary[type='button' data-bs-toggle='modal' data-bs-target="#modal-timed-roles-#{user.id}"] = icon('fa-solid', 'calendar') / = render partial: '/fauna/role_assignments/temporary_roles', locals: {user: user} diff --git a/app/views/fauna/users/index.html.slim b/app/views/fauna/users/index.html.slim index 9c01b5b..7b70fbc 100644 --- a/app/views/fauna/users/index.html.slim +++ b/app/views/fauna/users/index.html.slim @@ -7,12 +7,12 @@ section.container .users .row .col-sm-12 - table + table.table.table-hover.table-sm.m-2.table-responsive thead tr - th = User.human_attribute_name(:name) - th = User.human_attribute_name(:username) - th = User.human_attribute_name(:email) - th = User.human_attribute_name(:roles) + th.col-2[scope='col'] = User.human_attribute_name(:name) + th.col-2[scope='col'] = User.human_attribute_name(:username) + th.col-2[scope='col'] = User.human_attribute_name(:email) + th.col-6[scope='col'] = User.human_attribute_name(:roles) tbody#user_table = render @users diff --git a/config/initializers/bootswatch.rb b/config/initializers/bootswatch.rb deleted file mode 100644 index 7e94884..0000000 --- a/config/initializers/bootswatch.rb +++ /dev/null @@ -1 +0,0 @@ -Rails.application.config.assets.paths += Gem.loaded_specs["bootswatch"].load_paths diff --git a/config/initializers/simple_form_bootstrap.rb b/config/initializers/simple_form_bootstrap.rb index 97f813d..89a0b55 100644 --- a/config/initializers/simple_form_bootstrap.rb +++ b/config/initializers/simple_form_bootstrap.rb @@ -13,7 +13,7 @@ # Use this setup block to configure all options available in SimpleForm. SimpleForm.setup do |config| # Default class for buttons - config.button_class = 'btn' + config.button_class = 'btn btn-primary' # Define the default class of the input wrapper of the boolean input. config.boolean_label_class = 'form-check-label' @@ -156,8 +156,8 @@ b.optional :pattern b.optional :min_max b.optional :readonly - b.use :label, class: 'col-sm-3 col-form-label' - b.wrapper :grid_wrapper, class: 'col-sm-9' do |ba| + b.use :label, class: 'col-sm-3 col-md-2 col-form-label' + b.wrapper :grid_wrapper, class: 'col-sm-9 col-md-10' do |ba| ba.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' ba.use :full_error, wrap_with: { class: 'invalid-feedback' } ba.use :hint, wrap_with: { class: 'form-text' } @@ -168,7 +168,7 @@ config.wrappers :horizontal_boolean, class: 'row mb-3' do |b| b.use :html5 b.optional :readonly - b.wrapper :grid_wrapper, class: 'col-sm-9 offset-sm-3' do |wr| + b.wrapper :grid_wrapper, class: 'col-sm-9 col-md-10 offset-sm-3' do |wr| wr.wrapper :form_check_wrapper, class: 'form-check' do |bb| bb.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' bb.use :label, class: 'form-check-label' @@ -182,8 +182,8 @@ config.wrappers :horizontal_collection, item_wrapper_class: 'form-check', item_label_class: 'form-check-label', class: 'row mb-3' do |b| b.use :html5 b.optional :readonly - b.use :label, class: 'col-sm-3 col-form-label pt-0' - b.wrapper :grid_wrapper, class: 'col-sm-9' do |ba| + b.use :label, class: 'col-sm-3 col-md-2 col-form-label pt-0' + b.wrapper :grid_wrapper, class: 'col-sm-9 col-md-10' do |ba| ba.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' ba.use :full_error, wrap_with: { class: 'invalid-feedback d-block' } ba.use :hint, wrap_with: { class: 'form-text' } @@ -194,8 +194,8 @@ config.wrappers :horizontal_collection_inline, item_wrapper_class: 'form-check form-check-inline', item_label_class: 'form-check-label', class: 'row mb-3' do |b| b.use :html5 b.optional :readonly - b.use :label, class: 'col-sm-3 col-form-label pt-0' - b.wrapper :grid_wrapper, class: 'col-sm-9' do |ba| + b.use :label, class: 'col-sm-3 col-md-2 col-form-label pt-0' + b.wrapper :grid_wrapper, class: 'col-sm-9 col-md-10' do |ba| ba.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' ba.use :full_error, wrap_with: { class: 'invalid-feedback d-block' } ba.use :hint, wrap_with: { class: 'form-text' } @@ -209,8 +209,8 @@ b.optional :maxlength b.optional :minlength b.optional :readonly - b.use :label, class: 'col-sm-3 col-form-label' - b.wrapper :grid_wrapper, class: 'col-sm-9' do |ba| + b.use :label, class: 'col-sm-3 col-md-2 col-form-label' + b.wrapper :grid_wrapper, class: 'col-sm-9 col-md-10' do |ba| ba.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' ba.use :full_error, wrap_with: { class: 'invalid-feedback' } ba.use :hint, wrap_with: { class: 'form-text' } @@ -221,8 +221,8 @@ config.wrappers :horizontal_select, class: 'row mb-3' do |b| b.use :html5 b.optional :readonly - b.use :label, class: 'col-sm-3 col-form-label' - b.wrapper :grid_wrapper, class: 'col-sm-9' do |ba| + b.use :label, class: 'col-sm-3 col-md-2 col-form-label' + b.wrapper :grid_wrapper, class: 'col-sm-9 col-md-10' do |ba| ba.use :input, class: 'form-select', error_class: 'is-invalid', valid_class: 'is-valid' ba.use :full_error, wrap_with: { class: 'invalid-feedback' } ba.use :hint, wrap_with: { class: 'form-text' } @@ -233,8 +233,8 @@ config.wrappers :horizontal_multi_select, class: 'row mb-3' do |b| b.use :html5 b.optional :readonly - b.use :label, class: 'col-sm-3 col-form-label' - b.wrapper :grid_wrapper, class: 'col-sm-9' do |ba| + b.use :label, class: 'col-sm-3 col-md-2 col-form-label' + b.wrapper :grid_wrapper, class: 'col-sm-9 col-md-10' do |ba| ba.wrapper class: 'd-flex flex-row justify-content-between align-items-center' do |bb| bb.use :input, class: 'form-select mx-1', error_class: 'is-invalid', valid_class: 'is-valid' end @@ -249,8 +249,8 @@ b.use :placeholder b.optional :readonly b.optional :step - b.use :label, class: 'col-sm-3 col-form-label pt-0' - b.wrapper :grid_wrapper, class: 'col-sm-9' do |ba| + b.use :label, class: 'col-sm-3 col-md-2 col-form-label pt-0' + b.wrapper :grid_wrapper, class: 'col-sm-9 col-md-10' do |ba| ba.use :input, class: 'form-range', error_class: 'is-invalid', valid_class: 'is-valid' ba.use :full_error, wrap_with: { class: 'invalid-feedback' } ba.use :hint, wrap_with: { class: 'form-text' } diff --git a/config/locales/bg.yml b/config/locales/bg.yml index 7134c0d..c2168f2 100644 --- a/config/locales/bg.yml +++ b/config/locales/bg.yml @@ -132,6 +132,9 @@ bg: Ви свързани с устройствата пак ще се изпращат, но вместо с името ви, ще пише Someone who does not wish to be named, unlocked/locked/opened the door. и т.н. + personal_data: "Лични данни" + phones: "Достъп чрез телефон" + socials: "Социални мрежи" passwords: forgotten_password: Забравена парола send_instructions: Изпрати инструкции diff --git a/config/locales/en.yml b/config/locales/en.yml index 84d3e6c..b024d80 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -125,6 +125,9 @@ en: your actions on any of the devices will still be published, but instead of showing your username, the text will say Someone who does not wish to be named, unlocked/locked/opened the door, etc. + personal_data: "Personal Data" + phones: "Phone Access" + socials: "Social Networks" passwords: forgotten_password: Forgotten password send_instructions: Send instructions From a07e5de30cda61332bfb0a1cf36e999b82b42cda Mon Sep 17 00:00:00 2001 From: Albert Stefanov Date: Wed, 29 Apr 2026 13:50:24 +0300 Subject: [PATCH 19/19] Update user list icons --- app/views/fauna/users/_user.html.slim | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/fauna/users/_user.html.slim b/app/views/fauna/users/_user.html.slim index b2e92a0..90247c2 100644 --- a/app/views/fauna/users/_user.html.slim +++ b/app/views/fauna/users/_user.html.slim @@ -13,23 +13,23 @@ tr - if user.user_roles.find_by(role: predefined_role.id)&.end_time.nil? => icon('fa-solid', 'square-check') - else - => icon('fa-solid', 'calendar', html_options: { title: user.user_roles.find_by(role: predefined_role.id)&.end_time }) + => icon('fa-solid', :calendar, html_options: { title: user.user_roles.find_by(role: predefined_role.id)&.end_time }) = predefined_role.localized_name - elsif user.user_roles.future.find_by(role: predefined_role.id) / = link_to fauna_user_role_assignments_path(user.id, role: {name: predefined_role.name}), method: :post, remote: true, class: 'btn btn-sm btn-default', data: {disable: true, confirm: t('views.roles.are_you_sure')} do / button.btn.btn-sm.btn-default.disabled li.list-group-item.list-group-item-warning.px-1 - => icon('fa-regular', 'calendar', html_options: { title: user.user_roles.unscoped.future.find_by(role: predefined_role.id).end_time }) + => icon('fa-regular', :calendar, html_options: { title: user.user_roles.unscoped.future.find_by(role: predefined_role.id).end_time }) = predefined_role.localized_name - else li.list-group-item.px-1 - => icon('fa-regular', 'square-minus') + => icon('fa-regular', :square) = predefined_role.localized_name .d-inline.m-2 = link_to fauna_user_role_assignments_path(user.id), :class => 'btn btn-primary btn-sm' do - => icon('fa-solid', 'calendar') + => icon('fa-solid', :pencil) / = t 'views.navigation.view_edit' / button.btn.btn-sm.btn-primary[type='button' data-bs-toggle='modal' data-bs-target="#modal-timed-roles-#{user.id}"] - = icon('fa-solid', 'calendar') + = icon('fa-solid', :pencil) / = render partial: '/fauna/role_assignments/temporary_roles', locals: {user: user}