From 1ede4180536d1ae2e1b1d95ad5b705ca9c5fef7c Mon Sep 17 00:00:00 2001 From: Joe Krump Date: Wed, 12 Jul 2023 20:16:02 -0400 Subject: [PATCH 01/19] Add add devise gem and configure it --- Gemfile | 2 + Gemfile.lock | 14 + app/channels/import_users_progress_channel.rb | 9 + app/models/user.rb | 6 + config/initializers/devise.rb | 313 ++++++++++++++++++ config/locales/devise.en.yml | 65 ++++ config/routes.rb | 1 + .../20230713001009_devise_create_users.rb | 44 +++ db/schema.rb | 14 +- db/seeds.rb | 6 + ...ort_users_progress_channel_channel_spec.rb | 5 + spec/models/user_spec.rb | 5 + 12 files changed, 483 insertions(+), 1 deletion(-) create mode 100644 app/channels/import_users_progress_channel.rb create mode 100644 app/models/user.rb create mode 100644 config/initializers/devise.rb create mode 100644 config/locales/devise.en.yml create mode 100644 db/migrate/20230713001009_devise_create_users.rb create mode 100644 spec/channels/import_users_progress_channel_channel_spec.rb create mode 100644 spec/models/user_spec.rb diff --git a/Gemfile b/Gemfile index 2410d75..89ed5b5 100644 --- a/Gemfile +++ b/Gemfile @@ -51,3 +51,5 @@ gem 'sidekiq', '~> 6.4' gem 'vite_rails', '~> 3.0.10' gem 'nokogiri', '>= 1.13.6' + +gem "devise", "~> 4.9" diff --git a/Gemfile.lock b/Gemfile.lock index 9927df1..2f864b9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -68,6 +68,7 @@ GEM tzinfo (~> 2.0) addressable (2.8.4) public_suffix (>= 2.0.2, < 6.0) + bcrypt (3.1.19) bootsnap (1.16.0) msgpack (~> 1.2) builder (3.2.4) @@ -86,6 +87,12 @@ GEM connection_pool (2.4.0) crass (1.0.6) date (3.3.3) + devise (4.9.2) + bcrypt (~> 3.0) + orm_adapter (~> 0.1) + railties (>= 4.1.0) + responders + warden (~> 1.2.3) diff-lcs (1.5.0) dry-cli (1.0.0) erubi (1.12.0) @@ -137,6 +144,7 @@ GEM racc (~> 1.4) nokogiri (1.15.2-x86_64-darwin) racc (~> 1.4) + orm_adapter (0.5.0) playwright-ruby-client (1.35.0) concurrent-ruby (>= 1.1.6) mime-types (>= 3.0) @@ -183,6 +191,9 @@ GEM ffi (~> 1.0) redis (4.8.1) regexp_parser (2.7.0) + responders (3.1.0) + actionpack (>= 5.2) + railties (>= 5.2) rspec-core (3.12.2) rspec-support (~> 3.12.0) rspec-expectations (3.12.3) @@ -227,6 +238,8 @@ GEM dry-cli (>= 0.7, < 2) rack-proxy (~> 0.6, >= 0.6.1) zeitwerk (~> 2.2) + warden (1.2.9) + rack (>= 2.0.9) websocket-driver (0.7.5) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) @@ -244,6 +257,7 @@ DEPENDENCIES byebug capybara (>= 3.35) chunky_png (~> 1.4) + devise (~> 4.9) jbuilder (~> 2.7) name_of_person (~> 1.1, >= 1.1.1) nokogiri (>= 1.13.6) diff --git a/app/channels/import_users_progress_channel.rb b/app/channels/import_users_progress_channel.rb new file mode 100644 index 0000000..e09dafe --- /dev/null +++ b/app/channels/import_users_progress_channel.rb @@ -0,0 +1,9 @@ +class ImportUsersProgressChannel < ApplicationCable::Channel + def subscribed + # stream_from "some_channel" + end + + def unsubscribed + # Any cleanup needed when channel is unsubscribed + end +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000..4756799 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,6 @@ +class User < ApplicationRecord + # Include default devise modules. Others available are: + # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable + devise :database_authenticatable, :registerable, + :recoverable, :rememberable, :validatable +end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb new file mode 100644 index 0000000..b964c0d --- /dev/null +++ b/config/initializers/devise.rb @@ -0,0 +1,313 @@ +# frozen_string_literal: true + +# Assuming you have not yet modified this file, each configuration option below +# is set to its default value. Note that some are commented out while others +# are not: uncommented lines are intended to protect your configuration from +# breaking changes in upgrades (i.e., in the event that future versions of +# Devise change the default values for those options). +# +# Use this hook to configure devise mailer, warden hooks and so forth. +# Many of these configuration options can be set straight in your model. +Devise.setup do |config| + # The secret key used by Devise. Devise uses this key to generate + # random tokens. Changing this key will render invalid all existing + # confirmation, reset password and unlock tokens in the database. + # Devise will use the `secret_key_base` as its `secret_key` + # by default. You can change it below and use your own secret key. + # config.secret_key = '7aa88cc4f3ae326efd8335d6ffcedb9d434d3120813547637263a2bfc785f9f44b885bbcafe3849b15ed38d89f04914087ded826c24d83b06c5f13676c7c41fc' + + # ==> Controller configuration + # Configure the parent class to the devise controllers. + # config.parent_controller = 'DeviseController' + + # ==> Mailer Configuration + # Configure the e-mail address which will be shown in Devise::Mailer, + # note that it will be overwritten if you use your own mailer class + # with default "from" parameter. + config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' + + # Configure the class responsible to send e-mails. + # config.mailer = 'Devise::Mailer' + + # Configure the parent class responsible to send e-mails. + # config.parent_mailer = 'ActionMailer::Base' + + # ==> ORM configuration + # Load and configure the ORM. Supports :active_record (default) and + # :mongoid (bson_ext recommended) by default. Other ORMs may be + # available as additional gems. + require 'devise/orm/active_record' + + # ==> Configuration for any authentication mechanism + # Configure which keys are used when authenticating a user. The default is + # just :email. You can configure it to use [:username, :subdomain], so for + # authenticating a user, both parameters are required. Remember that those + # parameters are used only when authenticating and not when retrieving from + # session. If you need permissions, you should implement that in a before filter. + # You can also supply a hash where the value is a boolean determining whether + # or not authentication should be aborted when the value is not present. + # config.authentication_keys = [:email] + + # Configure parameters from the request object used for authentication. Each entry + # given should be a request method and it will automatically be passed to the + # find_for_authentication method and considered in your model lookup. For instance, + # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. + # The same considerations mentioned for authentication_keys also apply to request_keys. + # config.request_keys = [] + + # 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] + + # 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] + + # 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 + # given strategies, for example, `config.params_authenticatable = [:database]` will + # enable it only for database (email + password) authentication. + # config.params_authenticatable = true + + # Tell if authentication through HTTP Auth is enabled. False by default. + # It can be set to an array that will enable http authentication only for the + # given strategies, for example, `config.http_authenticatable = [:database]` will + # enable it only for database authentication. + # For API-only applications to support authentication "out-of-the-box", you will likely want to + # enable this with :database unless you are using a custom strategy. + # The supported strategies are: + # :database = Support basic authentication with authentication key + password + # config.http_authenticatable = false + + # If 401 status code should be returned for AJAX requests. True by default. + # config.http_authenticatable_on_xhr = true + + # The realm used in Http Basic Authentication. 'Application' by default. + # config.http_authentication_realm = 'Application' + + # It will change confirmation, password recovery and other workflows + # to behave the same regardless if the e-mail provided was right or wrong. + # Does not affect registerable. + # config.paranoid = true + + # By default Devise will store the user in session. You can skip storage for + # particular strategies by setting this option. + # Notice that if you are skipping storage for all authentication paths, you + # may want to disable generating routes to Devise's sessions controller by + # passing skip: :sessions to `devise_for` in your config/routes.rb + config.skip_session_storage = [:http_auth] + + # By default, Devise cleans up the CSRF token on authentication to + # avoid CSRF token fixation attacks. This means that, when using AJAX + # requests for sign in and sign up, you need to get a new CSRF token + # from the server. You can disable this option at your own risk. + # config.clean_up_csrf_token_on_authentication = true + + # When false, Devise will not attempt to reload routes on eager load. + # This can reduce the time taken to boot the app but if your application + # requires the Devise mappings to be loaded during boot time the application + # won't boot properly. + # config.reload_routes = true + + # ==> Configuration for :database_authenticatable + # For bcrypt, this is the cost for hashing the password and defaults to 12. If + # using other algorithms, it sets how many times you want the password to be hashed. + # The number of stretches used for generating the hashed password are stored + # with the hashed password. This allows you to change the stretches without + # invalidating existing passwords. + # + # Limiting the stretches to just one in testing will increase the performance of + # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use + # a value less than 10 in other environments. Note that, for bcrypt (the default + # algorithm), the cost increases exponentially with the number of stretches (e.g. + # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). + config.stretches = Rails.env.test? ? 1 : 12 + + # Set up a pepper to generate the hashed password. + # config.pepper = '386931f12efdad3e2db2a116ba175451e58f98616d166c0ee9569090f483282edb56dc451b4f64df6482d4955210231ae79778f11c80fc57fb766c07f6ea8e84' + + # Send a notification to the original email when the user's email is changed. + # config.send_email_changed_notification = false + + # Send a notification email when the user's password is changed. + # config.send_password_change_notification = false + + # ==> Configuration for :confirmable + # A period that the user is allowed to access the website even without + # confirming their account. For instance, if set to 2.days, the user will be + # able to access the website for two days without confirming their account, + # access will be blocked just in the third day. + # You can also set it to nil, which will allow the user to access the website + # without confirming their account. + # Default is 0.days, meaning the user cannot access the website without + # confirming their account. + # config.allow_unconfirmed_access_for = 2.days + + # A period that the user is allowed to confirm their account before their + # token becomes invalid. For example, if set to 3.days, the user can confirm + # their account within 3 days after the mail was sent, but on the fourth day + # their account can't be confirmed with the token any more. + # Default is nil, meaning there is no restriction on how long a user can take + # before confirming their account. + # config.confirm_within = 3.days + + # If true, requires any email changes to be confirmed (exactly the same way as + # initial account confirmation) to be applied. Requires additional unconfirmed_email + # db field (see migrations). Until confirmed, new email is stored in + # unconfirmed_email column, and copied to email column on successful confirmation. + config.reconfirmable = true + + # Defines which key will be used when confirming an account + # config.confirmation_keys = [:email] + + # ==> Configuration for :rememberable + # The time the user will be remembered without asking for credentials again. + # config.remember_for = 2.weeks + + # Invalidates all the remember me tokens when the user signs out. + config.expire_all_remember_me_on_sign_out = true + + # If true, extends the user's remember period when remembered via cookie. + # config.extend_remember_period = false + + # Options to be passed to the created cookie. For instance, you can set + # secure: true in order to force SSL only cookies. + # config.rememberable_options = {} + + # ==> Configuration for :validatable + # Range for password length. + config.password_length = 6..128 + + # Email regex used to validate email formats. It simply asserts that + # one (and only one) @ exists in the given string. This is mainly + # to give user feedback and not to assert the e-mail validity. + config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ + + # ==> Configuration for :timeoutable + # The time you want to timeout the user session without activity. After this + # time the user will be asked for credentials again. Default is 30 minutes. + # config.timeout_in = 30.minutes + + # ==> Configuration for :lockable + # Defines which strategy will be used to lock an account. + # :failed_attempts = Locks an account after a number of failed attempts to sign in. + # :none = No lock strategy. You should handle locking by yourself. + # config.lock_strategy = :failed_attempts + + # Defines which key will be used when locking and unlocking an account + # config.unlock_keys = [:email] + + # Defines which strategy will be used to unlock an account. + # :email = Sends an unlock link to the user email + # :time = Re-enables login after a certain amount of time (see :unlock_in below) + # :both = Enables both strategies + # :none = No unlock strategy. You should handle unlocking by yourself. + # config.unlock_strategy = :both + + # Number of authentication tries before locking an account if lock_strategy + # is failed attempts. + # config.maximum_attempts = 20 + + # Time interval to unlock the account if :time is enabled as unlock_strategy. + # config.unlock_in = 1.hour + + # Warn on the last attempt before the account is locked. + # config.last_attempt_warning = true + + # ==> Configuration for :recoverable + # + # Defines which key will be used when recovering the password for an account + # config.reset_password_keys = [:email] + + # Time interval you can reset your password with a reset password key. + # Don't put a too small interval or your users won't have the time to + # change their passwords. + config.reset_password_within = 6.hours + + # When set to false, does not sign a user in automatically after their password is + # reset. Defaults to true, so a user is signed in automatically after a reset. + # config.sign_in_after_reset_password = true + + # ==> Configuration for :encryptable + # Allow you to use another hashing or encryption algorithm besides bcrypt (default). + # You can use :sha1, :sha512 or algorithms from others authentication tools as + # :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20 + # for default behavior) and :restful_authentication_sha1 (then you should set + # stretches to 10, and copy REST_AUTH_SITE_KEY to pepper). + # + # Require the `devise-encryptable` gem when using anything other than bcrypt + # config.encryptor = :sha512 + + # ==> Scopes configuration + # Turn scoped views on. Before rendering "sessions/new", it will first check for + # "users/sessions/new". It's turned off by default because it's slower if you + # are using only default views. + # config.scoped_views = false + + # Configure the default scope given to Warden. By default it's the first + # devise role declared in your routes (usually :user). + # config.default_scope = :user + + # Set this configuration to false if you want /users/sign_out to sign out + # only the current scope. By default, Devise signs out all scopes. + # config.sign_out_all_scopes = true + + # ==> Navigation configuration + # Lists the formats that should be treated as navigational. Formats like + # :html should redirect to the sign in page when the user does not have + # access, but formats like :xml or :json, should return 401. + # + # If you have any extra navigational formats, like :iphone or :mobile, you + # should add them to the navigational formats lists. + # + # The "*/*" below is required to match Internet Explorer requests. + # config.navigational_formats = ['*/*', :html, :turbo_stream] + + # The default HTTP method used to sign out a resource. Default is :delete. + config.sign_out_via = :delete + + # ==> OmniAuth + # Add a new OmniAuth provider. Check the wiki for more information on setting + # up on your models and hooks. + # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' + + # ==> Warden configuration + # If you want to use other strategies, that are not supported by Devise, or + # change the failure app, you can configure them inside the config.warden block. + # + # config.warden do |manager| + # manager.intercept_401 = false + # manager.default_strategies(scope: :user).unshift :some_external_strategy + # end + + # ==> Mountable engine configurations + # When using Devise inside an engine, let's call it `MyEngine`, and this engine + # is mountable, there are some extra configurations to be taken into account. + # The following options are available, assuming the engine is mounted as: + # + # mount MyEngine, at: '/my_engine' + # + # The router that invoked `devise_for`, in the example above, would be: + # config.router_name = :my_engine + # + # When using OmniAuth, Devise cannot automatically set OmniAuth path, + # so you need to do it manually. For the users scope, it would be: + # config.omniauth_path_prefix = '/my_engine/users/auth' + + # ==> Hotwire/Turbo configuration + # When using Devise with Hotwire/Turbo, the http status for error responses + # and some redirects must match the following. The default in Devise for existing + # apps is `200 OK` and `302 Found respectively`, but new apps are generated with + # these new defaults that match Hotwire/Turbo behavior. + # Note: These might become the new default in future versions of Devise. + config.responder.error_status = :unprocessable_entity + config.responder.redirect_status = :see_other + + # ==> Configuration for :registerable + + # When set to false, does not sign a user in automatically after their password is + # changed. Defaults to true, so a user is signed in automatically after changing a password. + # config.sign_in_after_change_password = true +end diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml new file mode 100644 index 0000000..260e1c4 --- /dev/null +++ b/config/locales/devise.en.yml @@ -0,0 +1,65 @@ +# Additional translations at https://github.com/heartcombo/devise/wiki/I18n + +en: + devise: + confirmations: + confirmed: "Your email address has been successfully confirmed." + send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." + failure: + already_authenticated: "You are already signed in." + inactive: "Your account is not activated yet." + invalid: "Invalid %{authentication_keys} or password." + locked: "Your account is locked." + last_attempt: "You have one more attempt before your account is locked." + not_found_in_database: "Invalid %{authentication_keys} or password." + timeout: "Your session expired. Please sign in again to continue." + unauthenticated: "You need to sign in or sign up before continuing." + unconfirmed: "You have to confirm your email address before continuing." + mailer: + confirmation_instructions: + subject: "Confirmation instructions" + reset_password_instructions: + subject: "Reset password instructions" + unlock_instructions: + subject: "Unlock instructions" + email_changed: + subject: "Email Changed" + password_change: + subject: "Password Changed" + omniauth_callbacks: + failure: "Could not authenticate you from %{kind} because \"%{reason}\"." + success: "Successfully authenticated from %{kind} account." + passwords: + no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." + send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." + updated: "Your password has been changed successfully. You are now signed in." + updated_not_active: "Your password has been changed successfully." + registrations: + destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." + signed_up: "Welcome! You have signed up successfully." + signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." + signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." + signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." + update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirmation link to confirm your new email address." + updated: "Your account has been updated successfully." + updated_but_not_signed_in: "Your account has been updated successfully, but since your password was changed, you need to sign in again." + sessions: + signed_in: "Signed in successfully." + signed_out: "Signed out successfully." + already_signed_out: "Signed out successfully." + unlocks: + send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." + send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." + unlocked: "Your account has been unlocked successfully. Please sign in to continue." + errors: + messages: + already_confirmed: "was already confirmed, please try signing in" + confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" + expired: "has expired, please request a new one" + not_found: "not found" + not_locked: "was not locked" + not_saved: + one: "1 error prohibited this %{resource} from being saved:" + other: "%{count} errors prohibited this %{resource} from being saved:" diff --git a/config/routes.rb b/config/routes.rb index dc5f34b..482330a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,7 @@ require 'sidekiq/web' Rails.application.routes.draw do + devise_for :users get '/pandas', to: 'pandas#index' root to: 'home#index' diff --git a/db/migrate/20230713001009_devise_create_users.rb b/db/migrate/20230713001009_devise_create_users.rb new file mode 100644 index 0000000..43927db --- /dev/null +++ b/db/migrate/20230713001009_devise_create_users.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +class DeviseCreateUsers < ActiveRecord::Migration[7.0] + def change + create_table :users do |t| + ## Database authenticatable + t.string :email, null: false, default: "" + t.string :encrypted_password, null: false, default: "" + + ## Recoverable + t.string :reset_password_token + t.datetime :reset_password_sent_at + + ## Rememberable + t.datetime :remember_created_at + + ## Trackable + # t.integer :sign_in_count, default: 0, null: false + # t.datetime :current_sign_in_at + # t.datetime :last_sign_in_at + # t.string :current_sign_in_ip + # t.string :last_sign_in_ip + + ## Confirmable + # t.string :confirmation_token + # t.datetime :confirmed_at + # t.datetime :confirmation_sent_at + # t.string :unconfirmed_email # Only if using reconfirmable + + ## Lockable + # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts + # t.string :unlock_token # Only if unlock strategy is :email or :both + # t.datetime :locked_at + + + t.timestamps null: false + end + + add_index :users, :email, unique: true + add_index :users, :reset_password_token, unique: true + # add_index :users, :confirmation_token, unique: true + # add_index :users, :unlock_token, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index a3b0495..5c363fd 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,5 +10,17 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 0) do +ActiveRecord::Schema[7.0].define(version: 2023_07_13_001009) do + 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" + t.datetime "remember_created_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["email"], name: "index_users_on_email", unique: true + t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true + end + end diff --git a/db/seeds.rb b/db/seeds.rb index 1beea2a..c3f69f3 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -5,3 +5,9 @@ # # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) # Character.create(name: 'Luke', movie: movies.first) + +User.create!(email: 'han@rebels.com', password: 'password', password_confirmation: 'password') + +User.create!(email: 'obi@jedi.com', password: 'password', password_confirmation: 'password') + +User.create!(email: 'vader@imperials.com', password: 'password', password_confirmation: 'password') diff --git a/spec/channels/import_users_progress_channel_channel_spec.rb b/spec/channels/import_users_progress_channel_channel_spec.rb new file mode 100644 index 0000000..42df71c --- /dev/null +++ b/spec/channels/import_users_progress_channel_channel_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe ImportUsersProgressChannelChannel, type: :channel do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb new file mode 100644 index 0000000..47a31bb --- /dev/null +++ b/spec/models/user_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe User, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end From 1270bf9bcabd03b5cefcddeb4c4fd7a610d9ee1a Mon Sep 17 00:00:00 2001 From: Joe Krump Date: Wed, 12 Jul 2023 20:19:39 -0400 Subject: [PATCH 02/19] Setup base channel authentication for action cable --- app/channels/application_cable/connection.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb index 0ff5442..e5ede22 100644 --- a/app/channels/application_cable/connection.rb +++ b/app/channels/application_cable/connection.rb @@ -1,4 +1,19 @@ module ApplicationCable class Connection < ActionCable::Connection::Base + identified_by :current_user + + def connect + self.current_user = find_verified_user + end + + private + + def find_verified_user + if verified_user = User.find_by(id: cookies.signed['user.id']) + verified_user + else + reject_unauthorized_connection + end + end end end From e9751690c88fc682802abd7b7b555ae1020d22a3 Mon Sep 17 00:00:00 2001 From: Joe Krump Date: Wed, 12 Jul 2023 20:21:03 -0400 Subject: [PATCH 03/19] Fix lint error --- app/channels/application_cable/connection.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb index e5ede22..2b1b9f8 100644 --- a/app/channels/application_cable/connection.rb +++ b/app/channels/application_cable/connection.rb @@ -9,7 +9,7 @@ def connect private def find_verified_user - if verified_user = User.find_by(id: cookies.signed['user.id']) + if (verified_user = User.find_by(id: cookies.signed['user.id'])) verified_user else reject_unauthorized_connection From 4e2495ac99a75ee514dac6a158dec190237a41ca Mon Sep 17 00:00:00 2001 From: Joe Krump Date: Wed, 12 Jul 2023 20:24:45 -0400 Subject: [PATCH 04/19] Replace rails_helper with spec_helper in specs --- spec/channels/import_users_progress_channel_channel_spec.rb | 2 +- spec/models/user_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/channels/import_users_progress_channel_channel_spec.rb b/spec/channels/import_users_progress_channel_channel_spec.rb index 42df71c..c197dfa 100644 --- a/spec/channels/import_users_progress_channel_channel_spec.rb +++ b/spec/channels/import_users_progress_channel_channel_spec.rb @@ -1,4 +1,4 @@ -require 'rails_helper' +require 'spec_helper' RSpec.describe ImportUsersProgressChannelChannel, type: :channel do pending "add some examples to (or delete) #{__FILE__}" diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 47a31bb..bb3a5d4 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1,4 +1,4 @@ -require 'rails_helper' +require 'spec_helper' RSpec.describe User, type: :model do pending "add some examples to (or delete) #{__FILE__}" From 56cfe434c9969ab22e4021a85fe57bec555206d5 Mon Sep 17 00:00:00 2001 From: Joe Krump Date: Wed, 12 Jul 2023 20:24:58 -0400 Subject: [PATCH 05/19] Set the name of the channel to stream from --- app/channels/import_users_progress_channel.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/channels/import_users_progress_channel.rb b/app/channels/import_users_progress_channel.rb index e09dafe..4f78e4e 100644 --- a/app/channels/import_users_progress_channel.rb +++ b/app/channels/import_users_progress_channel.rb @@ -1,6 +1,7 @@ class ImportUsersProgressChannel < ApplicationCable::Channel def subscribed # stream_from "some_channel" + stream_from 'import_users_progress_channel' end def unsubscribed From 171fd379fa3e35409ccb46c769cc7916d9e2e114 Mon Sep 17 00:00:00 2001 From: Joe Krump Date: Wed, 12 Jul 2023 20:32:59 -0400 Subject: [PATCH 06/19] Continue setting up channel --- app/channels/import_users_progress_channel.rb | 5 +++++ app/frontend/channels/consumer.ts | 3 +++ 2 files changed, 8 insertions(+) create mode 100644 app/frontend/channels/consumer.ts diff --git a/app/channels/import_users_progress_channel.rb b/app/channels/import_users_progress_channel.rb index 4f78e4e..53c3f24 100644 --- a/app/channels/import_users_progress_channel.rb +++ b/app/channels/import_users_progress_channel.rb @@ -1,10 +1,15 @@ class ImportUsersProgressChannel < ApplicationCable::Channel def subscribed # stream_from "some_channel" + # stream_from "import_user_#{param[:user_id]}_progress_channel" stream_from 'import_users_progress_channel' end def unsubscribed # Any cleanup needed when channel is unsubscribed end + + def get_progress + ActionCable.server.broadcast 'import_users_progress_channel', { progress: 0 } + end end diff --git a/app/frontend/channels/consumer.ts b/app/frontend/channels/consumer.ts new file mode 100644 index 0000000..a1ea1bf --- /dev/null +++ b/app/frontend/channels/consumer.ts @@ -0,0 +1,3 @@ +import { createConsumer } from "@rails/actioncable"; + +export default createConsumer(); From 022238deb3a3ebbb9e9f8853527d99ef5e2c977e Mon Sep 17 00:00:00 2001 From: Joe Krump Date: Thu, 13 Jul 2023 11:07:34 -0400 Subject: [PATCH 07/19] Show progress on frontend from a background job --- Procfile.dev | 1 + app/channels/application_cable/connection.rb | 2 +- app/controllers/users_controller.rb | 11 +++++ app/frontend/entrypoints/turbo-vue.ts | 1 - .../entrypoints/views/users/Index.vue | 42 +++++++++++++++++++ app/frontend/entrypoints/views/users/index.ts | 4 ++ app/frontend/helpers/routes.ts | 4 ++ app/helpers/users_helper.rb | 2 + app/jobs/import_contacts_job.rb | 12 ++++++ app/views/shared/_header.html.erb | 11 ++++- app/views/users/index.html.erb | 3 ++ config/routes.rb | 2 + package.json | 1 + spec/helpers/users_helper_spec.rb | 15 +++++++ spec/jobs/import_contacts_job_spec.rb | 5 +++ spec/requests/users_spec.rb | 7 ++++ yarn.lock | 5 +++ 17 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 app/controllers/users_controller.rb create mode 100644 app/frontend/entrypoints/views/users/Index.vue create mode 100644 app/frontend/entrypoints/views/users/index.ts create mode 100644 app/helpers/users_helper.rb create mode 100644 app/jobs/import_contacts_job.rb create mode 100644 app/views/users/index.html.erb create mode 100644 spec/helpers/users_helper_spec.rb create mode 100644 spec/jobs/import_contacts_job_spec.rb create mode 100644 spec/requests/users_spec.rb diff --git a/Procfile.dev b/Procfile.dev index 9d235bd..772d95a 100644 --- a/Procfile.dev +++ b/Procfile.dev @@ -1,2 +1,3 @@ web: bin/rails server -p 3000 vite: bundle exec bin/vite dev +redis: redis-server diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb index 2b1b9f8..f31bdbe 100644 --- a/app/channels/application_cable/connection.rb +++ b/app/channels/application_cable/connection.rb @@ -9,7 +9,7 @@ def connect private def find_verified_user - if (verified_user = User.find_by(id: cookies.signed['user.id'])) + if (verified_user = env['warden'].user) verified_user else reject_unauthorized_connection diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb new file mode 100644 index 0000000..f18fa4b --- /dev/null +++ b/app/controllers/users_controller.rb @@ -0,0 +1,11 @@ +class UsersController < ApplicationController + before_action :authenticate_user! + + def index + Rails.cache.fetch('users/import_progress', expires_in: 2.minutes) do + ImportContactsJob.perform_later + + true + end + end +end diff --git a/app/frontend/entrypoints/turbo-vue.ts b/app/frontend/entrypoints/turbo-vue.ts index 39397bc..ed7f910 100644 --- a/app/frontend/entrypoints/turbo-vue.ts +++ b/app/frontend/entrypoints/turbo-vue.ts @@ -26,7 +26,6 @@ const mountApp = async (e: Event) => { if (component !== undefined) { component[1]() .then((c: Component) => { - console.debug(c); props = rootContainer.dataset.props; app = createApp(c, props ? JSON.parse(props) : undefined); components.push(app); diff --git a/app/frontend/entrypoints/views/users/Index.vue b/app/frontend/entrypoints/views/users/Index.vue new file mode 100644 index 0000000..86cced7 --- /dev/null +++ b/app/frontend/entrypoints/views/users/Index.vue @@ -0,0 +1,42 @@ + + + diff --git a/app/frontend/entrypoints/views/users/index.ts b/app/frontend/entrypoints/views/users/index.ts new file mode 100644 index 0000000..ee7316e --- /dev/null +++ b/app/frontend/entrypoints/views/users/index.ts @@ -0,0 +1,4 @@ +import { mountComponentOnTurboLoad } from "@/helpers/mount-component-on-turbo-load"; +import App from "@/entrypoints/views/users/index/App.vue"; + +mountComponentOnTurboLoad(App, "#root-view"); diff --git a/app/frontend/helpers/routes.ts b/app/frontend/helpers/routes.ts index 0a97162..0eb6502 100644 --- a/app/frontend/helpers/routes.ts +++ b/app/frontend/helpers/routes.ts @@ -5,7 +5,11 @@ const Zap = async () => const PandasApp = async () => (await import("@/entrypoints/views/pandas/index/App.vue")).default; +const UsersIndex = async () => + (await import("@/entrypoints/views/users/Index.vue")).default; + const routes = { + "/users": [["#vue-root", UsersIndex]], "/": [["#root-view", RootApp]], "/pandas": [ ["#pandas-view", PandasApp], diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb new file mode 100644 index 0000000..2310a24 --- /dev/null +++ b/app/helpers/users_helper.rb @@ -0,0 +1,2 @@ +module UsersHelper +end diff --git a/app/jobs/import_contacts_job.rb b/app/jobs/import_contacts_job.rb new file mode 100644 index 0000000..cd78d05 --- /dev/null +++ b/app/jobs/import_contacts_job.rb @@ -0,0 +1,12 @@ +class ImportContactsJob < ApplicationJob + queue_as :default + + def perform(*_args) + 100.times do |i| + ActionCable.server.broadcast 'import_users_progress_channel', { progress: i + 1 } + sleep 0.1 + end + + Rails.cache.delete('users/import_progress') + end +end diff --git a/app/views/shared/_header.html.erb b/app/views/shared/_header.html.erb index bad8f57..637863a 100644 --- a/app/views/shared/_header.html.erb +++ b/app/views/shared/_header.html.erb @@ -2,6 +2,15 @@ diff --git a/app/views/users/index.html.erb b/app/views/users/index.html.erb new file mode 100644 index 0000000..0546a40 --- /dev/null +++ b/app/views/users/index.html.erb @@ -0,0 +1,3 @@ +
+
+
diff --git a/config/routes.rb b/config/routes.rb index 482330a..22d7846 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,8 @@ require 'sidekiq/web' Rails.application.routes.draw do + get '/users', to: 'users#index' + devise_for :users get '/pandas', to: 'pandas#index' diff --git a/package.json b/package.json index 6737379..1bc249b 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "@rushstack/eslint-patch": "^1.1.0", "@types/jsdom": "^21.1.1", "@types/node": "^18.13.0", + "@types/rails__actioncable": "^6.1.6", "@vitejs/plugin-vue": "^3.0.1", "@vue/eslint-config-prettier": "^7.1.0", "@vue/eslint-config-typescript": "^11.0.3", diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb new file mode 100644 index 0000000..b2e3444 --- /dev/null +++ b/spec/helpers/users_helper_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +# Specs in this file have access to a helper object that includes +# the UsersHelper. For example: +# +# describe UsersHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +RSpec.describe UsersHelper, type: :helper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/jobs/import_contacts_job_spec.rb b/spec/jobs/import_contacts_job_spec.rb new file mode 100644 index 0000000..d82a602 --- /dev/null +++ b/spec/jobs/import_contacts_job_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe ImportContactsJob, type: :job do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/requests/users_spec.rb b/spec/requests/users_spec.rb new file mode 100644 index 0000000..2202557 --- /dev/null +++ b/spec/requests/users_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' + +RSpec.describe "Users", type: :request do + describe "GET /index" do + pending "add some examples (or delete) #{__FILE__}" + end +end diff --git a/yarn.lock b/yarn.lock index e84af7b..dd44322 100644 --- a/yarn.lock +++ b/yarn.lock @@ -317,6 +317,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.11.tgz#b3b790f09cb1696cffcec605de025b088fa4225f" integrity sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q== +"@types/rails__actioncable@^6.1.6": + version "6.1.6" + resolved "https://registry.yarnpkg.com/@types/rails__actioncable/-/rails__actioncable-6.1.6.tgz#c5427f7ba5a41e41e1237bc943c81d7f44b46167" + integrity sha512-/whIczf4uhpac/X19vMRTHOF4reN92I6C4uOmTTtbT1xdesjrStLNQq9v+jexHehc8RENUHk4sXVzwm5PBy2fA== + "@types/semver@^7.3.12": version "7.3.13" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" From 556c4b45b63b6d5c917e527df7ba60a726cabc95 Mon Sep 17 00:00:00 2001 From: Joe Krump Date: Thu, 13 Jul 2023 11:45:36 -0400 Subject: [PATCH 08/19] Clean up unnecessary bits and show progress bar --- app/controllers/users_controller.rb | 7 +++--- .../entrypoints/views/users/Index.vue | 22 ++++++++++++------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index f18fa4b..4ae0e2f 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -2,10 +2,11 @@ class UsersController < ApplicationController before_action :authenticate_user! def index - Rails.cache.fetch('users/import_progress', expires_in: 2.minutes) do + if Rails.cache.read('users/import_progress') != "in_progress" + Rails.logger.info "************************Importing contacts...*******************" ImportContactsJob.perform_later - - true end + + Rails.cache.write('users/import_progress', "in_progress", expires_in: 1.minute) end end diff --git a/app/frontend/entrypoints/views/users/Index.vue b/app/frontend/entrypoints/views/users/Index.vue index 86cced7..1746b86 100644 --- a/app/frontend/entrypoints/views/users/Index.vue +++ b/app/frontend/entrypoints/views/users/Index.vue @@ -3,13 +3,13 @@ import { onBeforeUnmount, onMounted, ref } from "vue"; import { type Subscription } from "@rails/actioncable"; import consumer from "@/channels/consumer.ts"; -let progress = ref(0); +const progress = ref(null); let subscription: Subscription | null = null; const createImportChannelSubscription = () => { return consumer.subscriptions.create("ImportUsersProgressChannel", { connected() { - this.perform("get_progress"); + console.log("connected"); }, disconnected() { console.log("disconnected"); @@ -22,14 +22,10 @@ const createImportChannelSubscription = () => { onMounted(() => { subscription = createImportChannelSubscription(); - console.debug(subscription); }); onBeforeUnmount(() => { - console.debug("onBeforeUnmount"); - if (subscription) { - subscription.unsubscribe(); - } + subscription?.unsubscribe(); }); @@ -37,6 +33,16 @@ onBeforeUnmount(() => {

users/index

-

Progress: {{ progress }}%

+
+

Import in progress

+
+
+ {{ progress }}% +
+
+
From b91d62544c6df444fc4aea38d3eb0fa24b111af3 Mon Sep 17 00:00:00 2001 From: Joe Krump Date: Thu, 13 Jul 2023 19:57:26 -0400 Subject: [PATCH 09/19] Remove expires in --- app/controllers/users_controller.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 4ae0e2f..dd9c8ad 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -3,10 +3,9 @@ class UsersController < ApplicationController def index if Rails.cache.read('users/import_progress') != "in_progress" - Rails.logger.info "************************Importing contacts...*******************" ImportContactsJob.perform_later end - Rails.cache.write('users/import_progress', "in_progress", expires_in: 1.minute) + Rails.cache.write('users/import_progress', "in_progress") end end From 1a6a73782f030121c538df4607ac5f649c7c820d Mon Sep 17 00:00:00 2001 From: Joe Krump Date: Thu, 13 Jul 2023 21:21:58 -0400 Subject: [PATCH 10/19] Set up the basics for importing users from a .csv file --- app/controllers/users_controller.rb | 18 ++++-- .../entrypoints/views/users/Index.vue | 60 +++++++++++++++---- app/services/users/csv_upload_parser.rb | 60 +++++++++++++++++++ app/views/layouts/application.html.erb | 2 + app/views/users/_progress_bar.html.erb | 13 ++++ app/views/users/csv_import_modal.html.erb | 31 ++++++++++ .../users/import_from_csv.turbo_stream.erb | 24 ++++++++ app/views/users/index.html.erb | 2 +- config/application.rb | 3 +- config/routes.rb | 2 + tailwind.config.ts | 2 +- users.csv | 17 ++++++ 12 files changed, 217 insertions(+), 17 deletions(-) create mode 100644 app/services/users/csv_upload_parser.rb create mode 100644 app/views/users/_progress_bar.html.erb create mode 100644 app/views/users/csv_import_modal.html.erb create mode 100644 app/views/users/import_from_csv.turbo_stream.erb create mode 100644 users.csv diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index dd9c8ad..4cbe82c 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -2,10 +2,20 @@ class UsersController < ApplicationController before_action :authenticate_user! def index - if Rails.cache.read('users/import_progress') != "in_progress" - ImportContactsJob.perform_later - end + # if Rails.cache.read('users/import_progress') != "in_progress" + # ImportContactsJob.perform_later + # end - Rails.cache.write('users/import_progress', "in_progress") + # Rails.cache.write('users/import_progress', "in_progress") + # + @data = { + users: User.all + } + end + + def csv_import_modal; end + + def import_from_csv + @import_result = ::Users::CsvUploadParser.new(params[:users_csv].read).import_users_from_csv end end diff --git a/app/frontend/entrypoints/views/users/Index.vue b/app/frontend/entrypoints/views/users/Index.vue index 1746b86..1fadddd 100644 --- a/app/frontend/entrypoints/views/users/Index.vue +++ b/app/frontend/entrypoints/views/users/Index.vue @@ -3,8 +3,21 @@ import { onBeforeUnmount, onMounted, ref } from "vue"; import { type Subscription } from "@rails/actioncable"; import consumer from "@/channels/consumer.ts"; +type User = { + id: number; + email: string; +}; + +defineProps({ + users: { + type: Array as () => User[], + required: true, + }, +}); + const progress = ref(null); let subscription: Subscription | null = null; +let importModal: HTMLDialogElement | null; const createImportChannelSubscription = () => { return consumer.subscriptions.create("ImportUsersProgressChannel", { @@ -20,12 +33,33 @@ const createImportChannelSubscription = () => { }); }; +const closeModal = () => { + importModal?.close(); +}; + +const openModal = (e: Event) => { + if (typeof e.target === "object" && e.target instanceof Element) { + importModal = e.target.querySelector("dialog"); + importModal?.showModal(); + + importModal + ?.querySelector("button[data-action='modal#close']") + ?.addEventListener("click", closeModal); + } +}; + onMounted(() => { subscription = createImportChannelSubscription(); + + document.addEventListener("turbo:frame-render", openModal); }); onBeforeUnmount(() => { subscription?.unsubscribe(); + document.removeEventListener("turbo:frame-render", openModal); + importModal + ?.querySelector("button[data-action='modal#close']") + ?.removeEventListener("click", closeModal); }); @@ -33,16 +67,22 @@ onBeforeUnmount(() => {

