The Goal:
Build a Person object for each row, with first_name, last_name, and email attributes. This list of people should be encapsulated in a class that can do stuff like return unique by different attributes, ie: people.unique_by_first_name. Also the people object should be Enumberable.
Attempt #3:
#!/usr/bin/env ruby
class Person
attr_accessor :first_name, :last_name, :email
def self.unique_by_first_name(people_array)
people_array.uniq{ |x| x.first_name } # see result below
end
def self.import_data
people_array = Array.new
file = DATA.read
file.each_line do |line|
person = Person.new
fields = line.split(',')
fields.each_with_index do |field, i|
if field.include? '#'
person.email = field.to_s.strip
fields.delete_at(i)
elsif field.empty?
fields.delete_at(i)
else
person.first_name = fields[0].to_s.strip
person.last_name = fields[1].to_s.strip
end
end
people_array.push(person)
end
Person.unique_by_first_name(people_array)
end
end
Person.import_data
__END__
John,Smith,john#foo.com
james#data.net,James
Phillip,Jones,phil#msn.com
,Irene,Smith
Robert,Chorley,rob#foo.com,
emma#hotmail.com,Emma,
David,Smith
james#game.net,James,Bond
james#game.net,James,Bond
Result:
[#<Person:0x007fd30228fdc0 #email="john#foo.com", #first_name="John", #last_name="Smith">,
#<Person:0x007fd30228f870 #email="james#data.net">,
#<Person:0x007fd30228f438 #email="phil#msn.com", #first_name="Phillip", #last_name="Jones">,
#<Person:0x007fd30228ea60 #first_name="Irene", #last_name="Smith">,
#<Person:0x007fd30228e420 #email="rob#foo.com", #first_name="Robert", #last_name="Chorley">,
#<Person:0x007fd3022879e0 #email="emma#hotmail.com", #first_name="Emma", #last_name="">,
#<Person:0x007fd302286680 #first_name="David", #last_name="Smith">,
#<Person:0x007fd302285e38 #email="james#game.net", #first_name="James", #last_name="Bond">]
Any Ideas for Improvement?
The code above produces the following results ^. Do you guys have any recommendations on how to improve this logic?
In order for you to access those attributes on the Person class you need to add:
attr_reader :first_name, :last_name, :email to the top of your class
Like this:
class Person
attr_reader :first_name, :last_name, :email
def initialize(first_name, last_name, email)
first_name = first_name
last_name = last_name
email = email
end
Now you can call your x.first_name on an object because you're explicitly allowing your program to read it. Here's a really famous SO answer on the subject.
Related
I'm building a simple web-scraper (scraping jobs from indeed.com) for practice and I'm trying to implement the following method (low_salary?(salary)). The aim is for the method to compare a minimum (i.e. desired) salary, compare it with the offered salary contained in the job object (#salary):
class Job
attr_reader :title, :company, :location, :salary, :url
def initialize(title, company, location, salary, url)
#title = title
#company = company
#location = location
#salary = salary
#url = url
end
def low_salary?(minimum_salary)
return if !#salary
minimum_salary < #salary.split(/[^\d]/)[1..2].join.to_i
end
end
The method works fine when comapring #salary and the min_salary variable given to it, the delete_if appropriately deletes the elements that return true for low_salary? and returns correctly when #salary is nil (indeed listings don't always include the salary so my assumption is that there will be some nil values) in the following test program (Also: I am unsure as to why minimum_salary < #salary works but #salary < minimum_salary doesn't, but this does exactly what I want it to do):
require_relative('job_class.rb')
job = Job.new("designer", "company", "location", "£23,000 a year", "url")
job_results = []
job_results.push(job)
min_salary = 50000
print job.low_salary?(min_salary)
job_results.delete_if { |job| job.low_salary?(min_salary) }
print job_results
However in my scraper program, I get a no method error when calling the method: job_class.rb:16:in "low_salary?": undefined method `join' for nil:NilClass (NoMethodError)
require 'nokogiri'
require 'open-uri'
require_relative 'job_class.rb'
class JobSearchTool
def initialize(job_title, location, salary)
#document = Nokogiri::HTML(open("https://uk.indeed.com/jobs?q=#{job_title.gsub('-', '+')}&l=#{location}"))
#job_listings = #document.css('div.mosaic-provider-jobcards > a')
#salary = salary.to_i
#job_results = []
end
def scrape_jobs
#job_listings.each do |job_card|
#job_results.push(Job.new(
job_card.css('h2 > span').text, #title
job_card.css('span.companyName').text, #company
job_card.css('div.companyLocation').text, #location
job_card.css('span.salary-snippet').text, #salary
job_card['href']) #url
)
end
end
def format_jobs
#job_results.each do |job|
puts <<~JOB
#{job.title} - #{job.company} in #{job.location} :#{job.salary}
Apply at: #{job.url}
---------------------------------------------------------------------------------
JOB
end
end
def check_salary
#job_results.delete_if { |job| job.low_salary?(#salary) }
end
def run
scrape_jobs
check_salary
format_jobs
end
if __FILE__ == $0
job_search_tool = JobSearchTool.new(ARGV[0], ARGV[1], ARGV[2])
job_search_tool.run
end
Obviously something from the scraper programme is influencing the method somehow, but I can't understand what it could be. I'm using the method in the exact same way as the test program, so what difference is causing the method not to return when #salary is nil?
A quick search on the URL you're scraping shows there are job posts that don't have a salary, so, when you get the data from that HTML element and initialize a new Job object, the salary is an empty string, and knowing that "".split(/[^\d]/)[1..2] returns nil, that's the error you get.
You must add a way to handle job posts without a salary:
class Job
attr_reader :title, :company, :location, :salary, :url
def initialize(title, company, location, salary, url)
#title = title
#company = company
#location = location
#salary = salary.to_s # Explicit conversion of nil to string
#url = url
end
def low_salary?(minimum_salary)
return if parsed_salary.zero? # parsed_salary returns always an integer,
# so you can check when is zero,
# and not just when is falsy
minimum_salary < parsed_salary
end
private
def parsed_salary
salary[/(?<=£)(\d|,)*(?=\s)/]
.to_s # converts nil to "" if the regex doesn't capture anything
.tr(",", "") # removes the commas to parse the string as an integer
.to_i # parses the string to its corresponding integer representation
end
end
Notice the regex isn't meant to capture everything, but it works with the salary as rendered in the website.
I'm practicing OOP for the first time by modeling my application domain (public high school) as objects, and I'm stuck on how to create relationships between the classes without introducing lots of external dependencies.
I have lots of relationships I want to construct, so in hopes of learning the general principle I'm giving two classes and sample objects here to illustrate the difficulty I'm having.
I have two classes Gradeand Transcript. Every instance of Transcript has an instance variable #mark, which right now is a string. I collected all the instances of each class a grades hash and a transcripts hash.
Question: How can I modify these classes so that #mark references the corresponding Grade instance?
(or, is that the wrong approach entirely?)
Grade has an instance for every possible final grade students can receive
class Grade
attr_accessor :mark, :alpha_equivalent, :numeric_range_low, :numeric_range_high, :numeric_qquivalent, :pass_fail_equivalent, :description
def initialize(args)
#mark = args["Mark"]
#alpha_equivalent = args["AlphaEquivalent"]
#numeric_range_low = args["NumericRangeLow"]
#numeric_range_high = args["NumericRangeHigh"]
#numeric_equivalent = args["NumericEquivalent"]
#pass_fail_equivalent = args["PassFailEquivalent"]
#description = args["Description"]
end
end
Sample object from the grades hash:
grades["100"] =>
#<Grade:0x007f9fcb077d68
#alpha_equivalent="100",
#description="100 out of 100",
#mark="100",
#numeric_equivalent="100",
#numeric_range_high="100",
#numeric_range_low="100",
#pass_fail_equivalent="P">
Transcript has instances for every final grade the student has ever received for all the courses they've studied
class Transcript
attr_accessor :student_id, :last_name, :first_name, :grade, :official_class, :school, :year, :term, :course, :course_title, :mark, :pass_fail, :credits
def initialize(args)
#student_id = args["StudentID"]
#last_name = args["LastName"]
#first_name = args["FirstName"]
#grade = args["Grade"]
#official_class = args["OffClass"]
#school = args["school"]
#year = args["Year"]
#term = args["Term"]
#course = args["Course"]
#course_title = args["Course Title"]
#mark = args["Mark"]
#credits = args["Credits"]
#grade_entry_cohort = args["GEC"]
end
end
Sample object from the transcripts hash:
transcripts["foobar-COURSE1-100"] =>
#<Transcript:0x007f9fce8786b8
#course="COURSE1",
#course_title="Example Course",
#credits="5",
#first_name="FOO",
#grade="100",
#grade_entry_cohort="V",
#last_name="BAR",
#mark="100",
#official_class="000",
#school="1",
#student_id="0123",
#term="1",
#year="2000">
I'm instantiating all the objects from CSV source files and then collecting them into a hash because I wanted to be able to address them directly.
Sounds like you need to want Transcript#grade to return a Grade instance. So let's make a method for that:
class Grade
def self.all
#all ||= {}
end
def self.find(mark)
all[mark]
end
end
Now, Grade.all needs to be populated. This could be achieved like this from your CSV:
grade_args = %w[alpha_equivalent description mark numeric_equivalent numeric_range_high numeric_range_low pass_fail_equivalent]
CSV.parse { |row| Grade.all.merge(csv['mark'] => Grade.new(row.slice(*grade_args)}
Now, we can modify Transcript like this:
class Transcript
def initialize(args)
#args = args
end
def grade
#grade ||= Grade.find(args['mark'])
end
private
attr_reader :args
end
Assuming that you've created the grades hash earlier:
# read args from csv
# id built from first, last, course, and grade
transcripts[id] = Transcript.
new(args.merge('Mark' => grades[args['Mark']])
It uses Hash#merge to extend args with an instance of Grade that was built earlier.
Any idea to refactor the code of the method self.import_data ? It's a method which allow the application to save CSV file in database (with some restriction on the user email). It's supposed to run every day at noon so it has to be quick.
Currently its very long to run when I have a big CSV file. I wonder if there a way to make this code more efficient and win some time (or to avoiding the loop or make less request...). I don't really know what makes the process so long actually and how to correct it.
Here is my model :
class Person < ActiveRecord::Base
has_paper_trail
validates :email, uniqueness: true
require 'csv'
def is_former_email?(update_email)
self.versions.each do |version|
next if version.object.nil?
return true if version.object.include?(update_email)
end
end
def self.import_data
filename = File.join Rails.root, '/vendor/people.csv'
CSV.foreach(filename, headers: true, col_sep: ',') do |row|
firstname, lastname, home_phone_number, mobile_phone_number, email, address = row
person = Person.find_or_create_by(firstname: row["firstname"], lastname: row['lastname'], address: row['address'] )
if person.is_former_email?(row['email']) == true
puts "not allowed"
else
person.update_attributes({firstname: row['firstname'], lastname: row['lastname'], home_phone_number: row['home_phone_number'], mobile_phone_number: row['mobile_phone_number'], address: row['address'], email: row['email']})
end
end
end
end
I was a little refactored your code, but for more efficiently I recommend to use gem activerecord-import and optimize versions model for search previous emails.
class Person < ActiveRecord::Base
require 'csv'
FILE_NAME = File.join Rails.root, '/vendor/people.csv'
validates :email, uniqueness: true
has_paper_trail
def self.import_data
people = CSV.new(File.new(FILE_NAME), headers: true, header_converters: :symbol, converters: :all).to_a.map(&:to_hash)
versions_by_item_id = Version.where(item_type: 'Person').select('item_id, object').group_by(&:item_id)
people.each do |person_params|
person = Person.find_or_create_by(person_params.slice(:firstname, :lastname, :address))
if versions_by_item_id[person.id] && versions_by_item_id[person.id].sum { |v| v.object.to_s }.include?(person_params[:email])
puts 'not allowed'
else
person.update_attributes(person_params.slice(:home_phone_number, :mobile_phone_number, :email))
end
end
end
end
I have the following classes mapped with STI:
class Employee < ActiveRecord::Base
end
class StudentEmployee < Employee
# I'd like to keep university only to StudentEmployee...
end
#Just to make this example easier to understand, not using migrations
ActiveRecord::Schema.define do
create_table :employees do |table|
table.column :name, :string
table.column :salary, :integer
table.column :university, :string # Only Students
end
end
emp = Employee.create(:name=>"Joe",:salary=>20000,:university=>"UCLA")
I'd like to prevent the setting of the university field for Employees, but allow it for StudentEmployees. I tried to use attr_protected, but it will only prevent mass setting:
class Employee < ActiveRecord::Base
attr_protected :university
end
class StudentEmployee < Employee
attr_accessible :university
end
#This time, UCLA will not be assigned here
emp = Employee.create(:name=>"Joe",:salary=>20000,:university=>"UCLA")
emp.university = "UCLA" # but this will assign university to any student...
emp.save
puts "only Students should have univesities, but this guy has one..."+emp.university.to_s
The problem here is that it will insert in the database a university for simple employees.
Another problem is that I think it would be better to say in the StudentEmployee class that university is an attribute, and not to say in the Employee that university "is not" a a visible attribute... it just goes in the inverse direction of natural abstraction.
Thanks.
I would try something like this:
class Employee < ActiveRecord::Base
validate :no_university, unless: lambda { |e| e.type === "StudentEmployee" }
def no_university
errors.add :university, "must be empty" unless university.nil?
end
end
It isn't the prettiest, but it should to work.
This came up a bit ago ( rails model attributes without corresponding column in db ) but it looks like the Rails plugin mentioned is not maintained ( http://agilewebdevelopment.com/plugins/activerecord_base_without_table ). Is there no way to do this with ActiveRecord as is?
If not, is there any way to get ActiveRecord validation rules without using ActiveRecord?
ActiveRecord wants the table to exist, of course.
This is an approach I have used in the past:
In app/models/tableless.rb
class Tableless < ActiveRecord::Base
def self.columns
#columns ||= [];
end
def self.column(name, sql_type = nil, default = nil, null = true)
columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default,
sql_type.to_s, null)
end
# Override the save method to prevent exceptions.
def save(validate = true)
validate ? valid? : true
end
end
In app/models/foo.rb
class Foo < Tableless
column :bar, :string
validates_presence_of :bar
end
In script/console
Loading development environment (Rails 2.2.2)
>> foo = Foo.new
=> #<Foo bar: nil>
>> foo.valid?
=> false
>> foo.errors
=> #<ActiveRecord::Errors:0x235b270 #errors={"bar"=>["can't be blank"]}, #base=#<Foo bar: nil>>
Validations are simply a module within ActiveRecord. Have you tried mixing them into your non-ActiveRecord model?
class MyModel
include ActiveRecord::Validations
# ...
end
I figure the more answers the better since this is one of the first results in google when searching for "rails 3.1 models without tables"
I've implements the same thing without using ActiveRecord::Base while including the ActiveRecord::Validations
The main goal was to get everything working in formtastic, and below I've included a sample payment that will not get saved anywhere but still has the ability to be validated using the validations we all know and love.
class Payment
include ActiveModel::Validations
attr_accessor :cc_number, :payment_type, :exp_mm, :exp_yy, :card_security, :first_name, :last_name, :address_1, :address_2, :city, :state, :zip_code, :home_telephone, :email, :new_record
validates_presence_of :cc_number, :payment_type, :exp_mm, :exp_yy, :card_security, :first_name, :last_name, :address_1, :address_2, :city, :state
def initialize(options = {})
if options.blank?
new_record = true
else
new_record = false
end
options.each do |key, value|
method_object = self.method((key + "=").to_sym)
method_object.call(value)
end
end
def new_record?
return new_record
end
def to_key
end
def persisted?
return false
end
end
I hope this helps someone as I've spent a few hours trying to figure this out today.
UPDATE: For Rails 3 this can be done very easy. In Rails 3+ you can use the new ActiveModel module and its submodules. This should work now:
class Tableless
include ActiveModel::Validations
attr_accessor :name
validates_presence_of :name
end
For more info, you can check out the Railscast (or read about it on AsciiCasts) on the topic, as well as this blog post by Yehuda Katz.
OLD ANSWER FOLLOWS:
You may need to add this to the solution, proposed by John Topley in the previous comment:
class Tableless
class << self
def table_name
self.name.tableize
end
end
end
class Foo < Tableless; end
Foo.table_name # will return "foos"
This provides you with a "fake" table name, if you need one. Without this method, Foo::table_name will evaluate to "tablelesses".
Just an addition to the accepted answer:
Make your subclasses inherit the parent columns with:
class FakeAR < ActiveRecord::Base
def self.inherited(subclass)
subclass.instance_variable_set("#columns", columns)
super
end
def self.columns
#columns ||= []
end
def self.column(name, sql_type = nil, default = nil, null = true)
columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null)
end
# Overrides save to prevent exceptions.
def save(validate = true)
validate ? valid? : true
end
end
This is a search form that presents an object called criteria that has a nested period object with beginning and end attributes.
The action in the controller is really simple yet it loads values from nested objects on the form and re-renders the same values with error messages if necessary.
Works on Rails 3.1.
The model:
class Criteria < ActiveRecord::Base
class << self
def column_defaults
{}
end
def column_names
[]
end
end # of class methods
attr_reader :period
def initialize values
values ||= {}
#period = Period.new values[:period] || {}
super values
end
def period_attributes
#period
end
def period_attributes= new_values
#period.attributes = new_values
end
end
In the controller:
def search
#criteria = Criteria.new params[:criteria]
end
In the helper:
def criteria_index_path ct, options = {}
url_for :action => :search
end
In the view:
<%= form_for #criteria do |form| %>
<%= form.fields_for :period do |prf| %>
<%= prf.text_field :beginning_as_text %>
<%= prf.text_field :end_as_text %>
<% end %>
<%= form.submit "Search" %>
<% end %>
Produces the HTML:
<form action="/admin/search" id="new_criteria" method="post">
<input id="criteria_period_attributes_beginning_as_text" name="criteria[period_attributes][beginning_as_text]" type="text">
<input id="criteria_period_attributes_end_as_text" name="criteria[period_attributes][end_as_text]" type="text">
Note: The action attribute provided by the helper and the nested attributes naming format that makes it so simple for the controller to load all the values at once
There is the activerecord-tableless gem. It's a gem to create tableless ActiveRecord models, so it has support for validations, associations, types. It supports Active Record 2.3, 3.0, 3.2
The recommended way to do it in Rails 3.x (using ActiveModel) has no support for associations nor types.