backlog 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +19 -0
- data/README.txt +8 -2
- data/Rakefile +3 -2
- data/Screenshot.png +0 -0
- data/app/controllers/groups_controller.rb +1 -2
- data/app/controllers/work_accounts_controller.rb +5 -2
- data/app/controllers/works_controller.rb +2 -1
- data/app/models/works_report_filter.rb +22 -0
- data/app/views/search/results.rhtml +1 -1
- data/app/views/work_accounts/_title.rhtml +1 -0
- data/app/views/work_accounts/list.rhtml +6 -7
- data/app/views/works/list.rhtml +36 -7
- data/lang/en.yaml +1 -0
- data/lang/no.yaml +1 -0
- data/test/performance/common.rb +1 -1
- data/test/performance/server_test.rb +5 -5
- data/vendor/plugins/has_history/README +12 -0
- data/vendor/plugins/has_history/Rakefile +22 -0
- data/vendor/plugins/has_history/generators/history_migration/history_migration_generator.rb +30 -0
- data/vendor/plugins/has_history/generators/history_migration/templates/migration.rb +14 -0
- data/vendor/plugins/has_history/generators/history_model/history_model_generator.rb +39 -0
- data/vendor/plugins/has_history/generators/history_model/templates/fixtures.yml +17 -0
- data/vendor/plugins/has_history/generators/history_model/templates/functional_test.rb +129 -0
- data/vendor/plugins/has_history/generators/history_model/templates/migration.rb +14 -0
- data/vendor/plugins/has_history/generators/history_model/templates/model.rb +4 -0
- data/vendor/plugins/has_history/generators/history_model/templates/unit_test.rb +75 -0
- data/vendor/plugins/has_history/init.rb +5 -0
- data/vendor/plugins/has_history/install.rb +1 -0
- data/vendor/plugins/has_history/lib/has_history.rb +178 -0
- data/vendor/plugins/has_history/tasks/has_history_tasks.rake +4 -0
- data/vendor/plugins/has_history/test/has_history_test.rb +31 -0
- data/vendor/plugins/has_history/test/ignorant_master.rb +3 -0
- data/vendor/plugins/has_history/test/ignorant_master_history.rb +4 -0
- data/vendor/plugins/has_history/test/lone_master.rb +42 -0
- data/vendor/plugins/has_history/test/paired_master.rb +5 -0
- data/vendor/plugins/has_history/test/paired_master_history.rb +5 -0
- data/vendor/plugins/has_history/test/test_helper.rb +154 -0
- data/vendor/plugins/has_history/uninstall.rb +1 -0
- metadata +36 -12
data/History.txt
CHANGED
@@ -1,3 +1,22 @@
|
|
1
|
+
== 0.29.0 2008-04-07
|
2
|
+
|
3
|
+
=== Features
|
4
|
+
|
5
|
+
* Tasks are now automatically grabbed when they are created.
|
6
|
+
* Added 8 hours of work for holidays and sick days in the weekly work sheet.
|
7
|
+
* Removed gem dependency to rmagick for easier installation.
|
8
|
+
* Improved work record search screen.
|
9
|
+
|
10
|
+
=== Fixes
|
11
|
+
|
12
|
+
* Fixed bug where validation errors for new record in the Daily Work Sheet redirected to the
|
13
|
+
"New Work" view instead of displaying the Daily Work Sheet with appropriate error message.
|
14
|
+
* Fixed bug where updates for rows without start time failed.
|
15
|
+
* Now treat times with only whitespace as empty.
|
16
|
+
* Added a standard link text to hits for empty work records in the search results view.
|
17
|
+
* Select empty work account on new rows in the Daily Work Sheet instead of the first work account.
|
18
|
+
* Sort memebers in a group by first name then last name.
|
19
|
+
|
1
20
|
== 0.28.0 2008-04-01
|
2
21
|
|
3
22
|
=== Features
|
data/README.txt
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
<img align="right" src="Screenshot.png"/>
|
2
|
+
|
1
3
|
== Backlog
|
2
4
|
|
3
5
|
Welcome to Backlog!
|
@@ -35,11 +37,15 @@ If you have concrete needs, and are willing to beta test the integration, please
|
|
35
37
|
* Install ruby
|
36
38
|
* Install RubyGems
|
37
39
|
* Install PostgreSQL (or other database management system)
|
38
|
-
*
|
39
|
-
* run <tt>sudo gem install backlog -y</tt>
|
40
|
+
* run <tt>sudo gem install backlog</tt>
|
40
41
|
* run <tt>sudo backlog setup_unix</tt>
|
41
42
|
* run <tt>sudo backlog start</tt>
|
42
43
|
|
44
|
+
===== Charts
|
45
|
+
|
46
|
+
* Install ImageMagick
|
47
|
+
* run <tt>sudo gem install rmagick</tt>
|
48
|
+
|
43
49
|
===== Updates
|
44
50
|
|
45
51
|
* run <tt>sudo gem update -y</tt>
|
data/Rakefile
CHANGED
@@ -32,8 +32,9 @@ Hoe.new("backlog", APP::VERSION) do |p|
|
|
32
32
|
}
|
33
33
|
p.need_zip = true
|
34
34
|
p.url = 'http://rubyforge.org/projects/backlog/'
|
35
|
-
p.extra_deps = [['rails', '= 1.2.4'], ['gruff', '~> 0.2.9'], ['
|
36
|
-
|
35
|
+
p.extra_deps = [['rails', '= 1.2.4'], ['gruff', '~> 0.2.9'], ['postgres', '~> 0.7.9'],
|
36
|
+
['slave', '~> 1.2.1']]
|
37
|
+
# ['rmagick', '~> 1.15.12'],
|
37
38
|
p.rsync_args = "-acv --delete --exclude=wiki*"
|
38
39
|
end
|
39
40
|
|
data/Screenshot.png
ADDED
Binary file
|
@@ -30,8 +30,7 @@ class GroupsController < ApplicationController
|
|
30
30
|
|
31
31
|
def edit
|
32
32
|
@group = Group.find(params[:id])
|
33
|
-
@users = User.find(:all)
|
34
|
-
@members = @group.users.to_s
|
33
|
+
@users = User.find(:all, :order => 'first_name, last_name')
|
35
34
|
@groups = Group.find(:all, :order => 'name')
|
36
35
|
end
|
37
36
|
|
@@ -57,9 +57,12 @@ class WorkAccountsController < ApplicationController
|
|
57
57
|
|
58
58
|
def works
|
59
59
|
work_account = WorkAccount.find(params[:id])
|
60
|
-
@report_filter =
|
60
|
+
@report_filter = WorksReportFilter.new(params[:report_filter])
|
61
61
|
@report_filter.title = "#{l :hours} for #{work_account.name} #{@report_filter.start_on && @report_filter.start_on.strftime('%Y-%m-%d - ')}#{@report_filter.end_on && @report_filter.end_on.strftime('%Y-%m-%d')}"
|
62
|
-
@works = Work.paginate :conditions => ["completed_at BETWEEN ? AND ? AND work_account_id = ?", @report_filter.start_on, @report_filter.end_on + 1, work_account.id
|
62
|
+
@works = Work.paginate :conditions => ["completed_at BETWEEN ? AND ? AND work_account_id = ? #{@report_filter.invoice.nil? ? '' : 'AND invoice = ?'} #{@report_filter.user_id.nil? ? '' : 'AND user_id = ?'}", @report_filter.start_on, @report_filter.end_on + 1, work_account.id, @report_filter.invoice, @report_filter.user_id].compact,
|
63
|
+
:page => params[:page], :per_page => @report_filter.page_size,
|
64
|
+
:order => 'started_on, start_time, completed_at'
|
65
|
+
@users = User.find(:all)
|
63
66
|
if params[:export] == 'excel'
|
64
67
|
render :template => '/works/list_excel', :layout => false
|
65
68
|
else
|
@@ -57,11 +57,12 @@ class WorksController < ApplicationController
|
|
57
57
|
@work.calculate_hours! unless @work.hours && @work.hours.to_f > 0
|
58
58
|
@work.user_id = current_user.id
|
59
59
|
end
|
60
|
-
if @work &&
|
60
|
+
if @work && @work.save
|
61
61
|
flash[:notice] = 'Work was successfully created.'
|
62
62
|
@work.task.grab if @work.task
|
63
63
|
else
|
64
64
|
if params[:detour]
|
65
|
+
flash[:notice] = 'Error creating new work record.'
|
65
66
|
back
|
66
67
|
else
|
67
68
|
new
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class WorksReportFilter < ReportFilter
|
2
|
+
attr_reader :invoice
|
3
|
+
attr_reader :user_id
|
4
|
+
|
5
|
+
def initialize(attributes)
|
6
|
+
@invoice = nil
|
7
|
+
@user_id = nil
|
8
|
+
|
9
|
+
if attributes
|
10
|
+
attributes = attributes.clone
|
11
|
+
|
12
|
+
invoice_param = attributes.delete(:invoice)
|
13
|
+
@invoice = invoice_param == 'true' if invoice_param && invoice_param.size > 0
|
14
|
+
|
15
|
+
user_id_param = attributes.delete(:user_id)
|
16
|
+
@user_id = user_id_param.to_i if user_id_param && user_id_param.size > 0
|
17
|
+
|
18
|
+
end
|
19
|
+
super(attributes)
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -17,7 +17,7 @@
|
|
17
17
|
</div>
|
18
18
|
<ul>
|
19
19
|
<% @work_accounts.each do |work_account| %>
|
20
|
-
<li><%=link_to
|
20
|
+
<li><%=link_to work_account.name, :controller => 'work_accounts', :action => :edit, :id => work_account%></li>
|
21
21
|
<% end %>
|
22
22
|
</ul>
|
23
23
|
</div>
|
@@ -1,4 +1,5 @@
|
|
1
1
|
<div class="btitle">
|
2
2
|
<%=image_detour_to('hammer.png', l(:work), :action => :works, :id => @work_account) %>
|
3
|
+
<%=image_detour_to('clipboard.png', l(:edit), :action => :edit, :id => @work_account) unless controller.action_name == 'edit' %>
|
3
4
|
<h4><%=@work_account.name %></h4>
|
4
5
|
</div>
|
@@ -8,17 +8,16 @@
|
|
8
8
|
|
9
9
|
<table>
|
10
10
|
<tr>
|
11
|
-
|
12
|
-
<th><%=
|
13
|
-
|
11
|
+
<th><%=l :name %></th>
|
12
|
+
<th><%=l :track_times %></th>
|
13
|
+
<th><%=l :invoice_code %></th>
|
14
14
|
</tr>
|
15
15
|
|
16
16
|
<% for work_account in @work_accounts %>
|
17
17
|
<tr>
|
18
|
-
|
19
|
-
<td><%=
|
20
|
-
|
21
|
-
<td><%= link_to 'Show', :action => 'show', :id => work_account %></td>
|
18
|
+
<td><%=link_to h(work_account.name), :action => 'show', :id => work_account %></td>
|
19
|
+
<td align="right"><%=work_account.track_times? %></td>
|
20
|
+
<td><%=h(work_account.invoice_code) %></td>
|
22
21
|
<td><%= link_to 'Edit', :action => 'edit', :id => work_account %></td>
|
23
22
|
<td><%= link_to 'Destroy', { :action => 'destroy', :id => work_account }, :confirm => 'Are you sure?', :method => :post %></td>
|
24
23
|
</tr>
|
data/app/views/works/list.rhtml
CHANGED
@@ -6,7 +6,12 @@
|
|
6
6
|
|
7
7
|
<% if @report_filter %>
|
8
8
|
<% form_for :report_filter, :html => {:method => :get} do |f| %>
|
9
|
-
|
9
|
+
<table>
|
10
|
+
<tr>
|
11
|
+
<td>
|
12
|
+
<label for="report_filter_start_on"><%=l :start_on%></label>
|
13
|
+
</td>
|
14
|
+
<td>
|
10
15
|
<%=f.text_field 'start_on', :size => 16, :value => @report_filter.start_on ? @report_filter.start_on.strftime('%Y-%m-%d') : nil %>
|
11
16
|
<button id="trigger1">...</button>
|
12
17
|
<script type="text/javascript">//<![CDATA[
|
@@ -23,8 +28,11 @@
|
|
23
28
|
timeInterval : 15
|
24
29
|
});
|
25
30
|
//]]></script>
|
26
|
-
|
31
|
+
</td>
|
32
|
+
<td>
|
27
33
|
<label for="report_filter_end_on"><%=l :completed_at%></label>
|
34
|
+
</td>
|
35
|
+
<td>
|
28
36
|
<%=f.text_field 'end_on', :size => 16, :value => (@report_filter.end_on && @report_filter.end_on.strftime('%Y-%m-%d')) %>
|
29
37
|
<button id="trigger2">...</button>
|
30
38
|
<script type="text/javascript">//<![CDATA[
|
@@ -41,13 +49,32 @@
|
|
41
49
|
timeInterval : 15
|
42
50
|
});
|
43
51
|
//]]></script>
|
52
|
+
</td>
|
53
|
+
</tr>
|
44
54
|
|
45
|
-
<
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
55
|
+
<tr>
|
56
|
+
<td>
|
57
|
+
<label for="report_filter_user_id"><%=l :user%></label>
|
58
|
+
</td>
|
59
|
+
<td>
|
60
|
+
<%=f.select :user_id, [[l(:all), nil]] + @users.map{|user|[user.name, user.id]}, {}, {:onchange => 'form.submit()'} %>
|
61
|
+
</td>
|
62
|
+
<td>
|
63
|
+
<label for="report_filter_invoice"><%=l :invoice%></label>
|
64
|
+
</td>
|
65
|
+
<td>
|
66
|
+
<%=f.select :invoice, [[l(:all), nil], [l(:yes), true], [l(:no), false]], {}, {:onchange => 'form.submit()'} %>
|
50
67
|
|
68
|
+
<label for="report_filter_page_size"><%=l :paging%></label>
|
69
|
+
<%=f.check_box :page_size, {:checked => @report_filter.page_size == 10, :onchange => 'form.submit()'}, 10, nil %>
|
70
|
+
</td>
|
71
|
+
</tr>
|
72
|
+
<tr>
|
73
|
+
<td colspan="4" align="right">
|
74
|
+
<%=submit_tag l(:search) %>
|
75
|
+
</td>
|
76
|
+
</tr>
|
77
|
+
</table>
|
51
78
|
<% end %>
|
52
79
|
<% end %>
|
53
80
|
|
@@ -67,6 +94,7 @@
|
|
67
94
|
<th><%=l :task %></th>
|
68
95
|
<th><%=l :user %></th>
|
69
96
|
<th><%=l :done %></th>
|
97
|
+
<th><%=l :invoice %></th>
|
70
98
|
<% if @period && @period.track_times? %>
|
71
99
|
<th><%=l :started_at %></th>
|
72
100
|
<% end %>
|
@@ -78,6 +106,7 @@
|
|
78
106
|
<td><%=work.task.description if work.task %></td>
|
79
107
|
<td><%=work.user && work.user.login %></td>
|
80
108
|
<td><%=work.hours %></td>
|
109
|
+
<td><%=work.invoice ? l(:yes) : '' %></td>
|
81
110
|
<% if @period && @period.track_times? %>
|
82
111
|
<td><%=work.started_at && work.started_at.strftime('%Y-%m-%d %H:%M:%S') %></td>
|
83
112
|
<% end %>
|
data/lang/en.yaml
CHANGED
data/lang/no.yaml
CHANGED
data/test/performance/common.rb
CHANGED
@@ -3,15 +3,15 @@ require File.dirname(__FILE__) + '/common'
|
|
3
3
|
|
4
4
|
class ServerTest < Test::Unit::TestCase
|
5
5
|
include Backlog::Performance::Tests
|
6
|
-
|
7
|
-
PORT = 4000
|
8
|
-
|
6
|
+
|
9
7
|
def setup
|
10
|
-
|
8
|
+
@port = 3000
|
9
|
+
@factor = 1.0
|
10
|
+
start_server
|
11
11
|
end
|
12
12
|
|
13
13
|
def teardown
|
14
14
|
stop_server
|
15
15
|
end
|
16
16
|
|
17
|
-
end
|
17
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
HasHistory
|
2
|
+
==========
|
3
|
+
|
4
|
+
This plugin adds recording of history for a given model. The design tries to follow the analysis given in
|
5
|
+
|
6
|
+
http://martinfowler.com/ap2/timeNarrative.html
|
7
|
+
|
8
|
+
The plugin provides a "has_history" class method to ActiveRecord::Base
|
9
|
+
|
10
|
+
See has_history.rb for usage.
|
11
|
+
|
12
|
+
The plugin also provides a history model generator that creates a model class with
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
|
5
|
+
desc 'Default: run unit tests.'
|
6
|
+
task :default => :test
|
7
|
+
|
8
|
+
desc 'Test the has_history plugin.'
|
9
|
+
Rake::TestTask.new(:test) do |t|
|
10
|
+
t.libs << 'lib'
|
11
|
+
t.pattern = 'test/**/*_test.rb'
|
12
|
+
t.verbose = true
|
13
|
+
end
|
14
|
+
|
15
|
+
desc 'Generate documentation for the has_history plugin.'
|
16
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
17
|
+
rdoc.rdoc_dir = 'rdoc'
|
18
|
+
rdoc.title = 'HasHistory'
|
19
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
20
|
+
rdoc.rdoc_files.include('README')
|
21
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
22
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class ActiveRecord::ConnectionAdapters::Column
|
2
|
+
def name=(name)
|
3
|
+
@name = name
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
7
|
+
class HistoryMigrationGenerator < Rails::Generator::NamedBase
|
8
|
+
def manifest
|
9
|
+
record do |m|
|
10
|
+
columns = class_name.constantize.columns
|
11
|
+
id_column = columns.find{|c| c.name == 'id'}
|
12
|
+
id_column.name = "#{table_name.singularize}_id"
|
13
|
+
|
14
|
+
columns.delete_if{|c| c.name == 'created_at'}
|
15
|
+
|
16
|
+
updated_at_column = columns.find{|c| c.name == 'updated_at'}
|
17
|
+
updated_at_column.name = 'valid_from' if updated_at_column
|
18
|
+
|
19
|
+
m.migration_template 'migration.rb', 'db/migrate', :assigns => {
|
20
|
+
:migration_name => "Create#{"#{class_name}History".pluralize.gsub(/::/, '')}", :table_name => "#{table_name.singularize}_histories", :columns => columns
|
21
|
+
}, :migration_file_name => "create_#{file_path.gsub(/\//, '_')}_#{'history'.pluralize}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
def banner
|
27
|
+
"Usage: ./script/generate history_migration MODELNAME"
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class <%= migration_name %> < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :<%= table_name %>, :force => true do |t|
|
4
|
+
# TODO: Remove the columns you don't want recorded.
|
5
|
+
<% for column in columns -%>
|
6
|
+
t.column :<%=column.name%>, :<%=column.type%><%=", :limit => #{column.limit}" unless column.limit.nil? %><%=", :null => #{column.null}" unless column.null %><%=", :default => #{column.default}" unless column.default.nil? %>
|
7
|
+
<% end -%>
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.down
|
12
|
+
drop_table "<%= table_name %>"
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
class HistoryModelGenerator < Rails::Generator::NamedBase
|
2
|
+
default_options :skip_migration => false
|
3
|
+
|
4
|
+
def manifest
|
5
|
+
record do |m|
|
6
|
+
# # Check for class naming collisions.
|
7
|
+
m.class_collisions class_path, "#{class_name}History"
|
8
|
+
# m.class_collisions class_path, "#{class_name}HistoryTest"
|
9
|
+
#
|
10
|
+
# # Model, test, and fixture directories.
|
11
|
+
m.directory File.join('app/models', class_path)
|
12
|
+
# m.directory File.join('test/unit', class_path)
|
13
|
+
# m.directory File.join('test/fixtures', class_path)
|
14
|
+
#
|
15
|
+
# # Model class, unit test, and fixtures.
|
16
|
+
m.template 'model.rb', File.join('app/models', class_path, "#{file_name}_history.rb")
|
17
|
+
# m.template 'unit_test.rb', File.join('test/unit', class_path, "#{file_name}_history_test.rb")
|
18
|
+
# m.template 'fixtures.yml', File.join('test/fixtures', class_path, "#{table_name}_history.yml")
|
19
|
+
|
20
|
+
m.dependency 'model', ["#{class_name}History"], :collision => :skip, :skip_migration => true
|
21
|
+
|
22
|
+
unless options[:skip_migration]
|
23
|
+
m.dependency 'history_migration', [class_name], :collision => :skip, :skip_migration => true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
def banner
|
30
|
+
"Usage: #{$0} generate ModelName"
|
31
|
+
end
|
32
|
+
|
33
|
+
def add_options!(opt)
|
34
|
+
opt.separator ''
|
35
|
+
opt.separator 'Options:'
|
36
|
+
opt.on("--skip-migration",
|
37
|
+
"Don't generate a migration file for this history model") { |v| options[:skip_migration] = v }
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
quentin:
|
2
|
+
id: 1
|
3
|
+
login: quentin
|
4
|
+
email: quentin@example.com
|
5
|
+
salt: 7e3041ebc2fc05a40c60028e2c4901a81035d3cd
|
6
|
+
crypted_password: 00742970dc9e6319f8019fd54864d3ea740f04b1 # test
|
7
|
+
#crypted_password: "ce2/iFrNtQ8=\n" # quentin, use only if you're using 2-way encryption
|
8
|
+
created_at: <%%= 5.days.ago.to_s :db %>
|
9
|
+
# activated_at: <%%= 5.days.ago.to_s :db %> # only if you're activating new signups
|
10
|
+
aaron:
|
11
|
+
id: 2
|
12
|
+
login: aaron
|
13
|
+
email: aaron@example.com
|
14
|
+
salt: 7e3041ebc2fc05a40c60028e2c4901a81035d3cd
|
15
|
+
crypted_password: 00742970dc9e6319f8019fd54864d3ea740f04b1 # test
|
16
|
+
# activation_code: aaronscode # only if you're activating new signups
|
17
|
+
created_at: <%%= 1.days.ago.to_s :db %>
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../test_helper'
|
2
|
+
require '<%= controller_file_name %>_controller'
|
3
|
+
|
4
|
+
# Re-raise errors caught by the controller.
|
5
|
+
class <%= controller_class_name %>Controller; def rescue_action(e) raise e end; end
|
6
|
+
|
7
|
+
class <%= controller_class_name %>ControllerTest < Test::Unit::TestCase
|
8
|
+
# Be sure to include AuthenticatedTestHelper in test/test_helper.rb instead
|
9
|
+
# Then, you can remove it from this and the units test.
|
10
|
+
include AuthenticatedTestHelper
|
11
|
+
|
12
|
+
fixtures :<%= table_name %>
|
13
|
+
|
14
|
+
def setup
|
15
|
+
@controller = <%= controller_class_name %>Controller.new
|
16
|
+
@request = ActionController::TestRequest.new
|
17
|
+
@response = ActionController::TestResponse.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_should_login_and_redirect
|
21
|
+
post :login, :login => 'quentin', :password => 'test'
|
22
|
+
assert session[:<%= file_name %>]
|
23
|
+
assert_response :redirect
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_should_fail_login_and_not_redirect
|
27
|
+
post :login, :login => 'quentin', :password => 'bad password'
|
28
|
+
assert_nil session[:<%= file_name %>]
|
29
|
+
assert_response :success
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_should_allow_signup
|
33
|
+
assert_difference <%= class_name %>, :count do
|
34
|
+
create_<%= file_name %>
|
35
|
+
assert_response :redirect
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_should_require_login_on_signup
|
40
|
+
assert_no_difference <%= class_name %>, :count do
|
41
|
+
create_<%= file_name %>(:login => nil)
|
42
|
+
assert assigns(:<%= file_name %>).errors.on(:login)
|
43
|
+
assert_response :success
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_should_require_password_on_signup
|
48
|
+
assert_no_difference <%= class_name %>, :count do
|
49
|
+
create_<%= file_name %>(:password => nil)
|
50
|
+
assert assigns(:<%= file_name %>).errors.on(:password)
|
51
|
+
assert_response :success
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_should_require_password_confirmation_on_signup
|
56
|
+
assert_no_difference <%= class_name %>, :count do
|
57
|
+
create_<%= file_name %>(:password_confirmation => nil)
|
58
|
+
assert assigns(:<%= file_name %>).errors.on(:password_confirmation)
|
59
|
+
assert_response :success
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_should_require_email_on_signup
|
64
|
+
assert_no_difference <%= class_name %>, :count do
|
65
|
+
create_<%= file_name %>(:email => nil)
|
66
|
+
assert assigns(:<%= file_name %>).errors.on(:email)
|
67
|
+
assert_response :success
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def test_should_logout
|
72
|
+
login_as :quentin
|
73
|
+
get :logout
|
74
|
+
assert_nil session[:<%= file_name %>]
|
75
|
+
assert_response :redirect
|
76
|
+
end
|
77
|
+
|
78
|
+
def test_should_remember_me
|
79
|
+
post :login, :login => 'quentin', :password => 'test', :remember_me => "1"
|
80
|
+
assert_not_nil @response.cookies["auth_token"]
|
81
|
+
end
|
82
|
+
|
83
|
+
def test_should_not_remember_me
|
84
|
+
post :login, :login => 'quentin', :password => 'test', :remember_me => "0"
|
85
|
+
assert_nil @response.cookies["auth_token"]
|
86
|
+
end
|
87
|
+
|
88
|
+
def test_should_delete_token_on_logout
|
89
|
+
login_as :quentin
|
90
|
+
get :logout
|
91
|
+
assert_equal @response.cookies["auth_token"], []
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_should_login_with_cookie
|
95
|
+
<%= table_name %>(:quentin).remember_me
|
96
|
+
@request.cookies["auth_token"] = cookie_for(:quentin)
|
97
|
+
get :index
|
98
|
+
assert @controller.send(:logged_in?)
|
99
|
+
end
|
100
|
+
|
101
|
+
def test_should_fail_expired_cookie_login
|
102
|
+
<%= table_name %>(:quentin).remember_me
|
103
|
+
users(:quentin).update_attribute :remember_token_expires_at, 5.minutes.ago
|
104
|
+
@request.cookies["auth_token"] = cookie_for(:quentin)
|
105
|
+
get :index
|
106
|
+
assert !@controller.send(:logged_in?)
|
107
|
+
end
|
108
|
+
|
109
|
+
def test_should_fail_cookie_login
|
110
|
+
<%= table_name %>(:quentin).remember_me
|
111
|
+
@request.cookies["auth_token"] = auth_token('invalid_auth_token')
|
112
|
+
get :index
|
113
|
+
assert !@controller.send(:logged_in?)
|
114
|
+
end
|
115
|
+
|
116
|
+
protected
|
117
|
+
def create_<%= file_name %>(options = {})
|
118
|
+
post :signup, :<%= file_name %> => { :login => 'quire', :email => 'quire@example.com',
|
119
|
+
:password => 'quire', :password_confirmation => 'quire' }.merge(options)
|
120
|
+
end
|
121
|
+
|
122
|
+
def auth_token(token)
|
123
|
+
CGI::Cookie.new('name' => 'auth_token', 'value' => token)
|
124
|
+
end
|
125
|
+
|
126
|
+
def cookie_for(<%= file_name %>)
|
127
|
+
auth_token <%= table_name %>(<%= file_name %>).remember_token
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class <%= migration_name %> < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :<%= table_name %>, :force => true do |t|
|
4
|
+
# Remove the columns you don't want recorded.
|
5
|
+
<% for column in columns -%>
|
6
|
+
t.column :<%=column.name%>, :<%=column.type%><%=", :limit => #{column.limit}" unless column.limit.nil? %><%=", :null => #{column.null}" unless column.null %><%=", :default => #{column.default}" unless column.default.nil? %>
|
7
|
+
<% end -%>
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.down
|
12
|
+
drop_table "<%= table_name %>"
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../test_helper'
|
2
|
+
|
3
|
+
class <%= class_name %>Test < Test::Unit::TestCase
|
4
|
+
# Be sure to include AuthenticatedTestHelper in test/test_helper.rb instead.
|
5
|
+
# Then, you can remove it from this and the functional test.
|
6
|
+
include AuthenticatedTestHelper
|
7
|
+
fixtures :<%= table_name %>
|
8
|
+
|
9
|
+
def test_should_create_<%= file_name %>
|
10
|
+
assert_difference <%= class_name %>, :count do
|
11
|
+
<%= file_name %> = create_<%= file_name %>
|
12
|
+
assert !<%= file_name %>.new_record?, "#{<%= file_name %>.errors.full_messages.to_sentence}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_should_require_login
|
17
|
+
assert_no_difference <%= class_name %>, :count do
|
18
|
+
u = create_<%= file_name %>(:login => nil)
|
19
|
+
assert u.errors.on(:login)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_should_require_password
|
24
|
+
assert_no_difference <%= class_name %>, :count do
|
25
|
+
u = create_<%= file_name %>(:password => nil)
|
26
|
+
assert u.errors.on(:password)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_should_require_password_confirmation
|
31
|
+
assert_no_difference <%= class_name %>, :count do
|
32
|
+
u = create_<%= file_name %>(:password_confirmation => nil)
|
33
|
+
assert u.errors.on(:password_confirmation)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_should_require_email
|
38
|
+
assert_no_difference <%= class_name %>, :count do
|
39
|
+
u = create_<%= file_name %>(:email => nil)
|
40
|
+
assert u.errors.on(:email)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_should_reset_password
|
45
|
+
<%= table_name %>(:quentin).update_attributes(:password => 'new password', :password_confirmation => 'new password')
|
46
|
+
assert_equal <%= table_name %>(:quentin), <%= class_name %>.authenticate('quentin', 'new password')
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_should_not_rehash_password
|
50
|
+
<%= table_name %>(:quentin).update_attributes(:login => 'quentin2')
|
51
|
+
assert_equal <%= table_name %>(:quentin), <%= class_name %>.authenticate('quentin2', 'test')
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_should_authenticate_<%= file_name %>
|
55
|
+
assert_equal <%= table_name %>(:quentin), <%= class_name %>.authenticate('quentin', 'test')
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_should_set_remember_token
|
59
|
+
<%= table_name %>(:quentin).remember_me
|
60
|
+
assert_not_nil <%= table_name %>(:quentin).remember_token
|
61
|
+
assert_not_nil <%= table_name %>(:quentin).remember_token_expires_at
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_should_unset_remember_token
|
65
|
+
<%= table_name %>(:quentin).remember_me
|
66
|
+
assert_not_nil <%= table_name %>(:quentin).remember_token
|
67
|
+
<%= table_name %>(:quentin).forget_me
|
68
|
+
assert_nil <%= table_name %>(:quentin).remember_token
|
69
|
+
end
|
70
|
+
|
71
|
+
protected
|
72
|
+
def create_<%= file_name %>(options = {})
|
73
|
+
<%= class_name %>.create({ :login => 'quire', :email => 'quire@example.com', :password => 'quire', :password_confirmation => 'quire' }.merge(options))
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
# Install hook code here
|
@@ -0,0 +1,178 @@
|
|
1
|
+
module HasHistory
|
2
|
+
# TODO (uwe)(2007-10-31): Allow different strategies: timestamped_master, separate_history_table, separate_history_table_with_last
|
3
|
+
|
4
|
+
# Adds a trigger to the model that saves a copy of the model in a history table.
|
5
|
+
#
|
6
|
+
# The default history model is called <ModelName>History, but can be overridden by the "class_name" option.
|
7
|
+
#
|
8
|
+
# All columns are copied unless excluded by the :except or :only options.
|
9
|
+
#
|
10
|
+
# The created_at field is never changed in the original model, and is therefore omitted in the history model.
|
11
|
+
#
|
12
|
+
# The updated_at column should preserve the original timestamps but changes meaning in the history table. It therefore is renamed to "valid_from".
|
13
|
+
#
|
14
|
+
# The original row is references by a foreign key column with default name "<lower case model name>_id".
|
15
|
+
# This can be overridden by the :foreign_key option.
|
16
|
+
#
|
17
|
+
# You can automagically generate the history model class at runtime by adding the :generate_model => true option.
|
18
|
+
# In that case, the following option is also available:
|
19
|
+
#
|
20
|
+
# * :allow_updates
|
21
|
+
#
|
22
|
+
# See HasHistory#acts_as_history for a description of this option.
|
23
|
+
#
|
24
|
+
# examples:
|
25
|
+
#
|
26
|
+
# class Person < ActiveRecord::Base
|
27
|
+
# has_history
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# class Person < ActiveRecord::Base
|
31
|
+
# has_history :class_name => 'PersonHistory'
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# class Person < ActiveRecord::Base
|
35
|
+
# has_history :except => :birthdate
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# class Person < ActiveRecord::Base
|
39
|
+
# has_history :except => [:birthdate, :social_security_no]
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# class Person < ActiveRecord::Base
|
43
|
+
# has_history :only => :weight
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# class Person < ActiveRecord::Base
|
47
|
+
# has_history :only => [:height, :weight]
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# class Person < ActiveRecord::Base
|
51
|
+
# has_history :foreign_key => :person_id
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# class Person < ActiveRecord::Base
|
55
|
+
# has_history :generate_model => true
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# class Person < ActiveRecord::Base
|
59
|
+
# has_history :generate_model => true, :allow_updates => true
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# class Person < ActiveRecord::Base
|
63
|
+
# has_history :generate_model => true, :allow_updates => true
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
def has_history options = {}
|
67
|
+
options[:only] = [options[:only]] if options[:only].is_a? Symbol
|
68
|
+
options[:except] = [options[:except]] if options[:except].is_a? Symbol
|
69
|
+
history_model_class_name = (options[:class_name] && options[:class_name].to_s) || "#{name}History"
|
70
|
+
|
71
|
+
if options[:generate_model]
|
72
|
+
Object::class_eval <<-EOF
|
73
|
+
class #{history_model_class_name} < ActiveRecord::Base
|
74
|
+
acts_as_history :class_name => :#{table_name.singularize}, :allow_updates => #{options[:allow_updates].inspect}
|
75
|
+
end
|
76
|
+
EOF
|
77
|
+
end
|
78
|
+
|
79
|
+
history_model = history_model_class_name.constantize
|
80
|
+
foreign_key = options[:foreign_key] || "#{table_name.singularize}_id".to_sym
|
81
|
+
|
82
|
+
before_update do |record|
|
83
|
+
old_values = find(record.id).attributes.symbolize_keys
|
84
|
+
new_values = record.attributes.symbolize_keys
|
85
|
+
values = {foreign_key => record.id}.update(old_values)
|
86
|
+
old_values.delete(:updated_at)
|
87
|
+
new_values.delete(:updated_at)
|
88
|
+
unless new_values == old_values
|
89
|
+
values[:valid_from] = values.delete(:updated_at) if values[:updated_at]
|
90
|
+
values.delete(:id)
|
91
|
+
values.delete_if {|k,v| not history_model.column_names.include? k.to_s}
|
92
|
+
values.delete_if {|k,v| not options[:only].include? k} if options[:only]
|
93
|
+
values.delete_if {|k,v| options[:except].include? k} if options[:except]
|
94
|
+
history_model.create!(values)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
if options[:order]
|
98
|
+
order = options[:order]
|
99
|
+
elsif history_model.columns.find {|c| c.name == 'valid_from'}
|
100
|
+
order = 'valid_from DESC'
|
101
|
+
else
|
102
|
+
order = 'id'
|
103
|
+
end
|
104
|
+
has_many history_model.table_name.pluralize.to_sym, :dependent => :destroy, :foreign_key => foreign_key, :order => order
|
105
|
+
end
|
106
|
+
|
107
|
+
# Adds a belongs_to relationship to a master model and blocks updates and destroy actions on this class.
|
108
|
+
#
|
109
|
+
# If the class name of the history model ends with "History", the default master model class name is automatically calculated (ie. PersonHistory => Person).
|
110
|
+
# If the history class name does not end in "History", or you want to use another class name, you must override it with the :class_name option.
|
111
|
+
#
|
112
|
+
# Normally the history model is blocked for updates, but updates can be enabled by the :allow_updates option.
|
113
|
+
#
|
114
|
+
# The original row is references by a foreign key column with default name "<lower case model name>_id".
|
115
|
+
# This can be overridden by the "foreign_key" option.
|
116
|
+
#
|
117
|
+
# If you would like to inject the update trigger in the master model from the history model, use the :inject_trigger => true option.
|
118
|
+
# If you do this, these additional options are available:
|
119
|
+
#
|
120
|
+
# * :only
|
121
|
+
# * :except
|
122
|
+
#
|
123
|
+
# See HasHistory#has_history for a description of these options.
|
124
|
+
#
|
125
|
+
# examples:
|
126
|
+
#
|
127
|
+
# class PersonHistory < ActiveRecord::Base
|
128
|
+
# acts_as_history
|
129
|
+
# end
|
130
|
+
#
|
131
|
+
# class PersonRecords < ActiveRecord::Base
|
132
|
+
# acts_as_history :class_name => 'Person'
|
133
|
+
# end
|
134
|
+
#
|
135
|
+
# class Person < ActiveRecord::Base
|
136
|
+
# acts_as_history :allow_updates => true
|
137
|
+
# end
|
138
|
+
#
|
139
|
+
# class Person < ActiveRecord::Base
|
140
|
+
# acts_as_history :inject_trigger => true, :except => :birthdate
|
141
|
+
# end
|
142
|
+
#
|
143
|
+
# class Person < ActiveRecord::Base
|
144
|
+
# acts_as_history :inject_trigger => true, :except => [:birthdate, :social_security_no]
|
145
|
+
# end
|
146
|
+
#
|
147
|
+
# class Person < ActiveRecord::Base
|
148
|
+
# acts_as_history :inject_trigger => true, :only => :weight
|
149
|
+
# end
|
150
|
+
#
|
151
|
+
# class Person < ActiveRecord::Base
|
152
|
+
# acts_as_history :inject_trigger => true, :only => [:height, :weight]
|
153
|
+
# end
|
154
|
+
#
|
155
|
+
# class Person < ActiveRecord::Base
|
156
|
+
# has_history :foreign_key => :person_id
|
157
|
+
# end
|
158
|
+
#
|
159
|
+
# class Person < ActiveRecord::Base
|
160
|
+
# has_history :generate_model => true
|
161
|
+
# end
|
162
|
+
#
|
163
|
+
def acts_as_history options = {}
|
164
|
+
model_name = options[:class_name] || (name =~ /(.*)History/ && $1.constantize.table_name.singularize.to_sym)
|
165
|
+
raise "Master model class name '#{$1}' is missing and cannot be deduced." if model_name.nil?
|
166
|
+
belongs_to model_name
|
167
|
+
unless options[:allow_updates]
|
168
|
+
validate_on_update do |record|
|
169
|
+
record.errors.add "History:", "You may not change history!"
|
170
|
+
end
|
171
|
+
before_destroy do |record|
|
172
|
+
record.errors.add :id, "You may not change history!"
|
173
|
+
false
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper.rb')
|
2
|
+
require 'test/unit'
|
3
|
+
|
4
|
+
class HasHistoryTest < Test::Unit::TestCase
|
5
|
+
def setup
|
6
|
+
@lone_master = LoneMaster.new
|
7
|
+
reset_connection
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_create
|
11
|
+
master = PairedMaster.create!
|
12
|
+
assert_equal 1, inserts.size
|
13
|
+
assert_equal 0, updates.size
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_update
|
17
|
+
master = PairedMaster.create!
|
18
|
+
master.a = 'b'
|
19
|
+
master.save!
|
20
|
+
assert_equal 2, inserts.size, "\n" + inserts.join("\n")
|
21
|
+
assert_equal 1, updates.size, updates.inspect
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_no_record_on_no_change_update
|
25
|
+
master = PairedMaster.create!
|
26
|
+
master.save!
|
27
|
+
assert_equal 1, inserts.size, "\n" + inserts.join("\n")
|
28
|
+
assert_equal 1, updates.size, updates.inspect
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
class LoneMaster < ActiveRecord::Base
|
2
|
+
abstract_class = true
|
3
|
+
has_history :generate_model => true
|
4
|
+
has_one :other_model, :class_name => 'ModelStub'
|
5
|
+
has_many :other_models, :class_name => 'ModelStub'
|
6
|
+
attr_accessor :a, :b, :c, :d
|
7
|
+
|
8
|
+
def other_model=(val)
|
9
|
+
@other_model = val
|
10
|
+
end
|
11
|
+
def other_model
|
12
|
+
@other_model || nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def other_models=(val)
|
16
|
+
@other_models = val
|
17
|
+
end
|
18
|
+
def other_models
|
19
|
+
@other_models || []
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.columns
|
23
|
+
@columns ||= [
|
24
|
+
ActiveRecord::ConnectionAdapters::Column.new(:a, ''),
|
25
|
+
ActiveRecord::ConnectionAdapters::Column.new(:b, ''),
|
26
|
+
ActiveRecord::ConnectionAdapters::Column.new(:c, ''),
|
27
|
+
ActiveRecord::ConnectionAdapters::Column.new(:d, '')
|
28
|
+
]
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.columns_hash
|
32
|
+
@columns_hash ||= columns.inject({}) { |hash, column| hash[column.name.to_s] = column; hash }
|
33
|
+
end
|
34
|
+
|
35
|
+
# column-level security methods, used for testing
|
36
|
+
def self.a_authorized_for_bar?(user)
|
37
|
+
true
|
38
|
+
end
|
39
|
+
def self.b_authorized?(user)
|
40
|
+
false
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
|
3
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '../../../../config/environment.rb'))
|
4
|
+
|
5
|
+
require File.dirname(__FILE__) + '/lone_master'
|
6
|
+
require File.dirname(__FILE__) + '/paired_master'
|
7
|
+
require File.dirname(__FILE__) + '/paired_master_history'
|
8
|
+
require File.dirname(__FILE__) + '/ignorant_master'
|
9
|
+
require File.dirname(__FILE__) + '/ignorant_master_history'
|
10
|
+
|
11
|
+
LoneMaster.connection.instance_eval do
|
12
|
+
SEQUENCE_COLUMNS = [
|
13
|
+
ActiveRecord::ConnectionAdapters::Column.new('value', nil, translate_field_type('INTEGER'), false)
|
14
|
+
]
|
15
|
+
MASTER_COLUMNS = [
|
16
|
+
ActiveRecord::ConnectionAdapters::Column.new('id', nil, translate_field_type('INTEGER'), false),
|
17
|
+
ActiveRecord::ConnectionAdapters::Column.new('created_at', nil, translate_field_type('TIMESTAMP'), false),
|
18
|
+
ActiveRecord::ConnectionAdapters::Column.new('updated_at', nil, translate_field_type('TIMESTAMP'), false),
|
19
|
+
ActiveRecord::ConnectionAdapters::Column.new('a', nil, translate_field_type('VARCHAR(1)'), false),
|
20
|
+
ActiveRecord::ConnectionAdapters::Column.new('b', nil, translate_field_type('BOOLEAN'), false),
|
21
|
+
ActiveRecord::ConnectionAdapters::Column.new('c', nil, translate_field_type('INTEGER'), false),
|
22
|
+
ActiveRecord::ConnectionAdapters::Column.new('d', nil, translate_field_type('FLOAT'), false),
|
23
|
+
]
|
24
|
+
HISTORY_COLUMNS = [
|
25
|
+
ActiveRecord::ConnectionAdapters::Column.new('id', nil, translate_field_type('INTEGER'), false),
|
26
|
+
ActiveRecord::ConnectionAdapters::Column.new('paired_master_id', nil, translate_field_type('INTEGER'), false),
|
27
|
+
ActiveRecord::ConnectionAdapters::Column.new('created_at', nil, translate_field_type('TIMESTAMP'), false),
|
28
|
+
ActiveRecord::ConnectionAdapters::Column.new('valid_from', nil, translate_field_type('TIMESTAMP'), false),
|
29
|
+
ActiveRecord::ConnectionAdapters::Column.new('a', nil, translate_field_type('VARCHAR(1)'), false),
|
30
|
+
ActiveRecord::ConnectionAdapters::Column.new('b', nil, translate_field_type('BOOLEAN'), false),
|
31
|
+
ActiveRecord::ConnectionAdapters::Column.new('c', nil, translate_field_type('INTEGER'), false),
|
32
|
+
ActiveRecord::ConnectionAdapters::Column.new('d', nil, translate_field_type('FLOAT'), false),
|
33
|
+
]
|
34
|
+
TABLES = {
|
35
|
+
'lone_masters' => {:columns => MASTER_COLUMNS},
|
36
|
+
'paired_masters' => {:columns => MASTER_COLUMNS},
|
37
|
+
'paired_master_histories' => {:columns => HISTORY_COLUMNS},
|
38
|
+
}
|
39
|
+
|
40
|
+
def statements
|
41
|
+
@statements
|
42
|
+
end
|
43
|
+
|
44
|
+
def inserts
|
45
|
+
@statements.select {|s| s =~ /^INSERT INTO /}
|
46
|
+
end
|
47
|
+
|
48
|
+
def updates
|
49
|
+
@statements.select {|s| s =~ /^UPDATE /}
|
50
|
+
end
|
51
|
+
|
52
|
+
def reset
|
53
|
+
@statements = []
|
54
|
+
@stored_rows = {}
|
55
|
+
end
|
56
|
+
|
57
|
+
def execute(sql, name = nil)
|
58
|
+
@statements << sql
|
59
|
+
case sql
|
60
|
+
when /BEGIN|COMMIT|ROLLBACK/
|
61
|
+
return
|
62
|
+
when /^SELECT currval/
|
63
|
+
return MockResult.new(SEQUENCE_COLUMNS, [['1']])
|
64
|
+
when /^INSERT INTO (.*) \((.*)\) VALUES\s*\((.*)\)$/
|
65
|
+
table_name, fields, values = $1, $2, $3
|
66
|
+
fields = fields.gsub('"', '').split(', ')
|
67
|
+
values = values.split(', ').map {|v| v == 'NULL' ? nil : v}
|
68
|
+
|
69
|
+
@stored_rows[table_name] ||= []
|
70
|
+
id = @stored_rows[table_name].size + 1
|
71
|
+
@stored_rows[table_name][id] = {}
|
72
|
+
fields.each_with_index do |field_name, i|
|
73
|
+
@stored_rows[table_name][id][fields[i]] = values[i]
|
74
|
+
end
|
75
|
+
return
|
76
|
+
when /^SELECT (.*) FROM (.*) WHERE \(.*\."id" = (\d*)\)/
|
77
|
+
fields, table_name, id = $1, $2, $3.to_i
|
78
|
+
raise "Oooops" unless fields == '*'
|
79
|
+
raise "Invalid ID" unless id >= 1
|
80
|
+
columns = (table_name =~ /_history/ ? HISTORY_COLUMNS : MASTER_COLUMNS)
|
81
|
+
row = [id] + columns[1..-1].map {|c| @stored_rows[table_name][id][c.name] }
|
82
|
+
return MockResult.new(columns, [row])
|
83
|
+
when /UPDATE (.*) SET (.*) WHERE "id" = (\d+)/
|
84
|
+
table_name, change_str, id = $1, $2, $3.to_i
|
85
|
+
raise "Invalid ID" unless id >= 1
|
86
|
+
changes_array = change_str.split(', ')
|
87
|
+
changes = {}
|
88
|
+
changes_array.each {|c| c =~ /"(.*)" = (.*)/ ; changes[$1] = $2 }
|
89
|
+
@stored_rows[table_name][id].update(changes)
|
90
|
+
return MockResult.new([], [1])
|
91
|
+
end
|
92
|
+
raise "Unknown SQL: '#{sql}'"
|
93
|
+
end
|
94
|
+
|
95
|
+
def columns(table_name, name = nil)
|
96
|
+
raise "Unknown table '#{table_name}'" unless TABLES[table_name]
|
97
|
+
TABLES[table_name][:columns]
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
class MockResult
|
103
|
+
attr_reader :fields
|
104
|
+
|
105
|
+
def initialize(columns, rows)
|
106
|
+
@fields = columns.map {|c| c.name}
|
107
|
+
@types = columns.map {|c| c.type}
|
108
|
+
@rows = rows
|
109
|
+
end
|
110
|
+
|
111
|
+
def type(i)
|
112
|
+
@types[i]
|
113
|
+
end
|
114
|
+
|
115
|
+
def result
|
116
|
+
@rows
|
117
|
+
end
|
118
|
+
|
119
|
+
def cmdtuples
|
120
|
+
@rows.size
|
121
|
+
end
|
122
|
+
|
123
|
+
def clear
|
124
|
+
@fields = nil
|
125
|
+
@types = nil
|
126
|
+
@rows = nil
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
|
131
|
+
class Test::Unit::TestCase
|
132
|
+
def statements
|
133
|
+
connection.statements
|
134
|
+
end
|
135
|
+
|
136
|
+
def inserts
|
137
|
+
connection.inserts
|
138
|
+
end
|
139
|
+
|
140
|
+
def updates
|
141
|
+
connection.updates
|
142
|
+
end
|
143
|
+
|
144
|
+
def reset_connection
|
145
|
+
connection.reset
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
|
150
|
+
def connection
|
151
|
+
ActiveRecord::Base.connection
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
# Uninstall hook code here
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: backlog
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.29.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Uwe Kubosch
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2008-04-
|
12
|
+
date: 2008-04-07 00:00:00 +02:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -30,15 +30,6 @@ dependencies:
|
|
30
30
|
- !ruby/object:Gem::Version
|
31
31
|
version: 0.2.9
|
32
32
|
version:
|
33
|
-
- !ruby/object:Gem::Dependency
|
34
|
-
name: rmagick
|
35
|
-
version_requirement:
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - ~>
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: 1.15.12
|
41
|
-
version:
|
42
33
|
- !ruby/object:Gem::Dependency
|
43
34
|
name: postgres
|
44
35
|
version_requirement:
|
@@ -66,7 +57,7 @@ dependencies:
|
|
66
57
|
- !ruby/object:Gem::Version
|
67
58
|
version: 1.5.1
|
68
59
|
version:
|
69
|
-
description: Backlog is a tool to help you collect and organize all your tasks, whether you are a single person or a small or large group.
|
60
|
+
description: Welcome to Backlog! Backlog is a tool to help you collect and organize all your tasks, whether you are a single person or a small or large group.
|
70
61
|
email: uwe@kubosch.no
|
71
62
|
executables:
|
72
63
|
- backlog
|
@@ -267,6 +258,7 @@ files:
|
|
267
258
|
- app/models/work_lock_notify.rb
|
268
259
|
- app/models/work.rb
|
269
260
|
- app/models/work_lock_nagger.rb
|
261
|
+
- app/models/works_report_filter.rb
|
270
262
|
- app/models/customer.rb
|
271
263
|
- app/models/period.rb
|
272
264
|
- app/models/task.rb
|
@@ -287,6 +279,7 @@ files:
|
|
287
279
|
- app/models/work_lock.rb
|
288
280
|
- app/models/work_account.rb
|
289
281
|
- no_test.rb~
|
282
|
+
- Screenshot.png
|
290
283
|
- lang
|
291
284
|
- lang/no.yaml
|
292
285
|
- lang/localizations.yaml
|
@@ -516,6 +509,37 @@ files:
|
|
516
509
|
- vendor/plugins/rails_time/lib
|
517
510
|
- vendor/plugins/rails_time/lib/time_of_day.rb
|
518
511
|
- vendor/plugins/rails_time/lib/activerecord_time_extension.rb
|
512
|
+
- vendor/plugins/has_history
|
513
|
+
- vendor/plugins/has_history/README
|
514
|
+
- vendor/plugins/has_history/init.rb
|
515
|
+
- vendor/plugins/has_history/generators
|
516
|
+
- vendor/plugins/has_history/generators/history_model
|
517
|
+
- vendor/plugins/has_history/generators/history_model/history_model_generator.rb
|
518
|
+
- vendor/plugins/has_history/generators/history_model/templates
|
519
|
+
- vendor/plugins/has_history/generators/history_model/templates/fixtures.yml
|
520
|
+
- vendor/plugins/has_history/generators/history_model/templates/unit_test.rb
|
521
|
+
- vendor/plugins/has_history/generators/history_model/templates/model.rb
|
522
|
+
- vendor/plugins/has_history/generators/history_model/templates/functional_test.rb
|
523
|
+
- vendor/plugins/has_history/generators/history_model/templates/migration.rb
|
524
|
+
- vendor/plugins/has_history/generators/history_migration
|
525
|
+
- vendor/plugins/has_history/generators/history_migration/history_migration_generator.rb
|
526
|
+
- vendor/plugins/has_history/generators/history_migration/templates
|
527
|
+
- vendor/plugins/has_history/generators/history_migration/templates/migration.rb
|
528
|
+
- vendor/plugins/has_history/tasks
|
529
|
+
- vendor/plugins/has_history/tasks/has_history_tasks.rake
|
530
|
+
- vendor/plugins/has_history/install.rb
|
531
|
+
- vendor/plugins/has_history/uninstall.rb
|
532
|
+
- vendor/plugins/has_history/test
|
533
|
+
- vendor/plugins/has_history/test/paired_master.rb
|
534
|
+
- vendor/plugins/has_history/test/ignorant_master_history.rb
|
535
|
+
- vendor/plugins/has_history/test/has_history_test.rb
|
536
|
+
- vendor/plugins/has_history/test/lone_master.rb
|
537
|
+
- vendor/plugins/has_history/test/ignorant_master.rb
|
538
|
+
- vendor/plugins/has_history/test/test_helper.rb
|
539
|
+
- vendor/plugins/has_history/test/paired_master_history.rb
|
540
|
+
- vendor/plugins/has_history/Rakefile
|
541
|
+
- vendor/plugins/has_history/lib
|
542
|
+
- vendor/plugins/has_history/lib/has_history.rb
|
519
543
|
- public
|
520
544
|
- "public/Frav\xC3\xA6rsskjema.xls"
|
521
545
|
- public/robots.txt
|