users/index

-
-

Import in progress

-
-
- {{ progress }}% -
-
+ + Import Users from CSV + + +
+
    +
  1. + {{ user.email }} +
  2. +
+
No users found
diff --git a/app/services/users/csv_upload_parser.rb b/app/services/users/csv_upload_parser.rb new file mode 100644 index 0000000..6011571 --- /dev/null +++ b/app/services/users/csv_upload_parser.rb @@ -0,0 +1,60 @@ +require 'csv' + +module Users + + class CsvUploadParser + def initialize(csv_string) + @csv_string = csv_string + end + + def import_users_from_csv + errors = {} + successfully_imported_count = 0 + row_index = 1 + header_rows_count = 1 + + total = @csv_string.lines.count - header_rows_count + + CSV.parse(@csv_string, headers: true) do |row| + user = User.create( + email: row["Email"], + password: row["Password"], + password_confirmation: row["Password"] + ) + + if user.valid? + user.save + successfully_imported_count += 1 + else + errors["Row #{row_index}"] = user.errors.objects.first.full_message + end + + row_index += 1 + processed_count = row_index - header_rows_count + + # ActionCable.server.broadcast 'import_users_progress_channel', { pct_complete: percentage_complete(processed_count, total) } + Turbo::StreamsChannel.broadcast_replace_later_to( + :users, + target: "users_progress_bar", + partial: "users/progress_bar", + locals: { + processed_count: processed_count, + total: total, + percentage_complete: percentage_complete(processed_count, total) + } + ) + end + + { + errors: errors, + successfully_imported_count: successfully_imported_count + } + end + + private + + def percentage_complete(row_index, total) + ((row_index.to_f / total.to_f) * 100).round(2) + end + end +end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index aeb93f3..cda41b3 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -5,6 +5,8 @@ <%= render "shared/header" %> + +
<%= content_for?(:content) ? yield(:content) : yield %>
diff --git a/app/views/users/_progress_bar.html.erb b/app/views/users/_progress_bar.html.erb new file mode 100644 index 0000000..55d13d2 --- /dev/null +++ b/app/views/users/_progress_bar.html.erb @@ -0,0 +1,13 @@ +
+
+
+ <%= "#{processed_count}/#{total}" %> rows processed +
+
+
+
+
diff --git a/app/views/users/csv_import_modal.html.erb b/app/views/users/csv_import_modal.html.erb new file mode 100644 index 0000000..de579ab --- /dev/null +++ b/app/views/users/csv_import_modal.html.erb @@ -0,0 +1,31 @@ +<%= turbo_frame_tag "modal" do %> + +
+

