acts_as_flashcard 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/GNU-LICENCE +676 -0
- data/Gemfile +17 -0
- data/README.rdoc +37 -0
- data/Rakefile +47 -0
- data/VERSION +1 -0
- data/acts_as_flashcard.gemspec +70 -0
- data/install.rb +1 -0
- data/lib/acts_as_flashcard.rb +201 -0
- data/lib/generators/flashcard/USAGE +5 -0
- data/lib/generators/flashcard/flashcard_generator.rb +25 -0
- data/lib/generators/flashcard/templates/add_flashcards_fields_migration.rb +27 -0
- data/lib/generators/flashcard/templates/create_flashcards_migration.rb +20 -0
- data/tasks/acts_as_flashcard_tasks.rake +4 -0
- data/test/acts_as_flashcard_test.rb +130 -0
- data/test/flashcards_migration_generator_test.rb +49 -0
- data/test/test_helper.rb +21 -0
- data/uninstall.rb +1 -0
- metadata +138 -0
data/Gemfile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
# Add dependencies required to use your gem here.
|
3
|
+
# Example:
|
4
|
+
# gem "activesupport", ">= 2.3.5"
|
5
|
+
gem "activerecord"
|
6
|
+
# Add dependencies to develop your gem here.
|
7
|
+
# Include everything needed to run rake, tests, features, etc.
|
8
|
+
group :development do
|
9
|
+
gem "bundler", "~> 1.0.0"
|
10
|
+
gem "jeweler", "~> 1.5.1"
|
11
|
+
end
|
12
|
+
|
13
|
+
group :test do
|
14
|
+
gem "shoulda"
|
15
|
+
gem "sqlite3-ruby"
|
16
|
+
gem "railties"
|
17
|
+
end
|
data/README.rdoc
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
= acts_as_flashcard
|
2
|
+
|
3
|
+
acts_as_flashcard is an adaptation of the Anki ( http://ichi2.net/anki/ ) algorithm for ruby on rails application.
|
4
|
+
|
5
|
+
== Requirements
|
6
|
+
|
7
|
+
Rails3 (for Rails2 version please check the corresponding branches)
|
8
|
+
Has been tested with ruby 1.9.1
|
9
|
+
|
10
|
+
== Installation
|
11
|
+
|
12
|
+
gem install acts_as_flashcard
|
13
|
+
|
14
|
+
== Example
|
15
|
+
|
16
|
+
class Card < ActiveRecord::Base
|
17
|
+
acts_as_flashcard :scope => :user_id
|
18
|
+
end
|
19
|
+
|
20
|
+
card = Card.create
|
21
|
+
card.answered!(4)
|
22
|
+
|
23
|
+
== Contributing to acts_as_flashcard
|
24
|
+
|
25
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
26
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
27
|
+
* Fork the project
|
28
|
+
* Start a feature/bugfix branch
|
29
|
+
* Commit and push until you are happy with your contribution
|
30
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
31
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
32
|
+
|
33
|
+
== Copyright
|
34
|
+
|
35
|
+
Copyright (c) 2010 Bastien Vaucher. See LICENSE.txt for
|
36
|
+
further details.
|
37
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
require 'rake'
|
11
|
+
|
12
|
+
require 'jeweler'
|
13
|
+
Jeweler::Tasks.new do |gem|
|
14
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
15
|
+
gem.name = "acts_as_flashcard"
|
16
|
+
gem.homepage = "http://github.com/bastien/acts_as_flashcard"
|
17
|
+
gem.license = "GNU"
|
18
|
+
gem.summary = "flashcard space repetition algorithm"
|
19
|
+
gem.description = "act_as_flashcard is an adaptation of the Anki space repetition algorithm for rails applications"
|
20
|
+
gem.email = "bastien.vaucher@gmail.com"
|
21
|
+
gem.authors = ["Bastien Vaucher"]
|
22
|
+
gem.add_dependency 'activerecord'
|
23
|
+
# Include your dependencies below. Runtime dependencies are required when using your gem,
|
24
|
+
# and development dependencies are only needed for development (ie running rake tasks, tests, etc)
|
25
|
+
# gem.add_runtime_dependency 'jabber4r', '> 0.1'
|
26
|
+
# gem.add_development_dependency 'rspec', '> 1.2.3'
|
27
|
+
end
|
28
|
+
Jeweler::RubygemsDotOrgTasks.new
|
29
|
+
|
30
|
+
require 'rake/testtask'
|
31
|
+
Rake::TestTask.new(:test) do |test|
|
32
|
+
test.libs << 'lib' << 'test'
|
33
|
+
test.pattern = 'test/**/test_*.rb'
|
34
|
+
test.verbose = true
|
35
|
+
end
|
36
|
+
|
37
|
+
task :default => :test
|
38
|
+
|
39
|
+
require 'rake/rdoctask'
|
40
|
+
Rake::RDocTask.new do |rdoc|
|
41
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
42
|
+
|
43
|
+
rdoc.rdoc_dir = 'rdoc'
|
44
|
+
rdoc.title = "acts_as_flashcard #{version}"
|
45
|
+
rdoc.rdoc_files.include('README*')
|
46
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
47
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{acts_as_flashcard}
|
8
|
+
s.version = "0.1.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Bastien Vaucher"]
|
12
|
+
s.date = %q{2010-11-26}
|
13
|
+
s.description = %q{act_as_flashcard is an adaptation of the Anki space repetition algorithm for rails applications}
|
14
|
+
s.email = %q{bastien.vaucher@gmail.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"README.rdoc"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
"GNU-LICENCE",
|
20
|
+
"Gemfile",
|
21
|
+
"README.rdoc",
|
22
|
+
"Rakefile",
|
23
|
+
"VERSION",
|
24
|
+
"acts_as_flashcard.gemspec",
|
25
|
+
"install.rb",
|
26
|
+
"lib/acts_as_flashcard.rb",
|
27
|
+
"lib/generators/flashcard/USAGE",
|
28
|
+
"lib/generators/flashcard/flashcard_generator.rb",
|
29
|
+
"lib/generators/flashcard/templates/add_flashcards_fields_migration.rb",
|
30
|
+
"lib/generators/flashcard/templates/create_flashcards_migration.rb",
|
31
|
+
"tasks/acts_as_flashcard_tasks.rake",
|
32
|
+
"test/acts_as_flashcard_test.rb",
|
33
|
+
"test/flashcards_migration_generator_test.rb",
|
34
|
+
"test/test_helper.rb",
|
35
|
+
"uninstall.rb"
|
36
|
+
]
|
37
|
+
s.homepage = %q{http://github.com/bastien/acts_as_flashcard}
|
38
|
+
s.licenses = ["GNU"]
|
39
|
+
s.require_paths = ["lib"]
|
40
|
+
s.rubygems_version = %q{1.3.7}
|
41
|
+
s.summary = %q{flashcard space repetition algorithm}
|
42
|
+
s.test_files = [
|
43
|
+
"test/acts_as_flashcard_test.rb",
|
44
|
+
"test/flashcards_migration_generator_test.rb",
|
45
|
+
"test/test_helper.rb"
|
46
|
+
]
|
47
|
+
|
48
|
+
if s.respond_to? :specification_version then
|
49
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
50
|
+
s.specification_version = 3
|
51
|
+
|
52
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
53
|
+
s.add_runtime_dependency(%q<activerecord>, [">= 0"])
|
54
|
+
s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
|
55
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.5.1"])
|
56
|
+
s.add_runtime_dependency(%q<activerecord>, [">= 0"])
|
57
|
+
else
|
58
|
+
s.add_dependency(%q<activerecord>, [">= 0"])
|
59
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
60
|
+
s.add_dependency(%q<jeweler>, ["~> 1.5.1"])
|
61
|
+
s.add_dependency(%q<activerecord>, [">= 0"])
|
62
|
+
end
|
63
|
+
else
|
64
|
+
s.add_dependency(%q<activerecord>, [">= 0"])
|
65
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
66
|
+
s.add_dependency(%q<jeweler>, ["~> 1.5.1"])
|
67
|
+
s.add_dependency(%q<activerecord>, [">= 0"])
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
data/install.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Install hook code here
|
@@ -0,0 +1,201 @@
|
|
1
|
+
#ActsAsFlashcard
|
2
|
+
module Flashcard #:nodoc:
|
3
|
+
module Base #:nodoc:
|
4
|
+
|
5
|
+
def self.included(base) #:nodoc:
|
6
|
+
base.extend ClassMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
MAX_SCHEDULE_TIME = 1825
|
10
|
+
INITIAL_FACTOR = 2.5
|
11
|
+
EASY_INTERVAL_MIN = 7
|
12
|
+
EASY_INTERVAL_MAX = 9
|
13
|
+
MID_INTERVAL_MIN = 3
|
14
|
+
MID_INTERVAL_MAX = 5
|
15
|
+
HARD_INTERVAL_MIN = 0.333
|
16
|
+
HARD_INTERVAL_MAX = 0.5
|
17
|
+
DELAY = 600
|
18
|
+
FACTOR_FOUR = 1.3
|
19
|
+
|
20
|
+
# successive :integer default(0)
|
21
|
+
# repetitions :integer default(0)
|
22
|
+
# yes_count :integer default(0)
|
23
|
+
# no_count :integer default(0)
|
24
|
+
# factor :float default(2.5)
|
25
|
+
# last_factor :float default(2.5)
|
26
|
+
# interval :integer default(0)
|
27
|
+
# last_interval :float default(0.0)
|
28
|
+
# due :datetime
|
29
|
+
# combined_due :integer default(0)
|
30
|
+
|
31
|
+
module ClassMethods
|
32
|
+
|
33
|
+
def acts_as_flashcard(options = {})
|
34
|
+
configuration = {:scope => "1 = 1"}
|
35
|
+
configuration.update(options) if options.is_a?(Hash)
|
36
|
+
|
37
|
+
configuration[:scope] = "#{configuration[:scope]}_id".to_sym if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
|
38
|
+
|
39
|
+
if configuration[:scope].is_a?(Symbol)
|
40
|
+
scope_condition_method = %(
|
41
|
+
def scope_condition
|
42
|
+
if #{configuration[:scope].to_s}.nil?
|
43
|
+
"#{configuration[:scope].to_s} IS NULL"
|
44
|
+
else
|
45
|
+
"#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
)
|
49
|
+
else
|
50
|
+
scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end"
|
51
|
+
end
|
52
|
+
|
53
|
+
class_eval <<-EOV
|
54
|
+
include Flashcard::Base::InstanceMethods
|
55
|
+
|
56
|
+
def acts_as_flashcard_class
|
57
|
+
::#{self.name}
|
58
|
+
end
|
59
|
+
|
60
|
+
#{scope_condition_method}
|
61
|
+
|
62
|
+
before_create :initialize_due_date
|
63
|
+
EOV
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
module InstanceMethods
|
68
|
+
|
69
|
+
# ease: 0 - 4
|
70
|
+
#
|
71
|
+
def answered!(ease)
|
72
|
+
increment_repetitions(ease)
|
73
|
+
last_delay_secs = (Time.now - self.combined_due).to_f
|
74
|
+
last_delay = last_delay_secs / 86400.0
|
75
|
+
self.interval = next_interval(ease) unless last_delay >= 0 # keep last interval if reviewing early
|
76
|
+
last_due = self.due
|
77
|
+
self.due = next_due(ease)
|
78
|
+
update_factor(ease) if last_delay >= 0
|
79
|
+
min_of_other_cards = acts_as_flashcard_class.minimum('interval', :conditions => "#{scope_condition} AND id != #{self.id}")
|
80
|
+
space = min_of_other_cards.nil? ? 0 : [min_of_other_cards, self.interval].min
|
81
|
+
space = space * 0.1.day
|
82
|
+
space = [space, 60].max
|
83
|
+
space = Time.now + space
|
84
|
+
self.combined_due = [space, due].max
|
85
|
+
save
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
# Set the due date to today when the card is created
|
91
|
+
#
|
92
|
+
def initialize_due_date
|
93
|
+
self.due = Time.now
|
94
|
+
end
|
95
|
+
|
96
|
+
# Increments the count of time this card has been reviewed,
|
97
|
+
# How many times it hasn't been answered wrong successively
|
98
|
+
# as well as the amount of time it has been answered right and wrong
|
99
|
+
#
|
100
|
+
def increment_repetitions(ease)
|
101
|
+
self.repetitions += 1
|
102
|
+
if ease > 1
|
103
|
+
self.successive += 1
|
104
|
+
else
|
105
|
+
self.successive = 0
|
106
|
+
end
|
107
|
+
if self.repetitions > 1
|
108
|
+
if ease < 2
|
109
|
+
self.no_count += 1
|
110
|
+
else
|
111
|
+
self.yes_count += 1
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def delay
|
117
|
+
return 0 if repetitions == 0
|
118
|
+
if self.combined_due <= Time.now.to_f
|
119
|
+
return Time.now - self.due
|
120
|
+
else
|
121
|
+
return Time.now - self.combined_due
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def next_interval(ease)
|
126
|
+
current_delay = delay.to_i
|
127
|
+
current_interval = self.interval
|
128
|
+
if current_delay < 0 && self.successive > 0
|
129
|
+
current_interval = [self.last_interval, self.interval + current_delay].max
|
130
|
+
if current_interval < MID_INTERVAL_MIN
|
131
|
+
current_interval = 0
|
132
|
+
end
|
133
|
+
current_delay = 0
|
134
|
+
end
|
135
|
+
if ease == 1
|
136
|
+
current_interval *= DELAY
|
137
|
+
current_interval = 0 if current_interval < HARD_INTERVAL_MIN
|
138
|
+
elsif current_interval == 0
|
139
|
+
case ease
|
140
|
+
when 2 then current_interval = rand(HARD_INTERVAL_MAX - HARD_INTERVAL_MIN) + HARD_INTERVAL_MIN
|
141
|
+
when 3 then current_interval = rand(MID_INTERVAL_MAX - MID_INTERVAL_MIN) + MID_INTERVAL_MIN
|
142
|
+
when 4 then current_interval = rand(EASY_INTERVAL_MAX - EASY_INTERVAL_MIN) + EASY_INTERVAL_MIN
|
143
|
+
end
|
144
|
+
else
|
145
|
+
if current_interval < HARD_INTERVAL_MAX && current_interval > 0.166
|
146
|
+
mid = MID_INTERVAL_MIN + MID_INTERVAL_MAX / 2
|
147
|
+
current_interval *= (mid / current_interval / self.factor)
|
148
|
+
case ease
|
149
|
+
when 2 then current_interval = (current_interval + current_delay/4) * 1.2
|
150
|
+
when 3 then current_interval = (current_interval + current_delay/2) * self.factor
|
151
|
+
when 4 then current_interval = (current_interval + current_delay) * self.factor * FACTOR_FOUR
|
152
|
+
end
|
153
|
+
fuzz = (rand(10) + 95).to_f / 100.0
|
154
|
+
current_interval *= fuzz
|
155
|
+
end
|
156
|
+
end
|
157
|
+
return [MAX_SCHEDULE_TIME, current_interval].min
|
158
|
+
end
|
159
|
+
|
160
|
+
# Average factor among all the cards
|
161
|
+
#
|
162
|
+
def average_factor
|
163
|
+
avg_factor = acts_as_flashcard_class.average("factor", :conditions => "#{scope_condition} AND id != #{self.id}")
|
164
|
+
return avg_factor ||= INITIAL_FACTOR
|
165
|
+
end
|
166
|
+
|
167
|
+
def update_factor(ease)
|
168
|
+
self.last_factor = self.factor
|
169
|
+
if repetitions == 0
|
170
|
+
self.factor = averageFactor
|
171
|
+
end
|
172
|
+
if being_learnt? && ([0, 1, 2].include?(ease))
|
173
|
+
self.factor -= 0.20 if (self.successive > 0) && (ease != 2)
|
174
|
+
else
|
175
|
+
case ease
|
176
|
+
when 0..1 then self.factor -= 0.20
|
177
|
+
when 2 then self.factor -= 0.15
|
178
|
+
when 4 then self.factor += 0.10
|
179
|
+
end
|
180
|
+
end
|
181
|
+
self.factor = [1.3, self.factor].max
|
182
|
+
end
|
183
|
+
|
184
|
+
def next_due(ease)
|
185
|
+
if ease == 1
|
186
|
+
return Time.now + DELAY
|
187
|
+
else
|
188
|
+
return Time.now + self.interval.day
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def being_learnt?
|
193
|
+
self.interval < EASY_INTERVAL_MIN
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
197
|
+
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
::ActiveRecord::Base.send :include, Flashcard::Base
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/migration'
|
3
|
+
|
4
|
+
class FlashcardGenerator < Rails::Generators::NamedBase
|
5
|
+
include Rails::Generators::Migration
|
6
|
+
|
7
|
+
def self.source_root
|
8
|
+
File.join(File.dirname(__FILE__), 'templates')
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.next_migration_number(dirname)
|
12
|
+
Time.now.strftime("%Y%m%d%H%m%s")
|
13
|
+
end
|
14
|
+
|
15
|
+
def install_flashcards
|
16
|
+
if File.exists?("#{destination_root}/db/schema.rb") &&
|
17
|
+
File.read("#{destination_root}/db/schema.rb") =~ /create_table \"#{table_name}\"/
|
18
|
+
migration_template("add_flashcards_fields_migration.rb","db/migrate/add_flashcards_fields.rb")
|
19
|
+
else
|
20
|
+
migration_template("create_flashcards_migration.rb","db/migrate/create_flashcards.rb")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class <%= @migration_class_name %> < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
add_column :<%= table_name %>, :successive, :integer, :default => 0
|
4
|
+
add_column :<%= table_name %>, :repetitions, :integer, :default => 0
|
5
|
+
add_column :<%= table_name %>, :yes_count, :integer, :default => 0
|
6
|
+
add_column :<%= table_name %>, :no_count, :integer, :default => 0
|
7
|
+
add_column :<%= table_name %>, :factor, :float, :default => 2.5
|
8
|
+
add_column :<%= table_name %>, :last_factor, :float, :default => 2.5
|
9
|
+
add_column :<%= table_name %>, :interval, :integer, :default => 0
|
10
|
+
add_column :<%= table_name %>, :last_interval,:float, :default => 0
|
11
|
+
add_column :<%= table_name %>, :due, :datetime
|
12
|
+
add_column :<%= table_name %>, :combined_due, :integer, :default => 0
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.down
|
16
|
+
remove_column :<%= table_name %>, :successive
|
17
|
+
remove_column :<%= table_name %>, :repetitions
|
18
|
+
remove_column :<%= table_name %>, :yes_count
|
19
|
+
remove_column :<%= table_name %>, :no_count
|
20
|
+
remove_column :<%= table_name %>, :factor
|
21
|
+
remove_column :<%= table_name %>, :last_factor
|
22
|
+
remove_column :<%= table_name %>, :interval
|
23
|
+
remove_column :<%= table_name %>, :last_interval
|
24
|
+
remove_column :<%= table_name %>, :due
|
25
|
+
remove_column :<%= table_name %>, :combined_due
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class <%= @migration_class_name %> < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table "<%= table_name %>" do |t|
|
4
|
+
t.column :successive, :integer, :default => 0
|
5
|
+
t.column :repetitions, :integer, :default => 0
|
6
|
+
t.column :yes_count, :integer, :default => 0
|
7
|
+
t.column :no_count, :integer, :default => 0
|
8
|
+
t.column :factor, :float, :default => 2.5
|
9
|
+
t.column :last_factor, :float, :default => 2.5
|
10
|
+
t.column :interval, :integer, :default => 0
|
11
|
+
t.column :last_interval,:float, :default => 0
|
12
|
+
t.column :due, :datetime
|
13
|
+
t.column :combined_due, :integer, :default => 0
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.down
|
18
|
+
drop_table "<%= table_name %>"
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper.rb'
|
2
|
+
|
3
|
+
$:.unshift File.dirname(__FILE__) + "/../lib"
|
4
|
+
#require File.dirname(__FILE__) + "/../init"
|
5
|
+
|
6
|
+
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
|
7
|
+
ActiveRecord::Migration.verbose = false
|
8
|
+
|
9
|
+
def setup_db
|
10
|
+
ActiveRecord::Schema.define(:version => 1) do
|
11
|
+
create_table :cards do |t|
|
12
|
+
t.column :successive, :integer, :default => 0
|
13
|
+
t.column :repetitions, :integer, :default => 0
|
14
|
+
t.column :yes_count, :integer, :default => 0
|
15
|
+
t.column :no_count, :integer, :default => 0
|
16
|
+
t.column :factor, :float, :default => 2.5
|
17
|
+
t.column :last_factor, :float, :default => 2.5
|
18
|
+
t.column :interval, :integer, :default => 0
|
19
|
+
t.column :last_interval, :float, :default => 0
|
20
|
+
t.column :due, :datetime
|
21
|
+
t.column :combined_due, :integer, :default => 0
|
22
|
+
t.column :created_at, :datetime
|
23
|
+
t.column :updated_at, :datetime
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def teardown_db
|
29
|
+
ActiveRecord::Base.connection.tables.each do |table|
|
30
|
+
ActiveRecord::Base.connection.drop_table(table)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Card < ActiveRecord::Base
|
35
|
+
acts_as_flashcard
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
class ActsAsFlashcardTest < Test::Unit::TestCase
|
40
|
+
|
41
|
+
def setup
|
42
|
+
setup_db
|
43
|
+
end
|
44
|
+
|
45
|
+
def teardown
|
46
|
+
teardown_db
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_due_date_updated_after_correct_answer
|
50
|
+
card = Card.create
|
51
|
+
due_before = card.due
|
52
|
+
card.answered!(4)
|
53
|
+
assert_equal (card.due > due_before), true
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_factor_should_increase_when_answered_right
|
57
|
+
card = Card.create(:repetitions => 5)
|
58
|
+
factor_before = card.factor
|
59
|
+
card.answered!(4)
|
60
|
+
assert_equal card.factor, factor_before + 0.10
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_factor_should_decrease_when_answered_wrong_if_not_learnt
|
64
|
+
card = Card.create(:interval => 7.5)
|
65
|
+
factor_before = card.factor
|
66
|
+
card.answered!(1)
|
67
|
+
assert_equal card.factor, factor_before - 0.20
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_should_return_the_average_actor
|
71
|
+
[1,2,3,4,5].each {|i| Card.create(:factor => i)}
|
72
|
+
card = Card.create
|
73
|
+
avg = card.__send__(:average_factor)
|
74
|
+
assert_equal avg, 3
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_when_answered_0_no_count_should_increase
|
78
|
+
card = Card.create(:repetitions => 3, :successive => 2, :no_count => 1, :yes_count => 3)
|
79
|
+
card.answered!(0)
|
80
|
+
assert_equal card.repetitions, 4
|
81
|
+
assert_equal card.successive, 0
|
82
|
+
assert_equal card.no_count, 2
|
83
|
+
assert_equal card.yes_count, 3
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_when_answered_2_yes_count_should_increase
|
87
|
+
card = Card.create(:repetitions => 3, :successive => 2, :no_count => 1, :yes_count => 3)
|
88
|
+
card.answered!(2)
|
89
|
+
assert_equal card.repetitions, 4
|
90
|
+
assert_equal card.successive, 3
|
91
|
+
assert_equal card.no_count, 1
|
92
|
+
assert_equal card.yes_count, 4
|
93
|
+
end
|
94
|
+
|
95
|
+
def test_update_increase_combined_due
|
96
|
+
card = Card.create()
|
97
|
+
combined_due_before = card.combined_due
|
98
|
+
card.answered!(2)
|
99
|
+
assert card.combined_due > combined_due_before
|
100
|
+
combined_due_before = card.combined_due
|
101
|
+
card.answered!(4)
|
102
|
+
assert card.combined_due > combined_due_before
|
103
|
+
combined_due_before = card.combined_due
|
104
|
+
card.answered!(3)
|
105
|
+
assert card.combined_due < combined_due_before
|
106
|
+
combined_due_before = card.combined_due
|
107
|
+
card.answered!(2)
|
108
|
+
assert card.combined_due < combined_due_before
|
109
|
+
end
|
110
|
+
|
111
|
+
def test_increase_interval_when_right
|
112
|
+
card = Card.create()
|
113
|
+
card.answered!(2)
|
114
|
+
card.answered!(2)
|
115
|
+
interval_before = card.interval
|
116
|
+
card.answered!(4)
|
117
|
+
card.answered!(4)
|
118
|
+
assert card.interval > interval_before
|
119
|
+
end
|
120
|
+
|
121
|
+
def test_decrease_interval_when_wrong
|
122
|
+
card = Card.create()
|
123
|
+
card.answered!(2)
|
124
|
+
card.answered!(4)
|
125
|
+
interval_before = card.interval
|
126
|
+
card.answered!(1)
|
127
|
+
assert card.interval >= interval_before
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper.rb'
|
2
|
+
#require 'rails/generators'
|
3
|
+
require 'generators/flashcard/flashcard_generator'
|
4
|
+
|
5
|
+
class MigrationGeneratorTest < Test::Unit::TestCase
|
6
|
+
|
7
|
+
def test_install_flashcard
|
8
|
+
FlashcardGenerator.start(["some_name_nobody_is_likely_to_ever_use_in_a_real_migration"], :destination_root => @destination)
|
9
|
+
new_file = (file_list - @original_files).first
|
10
|
+
assert_match /create_flashcards/, new_file
|
11
|
+
assert_match /create_table :some_name_nobody_is_likely_to_ever_use_in_a_real_migrations do |t|/, File.read(new_file)
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_add_flashcard_fields
|
15
|
+
create_schema_file
|
16
|
+
FlashcardGenerator.start(["some_name_nobody_is_likely_to_ever_use_in_a_real_migration"], :destination_root => @destination)
|
17
|
+
new_file = (file_list - @original_files).first
|
18
|
+
assert_match /add_flashcards_fields/, new_file
|
19
|
+
assert_match /add_column :some_name_nobody_is_likely_to_ever_use_in_a_real_migrations/, File.read(new_file)
|
20
|
+
end
|
21
|
+
|
22
|
+
def setup
|
23
|
+
@destination = File.join('tmp', 'test_app')
|
24
|
+
@source = FlashcardGenerator.source_root
|
25
|
+
@original_files = file_list
|
26
|
+
end
|
27
|
+
|
28
|
+
def teardown
|
29
|
+
FileUtils.rm_rf(@destination)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def file_list
|
35
|
+
Dir.glob(File.join(@destination, "db", "migrate", "*"))
|
36
|
+
end
|
37
|
+
|
38
|
+
def create_schema_file
|
39
|
+
FileUtils.mkdir_p(@destination)
|
40
|
+
Dir.mkdir(File.join(@destination, "db"))
|
41
|
+
File.open(File.join(@destination, "db", "schema.rb"), 'w') do |f|
|
42
|
+
f.write("ActiveRecord::Schema.define(:version => 20100201214414) do\n")
|
43
|
+
f.write("\tcreate_table \"some_name_nobody_is_likely_to_ever_use_in_a_real_migrations\", :force => true do |t|\n")
|
44
|
+
f.write("\t\tt.integer \"user_id\"\n")
|
45
|
+
f.write("\tend\nend")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
require 'test/unit'
|
11
|
+
# require 'shoulda'
|
12
|
+
require 'active_record'
|
13
|
+
require 'sqlite3'
|
14
|
+
|
15
|
+
|
16
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
17
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
18
|
+
require 'acts_as_flashcard'
|
19
|
+
|
20
|
+
class Test::Unit::TestCase
|
21
|
+
end
|
data/uninstall.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Uninstall hook code here
|