I'm trying out http://rom-rb.org/ and can't figure out how to get a presence validation to pass in the presence of multiple source models. I would expect the following script to save a new event and organiser, but instead it says that event_name is not present.
What am I missing?
require 'bundler/inline'
gemfile do
source 'https://rubygems.org'
gem 'rom'
gem 'rom-sql'
gem 'rom-rails'
gem 'activemodel'
gem 'sqlite3'
gem 'activesupport'
end
require 'rom'
require 'rom-rails'
`rm -Rf /tmp/romtest.sqlite`
ROM.setup(:sql, 'sqlite:///tmp/romtest.sqlite')
class Events < ROM::Relation[:sql]
end
class Organisers < ROM::Relation[:sql]
end
class CreateEvent < ROM::Commands::Create[:sql]
relation :events
register_as :create
result :one
associates :organiser, key: [:organiser_id, :id]
end
class CreateOrganiser < ROM::Commands::Create[:sql]
relation :organisers
register_as :create
result :one
end
class CreateEventWithOrganiser < ROM::Model::Form
commands organisers: :create, events: :create
input do
attribute :email
attribute :event_name
end
validations do
validates :event_name, presence: true
end
def commit!
command = organisers.create.with(
email: email,
) >> events.create.with(
name: event_name,
)
command.transaction do
command.call
end
end
end
ROM.finalize
rom = ROM.env
gateway = rom.gateways.fetch(:default)
migration = gateway.migration do
change do
create_table :organisers do
primary_key :id
column :email, String, null: false
end
create_table :events do
primary_key :id
column :name, String, null: false
column :organiser_id, Integer, null: false
end
end
end
migration.apply(gateway.connection, :up)
f = CreateEventWithOrganiser.build(
email: 'test#example.com',
event_name: 'Test Event'
)
# Unexpectedly fails
f.save
puts f.errors.full_messages
# => "Event name can't be blank"
Here's an updated version of your script which works:
require 'rom'
require 'rom-rails'
`rm -Rf /tmp/romtest.sqlite`
ROM.setup(:sql, 'sqlite:///tmp/romtest.sqlite')
class Events < ROM::Relation[:sql]
end
class Organisers < ROM::Relation[:sql]
end
class CreateEvent < ROM::Commands::Create[:sql]
relation :events
register_as :create
result :one
associates :organiser, key: [:organiser_id, :id]
end
class CreateOrganiser < ROM::Commands::Create[:sql]
relation :organisers
register_as :create
result :one
end
class CreateEventWithOrganiser < ROM::Model::Form
inject_commands_for :organisers, :events
input do
attribute :email
attribute :event_name
end
validations do
validates :event_name, presence: true
end
def commit!
validate!
return if errors.any?
command = organisers.create.with(
email: email
) >> events.create.with(
name: event_name
)
command.transaction do
command.call
end
end
end
ROM.finalize
rom = ROM.env
gateway = rom.gateways.fetch(:default)
migration = gateway.migration do
change do
create_table :organisers do
primary_key :id
column :email, String, null: false
end
create_table :events do
primary_key :id
column :name, String, null: false
column :organiser_id, Integer, null: false
end
end
end
migration.apply(gateway.connection, :up)
f = CreateEventWithOrganiser.build(
email: 'test#example.com',
event_name: 'Test Event'
)
puts f.save.result.inspect
# #<ROM::Commands::Result::Success:0x007fa92b589ea0 #value={:id=>1, :name=>"Test Event", :organiser_id=>1}>
The reason why it didn't work with commands is because this method will generate command objects for your form and set provided validations for each command, which will only work correctly if you used a single command. Otherwise same validator is used for each command which doesn't make sense. When you use inject_commands_for it will grab your own commands where validators are not set so you are free to handle validations yourself.
I think we should stop setting validators on commands which would make your original sample work but notice that you need to call validate! yourself.
I hope this helps.
I also created a gist showing how to do the same without a form: https://gist.github.com/solnic/3b68342482cf1414f719
Related
I am getting the following error while inserting the data into database through rails console using ROR.
Error:
irb(main):004:0> book.comments << comment
(1.0ms) begin transaction
(0.0ms) rollback transaction
ActiveModel::MissingAttributeError: can't write unknown attribute `book_id`
from C:/RailsInstaller/Ruby2.1.0/lib/ruby/gems/2.1.0/gems/activerecord-4
.2.5.1/lib/active_record/attribute.rb:138:in `with_value_from_database'
from C:/RailsInstaller/Ruby2.1.0/lib/ruby/gems/2.1.0/gems/activerecord-4
.2.5.1/lib/active_record/attribute_set.rb:39:in `write_from_user'
from C:/RailsInstaller/Ruby2.1.0/lib/ruby/gems/2.1.0/gems/activerecord-4
.2.5.1/lib/active_record/attribute_methods/write.rb:74:in `write_attribute_with_
type_cast'
Here I am trying to link one table (comments) with another table (books) by inserting some data. My code flow is below:
irb(main):004:0>book=Book.find(1)
comment = Comment.new :text => "This is an comment", :author => "Adam"
book.comments << comment
.......create_comments.rb:
class CreateComments < ActiveRecord::Migration
def change
create_table :comments do |t|
t.text :text
t.string :author
t.timestamps null: false
end
end
end
book.rb:
class Book < ActiveRecord::Base
has_many :comments
end
comment.rb:
class Comment < ActiveRecord::Base
belongs_to :book
end
While I am executing the last line this error is coming.
You have not book_id column in your comments table.
Follow this below steps:
rails g migration AddBookToComments book:references
which will create a migration file as:
class AddBookToComments < ActiveRecord::Migration
def change
add_reference :comments, :book, index: true, foreign_key: true
end
end
run rake db:migrate
add book_id in strong paremeters in comments_controller.rb
Then try:
> book=Book.find(1)
> comment = book.comments.new(:text => "This is an comment", :author => "Adam")
> comment.save!
Here's the error I'm getting when I try to run my tests with RSpec:
C:/Ruby193/lib/ruby/gems/1.9.1/gems/activesupport-3.2.11/lib/active_support/infl
ector/methods.rb:230:in `block in constantize': uninitialized constant User (Nam
eError)
I'm trying to run FactoryGirl with RSpec but without Rails. Here are the files that take part in the testing:
user_spec.rb
require 'spec_helper'
module Bluereader
describe User do
describe 'login' do
user = FactoryGirl.build(:user)
end
describe 'logout' do
end
describe 'create_account' do
end
describe 'delete_account' do
end
end
end
spec/spec_helper
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..'))
$LOAD_PATH.unshift(File.dirname(__FILE__))
require 'rspec'
require 'lib/bluereader'
require 'factory_girl'
FactoryGirl.find_definitions
spec/factories.rb
require 'digest/sha1'
FactoryGirl.define do
sequence(:username) { |n| "user-#{n}" }
factory :user do
username
encrypted_password Digest::SHA1.hexdigest('password')
full_name 'John Doe'
logged_in_at Time.now
logged_out_at 0
end
end
At this point I know that the factories.rb file is being loaded (I tried with the moronic print-debugging). When I remove the user = FactoryGirl.build(:user) line from user_spec.rb I get no errors (and the normal RSpec feedback telling me there are no tests, but no errors). If you are interested, here's my model:
require 'digest/sha1'
module Bluereader
class User < ActiveRecord::Base
has_many :categories, :foreign_key => :user_id
has_many :news, :foreign_key => :user_id
has_many :settings, :foreign_key => :user_id
attr_reader :full_name
class << self
def login(username, password)
encrypted_password = Digest::SHA1.hexdigest(password)
if not User.exists?(:username => username, :encrypted_password => encrypted_password)
user_id = User.id_from_username(username)
update(user_id, :logged_in_at => Time.now, :logged_out_at => 0)
end
end
def logout
update(current_user.id, :logged_out_at => Time.now)
end
def validate_account(username, password, full_name)
if username.empty? or password.empty or full_name.empty?
return 'Please fill in all the fields.'
end
if User.exists?(:username => username)
return 'That username is already in use.'
end
unless username =~ /^\w+$/
return 'Username field should contain only letters, numbers and underscores.'
end
''
end
def create_account(username, password, full_name)
encrypted_password = Digest::SHA1.hexdigest(password)
User.create(:username => username,
:encrypted_password => encrypted_password,
:full_name => full_name,
:logged_in_at => Time.now,
:logged_out_at => 0)
end
def delete_account
current_user.destroy
end
private
def id_from_username(username)
user = where(:username => username).first
user.nil? ? 0 : user.id
end
def current_user
where(:logged_out_at => 0).first
end
end
end
end
SOLUTION
The problem was that the class User was in a module, here's the solution:
factory :user, class: Bluereader::User do
You need to require the rails environment in your spec helper file. Add the following to spec/spec_helper.rb:
require File.expand_path("../../config/environment", __FILE__)
Update
Even if you're not using Rails, you'll still need to require the models in your spec helper.
Taken from the bottom of the question
The problem was that the class User was in a module, here's the solution:
factory :user, class: Bluereader::User do
For anyone clumsy like me, you may have FactoryGirl in your code where you meant to have FactoryBot
Given I have the following migration:
Sequel.migration do
up do
alter_table :users do
add_column :is_admin, :default => false
end
# Sequel runs a DESCRIBE table statement, when the model is loaded.
# At this point, it does not know that users have a is_admin flag.
# So it fails.
#user = User.find(:email => "admin#fancy-startup.example")
#user.is_admin = true
#user.save!
end
end
Then sequel does not automatically reload the table structure (see comment inline).
I am using this ugly hack to work around it:
# deep magic begins here. If you remove a single line, it will
# break the migration.
User.db.schema("users", :reload => true)
User.instance_variable_set(:#db_schema, nil)
User.columns
User.new.respond_to?(:is_admin=)
sleep 1
Is there a better way?
Much simpler than your hack is this hack: (re)set the dataset to the table name:
User.set_dataset :users
Seen in action:
require 'sequel'
DB = Sequel.sqlite
DB.create_table :users do
primary_key :id
String :name
end
class User < Sequel::Model; end
User << { name:"Bob" }
DB.alter_table :users do
add_column :is_admin, :boolean, default:false
end
p User.first #=> #<User #values={:id=>1, :name=>"Bob", :is_admin=>false}>
p User.setter_methods #=> ["name="]
User.set_dataset :users # Make the magic happen
p User.setter_methods #=> ["name=", "is_admin="]
#user = User.first
#user.is_admin = true
#user.save
p User.first #=> #<User #values={:id=>1, :name=>"Bob", :is_admin=>true}>
Note that there is no Sequel::Model#save! method; I changed it to save so that it would work.
What is the right way to handle exceptions coming from the model in Sequel? Particularly the thing I'm running into is when the unique constraint is applied to the login. The exception in this case appears to be coming from SQLite itself instead of Sequel which means it's not getting handled by "errors".
This is the error I'm getting after trying to create a user with a "non-unique" login:
Sequel::DatabaseError at /user/create
SQLite3::ConstraintException: column login is not unique
file: database.rb location: close line: 97
Here is my abbreviated code:
require 'sinatra'
require 'sequel'
DB.create_table :users do
primary_key :id
String :login, :key => true, :length => (3..40), :required => true, :unique => true
String :hashed_password, :required => true
String :salt
DateTime :created_at, :default => DateTime.now
end
class User < Sequel::Model
# password authentication code
end
get '/user/create' do
slim :user_create
end
post '/user/create' do
user = User.new
user.login = params['login']
user.password = params['password']
if user.save
'User created'
else
tmp = []
user.errors.each do |e|
tmp << (e.join('<br/>'))
end
tmp
end
end
You probably want to use the validation_helpers plugin and use validates_unique :login inside the validate method.
Add the code below to handle sequel errors in sinatra.
The error block in sinatra will handle any errors thrown in one of your routes. You can specify the type of error in the first line in order to only handle those types of errors.
error Sequel::Error do
e = env['sinatra.error']
content_type :json
status(400)
return {
message: e.message
}.to_json
end
If you wanted to handle all errors, you would insert the following block of code.
error Exception do
e = env['sinatra.error']
content_type :json
status(400)
return {
message: e.message
}.to_json
end
You can combine many of these blocks so that you are handling different types of errors differently.
First - use validation plugin
# models/account.rb
Sequel::Model.plugin :validation_helpers
class Account < Sequel::Model
def validate
super
validates_unique :login
end
end
Next - handle error in app
# app.rb
...
begin
Account.create
rescue => e
# do what you need
end
...
I'm just starting out with tests. When I run this one:
rake test test/models/user_test.rb
require 'test_helper'
class UserTest < ActiveSupport::TestCase
test "should not save without an email address" do
user = User.new
assert_not user.save
end
end
I get the following error:
1) Error:
UserTest#test_should_not_save_without_an_email_address:
ActiveRecord::StatementInvalid: SQLite3::ConstraintException: NOT NULL constraint failed: groups.name: INSERT INTO "groups" ("created_at", "updated_at", "id") VALUES ('2015-08-11 17:31:07', '2015-08-11 17:31:07', 980190962)
This is user.rb
class User < ActiveRecord::Base
has_many :groups
has_many :user_groups
attr_accessor :password
EMAIL_REGEX = /A[A-Z0-9._%+-]+#[A-Z0-9.-]+\.[A-Z]{2,4}\z/i
validates :password, :confirmation => true #password_confirmation attr
validates_length_of :password, :in => 6..20, :on => :create
validates :email, :presence => true, :uniqueness => true, :format => EMAIL_REGEX
before_save :encrypt_password
after_save :clear_password
def encrypt_password
if password.present?
self.salt = Digest::SHA1.hexdigest("# We add {self.email} as unique value and #{Time.now} as random value")
self.encrypted_password = Digest::SHA1.hexdigest("Adding #{self.salt} to {password}")
end
end
def clear_password
self.password = nil
end
end
This is test_helper.rb
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
class ActiveSupport::TestCase
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
fixtures :all
# Add more helper methods to be used by all tests here...
end
As far as I can tell I don't have any callback or otherwise that would attempt to write to the "groups" table. My "groups.yml" is default, but that shouldn't matter if I'm only testing this one model, correct? Any help as to where I could start looking would be much appreciated. Thanks!
test_helper.rb was setting up all my fixtures and they weren't defined. Commenting "fixtures :all" fixed it.