Permalink
Browse files

halfway rework of SortedHierarchy, its tests, and dependants; taxonom…

…y may be broken; introduced shoulda and factories for testing
  • Loading branch information...
1 parent d16e306 commit 40aecfba29acae41abe948c6cec29f2bab157de4 @khustochka committed Jan 21, 2010
View
4 app/controllers/taxonomy/taxa_controller.rb
@@ -63,7 +63,7 @@ def create
@taxon = model_class.new(params[model_sym])
respond_to do |format|
- @taxon.insert_mind_sorting
+ @taxon.save!
flash[:notice] = "#{model_name.humanize} was successfully created."
format.html { redirect_to @taxon, :action => :edit } # need to redirect to edit for species
#format.xml { render :xml => @taxon, :status => :created, :location => @taxon }
@@ -83,7 +83,7 @@ def update
def destroy
- @taxon.destroy_mind_sorting
+ @taxon.destroy
respond_to do |format|
format.html { redirect_to :action => "index" }
View
1 app/models/taxonomy/familia.rb
@@ -1,7 +1,6 @@
class Familia < Taxon
validates_format_of :name_la, :with => /^[A-Z][a-z]+dae$/
- validates_uniqueness_of :sort, :scope => :ordo_id
child_of :ordo
View
1 app/models/taxonomy/ordo.rb
@@ -1,7 +1,6 @@
class Ordo < Taxon
validates_format_of :name_la, :with => /^[A-Z][a-z]+formes$/
- validates_uniqueness_of :sort
parent_for :familiae, :order => "sort"
View
1 app/models/taxonomy/species.rb
@@ -3,7 +3,6 @@ class Species < Taxon
validates_format_of :code, :with => /^[a-z]{6}$/
validates_format_of :name_la, :with => /^[A-Z][a-z]+ [a-z]+$/
validates_uniqueness_of :code
- validates_uniqueness_of :sort, :scope => :familia_id
child_of :familia
View
4 app/models/taxonomy/taxon.rb
@@ -1,10 +1,10 @@
class Taxon < ActiveRecord::Base
- include ActiveRecord::SortedHierarchy
+ include SortedHierarchy::ActiveRecord
self.abstract_class = true
- validates_presence_of :name_la, :name_ru, :name_uk, :sort
+ validates_presence_of :name_la, :name_ru, :name_uk
validates_uniqueness_of :name_la, :name_ru, :name_uk
# Class methods
View
4 config/initializers/class_extensions.rb → config/initializers/load_libs.rb
@@ -11,6 +11,6 @@
require File.join(Rails.root, "lib/rails_ext/action_view/#{inc_ext}")
end
-%w(sorted_hierarchy).each do |inc_ext|
- require File.join(Rails.root, "lib/rails_ext/sorted_hierarchy/#{inc_ext}")
+%w(active_record).each do |inc_ext|
+ require File.join(Rails.root, "lib/sorted_hierarchy/#{inc_ext}")
end
View
63 ..._ext/sorted_hierarchy/sorted_hierarchy.rb → lib/sorted_hierarchy/active_record.rb
@@ -1,11 +1,15 @@
-module ActiveRecord
- module SortedHierarchy
+module SortedHierarchy
+ module ActiveRecord
def self.included(klass)
klass.extend ClassMethods
+ klass.validate :correctness_of_sort_value
+ klass.before_create :give_way_to_create
+ klass.after_destroy :fix_gap_after_destroy
end
module ClassMethods
+
def parent_for(association_id, options = {}, &extension)
has_many(association_id, options, &extension)
write_inheritable_hash :reflections, :children => read_inheritable_attribute(:reflections)[association_id]
@@ -65,24 +69,10 @@ def bottom_level?
self.class.bottom_level?
end
- def insert_mind_sorting
- latest = (self.top_level? ? self.class.count : self.parent.children.size) + 1
- self[get_sort_column] = latest if self[get_sort_column] > latest || self[get_sort_column] == 0
- conditions = ["#{get_sort_column} >= #{self[get_sort_column]}"]
- conditions.push(scope_condition) unless self.top_level?
- self.class.transaction do
- self.class.update_all("#{get_sort_column} = #{get_sort_column} + 1", conditions.join(" AND "))
- save!
- end
- end
-
def update_mind_sorting(attributes)
latest = self.top_level? ? self.class.count : self.parent.children.size
current = attributes[get_sort_column].to_i
- new_sort = attributes[get_sort_column] =
- current > latest || current == 0 ?
- latest :
- current
+ new_sort = attributes[get_sort_column] = latest if current.nil?
old_sort = self[get_sort_column].to_i
self.class.transaction do
@@ -98,20 +88,41 @@ def update_mind_sorting(attributes)
end
end
- def destroy_mind_sorting
- conditions = ["#{get_sort_column.to_s} > #{self[get_sort_column]}"]
- conditions.push(scope_condition) unless self.top_level?
- self.class.transaction do
- destroy
- self.class.update_all("#{get_sort_column} = #{get_sort_column} - 1", conditions.join(" AND "))
- end
- end
-
private
def scope_condition
fk = self.class.parent_key
"#{fk} = #{self[fk]}"
end
+ def correctness_of_sort_value
+ raw_value = send("#{get_sort_column}_before_type_cast")
+ unless raw_value.nil?
+ unless raw_value.to_s =~ /\A[+-]?\d+\Z/
+ errors.add(get_sort_column, :not_a_number, :value => raw_value)
+ else
+ latest = (self.top_level? ? self.class.count : self.parent.children.size) + (new_record? ? 1 : 0)
+ if self[get_sort_column] > latest
+ errors.add(get_sort_column, :less_than_or_equal_to, :value => raw_value, :count => latest)
+ end
+ end
+ end
+ end
+
+ def give_way_to_create
+ latest = (self.top_level? ? self.class.count : self.parent.children.size) + 1
+ self[get_sort_column] ||= latest
+ if self[get_sort_column] < latest
+ conditions = ["#{get_sort_column} >= #{self[get_sort_column]}"]
+ conditions.push(scope_condition) unless self.top_level?
+ self.class.update_all("#{get_sort_column} = #{get_sort_column} + 1", conditions.join(" AND "))
+ end
+ end
+
+ def fix_gap_after_destroy
+ conditions = ["#{get_sort_column.to_s} > #{self[get_sort_column]}"]
+ conditions.push(scope_condition) unless self.top_level?
+ self.class.update_all("#{get_sort_column} = #{get_sort_column} - 1", conditions.join(" AND "))
+ end
+
end
end
View
14 lib/tasks/db_test_prepare.rake
@@ -0,0 +1,14 @@
+# Taken from http://github.com/look/fixie
+namespace :db do
+ namespace :test do
+ desc 'Causes db:test:prepare to also run the fixture creation files in test/fixtures'
+ # Somewhat obscure(?) fact: if you create a rake task with the
+ # same name as another one (in this case test:db:prepare), it will
+ # be run after the first one. That's how this works.
+ task :prepare do
+ RAILS_ENV = 'test'
+ load 'test/unit/sorted_hierarchy/schema.rb'
+ Dir[File.join(RAILS_ROOT, 'test', 'fixtures', '*.rb')].sort.each { |fixture| load fixture }
+ end
+ end
+end
View
5 test/fixtures/cities_districts_streets.rb
@@ -0,0 +1,5 @@
+require 'factory_girl'
+require 'test/unit/sorted_hierarchy/model'
+require 'test/unit/sorted_hierarchy/factories'
+
+5.times { Factory.build(:city).save! }
View
38 test/test_helper.rb
@@ -1,8 +1,12 @@
-ENV["RAILS_ENV"] = "test"
-require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
+ENV['RAILS_ENV'] = 'test'
+require 'config/environment'
require 'test_help'
+require 'shoulda'
+require 'spec'
+require 'factory_girl'
class ActiveSupport::TestCase
+ include Spec::Matchers
# Transactional fixtures accelerate your tests by wrapping each test method
# in a transaction that's rolled back on completion. This ensures that the
# test database remains unchanged so your fixtures don't have to be reloaded
@@ -26,31 +30,35 @@ class ActiveSupport::TestCase
# test cases which use the @david style and don't mind the speed hit (each
# instantiated fixtures translates to a database query per test method),
# then set this back to true.
- self.use_instantiated_fixtures = false
+ self.use_instantiated_fixtures = false
# Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in alphabetical order.
#
# Note: You'll currently still have to declare fixtures explicitly in integration tests
# -- they do not yet inherit this setting
- fixtures :all
+# fixtures :all
# Add more helper methods to be used by all tests here...
- def assert_sorting_preserved(klass)
- if klass.top_level?
- assert_equal( (1..klass.count).to_a, klass.all(:order => :sort).map {|item| item[:sort] }, "Sorting invalid" )
- else
- klass.parent_class.all(:order => :sort, :include => :children).each do |parent|
- assert_equal( (1..parent.children.size).to_a, parent.children.map {|item| item[:sort] }, "Sorting invalid" )
- end
- end
- end
-
- def http_auth
+ def http_auth
unless CONFIG[:open_access]
session[CONFIG[:admin_session_ask].to_sym] = CONFIG[:admin_session_reply]
@request.env["HTTP_AUTHORIZATION"] = "Basic #{Base64.encode64("#{CONFIG[:admin_username]}:#{CONFIG[:admin_password]}")}"
end
end
+ def be_sorted
+ simple_matcher do |klass|
+ sort_column = klass.get_sort_column
+ if klass.top_level?
+ klass.all(:select => sort_column, :order => sort_column).map {|item| item[sort_column] }.should == Array(1..klass.count)
+ else
+ klass.parent_class.all(:order => sort_column, :include => :children).each do |parent|
+ parent.children.map {|item| item[sort_column] }.should == Array(1..parent.children.size)
+ end
+ end
+ end
+ end
+
+
end
View
6 test/unit/helpers/test_helper_test.rb
@@ -1,9 +1,5 @@
require 'test/test_helper'
class TestHelperTest < ActionView::TestCase
- test "assert_sorting_preserved helper is valid" do
- assert_sorting_preserved(Ordo)
- assert_sorting_preserved(Familia)
- assert_sorting_preserved(Species)
- end
+
end
View
4 test/unit/sorted_hierarchy/factories.rb
@@ -0,0 +1,4 @@
+Factory.define :city do |city|
+ city.name 'Brovary'
+ city.sort nil
+end
View
19 test/unit/sorted_hierarchy/model.rb
@@ -0,0 +1,19 @@
+class AdministrativeUnit < ActiveRecord::Base
+ include SortedHierarchy::ActiveRecord
+ self.abstract_class = true
+end
+
+class City < AdministrativeUnit
+ parent_for :districts
+end
+
+class District < AdministrativeUnit
+ parent_for :streets
+ child_of :city
+ set_sort_column :sort_num
+end
+
+class Street < ActiveRecord::Base
+ include SortedHierarchy::ActiveRecord
+ child_of :city
+end
View
19 test/unit/sorted_hierarchy/schema.rb
@@ -0,0 +1,19 @@
+ActiveRecord::Schema.define(:version => 1) do
+
+ create_table :cities do |t|
+ t.column :name, :string, :nil => false
+ t.column :sort, :integer, :nil => false
+ end
+
+ create_table :districts do |t|
+ t.column :name, :string, :nil => false
+ t.column :city_id, :integer, :nil => false
+ t.column :sort_num, :integer, :nil => false
+ end
+
+ create_table :streets do |t|
+ t.column :name, :string, :nil => false
+ t.column :district_id, :integer, :nil => false
+ t.column :sort, :integer, :nil => false
+ end
+end
View
3 test/unit/sorted_hierarchy/test_helper.rb
@@ -0,0 +1,3 @@
+require 'test/test_helper'
+require 'test/unit/sorted_hierarchy/model'
+require 'test/unit/sorted_hierarchy/factories'
View
52 test/unit/sorted_hierarchy/top_level_test.rb
@@ -0,0 +1,52 @@
+require 'test/unit/sorted_hierarchy/test_helper'
+
+class SortedHierarchyTopLevelTest < ActiveSupport::TestCase
+ context "Top level object" do
+
+ setup do
+ City.count.should == 5
+ City.should be_sorted
+ end
+
+ should "be created successfully with sort = 3" do
+ @new_city = Factory.build(:city, :sort => 3)
+ lambda {
+ @new_city.save!
+ }.should change(City, :count).by(1)
+ @new_city.sort.should == 3
+ City.should be_sorted
+ end
+
+ should "be created successfully with sort = nil" do
+ @new_city = Factory.build(:city)
+ lambda {
+ @new_city.save!
+ }.should change(City, :count).by(1)
+ @new_city.sort.should == City.count
+ City.should be_sorted
+ end
+
+ should "not be created with invalid sort" do
+ lambda {
+ Factory.build(:city, :sort => "first").save!
+ }.should raise_exception ActiveRecord::RecordInvalid
+ City.should be_sorted
+ end
+
+ should "not be created with sort too large" do
+ lambda {
+ Factory.build(:city, :sort => 56).save!
+ }.should raise_exception ActiveRecord::RecordInvalid
+ City.should be_sorted
+ end
+
+ should "preserve sorting when destroyed" do
+ @old_city = City.find_by_sort(3)
+ lambda {
+ @old_city.destroy
+ }.should change(City, :count).by(-1)
+ City.should be_sorted
+ end
+ end
+
+end

0 comments on commit 40aecfb

Please sign in to comment.