I have two Rails environments. One development environment running Postgres and Rails 5.0.6 and an almost identical environment on Heroku.
I have an Administrator class, which generates a username for an Administrator on a before_save callback based on the user's forename and surname fields.
class Administrator < ApplicationRecord
validates :username, uniqueness: true
validates :forename, presence: true
validates :surname, presence: true
before_save :generate_username
def generate_username
return if username.present?
proposed = "#{forename}#{surname}".downcase
existing_count = Administrator.where("username ILIKE ?", "#{proposed}%").size
self.username = existing_count.zero? ? proposed : "#{proposed}#{existing_count}"
end
end
After the user is validated, a username is generated in the form FORENAMESURNAMEX where X is an incrementing number (or nothing).
Here's the commands I run in the Rails console on my development machine.
irb(main):012:0> Administrator.create(email: 'edward#test.net', forename: 'Edward', surname: 'Scissorhands')
D, [2017-10-13T10:00:18.985765 #280] DEBUG -- : (0.2ms) BEGIN
D, [2017-10-13T10:00:18.987554 #280] DEBUG -- : Administrator Exists (0.5ms) SELECT 1 AS one FROM "administrators" WHERE "administrators"."email" = $1 LIMIT $2 [["email", "edward#test.net"], ["LIMIT", 1]]
D, [2017-10-13T10:00:18.988923 #280] DEBUG -- : Administrator Exists (0.4ms) SELECT 1 AS one FROM "administrators" WHERE "administrators"."username" IS NULL LIMIT $1 [["LIMIT", 1]]
D, [2017-10-13T10:00:18.990155 #280] DEBUG -- : (0.4ms) SELECT COUNT(*) FROM "administrators" WHERE (username ILIKE 'edwardscissorhands%')
D, [2017-10-13T10:00:18.992000 #280] DEBUG -- : SQL (0.5ms) INSERT INTO "administrators" ("email", "created_at", "updated_at", "username", "forename", "surname") VALUES ($1, $2, $3, $4, $5, $6) RETURNING "id" [["email", "edward#test.net"], ["created_at", "2017-10-13 10:00:18.990421"], ["updated_at", "2017-10-13 10:00:18.990421"], ["username", "edwardscissorhands"], ["forename", "Edward"], ["surname", "Scissorhands"]]
D, [2017-10-13T10:00:18.995845 #280] DEBUG -- : (1.8ms) COMMIT
=> #<Administrator id: 10, email: "edward#test.net", created_at: "2017-10-13 10:00:18", updated_at: "2017-10-13 10:00:18", role: nil, otp_public_key: nil, username: "edwardscissorhands", forename: "Edward", surname: "Scissorhands">
As you can see, the callback is executed and the user's username is generated and persisted to the database as expected.
However, when I run the same code on a our test environment running on Heroku (and Heroku Postgres), this is what happens:
irb(main):005:0> Administrator.create!(email: 'edward#test.net', forename: 'Edward', surname: 'Scissorhands')
(1.9ms) BEGIN
Administrator Exists (1.1ms) SELECT 1 AS one FROM "administrators" WHERE "administrators"."email" = $1 LIMIT $2 [["email", "edward#test.net"], ["LIMIT", 1]]
Administrator Exists (0.9ms) SELECT 1 AS one FROM "administrators" WHERE "administrators"."username" IS NULL LIMIT $1 [["LIMIT", 1]]
(0.9ms) ROLLBACK
ActiveRecord::RecordInvalid: Validation failed: Username has already been taken
(I'm using create! here instead of create to show the validation errors that do not occur in development.)
I don't see why the behaviour should differ between environments. Both are running identical versions of Rails (5.0.6) and are running identical codebases.
before_save is called after validation, hence the error.
Try before_validation instead.
For reference here's the order callbacks are called when creating an object:
before_validation
after_validation
before_save
around_save
before_create
around_create
after_create
after_save
after_commit/after_rollback
The logic in your code is flawed. This is a legitimate bug; you need to redesign how username generation words.
For example, suppose there is one user in your system called: edwardscissorhands1. There is no edwardscissorhands, and no edwardscissorhands2/3/4 etc.
The line: Administrator.where("username ILIKE ?", "edwardscissorhands%").size returns 1, and then your logic tries to create a new user that already exists.
... I cannot say for sure what has happened on your production server without seeing the actual data, but I bet it's something like this. It could be slightly more convoluted, e.g. the users: tom, tom3 and tomlord exist; therefore your logic tries to create a second tom3 user.
For example, this might have happened if you generated some edwardscissorhards users, then deleted one or more of them.
As an example, here's one way you could redesign the logic:
def generate_username
return if username.present?
proposed = "#{forename}#{surname}".downcase
return proposed unless Administrator.exists?("username ILIKE ?", proposed)
counter = 1
while(Administrator.exists?("username ILIKE ?", "#{proposed}#{counter}"))
counter += 1
end
"#{proposed}#{counter}"
end
This could probably be improved performance-wise, although the multiple database queries here are unlikely to be a major issue in the real application (assuming you don't get lots of administrators with the same name!).
Related
I have setup a Rails 7 project running in Docker. Using the whenever gem (using cron) I tried to execute either
a rails runner task
a rake task
Both shall do the same thing: call a class method WebpageChangeCheck.check_all which itself calls a method of a model. Which then creates an Active Job job. But both fail in the final step to create the job:
app/cron_jobs/webpage_change_check.rb:
class WebpageChangeCheck
def self.check_all
Webpage.all.each do |page|
if page.checking_active
page.check_for_change
end
end
end
end
app/models/webpage.rb:
def check_for_change
self.update(counter: self.counter += 1)
UpdateOffersHashJob.perform_later(self)
end
update_offers_hash_job.rb:
class UpdateOffersHashJob < ApplicationJob
queue_as :default
require 'nokogiri'
require 'open-uri'
require 'net/http'
after_perform do |job|
compare_hashes(job.arguments.first)
end
def perform(page)
page.update(offers_hash_old: page.offers_hash_new)
all_offers = ""
doc = Nokogiri::HTML(URI.open(page.url))
doc.css(page.selector).each do |offer|
all_offers += offer.to_s
end
page.update(offers_hash_new: all_offers.delete(" \t\r\n\ "))
end
private
def compare_hashes(page)
...
end
end
What works:
calling the same class method from rails console creates the Active Job as expected. I get the following output:
irb(main):002:0> WebpageChangeCheck.check_all
Webpage Load (1.2ms) SELECT "webpages".* FROM "webpages"
Webpage Update All (3.9ms) UPDATE "webpages" SET "counter" = COALESCE("counter", 0) + $1 WHERE "webpages"."id" = $2 [["counter", 1], ["id", 1]]
Enqueued UpdateOffersHashJob (Job ID: 707e164d-b8b9-407b-aa35-4b23c37b4f07) to Async(default) with arguments: #<GlobalID:0x00007f2e0b7c4878 #uri=#<URI::GID gid://my_rails_app/Webpage/1>>
=>
[#<Webpage:0x00007f2e0b7bded8
id: 1,
title: "example.com",
url: "https://www.example.com",
user_id: 1,
created_at: Wed, 18 Jan 2023 14:22:51.904097000 CET +01:00,
updated_at: Fri, 20 Jan 2023 00:12:02.749748000 CET +01:00,
interval: 1,
checking_active: true,
selector: ".headline_content",
counter: 386>]
Webpage Load (0.6ms) SELECT "webpages".* FROM "webpages" WHERE "webpages"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
Performing UpdateOffersHashJob (Job ID: 707e164d-b8b9-407b-aa35-4b23c37b4f07) from Async(default) enqueued at 2023-01-20T07:26:02Z with arguments: #<GlobalID:0x00007f2e092e1088 #uri=#<URI::GID gid://my_rails_app/Webpage/1>>
TRANSACTION (0.2ms) BEGIN
User Load (0.8ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
irb(main):003:0> TRANSACTION (1.0ms) COMMIT
TRANSACTION (0.4ms) BEGIN
Webpage Load (0.6ms) SELECT "webpages".* FROM "webpages" WHERE "webpages"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
Sim Create (1.4ms) INSERT INTO "sims" ("to", "time", "api_response", "success", "webpage_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING "id" [["to", 49123456789], ["time", nil], ["api_response", nil], ["success", nil], ["webpage_id", 1], ["created_at", "2023-01-20 07:26:03.108189"], ["updated_at", "2023-01-20 07:26:03.108189"]]
TRANSACTION (3.5ms) COMMIT
TRANSACTION (0.4ms) BEGIN
Sim Update (1.0ms) UPDATE "sims" SET "time" = $1, "api_response" = $2, "success" = $3, "updated_at" = $4 WHERE "sims"."id" = $5 [["time", "Fri, 20.01.23 - 08h26 03s"], ["api_response", "100\nVerbucht: 0\nPreis: 0.075\nGuthaben: 0.35\nText: ALERT\nSMS-Typ: direct\nFlash SMS: false\nEncoding: gsm\nGSM0338: true\nDebug: true"], ["success", true], ["updated_at", "2023-01-20 07:26:03.295980"], ["id", 22]]
TRANSACTION (3.8ms) COMMIT
Performed UpdateOffersHashJob (Job ID: 707e164d-b8b9-407b-aa35-4b23c37b4f07) from Async(default) in 585.89ms
What I see from bash (from rake or rails runner) the Active Job gets created but it will not perform:
root#f77855c949a8:/opt/app# rake debug check_all_pages
Webpage Load (1.5ms) SELECT "webpages".* FROM "webpages"
↳ app/cron_jobs/webpage_change_check.rb:4:in `check_all'
Webpage Update All (2.9ms) UPDATE "webpages" SET "counter" = COALESCE("counter", 0) + $1 WHERE "webpages"."id" = $2 [["counter", 1], ["id", 1]]
↳ app/models/webpage.rb:9:in `check_for_change'
[ActiveJob] Enqueued UpdateOffersHashJob (Job ID: f4495cb8-868f-4ed5-9f03-7f4407b5efa4) to Async(default) with arguments: #<GlobalID:0x00007fd55771ece0 #uri=#<URI::GID gid://my_rails_app/Webpage/1>>
root#f77855c949a8:/opt/app#
here you also go with the rake task:
my_rails_app/lib/tasks/checker_task.rake:
desc "checks all Webpages for changes. Called from cronjob."
task check_all_pages: :environment do
WebpageChangeCheck.check_all
end
Conclusion:
it seems like the environment loaded properly, including environment variables. I have access to my classes and models.
nevertheless there seems to be a difference which prevents the Job from being performed.
part of the problem might be there is not enough logging to debug it, so this could be improved as well!
Any ideas?
Thanks a lot!
You need to process your job queue in another process, e.g.
rake jobs:work
So, thanks to the help in the comments I realized I overlooked to install the delayed_job backend/processing queue incl. db table for Active Job cause I thought this comes with it. It is needed to enqueue and work off the jobs. Not suitable for large amounts of jobs but sufficient for my purpose.
Here is a complete guide:
https://axiomq.com/blog/deal-with-long-running-rails-tasks-with-delayed-job/
You can also check these docs:
https://guides.rubyonrails.org/active_job_basics.html
https://github.com/collectiveidea/delayed_job#active-job
Short version:
I added the gem 'delayed_job_active_record'
Add and execute
bundle install
config/application.rb:
config.active_job.queue_adapter = :delayed_job
rails generate delayed_job:active_record
rails db:migrate
rails jobs:work
As mentioned, this is a seperate process that has to be run.
Question remains why the job performs when being run through rails console. Maybe it won't queue then but execute rightaway ..
On my test I'm seeing that after_update is been called after calling FactoryBot.create(:object). Is it normal? As far as I know, it should be called only when a record gets updated, no?
I can see someone reporting this as a bug, with a good explanation here.
To take the essentials from this, if your factory is adding an association (this is an assumption at this stage - if you could add a little more to your question, that'd be great), the code runs as follows:
Example factory
FactoryGirl.create(
:user,
:account => FactoryGirl.create(:account)
)
How this is invoked:
account = Account.new
account.save! # Since this is Ruby, it'll evaluate this line as part of the hash first, before creating the user
user = User.new
user.account = account
user.save! # The hash has been evaluated and we're assigning the account created from the hash
So, if you have an association in there, the account, in this case, would be created, then updated as the association is saved.
To setup your factory to overcome this, you can use the following:
factory :user do
factory :user_with_account do
after_create do |user|
FactoryGirl.create(:account, :user => user)
end
end
end
factory :account do
user
end
How does that apply to your setup? Have a shot and see if it provides a solution - let me know how you get on :)
after_update will only be called when the object is updated, however if your factory has associations or after_create actions, these will often cause the model to be updated, causing after_update to be triggered.
An example, using ActiveRecord 5:
class Client < ApplicationRecord
after_create :ping
after_update :pong
def ping
logger.info("---> after_create HOOK CALLED")
end
def pong
logger.info("---> after_update HOOK CALLED")
end
end
Creating and updating the object act as expected:
c = Client.create!(name: "test")
# (0.4ms) BEGIN
# Client Create (1.4ms) INSERT INTO "clients" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["name", "test"], ["created_at", "2018-05-24 17:06:24.076085"], ["updated_at", "2018-05-24 17:06:24.076085"]]
# ---> after_create HOOK CALLED
# (4.0ms) COMMIT
c.update! name: "test2"
# (0.8ms) BEGIN
# Client Update (2.3ms) UPDATE "clients" SET "name" = $1, "updated_at" = $2 WHERE "clients"."id" = $3 [["name", "test2"], ["updated_at", "2018-05-24 17:06:36.525448"], ["id", "a3d49153-2f25-48c3-8319-61c2fb6ea173"]]
# ---> after_update HOOK CALLED
# (0.9ms) COMMIT
]
And FactoryBot behaves the same:
FactoryBot.create(:client)
# (1.2ms) BEGIN
# Client Create (0.9ms) INSERT INTO "clients" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["name", "Montana tigers"], ["created_at", "2018-05-24 17:11:57.138995"], ["updated_at", "2018-05-24 17:11:57.138995"]]
# ---> after_create HOOK CALLED
# (1.1ms) COMMIT
Here I'm testing the changes in current_user.messages.count after the current user sends a valid message. Here's my code:
spec
scenario 'adds to their messages', js: true do
expect { find('#message_content').send_keys(:enter) }.to \
change(current_user.messages, :count).by(1)
end
test.log
# ...
ConversationChannel is transmitting the subscription confirmation
ConversationChannel is streaming from conversation_channel_1
(0.6ms) SELECT COUNT(*) FROM "messages" WHERE "messages"."user_id" = $1 [["user_id", 1]]
ConversationChannel#send_message({"content"=>"foobar\n", "conversation_id"=>"1"})
(0.3ms) BEGIN
(0.9ms) SELECT COUNT(*) FROM "messages" WHERE "messages"."user_id" = $1 [["user_id", 1]]
Conversation Load (1.6ms) SELECT "conversations".* FROM "conversations" WHERE "conversations"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
(0.7ms) SELECT "users"."id" FROM "users" INNER JOIN "user_conversations" ON "users"."id" = "user_conversations"."user_id" WHERE "user_conversations"."conversation_id" = $1 [["conversation_id", 1]]
SQL (1.0ms) INSERT INTO "messages" ("content", "user_id", "conversation_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["content", "foobar\n"], ["user_id", 1], ["conversation_id", 1], ["created_at", "2018-01-29 11:27:13.095277"], ["updated_at", "2018-01-29 11:27:13.095277"]]
Finished "/cable/" [WebSocket] for 127.0.0.1 at 2018-01-29 19:27:13 +0800
ConversationChannel stopped streaming from conversation_channel_1
(0.2ms) BEGIN
(58.8ms) COMMIT
(16.7ms) ALTER TABLE "schema_migrations" DISABLE TRIGGER ALL;ALTER TABLE "ar_internal_metadata" DISABLE TRIGGER ALL;ALTER TABLE "conversations" DISABLE TRIGGER ALL;ALTER TABLE "messages" DISABLE TRIGGER ALL;ALTER TABLE "user_conversations" DISABLE TRIGGER ALL;ALTER TABLE "users" DISABLE TRIGGER ALL
Rendered messages/_message.html.erb (0.6ms)
[ActionCable] Broadcasting to conversation_channel_1: {:message=>"<p>User 1: foobar\n</p>\n"}
# ...
The spec fails expected #count to have changed by 1, but was changed by 0 even though in the log shows INSERT INTO actually happen.
This doesn't work because you're not waiting long enough for the message addition to actually occur. send_keys returns as soon as the browser has been sent the key event, but knows nothing at all about any request/action triggered by that key press in the browser. This is why direct DB access tests are generally a bad idea in feature/system tests (which should generally just test user visible changes/interactions) and make more sense as request or controller.
That being said you could fix this by just sleeping after sending the key, but a better solution is to use one of the Capybara provided matchers (have waiting/retrying behavior) to synchronize the test.
scenario 'adds to their messages', js: true do
expect do
find('#message_content').send_keys(:enter) }
expect(page).to have_css(...) # check for whatever visible change on the page indicates the action triggered by send_keys has completed
end.to change { current_user.reload.messages.count }.by(1)
end
Note: This test is also very simple for a feature test. It's okay to have multiple expectations in a feature test since it's really meant to test a whole user interaction with a specific feature of your app. You might want to look at combining this test with other tests of the same part of your app.
Try to write :
change{current_user.messages, :count}.by(1)
with {}
I have one particular user in my rails app for which I can't log in anymore, the password is always invalid, even though I changed it manually. Here's what I do in rails console :
> me = User.find(10)
> me.password = '123456789'
> me.save
(0.3ms) BEGIN
User Exists (0.6ms) SELECT 1 AS one FROM "users" WHERE ("users"."email" = 'myemail#gmail.com' AND "users"."id" != 10) LIMIT 1
SQL (0.7ms) UPDATE "users" SET "encrypted_password" = $1, "updated_at" = $2 WHERE "users"."id" = $3 [["encrypted_password", "$2a$10$mrhWiOT3pu6YldtYRD/bC.wuqPthyfJhiqdGkYv14xCafVQNTodWG"], ["updated_at", "2016-08-08 10:43:34.715229"], ["id", 10]]
(31.3ms) COMMIT
=> true
> me.valid_password?('123456789')
=> nil
This is only with this particular user id 10. I do the exact same thing with any other user it works. What could be wrong ?
EDIT : I tried also with password confirmation but that's not the issue. As I said, the exact manipulation works fine with any user except this one of ID 10
EDIT 2 : I found the solution in this thread : Rails/Devise: valid_password? method returns nil
I think probably you need to set password_confirmation as well, Try below code.
> me = User.find(10)
> me.password = '123456789'
> me.password_confirmation = '123456789'
> me.save
The update function is working within my ruby console as shown below, but the updates aren't being reflected in the database for this particular user.
For all other users it works perfectly, and using update_all works, but also updates all other users. Any idea why?
ruby-1.9.2-p290 :002 > User.update(51, :introduction => "testupdate")
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 51]]
(0.3ms) UPDATE "users" SET "introduction" = 'testupdate', "updated_at" = '2011-09-13 18:53:25.896711' WHERE "users"."id" = 51
=> #<User id: 51, email: "example#email.com", introduction: "testupdate", created_at: "2011-09-13 18:45:18", updated_at: "2011-09-13 18:53:25">
FYI The user I'm trying to update is the only user created manually via log-in, the others are created as dummy accounts via seeds.rb
I accidentally had an after_save in the User model that was actually causing a validation failure. Thanks #meagar