radiant-comments-extension 0.0.6 → 0.0.8
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/.gitignore +1 -1
- data/CHANGELOG +6 -0
- data/Gemfile +4 -0
- data/README.rdoc +9 -0
- data/Rakefile +0 -16
- data/TODO +0 -1
- data/app/controllers/admin/comments_controller.rb +1 -5
- data/app/controllers/comments_controller.rb +46 -4
- data/app/models/akismet_spam_filter.rb +1 -1
- data/app/models/comment.rb +7 -1
- data/app/models/mollom_spam_filter.rb +17 -7
- data/app/models/spam_filter.rb +1 -0
- data/app/views/admin/comments/_comment.html.haml +26 -0
- data/app/views/admin/comments/_form.html.haml +37 -0
- data/app/views/admin/comments/edit.html.haml +4 -0
- data/app/views/admin/comments/index.html.haml +57 -0
- data/app/views/admin/pages/{_comments.rhtml → _comments.html.haml} +0 -0
- data/app/views/admin/pages/_edit_comments_enabled.html.haml +7 -0
- data/app/views/admin/pages/_index_head_view_comments.html.haml +1 -0
- data/app/views/admin/pages/_index_view_comments.html.haml +7 -0
- data/comments_extension.rb +11 -27
- data/config/locales/en.yml +26 -0
- data/config/locales/nl.yml +26 -0
- data/config/routes.rb +16 -0
- data/db/migrate/002_create_snippets.rb +4 -4
- data/lib/comment_page_extensions.rb +15 -2
- data/lib/comment_tags.rb +55 -0
- data/lib/radiant-comments-extension.rb +2 -0
- data/lib/radiant-comments-extension/version.rb +3 -0
- data/public/stylesheets/admin/comments.css +21 -29
- data/radiant-comments-extension.gemspec +8 -117
- data/spec/controllers/page_postback_spec.rb +2 -1
- data/spec/models/comment_spec.rb +7 -23
- data/spec/models/mollom_spam_filter_spec.rb +5 -0
- metadata +30 -32
- data/VERSION +0 -1
- data/app/views/admin/comments/_comment.rhtml +0 -34
- data/app/views/admin/comments/_form.rhtml +0 -36
- data/app/views/admin/comments/edit.rhtml +0 -5
- data/app/views/admin/comments/index.rhtml +0 -55
- data/app/views/admin/pages/_edit_comments_enabled.rhtml +0 -8
- data/app/views/admin/pages/_index_head_view_comments.rhtml +0 -1
- data/app/views/admin/pages/_index_view_comments.rhtml +0 -11
- data/app/views/comments/_comment.rhtml +0 -1
- data/app/views/comments/_form.rhtml +0 -23
- data/app/views/comments/_new.rhtml +0 -5
- data/lib/mollom.rb +0 -246
data/.gitignore
CHANGED
data/CHANGELOG
CHANGED
@@ -1,5 +1,11 @@
|
|
1
1
|
= Git
|
2
2
|
|
3
|
+
= 0.0.7
|
4
|
+
|
5
|
+
* Updated to use of the Mollom gem i.o. local copy [Benny Degezelle]
|
6
|
+
* Updated to use mollom to it's full potential (show CAPTCHA when unsure) [Benny Degezelle]
|
7
|
+
* Added r:comments:spam_message which only expands if a posted comment is thought to be spam [Benny Degezelle]
|
8
|
+
|
3
9
|
= 0.0.6
|
4
10
|
|
5
11
|
* Added spam_answer_tag for simple spam protection or CAPTCHA
|
data/Gemfile
ADDED
data/README.rdoc
CHANGED
@@ -15,6 +15,7 @@ If you want to contribute features or fixes please write your specs and code and
|
|
15
15
|
* Requires will_paginate Rails plugin (http://github.com/mislav/will_paginate/tree/master)
|
16
16
|
* Requires fastercsv 1.2.3 or greater.
|
17
17
|
* Requires sanitize 1.0.8 or greater.
|
18
|
+
* Requires mollom 0.2.2 or greater.
|
18
19
|
|
19
20
|
== Installation
|
20
21
|
|
@@ -24,6 +25,14 @@ To install Comments, run:
|
|
24
25
|
|
25
26
|
This _WILL_ delete any snippets named 'comment', 'comments' and 'comment_form' if these exist.
|
26
27
|
|
28
|
+
If you are upgrading from a previous version with out the sanitizer run:
|
29
|
+
|
30
|
+
rake radiant:extensions:comments:initialize
|
31
|
+
|
32
|
+
or if you want to clean up your initializer for the sanitize gem run:
|
33
|
+
|
34
|
+
rake radiant:extensions:comments:forced_initialize
|
35
|
+
|
27
36
|
== More Help
|
28
37
|
|
29
38
|
See the included HELP docs here or read them in your Radiant interface with the Help extension installed.
|
data/Rakefile
CHANGED
@@ -1,19 +1,3 @@
|
|
1
|
-
begin
|
2
|
-
require 'jeweler'
|
3
|
-
Jeweler::Tasks.new do |gem|
|
4
|
-
gem.name = "radiant-comments-extension"
|
5
|
-
gem.summary = %Q{Comments Extension for Radiant CMS}
|
6
|
-
gem.description = %Q{Adds blog-like comment functionality to Radiant.}
|
7
|
-
gem.email = "jim@saturnflyer.com"
|
8
|
-
gem.homepage = "http://github.com/saturnflyer/radiant-comments-extension"
|
9
|
-
gem.authors = ['Jim Gay', 'Ryan Heneise', 'Sean Cribbs','John Muhl', 'Sven Schwyn', 'Gerrit Kaiser', 'Stephen Lombardo', 'Benny Degezelle', 'Frank Louwers', 'Michael Hale', 'Nathaniel Talbott', 'John Croisant', 'Jon Leighton', 'Witter Cheng', 'Keith Bingman']
|
10
|
-
gem.add_development_dependency "radiant"
|
11
|
-
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
12
|
-
end
|
13
|
-
rescue LoadError
|
14
|
-
puts "Jeweler (or a dependency) not available. This is only required if you plan to package dashboard as a gem."
|
15
|
-
end
|
16
|
-
|
17
1
|
# Determine where the RSpec plugin is by loading the boot
|
18
2
|
unless defined? RADIANT_ROOT
|
19
3
|
ENV["RAILS_ENV"] = "test"
|
data/TODO
CHANGED
@@ -114,11 +114,7 @@ class Admin::CommentsController < ApplicationController
|
|
114
114
|
end
|
115
115
|
|
116
116
|
def clear_cache
|
117
|
-
|
118
|
-
ResponseCache.instance.clear
|
119
|
-
else
|
120
|
-
Radiant::Cache.clear
|
121
|
-
end
|
117
|
+
Radiant::Cache.clear
|
122
118
|
end
|
123
119
|
|
124
120
|
def clear_single_page_cache(comment)
|
@@ -24,13 +24,42 @@ class CommentsController < ApplicationController
|
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
|
-
|
28
|
-
|
27
|
+
if comment.approved?
|
28
|
+
redirect_to "#{@page.url}comments#comment-#{comment.id}"
|
29
|
+
elsif Comment.spam_filter == MollomSpamFilter && MollomSpamFilter.mollom_response(comment).to_s == 'unsure'
|
30
|
+
set_up_page_with_captcha(comment)
|
31
|
+
render :text => @page.render and return
|
32
|
+
else
|
33
|
+
@page.posted_comment_is_spam = true
|
34
|
+
#comment.destroy
|
35
|
+
render :text => @page.render
|
36
|
+
end
|
29
37
|
rescue ActiveRecord::RecordInvalid
|
38
|
+
if comment.errors.on(:spam_answer)
|
39
|
+
@page.posted_comment_is_spam = true
|
40
|
+
end
|
30
41
|
@page.last_comment = comment
|
31
42
|
render :text => @page.render
|
32
|
-
|
33
|
-
|
43
|
+
rescue SpamFilter::Spam
|
44
|
+
@page.posted_comment_is_spam = true
|
45
|
+
#comment.destroy
|
46
|
+
render :text => @page.render
|
47
|
+
end
|
48
|
+
|
49
|
+
def solve_captcha
|
50
|
+
comment = Comment.find_by_mollom_id(params[:comment_mollom_id])
|
51
|
+
answer = params[:captcha_answer]
|
52
|
+
mollom = Comment.spam_filter.mollom
|
53
|
+
if mollom.valid_captcha? :session_id => comment.mollom_id, :solution => answer
|
54
|
+
comment.approve!
|
55
|
+
redirect_to "#{@page.url}comments#comment-#{comment.id}"
|
56
|
+
else
|
57
|
+
set_up_page_with_captcha(comment)
|
58
|
+
render :text => @page.render
|
59
|
+
end
|
60
|
+
rescue Mollom::Error # when you don't provide 'correct' or 'incorrect' in mollom developer mode
|
61
|
+
set_up_page_with_captcha(comment)
|
62
|
+
render :text => @page.render
|
34
63
|
end
|
35
64
|
|
36
65
|
private
|
@@ -55,5 +84,18 @@ class CommentsController < ApplicationController
|
|
55
84
|
end
|
56
85
|
end
|
57
86
|
end
|
87
|
+
|
88
|
+
def set_up_page_with_captcha(comment)
|
89
|
+
mollom = Comment.spam_filter.mollom
|
90
|
+
if comment.mollom_id.nil?
|
91
|
+
captcha = mollom.image_captcha
|
92
|
+
comment.update_attribute(:mollom_id, captcha['session_id'])
|
93
|
+
else
|
94
|
+
captcha = mollom.image_captcha(:session_id => comment.mollom_id)
|
95
|
+
end
|
96
|
+
@page.last_comment = comment
|
97
|
+
@page.captcha_url = captcha['url']
|
98
|
+
@page.comment_mollom_id = @page.captcha_url[/\/([a-z0-9]*).png/, 1]
|
99
|
+
end
|
58
100
|
|
59
101
|
end
|
@@ -10,7 +10,7 @@ class AkismetSpamFilter < SpamFilter
|
|
10
10
|
|
11
11
|
def approved?(comment)
|
12
12
|
(akismet.valid? && ham?(comment)) || raise(SpamFilter::Spam)
|
13
|
-
rescue
|
13
|
+
rescue StandardError, TimeoutError
|
14
14
|
# Spam and anything raised by Net::HTTP, e.g. Errno, Timeout stuff
|
15
15
|
false
|
16
16
|
end
|
data/app/models/comment.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'digest/md5'
|
2
|
+
require 'sanitize'
|
2
3
|
class Comment < ActiveRecord::Base
|
3
4
|
belongs_to :page, :counter_cache => true
|
4
5
|
|
@@ -35,8 +36,12 @@ class Comment < ActiveRecord::Base
|
|
35
36
|
self.referrer = request.env['HTTP_REFERER']
|
36
37
|
end
|
37
38
|
|
39
|
+
def is_ham?
|
40
|
+
spam_filter.valid?(self)
|
41
|
+
end
|
42
|
+
|
38
43
|
def auto_approve?
|
39
|
-
Radiant::Config['comments.auto_approve'] == "true" &&
|
44
|
+
Radiant::Config['comments.auto_approve'] == "true" && is_ham?
|
40
45
|
end
|
41
46
|
|
42
47
|
def unapproved?
|
@@ -75,6 +80,7 @@ class Comment < ActiveRecord::Base
|
|
75
80
|
|
76
81
|
def auto_approve
|
77
82
|
self.approved_at = Time.now if auto_approve?
|
83
|
+
true
|
78
84
|
end
|
79
85
|
|
80
86
|
def apply_filter
|
@@ -8,9 +8,14 @@ class MollomSpamFilter < SpamFilter
|
|
8
8
|
!Radiant::Config['comments.mollom_publickey'].blank?
|
9
9
|
end
|
10
10
|
|
11
|
+
def valid?(comment)
|
12
|
+
approved?(comment)
|
13
|
+
rescue SpamFilter::Unsure
|
14
|
+
end
|
15
|
+
|
11
16
|
def approved?(comment)
|
12
17
|
(mollom.key_ok? && ham?(comment)) || raise(SpamFilter::Spam)
|
13
|
-
rescue Mollom::Error, SpamFilter::Spam
|
18
|
+
rescue Mollom::Error, SpamFilter::Spam, TimeoutError
|
14
19
|
false
|
15
20
|
end
|
16
21
|
|
@@ -32,19 +37,24 @@ class MollomSpamFilter < SpamFilter
|
|
32
37
|
end
|
33
38
|
end
|
34
39
|
|
35
|
-
|
36
|
-
|
37
|
-
response = mollom.check_content(
|
40
|
+
def mollom_response(comment)
|
41
|
+
mollom.check_content(
|
38
42
|
:author_name => comment.author, # author name
|
39
43
|
:author_mail => comment.author_email, # author email
|
40
44
|
:author_url => comment.author_url, # author url
|
41
|
-
:post_body => comment.content # comment text
|
45
|
+
:post_body => comment.content, # comment text
|
46
|
+
:author_ip => comment.author_ip
|
42
47
|
)
|
43
|
-
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def ham?(comment)
|
53
|
+
response = mollom_response(comment)
|
44
54
|
save_mollom_servers
|
45
55
|
response.ham?
|
46
56
|
end
|
47
|
-
|
57
|
+
|
48
58
|
def save_mollom_servers
|
49
59
|
Rails.cache.write('MOLLOM_SERVER_CACHE', mollom.server_list.to_yaml) if mollom.key_ok?
|
50
60
|
rescue Mollom::Error #TODO: something with this error...
|
data/app/models/spam_filter.rb
CHANGED
@@ -0,0 +1,26 @@
|
|
1
|
+
%tr{:id => dom_id(comment), :class => "comment comment-#{comment.ap_status}"}
|
2
|
+
%td.content{:title => comment.content.size >= 70 ? t('click_to_toggle') : "" }
|
3
|
+
%blockquote.short= escape_once(truncate(comment.content, :length => 70))
|
4
|
+
- if comment.content.size >= 70
|
5
|
+
%blockquote.expanded{:style => "display:none"}
|
6
|
+
= escape_once(comment.content)
|
7
|
+
%td.date-column
|
8
|
+
= comment.created_at.strftime("%b %e, %Y at %I:%M%p")
|
9
|
+
%td.author-column
|
10
|
+
= escape_once(comment.author)
|
11
|
+
- unless comment.author_email.blank?
|
12
|
+
= mail_to(comment.author_email, image_tag("admin/email.png"))
|
13
|
+
- unless comment.author_url.blank?
|
14
|
+
= link_to(image_tag("admin/link.png"), comment.author_url)
|
15
|
+
- unless @page
|
16
|
+
%td.page-column
|
17
|
+
= link_to truncate(comment.page.title, :length => 40), comment.page.url, :class => 'view-page'
|
18
|
+
= link_to image_tag("admin/page_white_edit.png"), edit_admin_page_path(comment.page), :title => t('edit_page')
|
19
|
+
%td.controls-column
|
20
|
+
- if comment.approved?
|
21
|
+
= link_to(image_tag('admin/error.png'), unapprove_admin_comment_path(comment), :method => :put, :title => t('unapprove_comment'))
|
22
|
+
- else
|
23
|
+
= link_to(image_tag('admin/accept.png'), approve_admin_comment_path(comment), :title => t('approve_comment'), :method => :put)
|
24
|
+
|
25
|
+
= link_to image_tag("admin/delete.png"), admin_comment_path(comment), :method => :delete, :confirm => t('are_you_sure_you_want_to_delete_this_comment'), :title => t('delete_comment')
|
26
|
+
= link_to image_tag("admin/comment_edit.png"), edit_admin_comment_path(comment), :title => t('edit_comment')
|
@@ -0,0 +1,37 @@
|
|
1
|
+
- if Comment.spam_filter == SimpleSpamFilter
|
2
|
+
%div{:style => "display:none"}
|
3
|
+
= f.hidden_field :spam_answer, :value => "hemidemisemiquaver"
|
4
|
+
= f.hidden_field :valid_spam_answer, :value => Digest::MD5.hexdigest("hemidemisemiquaver")
|
5
|
+
|
6
|
+
#comment_form_container.form-area
|
7
|
+
.set
|
8
|
+
%p.title
|
9
|
+
= f.label :author, t('author')
|
10
|
+
= f.text_field :author
|
11
|
+
%p
|
12
|
+
= f.label :author_url, t('url')
|
13
|
+
= f.text_field :author_url
|
14
|
+
%p
|
15
|
+
= f.label :author_email, t('email')
|
16
|
+
= f.text_field :author_email
|
17
|
+
%p
|
18
|
+
%label{:for => "comment_content"}= t('comment')
|
19
|
+
%br/
|
20
|
+
= f.text_area :content, :rows => 10, :cols => 72, :class => "textarea"
|
21
|
+
%p
|
22
|
+
%label{:for => "comment_filter_id"}= t('filter')
|
23
|
+
= f.select :filter_id, [['<none>', '']] + TextFilter.descendants.map { |s| s.filter_name }.sort
|
24
|
+
%p
|
25
|
+
Referred by:
|
26
|
+
= f.object.referrer
|
27
|
+
%p
|
28
|
+
Browser:
|
29
|
+
= f.object.user_agent
|
30
|
+
%p
|
31
|
+
Author's IP Address:
|
32
|
+
= f.object.author_ip
|
33
|
+
|
34
|
+
%p.buttons
|
35
|
+
= save_model_button(@comment)
|
36
|
+
= t('or')
|
37
|
+
= link_to t('cancel'), :back
|
@@ -0,0 +1,57 @@
|
|
1
|
+
- content_for :page_scripts do
|
2
|
+
:plain
|
3
|
+
document.observe('dom:loaded', function(){
|
4
|
+
Event.addBehavior({
|
5
|
+
'tr.comment td.content': function(event){
|
6
|
+
$(this).observe('click', function(event){
|
7
|
+
if($(this).down('blockquote.expanded')){
|
8
|
+
$(this).down('blockquote.expanded').toggle();
|
9
|
+
$(this).down('blockquote.short').toggle();
|
10
|
+
}
|
11
|
+
event.stop();
|
12
|
+
});
|
13
|
+
}
|
14
|
+
});
|
15
|
+
});
|
16
|
+
|
17
|
+
- include_stylesheet 'admin/comments'
|
18
|
+
|
19
|
+
.outset
|
20
|
+
#filters
|
21
|
+
%ul.comment-nav
|
22
|
+
- if @page
|
23
|
+
%li
|
24
|
+
= "#{@page.comments.count.to_s} #{t("comment_states.#{params[:status]}")} " if params[:status]
|
25
|
+
= "#{pluralize(@page.comments.count, 'Comment')} on #{link_to @page.title, edit_admin_page_path(@page)}"
|
26
|
+
%li.all
|
27
|
+
= link_or_span_unless_current(t('comment_states.all'), :status => 'all', :page_id => params[:page_id])
|
28
|
+
%li.approved
|
29
|
+
= link_or_span_unless_current(t('comment_states.approved'), :status => "approved", :page_id => params[:page_id])
|
30
|
+
%li.unapproved
|
31
|
+
= link_or_span_unless_current(t('comment_states.unapproved'), :status => "unapproved", :page_id => params[:page_id])
|
32
|
+
|
33
|
+
|
34
|
+
#comments_table
|
35
|
+
%table#comments.index
|
36
|
+
%thead
|
37
|
+
%tr
|
38
|
+
%th= t('content')
|
39
|
+
%th= t('date_string')
|
40
|
+
%th= t('author')
|
41
|
+
- unless @page
|
42
|
+
%th= t('page')
|
43
|
+
%th= t('actions')
|
44
|
+
%tbody
|
45
|
+
= render(:partial => "comment", :collection => @comments) || %Q[<tr><td class="note" colspan="#{@page ? 4 : 5}">No comments</td></tr>]
|
46
|
+
%p
|
47
|
+
%small.notice
|
48
|
+
= Comment.spam_filter.message
|
49
|
+
|
50
|
+
|
51
|
+
#actions
|
52
|
+
= will_paginate @comments
|
53
|
+
%ul
|
54
|
+
%li
|
55
|
+
= link_to "#{image('table_save')} #{t('download_csv')}", :format => :csv
|
56
|
+
%li
|
57
|
+
= link_to "#{image('comments_delete')} #{t('delete_unapproved')}", destroy_unapproved_admin_comments_url, :class => 'delete-unapproved', :method => :delete, :confirm => 'Is it OK to delete ALL Unapproved Comments?'
|
File without changes
|
@@ -0,0 +1,7 @@
|
|
1
|
+
%p{:style => "clear:left"}
|
2
|
+
%label{:for => "page_enable_comments"}
|
3
|
+
= check_box "page", "enable_comments"
|
4
|
+
= t('allow_comments_on_page')
|
5
|
+
- unless @page.new_record? or @page.comments_count < 1
|
6
|
+
%small
|
7
|
+
= link_to t('currently_x_view_comments', :count => @page.comments_count), admin_page_comments_path(:page_id => @page.id)
|
@@ -0,0 +1 @@
|
|
1
|
+
%th.comments= t('comments')
|
@@ -0,0 +1,7 @@
|
|
1
|
+
%td.comment_link
|
2
|
+
- if page.respond_to?('enable_comments')
|
3
|
+
%small
|
4
|
+
- if page.enable_comments
|
5
|
+
= link_to "#{page.comments_count} comments", admin_page_comments_path(:page_id => page.id)
|
6
|
+
- else
|
7
|
+
= link_to "Enable", admin_page_enable_comments_path(:page_id => page.id), :method => :put
|
data/comments_extension.rb
CHANGED
@@ -1,28 +1,15 @@
|
|
1
|
+
require File.expand_path("../lib/radiant-comments-extension/version", __FILE__)
|
1
2
|
class CommentsExtension < Radiant::Extension
|
2
|
-
version
|
3
|
+
version RadiantCommentsExtension::VERSION
|
3
4
|
description "Adds blog-like comments and comment functionality to pages."
|
4
5
|
url "http://github.com/saturnflyer/radiant-comments"
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
admin.connect 'comments/:status.:format', :controller => 'comments', :status => /all|approved|unapproved/, :conditions => { :method => :get }
|
10
|
-
admin.resources :comments, :member => { :remove => :get, :approve => :put, :unapprove => :put }, :collection => {:destroy_unapproved => :delete}
|
11
|
-
admin.page_enable_comments '/pages/:page_id/comments/enable', :controller => 'comments', :action => 'enable', :conditions => {:method => :put}
|
12
|
-
end
|
13
|
-
map.with_options(:controller => 'admin/comments') do |comments|
|
14
|
-
comments.connect 'admin/pages/:page_id/comments/:status', :status => /all|approved|unapproved/, :conditions => { :method => :get }
|
15
|
-
comments.connect 'admin/pages/:page_id/comments/:status.:format', :status => /all|approved|unapproved/, :conditions => { :method => :get }
|
16
|
-
comments.admin_page_comments 'admin/pages/:page_id/comments/:action'
|
17
|
-
comments.admin_page_comment 'admin/pages/:page_id/comments/:id/:action'
|
18
|
-
end
|
19
|
-
# This needs to be last, otherwise it hoses the admin routes.
|
20
|
-
map.resources :comments, :name_prefix => "page_", :path_prefix => "*url", :controller => "comments"
|
6
|
+
|
7
|
+
extension_config do |config|
|
8
|
+
config.gem 'sanitize'
|
9
|
+
config.gem 'mollom'
|
21
10
|
end
|
22
11
|
|
23
12
|
def activate
|
24
|
-
require 'sanitize'
|
25
|
-
|
26
13
|
Dir["#{File.dirname(__FILE__)}/app/models/*_filter.rb"].each do |file|
|
27
14
|
require file
|
28
15
|
end
|
@@ -37,14 +24,11 @@ class CommentsExtension < Radiant::Extension
|
|
37
24
|
admin.page.index.add :sitemap_head, "index_head_view_comments"
|
38
25
|
admin.page.index.add :node, "index_view_comments"
|
39
26
|
end
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
else
|
46
|
-
admin.tabs.add "Comments", "/admin/comments", :visibility => [:all]
|
47
|
-
end
|
27
|
+
|
28
|
+
tab "Content" do
|
29
|
+
add_item("Comments", "/admin/comments")
|
30
|
+
end.
|
31
|
+
|
48
32
|
require "fastercsv"
|
49
33
|
|
50
34
|
ActiveRecord::Base.class_eval do
|