Import Users

+
+ <%= form_with url: users_import_from_csv_path, multipart: true, method: "post" do |form| %> +
+ +
+

Upload a CSV here to import users

+ <%= file_field_tag :users_csv, accept: ".csv", class: "text-purple-500 hover:text-purple-600" %> +
+ <%= turbo_stream_from 'users' %> +
+ +
+
+ + +
+ <% end %> +
+<% end %> diff --git a/app/views/users/import_from_csv.turbo_stream.erb b/app/views/users/import_from_csv.turbo_stream.erb new file mode 100644 index 0000000..0d2db0c --- /dev/null +++ b/app/views/users/import_from_csv.turbo_stream.erb @@ -0,0 +1,24 @@ +<% if @import_result[:successfully_imported_count] > 0 %> + + + +<% end %> + +<% if @import_result[:errors] %> + + + +<% end %> diff --git a/app/views/users/index.html.erb b/app/views/users/index.html.erb index 0546a40..cd9bea6 100644 --- a/app/views/users/index.html.erb +++ b/app/views/users/index.html.erb @@ -1,3 +1,3 @@
-
+
>
diff --git a/config/application.rb b/config/application.rb index 3aa06c8..ac59cc6 100644 --- a/config/application.rb +++ b/config/application.rb @@ -19,7 +19,8 @@ class Application < Rails::Application # in config/environments, which are processed later. # # config.time_zone = "Central Time (US & Canada)" - # config.eager_load_paths << Rails.root.join("extras") + config.eager_load_paths << Rails.root.join("services") + config.generators do |g| g.orm :active_record g.test_framework :rspec diff --git a/config/routes.rb b/config/routes.rb index 22d7846..7c12bec 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,6 +2,8 @@ Rails.application.routes.draw do get '/users', to: 'users#index' + get '/users/csv_import_modal', to: 'users#csv_import_modal' + post '/users/import_from_csv', to: 'users#import_from_csv' devise_for :users get '/pandas', to: 'pandas#index' diff --git a/tailwind.config.ts b/tailwind.config.ts index 60ce086..be93787 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -2,7 +2,7 @@ import type { Config } from "tailwindcss"; export default { content: [ - "./app/views/**/*.html.erb", + "./app/views/**/*.erb", "./app/frontend/**/*.vue", "./lib/**/*.html.erb.tt", ], diff --git a/users.csv b/users.csv new file mode 100644 index 0000000..bb3ef0a --- /dev/null +++ b/users.csv @@ -0,0 +1,17 @@ +Email,Password +hello1@test.com,testtesttest +hello2@test.com,testtesttest +hello3@test.com,testtesttest +hello4@test.com,testtesttest +hello5@test.com,testtesttest +hello6@test.com,testtesttest +hello7@test.com,testtesttest +hello8@test.com, +hello9@test.com,testtesttest +hello10@test.com,testtesttest +hello11@test.com,testtesttest +hello12@test.com,testtesttest +hello20@test.com,testtesttest +hello21@test.com,testtesttest +hello22@test.com,testtesttest +hello27@test.com,testtesttest From 18a0085bbe4ce811aa720f496a8e8c25162dd4db Mon Sep 17 00:00:00 2001 From: Joe Krump Date: Thu, 13 Jul 2023 21:46:18 -0400 Subject: [PATCH 11/19] Add show view for users --- app/controllers/users_controller.rb | 4 ++ app/frontend/entrypoints/turbo-vue.ts | 16 +++++- .../entrypoints/views/users/Index.vue | 57 +++++++------------ app/views/layouts/application.html.erb | 2 +- app/views/shared/_header.html.erb | 4 +- app/views/users/csv_import_modal.html.erb | 2 +- app/views/users/index.html.erb | 2 +- app/views/users/show.html.erb | 3 + config/routes.rb | 13 +++-- 9 files changed, 57 insertions(+), 46 deletions(-) create mode 100644 app/views/users/show.html.erb diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 4cbe82c..9994822 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -18,4 +18,8 @@ def csv_import_modal; end def import_from_csv @import_result = ::Users::CsvUploadParser.new(params[:users_csv].read).import_users_from_csv end + + def show + @user = User.find(params[:id]) + end end diff --git a/app/frontend/entrypoints/turbo-vue.ts b/app/frontend/entrypoints/turbo-vue.ts index ed7f910..e14d1e5 100644 --- a/app/frontend/entrypoints/turbo-vue.ts +++ b/app/frontend/entrypoints/turbo-vue.ts @@ -3,6 +3,12 @@ import { createApp, type App, type Component } from "vue"; let components: App[] = []; +const onTurboLoad = async (e: Event) => { + await mountApp(e); + + document.addEventListener("turbo:frame-render", openModal); +}; + const mountApp = async (e: Event) => { const vueComponentsForPage = getVueComponents(window.location.pathname); let app: App; @@ -69,7 +75,7 @@ const mountApp = async (e: Event) => { } }; -document.addEventListener("turbo:load", mountApp); +document.addEventListener("turbo:load", onTurboLoad); document.addEventListener("turbo:visit", () => { if (components.length > 0) { @@ -79,8 +85,16 @@ document.addEventListener("turbo:visit", () => { components = []; } + + document.removeEventListener("turbo:frame-render", openModal); }); +const openModal = (e: Event) => { + if (typeof e.target === "object" && e.target instanceof Element) { + e.target.querySelector("dialog")?.showModal(); + } +}; + function clearInitialPropsFromDOM(element: HTMLElement) { element.removeAttribute("data-props"); } diff --git a/app/frontend/entrypoints/views/users/Index.vue b/app/frontend/entrypoints/views/users/Index.vue index 1fadddd..46c7798 100644 --- a/app/frontend/entrypoints/views/users/Index.vue +++ b/app/frontend/entrypoints/views/users/Index.vue @@ -17,7 +17,6 @@ defineProps({ const progress = ref(null); let subscription: Subscription | null = null; -let importModal: HTMLDialogElement | null; const createImportChannelSubscription = () => { return consumer.subscriptions.create("ImportUsersProgressChannel", { @@ -33,56 +32,42 @@ const createImportChannelSubscription = () => { }); }; -const closeModal = () => { - importModal?.close(); -}; - -const openModal = (e: Event) => { - if (typeof e.target === "object" && e.target instanceof Element) { - importModal = e.target.querySelector("dialog"); - importModal?.showModal(); - - importModal - ?.querySelector("button[data-action='modal#close']") - ?.addEventListener("click", closeModal); - } -}; - onMounted(() => { subscription = createImportChannelSubscription(); - - document.addEventListener("turbo:frame-render", openModal); }); onBeforeUnmount(() => { subscription?.unsubscribe(); - document.removeEventListener("turbo:frame-render", openModal); - importModal - ?.querySelector("button[data-action='modal#close']") - ?.removeEventListener("click", closeModal); }); diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index cda41b3..526d723 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -7,7 +7,7 @@ -
+
<%= content_for?(:content) ? yield(:content) : yield %>
diff --git a/app/views/shared/_header.html.erb b/app/views/shared/_header.html.erb index 637863a..22b1968 100644 --- a/app/views/shared/_header.html.erb +++ b/app/views/shared/_header.html.erb @@ -2,12 +2,12 @@