padrino-admin 0.2.9 → 0.4.5
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/Rakefile +9 -8
- data/VERSION +1 -1
- data/lib/padrino-admin.rb +11 -2
- data/lib/padrino-admin/access_control.rb +353 -0
- data/lib/padrino-admin/access_control/helpers.rb +81 -0
- data/lib/padrino-admin/adapters.rb +82 -0
- data/lib/padrino-admin/adapters/ar.rb +75 -0
- data/lib/padrino-admin/adapters/dm.rb +131 -0
- data/lib/padrino-admin/adapters/mm.rb +49 -0
- data/lib/padrino-admin/ext_js/config.rb +153 -151
- data/lib/padrino-admin/ext_js/controller.rb +167 -0
- data/lib/padrino-admin/generators/backend.rb +1 -1
- data/lib/padrino-admin/locale/en.yml +7 -0
- data/lib/padrino-admin/support.rb +12 -0
- data/lib/padrino-admin/utils/crypt.rb +29 -0
- data/padrino-admin.gemspec +27 -6
- data/test/fixtures/active_record.rb +17 -0
- data/test/fixtures/data_mapper.rb +36 -0
- data/test/fixtures/mongo_mapper.rb +12 -0
- data/test/helper.rb +44 -48
- data/test/test_access_control.rb +98 -0
- data/test/test_active_record.rb +28 -0
- data/test/test_admin_application.rb +38 -0
- data/test/test_controller.rb +28 -0
- data/test/test_data_mapper.rb +32 -0
- data/test/test_mongo_mapper.rb +28 -0
- data/test/test_parsing.rb +12 -12
- metadata +33 -5
- data/test/test_padrino_admin.rb +0 -7
@@ -0,0 +1,167 @@
|
|
1
|
+
module Padrino
|
2
|
+
module ExtJs
|
3
|
+
# Return column config, and store config/data for ExtJS ColumnModel and Store
|
4
|
+
#
|
5
|
+
# Examples:
|
6
|
+
#
|
7
|
+
# # app/controllers/backend/debtors_controller.rb
|
8
|
+
# def index
|
9
|
+
# @column_store = column_store_for Debtor do |cm|
|
10
|
+
# cm.add :id
|
11
|
+
# cm.add "full_name_or_company.upcase", "Full Name", :sortable => true, :dataIndex => :company
|
12
|
+
# cm.add :surname # Header will be autogenerated
|
13
|
+
# cm.add :email, "Email", :sortable => true
|
14
|
+
# cm.add :piva, "Piva", :sortable => true
|
15
|
+
# cm.add :created_at, "Creato il", :sortable => true, :renderer => :date, :align => :right
|
16
|
+
# cm.add :updated_at, "Aggiornato il", :sortable => true, :renderer => :datetime, :align => :right
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# respond_to do |format|
|
20
|
+
# format.js
|
21
|
+
# format.json do
|
22
|
+
# render :json => @column_store.store_data(params)
|
23
|
+
#
|
24
|
+
# # or you can manually do:
|
25
|
+
# # debtors = Debtor.search(params)
|
26
|
+
# # debtors_count = debtors.size
|
27
|
+
# # debtors_paginated = debtors.paginate(params)
|
28
|
+
# # render :json => { :results => @column_store.store_data_from(debtors_paginated), :count => debtors_count }
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# # app/views/backend/index.rjs
|
34
|
+
# page.grid do |grid|
|
35
|
+
# grid.id "debtors-grid" # If you don't set this columns are not saved in cookies
|
36
|
+
# grid.title "List al debtors"
|
37
|
+
# grid.base_path "/backend/debtors"
|
38
|
+
# grid.forgery_protection_token request_forgery_protection_token
|
39
|
+
# grid.authenticity_token form_authenticity_token
|
40
|
+
# grid.tbar :default
|
41
|
+
# grid.store do |store|
|
42
|
+
# store.url "/backend/debtors.json"
|
43
|
+
# store.fields @column_store.store_fields
|
44
|
+
# end
|
45
|
+
# grid.columns do |columns|
|
46
|
+
# columns.fields @column_store.column_fields
|
47
|
+
# end
|
48
|
+
# grid.bbar :store => grid.get_store, :pageSize => params[:limit] # Remember to add after defining store!
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
module Controller
|
52
|
+
def self.column_store_for(model, &block)
|
53
|
+
ColumnStore.new(model, &block)
|
54
|
+
end
|
55
|
+
|
56
|
+
class ColumnStore #:nodoc:
|
57
|
+
attr_reader :data
|
58
|
+
|
59
|
+
def initialize(model, &block) #:nodoc
|
60
|
+
@model = model
|
61
|
+
@data = []
|
62
|
+
yield self
|
63
|
+
end
|
64
|
+
|
65
|
+
# Method for add columns to the Column Model
|
66
|
+
def add(*args)
|
67
|
+
|
68
|
+
# First we need to check that our method is a symbol
|
69
|
+
raise Padrino::ExtJs::ConfigError, "First args must be a symbol like: :name or :account.name" unless args[0].is_a?(Symbol)
|
70
|
+
|
71
|
+
# Construct our options
|
72
|
+
options = { :method => args[0] }
|
73
|
+
|
74
|
+
# If we have a second args that is not an hash maybe an header
|
75
|
+
options[:header] = args[1].is_a?(String) || args[1].is_a?(Symbol) ? args[1].to_s : nil
|
76
|
+
|
77
|
+
args.each { |a| options.merge!(a) if a.is_a?(Hash) }
|
78
|
+
|
79
|
+
# Add some defaults
|
80
|
+
options[:header] ||= options[:method].to_s
|
81
|
+
options[:sortable] = options[:sortable].nil? ? true : options[:sortable]
|
82
|
+
|
83
|
+
# Try to translate header
|
84
|
+
options[:header] = @model.human_attribute_name(options[:header].to_s)
|
85
|
+
|
86
|
+
# Reformat DataIndex
|
87
|
+
#
|
88
|
+
# If we don't have nothing we use the method
|
89
|
+
options[:dataIndex] ||= options[:method]
|
90
|
+
|
91
|
+
data_indexes = Array(options[:dataIndex]).collect do |data_index|
|
92
|
+
case data_index
|
93
|
+
when String then data_index
|
94
|
+
when Symbol
|
95
|
+
if data_index.missing_methods.size == 1
|
96
|
+
options[:name] ||= "#{@model.table_name.singularize}[#{data_index}]"
|
97
|
+
data_index = "#{@model.table_name}.#{data_index}"
|
98
|
+
else
|
99
|
+
columns = data_index.missing_methods
|
100
|
+
options[:name] ||= columns.at(0) + "[" + columns[1..-1].collect(&:to_s).join("][") + "]"
|
101
|
+
data_index = columns[0..-2].collect { |c| c.to_s.pluralize }.join(".") + "." + columns.at(-1).to_s
|
102
|
+
end
|
103
|
+
end
|
104
|
+
data_index
|
105
|
+
end
|
106
|
+
|
107
|
+
options[:dataIndex] = data_indexes.compact.uniq.join(",")
|
108
|
+
|
109
|
+
# Reformat mapping like a div id
|
110
|
+
options[:mapping] ||= options[:name].sub(/\[/,"_").sub(/\]$/, "").sub(/\]\[/,"_")
|
111
|
+
|
112
|
+
# Now is necessary for our columns an ID
|
113
|
+
# TODO: check duplicates here
|
114
|
+
options[:id] = options[:mapping]
|
115
|
+
|
116
|
+
@data << options
|
117
|
+
end
|
118
|
+
|
119
|
+
# Return an array config for build an Ext.grid.ColumnModel() config
|
120
|
+
def column_fields
|
121
|
+
@data.map do |data|
|
122
|
+
data.delete(:method)
|
123
|
+
data.delete(:mapping)
|
124
|
+
data
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Return an array config for build an Ext.data.GroupingStore()
|
129
|
+
def store_fields
|
130
|
+
@data.map do |data|
|
131
|
+
hash = { :name => data[:dataIndex], :mapping => data[:mapping] }
|
132
|
+
hash.merge!(:type => data[:renderer]) if data[:renderer] && [:date, :datetime, :time_to_date].include?(data[:renderer])
|
133
|
+
hash
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Return data for a custom collection for the ExtJS Ext.data.GroupingStore() json
|
138
|
+
def store_data_from(collection)
|
139
|
+
collection.map do |c|
|
140
|
+
@data.dup.inject({ "id" => c.id }) do |options, data|
|
141
|
+
options[data[:mapping]] = (c.instance_eval(data[:method].to_s) rescue I18n.t("padrino.admin.labels.not_found"))
|
142
|
+
options
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Return a searched and paginated data collection for the ExtJS Ext.data.GroupingStore() json
|
148
|
+
# You can pass options like:
|
149
|
+
#
|
150
|
+
# Examples
|
151
|
+
#
|
152
|
+
# store_data(params, :conditions => "found = 1")
|
153
|
+
# store_data(params, :include => :posts)
|
154
|
+
#
|
155
|
+
def store_data(params={}, options={})
|
156
|
+
# Some can tell me that this method made two identical queries one for count one for paginate.
|
157
|
+
# We don't use the select count because in some circumstances require much time than select *.
|
158
|
+
collection = @model.all(options).ext_search(params)
|
159
|
+
collection_count = collection.length
|
160
|
+
collection_paginated = collection.ext_paginate(params)
|
161
|
+
{ :results => store_data_from(collection_paginated), :count => collection_count }
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class Symbol
|
2
|
+
|
3
|
+
def method_missing(method, *args)
|
4
|
+
super and return if method.to_s =~ /table_name/
|
5
|
+
(self.to_s + ".#{method}(#{args.collect(&:inspect).join(",")})").to_sym
|
6
|
+
end
|
7
|
+
|
8
|
+
def missing_methods
|
9
|
+
self.to_s.gsub(/(\(.*\))/,"").split(".")
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Padrino
|
2
|
+
module Admin
|
3
|
+
module Utils
|
4
|
+
# This util it's used for encrypt/decrypt password.
|
5
|
+
# We want password decryptable because generally for our sites we have: password_lost.
|
6
|
+
# We prefer send original password instead reset them.
|
7
|
+
module Crypt
|
8
|
+
# Decrypts the current string using the current key and algorithm specified
|
9
|
+
def decrypt(password)
|
10
|
+
cipher = build_cipher(:decrypt, password)
|
11
|
+
cipher.update(self.unpack('m')[0]) + cipher.final
|
12
|
+
end
|
13
|
+
|
14
|
+
# Encrypts the current string using the current key and algorithm specified
|
15
|
+
def encrypt(password)
|
16
|
+
cipher = build_cipher(:encrypt, password)
|
17
|
+
[cipher.update(self) + cipher.final].pack('m').chomp
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
def build_cipher(type, password) #:nodoc:
|
22
|
+
cipher = OpenSSL::Cipher::Cipher.new("DES-EDE3-CBC").send(type)
|
23
|
+
cipher.pkcs5_keyivgen(password)
|
24
|
+
cipher
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/padrino-admin.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{padrino-admin}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.4.5"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Padrino Team", "Nathan Esquenazi", "Davide D'Agostino", "Arthur Chiu"]
|
12
|
-
s.date = %q{
|
12
|
+
s.date = %q{2010-01-06}
|
13
13
|
s.description = %q{Admin View for Padrino applications}
|
14
14
|
s.email = %q{nesquena@gmail.com}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -23,11 +23,29 @@ Gem::Specification.new do |s|
|
|
23
23
|
"Rakefile",
|
24
24
|
"VERSION",
|
25
25
|
"lib/padrino-admin.rb",
|
26
|
+
"lib/padrino-admin/access_control.rb",
|
27
|
+
"lib/padrino-admin/access_control/helpers.rb",
|
28
|
+
"lib/padrino-admin/adapters.rb",
|
29
|
+
"lib/padrino-admin/adapters/ar.rb",
|
30
|
+
"lib/padrino-admin/adapters/dm.rb",
|
31
|
+
"lib/padrino-admin/adapters/mm.rb",
|
26
32
|
"lib/padrino-admin/ext_js/config.rb",
|
33
|
+
"lib/padrino-admin/ext_js/controller.rb",
|
27
34
|
"lib/padrino-admin/generators/backend.rb",
|
35
|
+
"lib/padrino-admin/locale/en.yml",
|
36
|
+
"lib/padrino-admin/support.rb",
|
37
|
+
"lib/padrino-admin/utils/crypt.rb",
|
28
38
|
"padrino-admin.gemspec",
|
39
|
+
"test/fixtures/active_record.rb",
|
40
|
+
"test/fixtures/data_mapper.rb",
|
41
|
+
"test/fixtures/mongo_mapper.rb",
|
29
42
|
"test/helper.rb",
|
30
|
-
"test/
|
43
|
+
"test/test_access_control.rb",
|
44
|
+
"test/test_active_record.rb",
|
45
|
+
"test/test_admin_application.rb",
|
46
|
+
"test/test_controller.rb",
|
47
|
+
"test/test_data_mapper.rb",
|
48
|
+
"test/test_mongo_mapper.rb",
|
31
49
|
"test/test_parsing.rb"
|
32
50
|
]
|
33
51
|
s.homepage = %q{http://github.com/padrino/padrino-framework/tree/master/padrino-admin}
|
@@ -41,16 +59,18 @@ Gem::Specification.new do |s|
|
|
41
59
|
s.specification_version = 3
|
42
60
|
|
43
61
|
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
44
|
-
s.add_runtime_dependency(%q<
|
62
|
+
s.add_runtime_dependency(%q<json_pure>, [">= 1.2.0"])
|
45
63
|
s.add_runtime_dependency(%q<padrino-core>, [">= 0.1.1"])
|
64
|
+
s.add_development_dependency(%q<dm-core>, [">= 0.10.2"])
|
46
65
|
s.add_development_dependency(%q<haml>, [">= 2.2.1"])
|
47
66
|
s.add_development_dependency(%q<shoulda>, [">= 0"])
|
48
67
|
s.add_development_dependency(%q<mocha>, [">= 0.9.7"])
|
49
68
|
s.add_development_dependency(%q<rack-test>, [">= 0.5.0"])
|
50
69
|
s.add_development_dependency(%q<webrat>, [">= 0.5.1"])
|
51
70
|
else
|
52
|
-
s.add_dependency(%q<
|
71
|
+
s.add_dependency(%q<json_pure>, [">= 1.2.0"])
|
53
72
|
s.add_dependency(%q<padrino-core>, [">= 0.1.1"])
|
73
|
+
s.add_dependency(%q<dm-core>, [">= 0.10.2"])
|
54
74
|
s.add_dependency(%q<haml>, [">= 2.2.1"])
|
55
75
|
s.add_dependency(%q<shoulda>, [">= 0"])
|
56
76
|
s.add_dependency(%q<mocha>, [">= 0.9.7"])
|
@@ -58,8 +78,9 @@ Gem::Specification.new do |s|
|
|
58
78
|
s.add_dependency(%q<webrat>, [">= 0.5.1"])
|
59
79
|
end
|
60
80
|
else
|
61
|
-
s.add_dependency(%q<
|
81
|
+
s.add_dependency(%q<json_pure>, [">= 1.2.0"])
|
62
82
|
s.add_dependency(%q<padrino-core>, [">= 0.1.1"])
|
83
|
+
s.add_dependency(%q<dm-core>, [">= 0.10.2"])
|
63
84
|
s.add_dependency(%q<haml>, [">= 2.2.1"])
|
64
85
|
s.add_dependency(%q<shoulda>, [">= 0"])
|
65
86
|
s.add_dependency(%q<mocha>, [">= 0.9.7"])
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
|
4
|
+
|
5
|
+
ActiveRecord::Schema.define do
|
6
|
+
create_table :accounts, :force => true do |t|
|
7
|
+
t.column :id, :integer
|
8
|
+
t.column :name, :string
|
9
|
+
t.column :role, :string
|
10
|
+
t.column :crypted_password, :string
|
11
|
+
t.column :salt, :string
|
12
|
+
t.column :email, :string
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class Account < ActiveRecord::Base; end
|
17
|
+
Padrino::Admin::Adapters.register(:active_record)
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'dm-core'
|
2
|
+
require 'dm-validations'
|
3
|
+
|
4
|
+
DataMapper.setup(:default, 'sqlite3::memory:')
|
5
|
+
|
6
|
+
# Fake Category Model
|
7
|
+
class Category
|
8
|
+
include DataMapper::Resource
|
9
|
+
property :id, Serial
|
10
|
+
property :name, String
|
11
|
+
belongs_to :account
|
12
|
+
end
|
13
|
+
|
14
|
+
# Fake Account Model
|
15
|
+
class Account
|
16
|
+
include DataMapper::Resource
|
17
|
+
property :id, Serial
|
18
|
+
property :name, String
|
19
|
+
has n, :categories
|
20
|
+
def self.admin; first(:role => "Admin"); end
|
21
|
+
def self.editor; first(:role => "Editor"); end
|
22
|
+
end
|
23
|
+
|
24
|
+
Padrino::Admin::Adapters.register(:data_mapper)
|
25
|
+
DataMapper.auto_migrate!
|
26
|
+
|
27
|
+
# We build some fake accounts
|
28
|
+
admin = Account.create(:name => "DAddYE", :role => "Admin", :email => "d.dagostino@lipsiasoft.com",
|
29
|
+
:password => "some", :password_confirmation => "some")
|
30
|
+
editor = Account.create(:name => "Dexter", :role => "Editor", :email => "editor@lipsiasoft.com",
|
31
|
+
:password => "some", :password_confirmation => "some")
|
32
|
+
|
33
|
+
%w(News Press HowTo).each do |c|
|
34
|
+
admin.categories.create(:name => c)
|
35
|
+
editor.categories.create(:name => c)
|
36
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'mongo_mapper'
|
2
|
+
|
3
|
+
MongoMapper.connection = Mongo::Connection.new("127.0.0.1")
|
4
|
+
MongoMapper.database = 'test'
|
5
|
+
|
6
|
+
class Account
|
7
|
+
include MongoMapper::Document
|
8
|
+
key :name, String
|
9
|
+
end
|
10
|
+
|
11
|
+
Account.collection.remove
|
12
|
+
Padrino::Admin::Adapters.register(:mongo_mapper)
|
data/test/helper.rb
CHANGED
@@ -1,58 +1,51 @@
|
|
1
|
+
ENV['PADRINO_ENV'] = 'test'
|
2
|
+
PADRINO_ROOT = File.dirname(__FILE__) unless defined? PADRINO_ROOT
|
3
|
+
|
1
4
|
require 'rubygems'
|
2
5
|
require 'test/unit'
|
3
|
-
require 'shoulda'
|
4
|
-
require 'mocha'
|
5
6
|
require 'rack/test'
|
6
|
-
require '
|
7
|
-
|
8
|
-
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
9
|
-
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
10
|
-
|
11
|
-
require 'padrino-gen'
|
7
|
+
require 'rack'
|
8
|
+
require 'shoulda'
|
12
9
|
require 'padrino-admin'
|
13
10
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
11
|
+
module Kernel
|
12
|
+
# Silences the output by redirecting to stringIO
|
13
|
+
# silence_logger { ...commands... } => "...output..."
|
14
|
+
def silence_logger(&block)
|
15
|
+
$stdout = $stderr = log_buffer = StringIO.new
|
16
|
+
block.call
|
17
|
+
$stdout = STDOUT
|
18
|
+
$stderr = STDERR
|
19
|
+
log_buffer.string
|
21
20
|
end
|
21
|
+
alias :silence_stdout :silence_logger
|
22
22
|
|
23
|
-
def
|
24
|
-
|
25
|
-
|
26
|
-
|
23
|
+
def load_fixture(file)
|
24
|
+
Object.send(:remove_const, :Account) if defined?(Account)
|
25
|
+
file += ".rb" if file !~ /.rb$/
|
26
|
+
load File.join(File.dirname(__FILE__), "fixtures", file)
|
27
|
+
# silence_stdout { }
|
27
28
|
end
|
29
|
+
end
|
28
30
|
|
29
|
-
|
30
|
-
#
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
end
|
31
|
+
class Class
|
32
|
+
# Allow assertions in request context
|
33
|
+
include Test::Unit::Assertions
|
34
|
+
end
|
35
|
+
|
36
|
+
class Test::Unit::TestCase
|
37
|
+
include Rack::Test::Methods
|
37
38
|
|
38
|
-
#
|
39
|
-
#
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
raise "Please specify a block!" if html.blank?
|
45
|
-
assert matcher.matches?(html), matcher.failure_message
|
39
|
+
# Sets up a Sinatra::Base subclass defined with the block
|
40
|
+
# given. Used in setup or individual spec methods to establish
|
41
|
+
# the application.
|
42
|
+
def mock_app(base=Padrino::Application, &block)
|
43
|
+
base.use Rack::Session::Cookie # Need this because Sinatra 0.9.4 have use Rack::Session::Cookie if sessions? && !test?
|
44
|
+
@app = Sinatra.new(base, &block)
|
46
45
|
end
|
47
46
|
|
48
|
-
|
49
|
-
|
50
|
-
def silence_logger(&block)
|
51
|
-
orig_stdout = $stdout
|
52
|
-
$stdout = log_buffer = StringIO.new
|
53
|
-
block.call
|
54
|
-
$stdout = orig_stdout
|
55
|
-
log_buffer.rewind && log_buffer.read
|
47
|
+
def app
|
48
|
+
Rack::Lint.new(@app)
|
56
49
|
end
|
57
50
|
|
58
51
|
# Asserts that a file matches the pattern
|
@@ -60,12 +53,15 @@ class Test::Unit::TestCase
|
|
60
53
|
assert File.exist?(file), "File '#{file}' does not exist!"
|
61
54
|
assert_match pattern, File.read(file)
|
62
55
|
end
|
63
|
-
end
|
64
56
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
57
|
+
# Delegate other missing methods to response.
|
58
|
+
def method_missing(name, *args, &block)
|
59
|
+
if response && response.respond_to?(name)
|
60
|
+
response.send(name, *args, &block)
|
61
|
+
else
|
62
|
+
super
|
69
63
|
end
|
70
64
|
end
|
65
|
+
|
66
|
+
alias :response :last_response
|
71
67
|
end
|