The problem
From labs/3-activities.rb in your copy of models-and-tests:
An activity is one thing a salesperson did with one contact: "met at Cupertino". It holds two ids, the salesperson's and the contact's.
This is the lab about the ids. An activity doesn't contain a salesperson or a contact; it holds their ids, and every question you ask of it ("whose activity is this?", "what did Ben do with Tim?") is answered by looking rows up by those ids with where or find_by. There is no has_many or belongs_to in this repo, on purpose: the rules in AGENTS.md ask your agent for the lookup, so you can see it.
1. The rules
Under Activity in DONE.md:
## Activity
An activity can't be saved without a salesperson.
An activity can't be saved without a contact.
I can find one salesperson's activities with one contact.
The third is a different kind of rule from the first two. It doesn't forbid anything; it says what you must be able to find. Both kinds belong in DONE.md.
2. Tests first, red
Your prompt, in your own words, along the lines of lab 2's: a test for each sentence, run them, show them failing, don't change the model yet. The agent should produce something like:
require "test_helper"
class ActivityTest < ActiveSupport::TestCase
# One test for each sentence under Activity in DONE.md, named with that sentence.
test "an activity can't be saved without a salesperson" do
activity = Activity.new
activity["contact_id"] = 1
activity["note"] = "met at Cupertino"
activity.save
assert_equal 0, Activity.all.count
end
test "an activity can't be saved without a contact" do
activity = Activity.new
activity["salesperson_id"] = 1
activity["note"] = "met at Cupertino"
activity.save
assert_equal 0, Activity.all.count
end
test "I can find one salesperson's activities with one contact" do
for note in ["quick checkin over facetime", "met at Cupertino"]
activity = Activity.new
activity["salesperson_id"] = 1
activity["contact_id"] = 1
activity["note"] = note
activity.save
end
activity = Activity.new
activity["salesperson_id"] = 1
activity["contact_id"] = 2
activity["note"] = "met at Amazon HQ"
activity.save
found = Activity.where({ "salesperson_id" => 1, "contact_id" => 1 })
assert_equal 2, found.count
end
end
3 runs, 3 assertions, 2 failures, 0 errors, 0 skips
Two fail, one passes, and that is the right first run. The two "can't be saved" rules fail because nothing enforces them yet. The "I can find" rule passes on the empty model, because
finding rows by a column comes free with the table: where needs no code of yours. A rule that passes before you've written anything is worth a second look (is it testing something?), and here the answer is yes: it checks that two of the three activities come back, not three.
Notice the ids in the tests are made up: 1, 2. The test database starts empty for every test, so there's no salesperson 1 to look up; the test only needs an activity to hold an id. That is both convenient and a trap, and lab 4 springs it.
3. Check, then green
DONE.md beside test/models/activity_test.rb: three sentences, three tests, each named with its sentence, each checking what it says. Then "Make them pass":
class Activity < ApplicationRecord
validates :salesperson_id, presence: true
validates :contact_id, presence: true
end
bin/rails test
Every test in the repo green, Company and Contact included.
4. The script
scripts/activities.rb needs rows to point at, so run scripts/list-contacts.rb (from class) and scripts/salespeople.rb (lab 2) first.
# Start empty, so this can run more than once
Activity.destroy_all
ben = Salesperson.find_by({ "first_name" => "Ben", "last_name" => "Block" })
tim = Contact.find_by({ "first_name" => "Tim", "last_name" => "Cook" })
andy = Contact.find_by({ "first_name" => "Andy", "last_name" => "Jassy" })
activity = Activity.new
activity["salesperson_id"] = ben["id"]
activity["contact_id"] = tim["id"]
activity["note"] = "quick checkin over facetime"
activity.save
activity = Activity.new
activity["salesperson_id"] = ben["id"]
activity["contact_id"] = tim["id"]
activity["note"] = "met at Cupertino"
activity.save
activity = Activity.new
activity["salesperson_id"] = ben["id"]
activity["contact_id"] = andy["id"]
activity["note"] = "met at Amazon HQ"
activity.save
puts "Activities between Ben and Tim Cook:"
for activity in Activity.where({ "salesperson_id" => ben["id"], "contact_id" => tim["id"] })
puts "- #{activity["note"]}"
end
Activities between Ben and Tim Cook:
- quick checkin over facetime
- met at Cupertino
Read how it found them. The script never stores "Ben" in an activity. It looks Ben up once, takes his id, and puts that number in each row. The question "what did Ben do with Tim?" is then one where with two ids. That is the same WHERE salesperson_id = 1 AND contact_id = 1 you'd write in SQL, and it's in log/development.log if you want to see it.
If your agent wrote activity.salesperson or ben.activities, it reached for an association. Point it at AGENTS.md.
Challenge 4 and 4a: all of Ben's activities, with the company
puts "Ben's Activities:"
for activity in Activity.where({ "salesperson_id" => ben["id"] })
contact = Contact.find_by({ "id" => activity["contact_id"] })
company = Company.find_by({ "id" => contact["company_id"] })
puts "#{contact["first_name"]} #{contact["last_name"]} (#{company["name"]}) - #{activity["note"]}"
end
Ben's Activities:
Tim Cook (Apple) - quick checkin over facetime
Tim Cook (Apple) - met at Cupertino
Andy Jassy (Amazon) - met at Amazon HQ
Two lookups per line, following the ids: the activity holds a contact_id, so find the contact; the contact holds a company_id, so find the company. Each find_by on an id is one row, so the result is a single contact and a single company, not a list.
Challenge 5: a count per salesperson
for salesperson in Salesperson.all
count = Activity.where({ "salesperson_id" => salesperson["id"] }).count
puts "#{salesperson["first_name"]} #{salesperson["last_name"]}: #{count} activities"
end
Ben Block: 3 activities
Brian Eng: 0 activities
Brian's zero is the line to check. where with an id nobody used returns an empty list, and an empty list counts as 0, so a salesperson with no activities still gets a line. If your
version skipped Brian, it probably looped over activities instead of salespeople.