-
-
Save burlesona/4056285 to your computer and use it in GitHub Desktop.
Example of user-friendly association of City and Districts to other models in Rails
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 1. Excerpt of Photo and User model | |
# Photos can belong to Cities and Districts, and users can browse Cities and | |
# Districts to see the photos in them. When a user uploads a photo I really want | |
# them to tell me where the photo came from. | |
# Also, Users can be from a City and District. This is not currently used for very | |
# much, but gives me a path to add location-enhanced results for other stuff in the | |
# future. | |
# Relevant excerpt of Photo model: | |
class Photo | |
belongs_to :city | |
belongs_to :district | |
... | |
end | |
# Relevant excerpt of User model: | |
class User | |
belongs_to :city | |
belongs_to :district | |
... | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-# 2. Location Selector Template | |
-# When a user is viewing a page where they can edit a photo, the get this | |
-# location-selection widget. Relevant data is attached to the container, which | |
-# gets used by the client-side JS. | |
.location_select{ :data => { 'cities-path' => cities_path, 'object' => object.class, 'object-id' => object.id, 'city-id' => object.city.try(:id), 'district-id' => object.district.try(:id) } } | |
.input.city_select | |
= label_tag :city_name, 'City' | |
= text_field_tag :city_name, object.city.try(:full_name), :data => { :autocomplete_source => lookup_cities_path }, :id => nil, :class => 'city_name' | |
%span.status | |
.message | |
.input.district_select | |
= label_tag 'District / Neighborhood' | |
= text_field_tag :district_name, object.district.try(:name), :id => nil, :class => 'district_name' | |
%span.status | |
%span.hint Optional, leave blank if unknown. | |
.message | |
-# Note the partial is polymorphic so it takes the local variable `object` | |
-# instead of something more specific. It can be called in a view like so: | |
-# = render :partial => 'places/location_selector', :locals => { :object => @photo } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 3. Location Selector CoffeeScript | |
# This is the heart of the widget, it sets up three handler objects. For each | |
# widget: a `LocationSelector` parent object, with a `CityHandler` and | |
# `DistrictHandler` attached. | |
# PLACES | |
# Initializers | |
ajaxReady -> | |
$('div.location_select').each -> | |
new LocationSelector $(this) | |
# Global Scope | |
root = exports ? this | |
root.S_KEYS = [8,27,32,46].concat([65..90]); | |
# HANDLER OBJECTS | |
# -- Location Selector -- | |
# The location selector is simple object to bind the elements together | |
# and share some common functionality between them. | |
root.LocationSelector = ( container ) -> | |
@container= container | |
@city_handler = null | |
@district_handler = null | |
@located = false | |
this.initialize() | |
root.LocationSelector.prototype = | |
# On initialize it passes itself to a new CityHandler and | |
# DistrictHandler, then initializes the City Handler. | |
initialize: -> | |
@city_handler= new CityHandler(this) | |
@district_handler= new DistrictHandler(this) | |
@city_handler.initialize() | |
# Display is a shared method for updating the DOM with information | |
# for the user. | |
display: (element, status, message) -> | |
element.find('span.status').attr('class', 'status ' + status) | |
msgdiv = element.find('div.message') | |
if message | |
msgdiv.attr('class', 'message ' + status).html( message ).slideDown() | |
else | |
msgdiv.attr('class', 'message').html( message ).slideUp() | |
# This checks for a city-id set on the container. | |
city_id: -> | |
@container.data 'city-id' | |
# This checks for a district-id set on the container. | |
district_id: -> | |
@container.data 'district-id' | |
# This sets a city id in the object and on the container, and | |
# triggers the district handler. It also sends the `located` | |
# event, which other parts of the UI may be listening for. | |
setLocation: -> | |
@container.data 'city-id', @city_handler.id | |
@located = true | |
@district_handler.initialize() | |
@container.triggerHandler 'located' | |
# This unsets a city id in the object and the container, and | |
# triggers the district handler. It also sends the 'notlocated' | |
# event, which other parts of the UI may be listening for. | |
unsetLocation: -> | |
@container.data 'city-id', null | |
@located = false | |
@district_handler.initialize() | |
@container.triggerHandler 'notLocated' | |
# -- City Handler -- | |
# The City Handler takes care of the `city_name` input. | |
root.CityHandler = (locationSelector) -> | |
@LS = locationSelector | |
@container= locationSelector.container | |
@name_selector= @container.find('input.city_name') | |
@id = null | |
@timer= null | |
return this | |
root.CityHandler.prototype = | |
# On initialize this attaches a listener to the city_name input | |
# and sets a status of 'success' if the LocationSelector already | |
# knows its CityID (ie the object already is associated with a city). | |
initialize: -> | |
this.bindName() | |
if @LS.city_id() | |
this.setStatus 'success' | |
this.setID @LS.city_id() | |
# This sets the status of the city selector. | |
setStatus: (status, message) -> | |
@LS.display @container.find('div.city_select'), status, message | |
# This listens to the city_name input and triggers the .get() method | |
# when a user stops typing. It also binds the jQuery UI autocomplete | |
# method to provide suggested cities to the user. | |
bindName: -> | |
self = this | |
@name_selector.bindAutocomplete | |
select: -> | |
self.get() | |
window.clearTimeout( self.timer ) | |
@name_selector.on 'keyup', (event) -> | |
if event.keyCode in S_KEYS | |
self.setStatus 'loading' | |
self.clearID() | |
if self.timer | |
window.clearTimeout( self.timer ) | |
self.timer = window.setTimeout( | |
-> self.get() | |
2000 | |
) | |
# This method queries the server with the text the user has input so far. | |
# It's not possible to know whether an obscure placename is totally valid | |
# so any input may be accepted, but the association will not be created | |
# unless an existing match is found in the DB, or the user confirms the request. | |
get: (create) -> | |
self = this | |
unless @name_selector.val() | |
self.setStatus '' | |
return false | |
params = {location_string: @name_selector.val(), object: @container.data('object'), object_id: @container.data('object-id')} | |
if create == true | |
params["create"] = true | |
$.ajax | |
type: 'POST' | |
url: self.container.data('cities-path') | |
dataType: 'json' | |
data: params | |
error: (xhr, status, error) -> | |
self.setStatus 'error', 'Unable to find city, please try adding a state or country name.' | |
# If the request is successful either the object is associated with an | |
# existing city record, or the user is asked to confirm the creation of a new | |
# city. | |
success: (data, status, xhr) -> | |
if data.city.new_record | |
self.confirmCreate data.city | |
else | |
self.setStatus 'success' | |
self.name_selector.val( data.city.full_name ) | |
self.setID data.city.id | |
# This method prompts the user to confirm their selection if it is new to the app, and | |
# resubmits the request confirming creation of a new record. | |
confirmCreate: (city) -> | |
self = this | |
message = 'No record for ' + city.full_name + '. <a href="#" class="confirm_city button small">Create it?</a>' | |
this.setStatus 'notice', message | |
@container.find('a.confirm_city').click (event) -> | |
event.preventDefault() | |
self.setStatus 'loading' | |
self.get(true) | |
# This method unsets the association. | |
clearID: -> | |
@id = null | |
@LS.unsetLocation() | |
# This method sets the association. | |
setID: (id) -> | |
@id = id | |
@LS.setLocation() | |
# -- District Handler -- | |
# This is turned on or "rebooted" by changes to the location selector | |
# triggered by the City Handler. It works in virtually the same way. | |
root.DistrictHandler = (locationSelector) -> | |
@LS = locationSelector | |
@container= locationSelector.container | |
@name_selector= @container.find('input.district_name') | |
@id= null | |
@timer= null | |
this.disable() | |
return this | |
root.DistrictHandler.prototype = | |
# Districts are nested under cities. This method builds a URL to | |
# post to. | |
path: -> | |
@container.data('cities-path') + '/' + @LS.city_handler.id + '/districts' | |
# This method ensures the City association is present and then binds | |
# the district name input to a listener. | |
initialize: -> | |
if @LS.located | |
this.enable() | |
if @LS.district_id() | |
this.setID @LS.district_id() | |
this.setStatus 'success' | |
# This sets the status of the district_name input. | |
setStatus: (status, message) -> | |
@LS.display @container.find('div.district_select'), status, message | |
# This disables the district_name input. | |
disable: -> | |
@name_selector.attr 'disabled', true | |
@name_selector.off 'autocomplete' | |
@name_selector.off 'keyup' | |
# This enables the district name input. | |
enable: -> | |
@name_selector.removeAttr('disabled') | |
this.bindName() | |
# This method sets up autocomplete and the listener on the | |
# district name input, and triggers the .get() method when | |
# the user stops typing. | |
bindName: -> | |
self = this | |
@name_selector.autocomplete {source: self.path()}, | |
select: -> | |
self.get() | |
window.clearTimeout( self.timer ) | |
@name_selector.on 'keyup', (event) -> | |
if event.keyCode in S_KEYS | |
self.setStatus 'loading' | |
self.clearID() | |
if self.timer | |
window.clearTimeout( self.timer ) | |
self.timer = window.setTimeout( | |
-> self.get() | |
2000 | |
) | |
# This method queries the server to see if a matching district | |
# for the given city exists. If so it creates the association, | |
# if not it prompts the user to confirm. | |
get: (create) -> | |
self = this | |
unless @name_selector.val() | |
self.setStatus '' | |
return false | |
params = { name: @name_selector.val(), object: @container.data('object'), object_id: @container.data('object-id') } | |
if create == true | |
params["create"] = true | |
$.ajax | |
type: 'POST' | |
url: self.path() | |
dataType: 'json' | |
data: params | |
error: (xhr, status, error) -> | |
self.clearID() | |
self.setStatus 'error', 'Server error, please try again.' | |
success: (data, status, xhr) -> | |
if data.district.new_record | |
self.confirmCreate data.district | |
else | |
self.setStatus 'success' | |
self.name_selector.val( data.district.name ) | |
self.setID data.district.id | |
# This method prompts the user to confirm creation of a new | |
# district record, and resubmits the query with confirmation. | |
confirmCreate: (district) -> | |
self = this | |
message = 'No record for ' +district.name+ ' (' +district.city_name+ ') <a href="#" class="confirm_district button small">Create it?</a>' | |
this.setStatus 'notice', message | |
@container.find('a.confirm_district').click (event) -> | |
event.preventDefault() | |
self.setStatus 'loading' | |
self.get(true) | |
# This method clears the association (for instance if the user | |
# changed the name to something invalid). | |
clearID: -> | |
@id = null | |
# This method records the association. | |
setID: (id) -> | |
@id = id |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 4. CitiesController | |
# The CitiesController responds to requests from the client JS above in the | |
# following actions: | |
class CitiesController < ApplicationController | |
respond_to :html, :json | |
... | |
# This method receives js post requests to /cities and responds to them. | |
def create | |
@city = City.find_or_init_or_create_by_string(params[:location_string],params[:create]) | |
associate_object if @city.valid? && params[:object] | |
respond_with @city | |
end | |
# This method is for jQuery UI autocomplete. | |
def lookup | |
@cities = City.order(:name).where( "name like ?", "%#{params[:term]}%" ) | |
render :json => @cities.map(&:full_name) | |
end | |
private | |
# This method is for handling polymorphism, as the object could be a user | |
# or a city. This is my least-favorite part of the process, I think it | |
# could be more elegant, but it works as expected. | |
def associate_object | |
if params[:object] == 'Photo' && params[:object_id] | |
@city.photos << Photo.find(params[:object_id]) | |
elsif params[:object] == 'User' && params[:object_id] | |
@city.users << User.find(params[:object_id]) | |
end | |
end | |
... | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 5. City JSON Template (rabl) | |
# This is a JSON template for the City object that the Cities Controller | |
# responds with. | |
object @city | |
attributes :id, :name, :state, :country | |
node(:full_name) { |city| city.full_name } | |
node(:new_record) { |city| city.new_record? } | |
if @city.errors.any? | |
node(:errors) { |city| city.errors.full_messages.join(', ') } | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 6. DistrictsController | |
class DistrictsController < ApplicationController | |
before_filter :load_city, :except => :show | |
respond_to :html, :json | |
... | |
# This method responds to Autocomplete. In this case, districts are | |
# shown in the parent city view, so the index template is not needed | |
# for html views as it is in the case of Cities (so no need for a `lookup` action) | |
def index | |
@districts = @city.districts.order(:name).where( "name like ?", "%#{params[:term]}%" ) | |
respond_with @districts do |format| | |
format.json { render :json => @districts.map(&:name) } | |
end | |
end | |
# This responds to JS requests from the location selector. | |
def create | |
@district = @city.districts.find_or_init_or_create_by_name( params[:name], params[:create] ) | |
associate_object if @district.valid? && !@district.new_record? | |
respond_with @district | |
end | |
private | |
# This loads cities that districts are nested under. | |
def load_city | |
@city = City.find( params[:city_id] ) | |
end | |
# This makes the association between a district and a given object. | |
# Again this isn't my favorite part, but it works. | |
def associate_object | |
if params[:object] == 'Photo' && params[:object_id] | |
@district.photos << Photo.find(params[:object_id]) | |
elsif params[:object] == 'User' && params[:object_id] | |
@district.users << User.find(params[:object_id]) | |
end | |
end | |
... | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 7. District JSON Template (rabl) | |
# This is the JSON template the Districts Controller Responds with. | |
object @district | |
attributes :id, :name, :city_id | |
node(:new_record) { |district| district.new_record? } | |
node(:city_name) { |district| district.city.name } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 8. City Model and Spec | |
# This class does a lot, but the specs explain it pretty well so I'll mostly | |
# leave the description there. The main thing is it uses Ruby Geocoder to | |
# make Google Maps API calls to convert strings into locations. | |
class City < Place | |
has_many :districts | |
has_many :photos | |
has_many :users | |
validates_presence_of :name, :country | |
validate :country_code_exists | |
validate :state_code_exists | |
validates_presence_of :state, :if => :north_american? | |
validates_length_of :name, :minimum => 3 | |
validates_uniqueness_of :name, :scope => [:state, :country], :if => :north_american? | |
validates_uniqueness_of :name, :scope => :country, :unless => :north_american? | |
scope :with_photos, includes(:photos).where{ photos.city_id.not_eq nil } | |
scope :without_photos, includes(:photos).where( :photos => {:city_id=>nil} ) | |
extend FriendlyId | |
friendly_id :full_name, :use => :slugged | |
geocoded_by :full_name, :latitude => :lat, :longitude => :lng | |
before_save :geocode unless Rails.env.test? | |
def to_s | |
name | |
end | |
def full_name | |
if north_american? | |
[name, state].join(', ') | |
elsif country.present? | |
[name, Carmen.country_name( country )].join(', ') | |
else | |
nil | |
end | |
end | |
def north_american? | |
['US','CA'].include? country | |
end | |
def self.find_by_string( string ) | |
result = Geocoder.search( string )[0] | |
if result | |
if ['US','CA'].include?(result.country_code) | |
city = self.find_or_initialize_by_name_and_state_and_country( result.city, result.state_code, result.country_code ) | |
else | |
city = self.find_or_initialize_by_name_and_country( result.city, result.country_code ) | |
end | |
else | |
city = City.new | |
end | |
end | |
def self.find_or_init_or_create_by_string( string, create=false ) | |
city = self.find_by_string( string ) | |
if city.new_record? && create | |
city.save | |
end | |
return city | |
end | |
def state=(input) | |
write_attribute(:state, input.to_s.upcase) | |
end | |
def country=(input) | |
write_attribute(:country, input.to_s.upcase) | |
end | |
private | |
def country_code_exists | |
errors.add(:country, "Invalid Country Code") unless Carmen.country_codes.include? country | |
end | |
def state_code_exists | |
list ||= Carmen.state_codes('US') + Carmen.state_codes('CA') | |
if north_american? | |
errors.add(:state, "Invalid State Code") unless list.include? state | |
end | |
end | |
def self.country_for_state( state ) | |
if Carmen.state_codes('US').include? state | |
'US' | |
elsif Carmen.state_codes('CA').include? state | |
'CA' | |
else | |
nil | |
end | |
end | |
def should_generate_new_friendly_id? | |
name.present? && new_record? | |
end | |
end | |
# 9. City Spec | |
# This spec describes the City model. | |
require 'spec_helper' | |
describe City do | |
let(:user) { Fabricate :user } | |
let(:city) { Fabricate :city } | |
let(:houston) { City.new(:name => "Houston", :state => "TX", :country => "US") } | |
let(:toronto) { City.new(:name => "Toronto", :state => "ON", :country => "CA") } | |
let(:london) { City.new(:name => "London", :country => "GB" ) } | |
describe "associations" do | |
it "should have a districts association" do | |
city.districts.should be_empty | |
end | |
it "should have districts" do | |
district = city.districts.create(:name => "Example") | |
district.city.should == city | |
end | |
it "should find only cities with photos" do | |
city1 = Fabricate :city | |
city2 = Fabricate :city | |
photo = Fabricate :photo | |
photo.city = city2 | |
photo.save | |
City.with_photos.include?(city2).should be_true | |
City.with_photos.include?(city1).should_not be_true | |
end | |
end | |
describe "validations" do | |
it "should have only one of the same name per state" do | |
other_city = City.new(:name => city.name, :state => city.state, :country => city.country) | |
other_city.should_not be_valid | |
end | |
it "should only have one of the same name per country outside of north america" do | |
city = City.create(:name => "Barville", :country => "GB") | |
other_city = City.new(:name => "Barville", :country => "GB") | |
other_city.should_not be_valid | |
end | |
it "should require states if the country is US" do | |
city = City.new(:name => "Barville", :country => "US") | |
city.should_not be_valid | |
end | |
it "should require provinces if the country is CA" do | |
city = City.new(:name => "Barville", :country => "CA") | |
city.should_not be_valid | |
end | |
it "should not require states if country is not US or CA" do | |
city = City.new(:name => "Barville", :country => "GB") | |
city.should be_valid | |
end | |
it "should require countries to be the two letter country code" do | |
good_city = City.new(:name => "Barville", :country => "Great Britain") | |
good_city.should_not be_valid | |
end | |
it "should not accept bogus country codes" do | |
bad_city = City.new(:name => "Barville", :country => "ZZ") | |
bad_city.should_not be_valid | |
end | |
end | |
describe "display" do | |
it "should return name when coerced to string" do | |
houston.to_s.should == "Houston" | |
end | |
it "should return full_name like Houston, TX for US cities" do | |
houston.full_name.should == "Houston, TX" | |
end | |
it "should return full_name like Toronto, ON for CA cities" do | |
toronto.full_name.should == "Toronto, ON" | |
end | |
it "should return full_name like London, United Kingdom for international cities" do | |
london.full_name.should == "London, United Kingdom" | |
end | |
end | |
describe "input" do | |
it "should upcase state and city" do | |
city = City.new( :name => "Austin", :state => "tx", :country => "us" ) | |
city.state.should == "TX" | |
city.country.should == "US" | |
end | |
end | |
describe "location" do | |
it "should identify North American cities" do | |
houston.north_american?.should be_true | |
toronto.north_american?.should be_true | |
end | |
it "should identify European cities as not North American" do | |
london.north_american?.should_not be_true | |
end | |
end | |
describe "find by string" do | |
it "should work for major international cities" do | |
city = City.find_by_string("London") | |
city.new_record?.should be_true | |
city.name.should == "London" | |
city.country.should == "GB" | |
end | |
it "should work for minor international cities" do | |
city = City.find_by_string("Castiglion Fiorentino, IT") | |
city.new_record?.should be_true | |
city.name.should == "Castiglion Fiorentino" | |
city.country.should == "IT" | |
end | |
it "should assume the larger city" do | |
city = City.find_by_string("Paris") | |
city.new_record?.should be_true | |
city.name.should == "Paris" | |
city.country.should == "FR" | |
end | |
it "should find smaller cities with help" do | |
city = City.find_by_string("Paris, TX") | |
city.new_record?.should be_true | |
city.name.should == "Paris" | |
city.state.should == "TX" | |
city.country.should == "US" | |
end | |
end | |
describe "states" do | |
it "should ignore state names in europe" do | |
a = City.create :name => "London", :country => "GB" | |
b = City.new :name => "London", :state => "London", :country => "GB" | |
b.valid?.should_not be_true | |
end | |
end | |
describe "find or create by string" do | |
it "should find existing cities when the create arg is false" do | |
a = City.create :name => "London",:country=>"GB" | |
b = City.find_or_init_or_create_by_string("London") | |
b.should == a | |
end | |
it "should find existing cities when the create arg is true" do | |
a = City.create :name => "London",:country=>"GB" | |
b = City.find_or_init_or_create_by_string("London", true) | |
b.should == a | |
end | |
it "should ignore state names in european cities" do | |
a = City.create :name => "London", :state => "LO", :country=>"GB" | |
b = City.find_or_init_or_create_by_string("London") | |
b.should == a | |
end | |
it "should only initialize cities when the create arg is false" do | |
city = City.find_or_init_or_create_by_string("Paris, TX") | |
city.new_record?.should be_true | |
city.name.should == "Paris" | |
city.state.should == "TX" | |
city.country.should == "US" | |
end | |
it "should create cities when the create arg is true" do | |
city = City.find_or_init_or_create_by_string("Paris, TX", true) | |
city.new_record?.should be_false | |
city.name.should == "Paris" | |
city.state.should == "TX" | |
city.country.should == "US" | |
end | |
it "should create cities when the create arg is a true string" do | |
city = City.find_or_init_or_create_by_string("Paris, TX", "true") | |
city.new_record?.should be_false | |
city.name.should == "Paris" | |
city.state.should == "TX" | |
city.country.should == "US" | |
end | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 9. District Model and Spec | |
# This model is like City but simpler, it mostly just holds a name | |
# for neighborhoods. It is also described by its spec. | |
class District < Place | |
belongs_to :city | |
has_many :photos | |
has_many :users | |
validates_presence_of :city | |
validates_uniqueness_of :name, :scope => :city_id | |
validates_length_of :name, :minimum => 3 | |
scope :with_photos, includes(:photos).where{ photos.district_id.not_eq nil } | |
scope :without_photos, includes(:photos).where( :photos => {:district_id=>nil} ) | |
extend FriendlyId | |
friendly_id :name, :use => :scoped, :scope => :city | |
def to_s | |
name | |
end | |
def name=(input) | |
write_attribute(:name, input.to_s.titleize) | |
end | |
def self.find_or_init_or_create_by_name(name, create=false) | |
safe_name = name.to_s.titleize #For whatever reason the find or initialize function doesn't use the name= method. | |
district = self.find_or_initialize_by_name(safe_name) | |
if district.new_record? && create | |
district.save | |
end | |
return district | |
end | |
end | |
# This is the limit of how many files you can put in a Gist, so here is the | |
# last part, this is a spec to describe Districts. | |
require 'spec_helper' | |
describe District do | |
let(:district) { Fabricate :district } | |
let(:city) { Fabricate :city } | |
context "find, init, or create" do | |
it "should find existing records by name" do | |
city = district.city | |
dist2 = city.districts.find_or_init_or_create_by_name( district.name ) | |
dist2.should == district | |
end | |
it "should initialize new records" do | |
dist = city.districts.find_or_init_or_create_by_name "Fooville" | |
dist.name.should == "Fooville" | |
dist.new_record?.should be_true | |
end | |
it "should create new records" do | |
dist = city.districts.find_or_init_or_create_by_name "Fooville", true | |
dist.name.should == "Fooville" | |
dist.new_record?.should be_false | |
end | |
it "should create new records with create arg passed by string" do | |
dist = city.districts.find_or_init_or_create_by_name "Fooville", "true" | |
dist.name.should == "Fooville" | |
dist.new_record?.should be_false | |
end | |
it "should not find a district with the same name and different city" do | |
city1 = Fabricate :city | |
dist1 = city1.districts.create(:name => "Barton") | |
city2 = Fabricate :city | |
dist2 = city2.districts.find_or_init_or_create_by_name "Barton" | |
dist2.new_record?.should be_true | |
dist2.should_not == dist1 | |
end | |
end | |
context "names" do | |
it "should capitalize a one-word name on input" do | |
d = District.new :name => 'test' | |
d.name.should == 'Test' | |
end | |
it "should capitalize a two-word name on input" do | |
d = District.new :name => 'another test' | |
d.name.should == 'Another Test' | |
end | |
it "should find an existing district no matter the capitalization" do | |
d1 = city.districts.create :name => "Example" | |
d2 = city.districts.find_or_init_or_create_by_name "example" | |
d2.should == d1 | |
end | |
end | |
end | |
# So, that's the full-stack of code for the location_selector widget. | |
# There are also stylesheets (.scss) and images that indicate status | |
# but that's pretty basic stuff. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment