radiant-comments-extension 0.0.6
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 +3 -0
- data/CHANGELOG +40 -0
- data/HELP_admin.markdown +52 -0
- data/HELP_designer.markdown +36 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +53 -0
- data/Rakefile +133 -0
- data/TODO +6 -0
- data/VERSION +1 -0
- data/app/controllers/admin/comments_controller.rb +130 -0
- data/app/controllers/comments_controller.rb +59 -0
- data/app/helpers/admin/comments_helper.rb +7 -0
- data/app/models/akismet_spam_filter.rb +37 -0
- data/app/models/comment.rb +121 -0
- data/app/models/comment_mailer.rb +24 -0
- data/app/models/mollom_spam_filter.rb +52 -0
- data/app/models/simple_spam_filter.rb +38 -0
- data/app/models/spam_filter.rb +43 -0
- data/app/views/admin/comments/_comment.rhtml +34 -0
- data/app/views/admin/comments/_form.rhtml +36 -0
- data/app/views/admin/comments/edit.rhtml +5 -0
- data/app/views/admin/comments/index.rhtml +55 -0
- data/app/views/admin/pages/_comments.rhtml +0 -0
- data/app/views/admin/pages/_edit_comments_enabled.rhtml +8 -0
- data/app/views/admin/pages/_index_head_view_comments.rhtml +1 -0
- data/app/views/admin/pages/_index_view_comments.rhtml +11 -0
- data/app/views/comment_mailer/comment_notification.rhtml +21 -0
- data/app/views/comments/_comment.rhtml +1 -0
- data/app/views/comments/_form.rhtml +23 -0
- data/app/views/comments/_new.rhtml +5 -0
- data/autotest/discover.rb +3 -0
- data/comments_extension.rb +81 -0
- data/cucumber.yml +1 -0
- data/db/migrate/001_create_comments.rb +29 -0
- data/db/migrate/002_create_snippets.rb +115 -0
- data/db/migrate/003_change_filter_id_from_integer_to_string.rb +10 -0
- data/db/migrate/004_add_approval_columns.rb +13 -0
- data/db/migrate/005_add_mollomid_column.rb +11 -0
- data/db/migrate/006_move_config_to_migrations.rb +22 -0
- data/db/migrate/007_add_preference_for_simple_spamcheck.rb +12 -0
- data/features/support/env.rb +16 -0
- data/features/support/paths.rb +16 -0
- data/lib/akismet.rb +134 -0
- data/lib/comment_page_extensions.rb +41 -0
- data/lib/comment_tags.rb +338 -0
- data/lib/mollom.rb +246 -0
- data/lib/radiant-comments-extension.rb +0 -0
- data/lib/tasks/comments_extension_tasks.rake +68 -0
- data/public/images/admin/accept.png +0 -0
- data/public/images/admin/comment_edit.png +0 -0
- data/public/images/admin/comments.png +0 -0
- data/public/images/admin/comments_delete.png +0 -0
- data/public/images/admin/delete.png +0 -0
- data/public/images/admin/email.png +0 -0
- data/public/images/admin/error.png +0 -0
- data/public/images/admin/link.png +0 -0
- data/public/images/admin/page_white_edit.png +0 -0
- data/public/images/admin/table_save.png +0 -0
- data/public/images/admin/tick.png +0 -0
- data/public/stylesheets/admin/comments.css +41 -0
- data/radiant-comments-extension.gemspec +133 -0
- data/spec/controllers/admin/comments_controller_spec.rb +57 -0
- data/spec/controllers/admin/comments_routing_spec.rb +43 -0
- data/spec/controllers/page_postback_spec.rb +51 -0
- data/spec/datasets/comments_dataset.rb +7 -0
- data/spec/models/akismet_spam_filter_spec.rb +61 -0
- data/spec/models/comment_spec.rb +148 -0
- data/spec/models/comment_tags_spec.rb +55 -0
- data/spec/models/mollom_spam_filter_spec.rb +103 -0
- data/spec/models/simple_spam_filter_spec.rb +44 -0
- data/spec/models/spam_filter_spec.rb +38 -0
- data/spec/spec.opts +6 -0
- data/spec/spec_helper.rb +36 -0
- data/test/fixtures/users.yml +6 -0
- data/test/integration/comment_enabling_test.rb +18 -0
- data/test/test_helper.rb +24 -0
- data/test/unit/comment_test.rb +52 -0
- metadata +177 -0
@@ -0,0 +1,55 @@
|
|
1
|
+
<% content_for :page_scripts do -%>
|
2
|
+
document.observe('dom:loaded', function(){
|
3
|
+
Event.addBehavior({
|
4
|
+
'tr.comment td.content': function(event){
|
5
|
+
$(this).observe('click', function(event){
|
6
|
+
if($(this).down('blockquote.expanded')){
|
7
|
+
$(this).down('blockquote.expanded').toggle();
|
8
|
+
$(this).down('blockquote.short').toggle();
|
9
|
+
}
|
10
|
+
event.stop();
|
11
|
+
});
|
12
|
+
}
|
13
|
+
});
|
14
|
+
});
|
15
|
+
<% end %>
|
16
|
+
<%- include_stylesheet 'admin/comments' %>
|
17
|
+
<h1>
|
18
|
+
<% if @page -%>
|
19
|
+
<%= @page.comments.count %> <%= "#{params[:status].titleize} " if params[:status] %><%= pluralize(@page.comments.count, 'Comment') %> on <%= link_to @page.title, edit_admin_page_path(@page) %>
|
20
|
+
<% else -%>
|
21
|
+
<%= params[:status].titleize if params[:status] %> Comments
|
22
|
+
<% end -%>
|
23
|
+
</h1>
|
24
|
+
|
25
|
+
<ul id="comment-nav">
|
26
|
+
<li class="all"><%= link_or_span_unless_current("All", :status => 'all', :page_id => params[:page_id]) %></li>
|
27
|
+
<li class="approved"><%= link_or_span_unless_current("Approved", :status => "approved", :page_id => params[:page_id]) %></li>
|
28
|
+
<li class="unapproved"><%= link_or_span_unless_current("Unapproved", :status => "unapproved", :page_id => params[:page_id]) %></li>
|
29
|
+
<li class="csv"><%= link_to "Download CSV", :format => :csv %></li>
|
30
|
+
</ul>
|
31
|
+
|
32
|
+
<%= will_paginate @comments %>
|
33
|
+
|
34
|
+
<table id="comments" class="index" border="0" cellspacing="0" cellpadding="0">
|
35
|
+
<thead>
|
36
|
+
<tr>
|
37
|
+
<th>Content</th>
|
38
|
+
<th>Date</th>
|
39
|
+
<th>Author</th>
|
40
|
+
<% unless @page %><th>Page</th><% end %>
|
41
|
+
<th>Actions</th>
|
42
|
+
</tr>
|
43
|
+
</thead>
|
44
|
+
<tbody>
|
45
|
+
<%= render(:partial => "comment", :collection => @comments) || %Q[<tr><td class="note" colspan="#{@page ? 4 : 5}">No comments</td></tr>] %>
|
46
|
+
</tbody>
|
47
|
+
</table>
|
48
|
+
|
49
|
+
<%= will_paginate @comments %>
|
50
|
+
|
51
|
+
<% form_tag destroy_unapproved_admin_comments_url, :method => :delete do %>
|
52
|
+
<p><button type="submit" class="delete-unapproved"><%= image_tag("admin/comments_delete.png") %> Delete All Unapproved</button></p>
|
53
|
+
<% end %>
|
54
|
+
|
55
|
+
<p><small class="notice"><%= Comment.spam_filter.message %></small></p>
|
File without changes
|
@@ -0,0 +1,8 @@
|
|
1
|
+
<p style="clear:left">
|
2
|
+
<label for="page_enable_comments">Allow Comments on this page?</label>
|
3
|
+
<%= check_box "page", "enable_comments" %>
|
4
|
+
|
5
|
+
<small>
|
6
|
+
<%= link_to "Currently #{@page.comments_count} — View comments", admin_page_comments_path(:page_id => @page.id) unless @page.new_record? or @page.comments_count < 1 %>
|
7
|
+
</small>
|
8
|
+
</p>
|
@@ -0,0 +1 @@
|
|
1
|
+
<th class="comments">Comments</th>
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<td class="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 %>
|
8
|
+
<% end %>
|
9
|
+
</small>
|
10
|
+
<% end %>
|
11
|
+
</td>
|
@@ -0,0 +1,21 @@
|
|
1
|
+
A new <%= @comment.ap_status %> comment has been posted on the <%= @site_name %> article <%= @comment.page.title %> (<%= @page_url %>)
|
2
|
+
|
3
|
+
Author : <%= @comment.author %> (IP: <%= @comment.author_ip %>)
|
4
|
+
E-mail : <%= @comment.author_email %>
|
5
|
+
URL : <%= @comment.author_url %>
|
6
|
+
Whois : http://ws.arin.net/cgi-bin/whois.pl?queryinput=<%= @comment.author_ip %>
|
7
|
+
Comment:
|
8
|
+
|
9
|
+
|
10
|
+
<%= @comment.content %>
|
11
|
+
|
12
|
+
|
13
|
+
|
14
|
+
You can see all comments on this post here:
|
15
|
+
<%= admin_page_comments_url(@comment.page) %>;
|
16
|
+
|
17
|
+
Approve it:
|
18
|
+
<%= approve_admin_comment_url(@comment) %>
|
19
|
+
|
20
|
+
View all comments awaiting approval:
|
21
|
+
<%= admin_comments_url(:status => :unapproved) %>
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= comment.inspect %>
|
@@ -0,0 +1,23 @@
|
|
1
|
+
<h1>Comment Form</h1>
|
2
|
+
|
3
|
+
<div id="comment_form_container">
|
4
|
+
|
5
|
+
<fieldset class="comment_form">
|
6
|
+
|
7
|
+
<a name="comment_form"></a>
|
8
|
+
|
9
|
+
<%= f.text_field :author, :label => "Your Name (as you'd like it to appear on this comment)", :class => "regular" %>
|
10
|
+
|
11
|
+
<%= f.text_field :author_url %>
|
12
|
+
|
13
|
+
<%= f.text_field :author_email %>
|
14
|
+
|
15
|
+
<%= f.text_area :content, :label => "Your comment", :rows => 5, :class => "regular" %>
|
16
|
+
|
17
|
+
<%= submit_tag("Save comment", :class => "button", :id => "comment_submit_button") %>
|
18
|
+
|
19
|
+
<%= f.hidden_field :page_id %>
|
20
|
+
|
21
|
+
</fieldset>
|
22
|
+
|
23
|
+
</div>
|
@@ -0,0 +1,81 @@
|
|
1
|
+
class CommentsExtension < Radiant::Extension
|
2
|
+
version "#{File.read(File.expand_path(File.dirname(__FILE__)) + '/VERSION')}"
|
3
|
+
description "Adds blog-like comments and comment functionality to pages."
|
4
|
+
url "http://github.com/saturnflyer/radiant-comments"
|
5
|
+
|
6
|
+
define_routes do |map|
|
7
|
+
map.namespace :admin do |admin|
|
8
|
+
admin.connect 'comments/:status', :controller => 'comments', :status => 'unapproved', :conditions => { :method => :get }, :requirements => { :status => /all|unapproved|approved/ }
|
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"
|
21
|
+
end
|
22
|
+
|
23
|
+
def activate
|
24
|
+
require 'sanitize'
|
25
|
+
|
26
|
+
Dir["#{File.dirname(__FILE__)}/app/models/*_filter.rb"].each do |file|
|
27
|
+
require file
|
28
|
+
end
|
29
|
+
|
30
|
+
Page.class_eval do
|
31
|
+
include CommentPageExtensions
|
32
|
+
include CommentTags
|
33
|
+
end
|
34
|
+
|
35
|
+
if admin.respond_to? :page
|
36
|
+
admin.page.edit.add :parts_bottom, "edit_comments_enabled", :before => "edit_timestamp"
|
37
|
+
admin.page.index.add :sitemap_head, "index_head_view_comments"
|
38
|
+
admin.page.index.add :node, "index_view_comments"
|
39
|
+
end
|
40
|
+
|
41
|
+
if self.respond_to? :tab
|
42
|
+
tab "Content" do
|
43
|
+
add_item('Comments', '/admin/comments')
|
44
|
+
end
|
45
|
+
else
|
46
|
+
admin.tabs.add "Comments", "/admin/comments", :visibility => [:all]
|
47
|
+
end
|
48
|
+
require "fastercsv"
|
49
|
+
|
50
|
+
ActiveRecord::Base.class_eval do
|
51
|
+
def self.to_csv(*args)
|
52
|
+
find(:all).to_csv(*args)
|
53
|
+
end
|
54
|
+
|
55
|
+
def export_columns(format = nil)
|
56
|
+
self.class.content_columns.map(&:name) - ['created_at', 'updated_at']
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_row(format = nil)
|
60
|
+
export_columns(format).map { |c| self.send(c) }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
Array.class_eval do
|
65
|
+
def to_csv(options = {})
|
66
|
+
return "" if first.nil?
|
67
|
+
if all? { |e| e.respond_to?(:to_row) }
|
68
|
+
header_row = first.export_columns(options[:format]).to_csv
|
69
|
+
content_rows = map { |e| e.to_row(options[:format]) }.map(&:to_csv)
|
70
|
+
([header_row] + content_rows).join
|
71
|
+
else
|
72
|
+
FasterCSV.generate_line(self, options)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def deactivate
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
data/cucumber.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
default: --format progress features --tags ~@proposed,~@in_progress
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class CreateComments < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :comments do |t|
|
4
|
+
t.column :page_id, :integer
|
5
|
+
t.column :author, :string
|
6
|
+
t.column :author_url, :string
|
7
|
+
t.column :author_email, :string
|
8
|
+
t.column :author_ip, :string
|
9
|
+
t.column :content, :text
|
10
|
+
t.column :content_html, :text
|
11
|
+
t.column :created_at, :datetime
|
12
|
+
t.column :updated_at, :datetime
|
13
|
+
t.column :filter_id, :integer
|
14
|
+
t.column :user_agent, :string
|
15
|
+
t.column :referrer, :string
|
16
|
+
end
|
17
|
+
|
18
|
+
add_column :pages, :enable_comments, :boolean, :default => false
|
19
|
+
add_column :pages, :comments_count, :integer, :default => 0
|
20
|
+
Page.reset_column_information
|
21
|
+
Page.update_all("comments_count = 0")
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.down
|
25
|
+
drop_table :comments
|
26
|
+
remove_column :pages, :enable_comments
|
27
|
+
remove_column :pages, :comments_count
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
class CreateSnippets < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
|
4
|
+
# This code _WILL_ override snippets with the name comments, comment and comment_form
|
5
|
+
# if they exists.
|
6
|
+
|
7
|
+
# Comments snippet
|
8
|
+
Snippet.new do |s|
|
9
|
+
s.name = "comments"
|
10
|
+
s.content = <<CONTENT
|
11
|
+
<r:if_comments>
|
12
|
+
<div class="comments">
|
13
|
+
<h2>Comments</h2>
|
14
|
+
<r:comments:each>
|
15
|
+
<r:snippet name="comment" />
|
16
|
+
</r:comments:each>
|
17
|
+
</div>
|
18
|
+
</r:if_comments>
|
19
|
+
<r:snippet name="comment_form" />
|
20
|
+
CONTENT
|
21
|
+
end.save
|
22
|
+
|
23
|
+
# Comment snippet
|
24
|
+
Snippet.new do |s|
|
25
|
+
s.name = "comment"
|
26
|
+
s.content = <<CONTENT
|
27
|
+
<r:comments:field>
|
28
|
+
<div class="comment" id="comment-<r:id/>">
|
29
|
+
<p class="author">
|
30
|
+
<r:if_author_url><a href="<r:author_url/>" title="Visit <r:author/>'s website"></r:if_author_url>
|
31
|
+
<r:author/>
|
32
|
+
<r:if_author_url></a></r:if_author_url>
|
33
|
+
said on <r:date/>:
|
34
|
+
</p>
|
35
|
+
|
36
|
+
<div class="content_html"><r:content_html /></div>
|
37
|
+
|
38
|
+
<r:if_selected><p><em>
|
39
|
+
<r:if_approved>Thanks for your comment!</r:if_approved>
|
40
|
+
<r:unless_approved>Thanks for your comment, it has gone into the moderation queue and will be dealt with shortly.</r:unless_approved>
|
41
|
+
</em></p></r:if_selected>
|
42
|
+
</div>
|
43
|
+
</r:comments:field>
|
44
|
+
CONTENT
|
45
|
+
end.save
|
46
|
+
|
47
|
+
# comment_spam_block snippet
|
48
|
+
Snippet.new do |s|
|
49
|
+
s.name = 'comment_spam_block'
|
50
|
+
s.content = <<CONTENT
|
51
|
+
<r:random>
|
52
|
+
<r:error on="spam_answer"><p style="color:red">Answer <r:message /></p></r:error>
|
53
|
+
<r:option>
|
54
|
+
<p><label for="comment_spam_answer">What day of the week has the letter "h" in it's name?</label> (required)<br />
|
55
|
+
<r:spam_answer_tag answer="Thursday" /></p>
|
56
|
+
</r:option>
|
57
|
+
<r:option>
|
58
|
+
<p><label for="comment_spam_answer">Yellow and blue together make what color?</label> (required)<br />
|
59
|
+
<r:spam_answer_tag answer="green" /></p>
|
60
|
+
</r:option>
|
61
|
+
<r:option>
|
62
|
+
<p><label for="comment_spam_answer">What is SPAM spelled backwards?</label> (required)<br />
|
63
|
+
<r:spam_answer_tag answer="MAPS" /></p>
|
64
|
+
</r:option>
|
65
|
+
</r:random>
|
66
|
+
CONTENT
|
67
|
+
end.save
|
68
|
+
|
69
|
+
# Comment_form snippet
|
70
|
+
Snippet.new do |s|
|
71
|
+
s.name = "comment_form"
|
72
|
+
s.content = <<CONTENT
|
73
|
+
<r:page>
|
74
|
+
<r:if_enable_comments>
|
75
|
+
<r:comments:form>
|
76
|
+
<h3>Post a comment</h3>
|
77
|
+
<r:error><p style="color:red">Please correct the errors below.</p></r:error>
|
78
|
+
<p><label for="comment_author">Your Name</label><br />
|
79
|
+
<r:error on="author"><p style="color:red">Name <r:message /></p></r:error>
|
80
|
+
<r:text_field_tag name="author" id="author" class="required" /></p>
|
81
|
+
|
82
|
+
<p><label for="comment_author_email">Your Email Address</label> (required, but not displayed)<br />
|
83
|
+
<r:error on="author_email"><p style="color:red">Email <r:message /></p></r:error>
|
84
|
+
<r:text_field_tag name="author_email" class="required" /></p>
|
85
|
+
|
86
|
+
<p><label for="comment_author_url">Your Web Address</label> (optional)<br />
|
87
|
+
<r:error on="author_url"><p style="color:red">Web Address <r:message /></p></r:error>
|
88
|
+
<r:text_field_tag name="author_url" /></p>
|
89
|
+
|
90
|
+
<p><label for="comment_content">Your Comment</label><br />
|
91
|
+
<r:error on="content"><p style="color:red">Comment <r:message /></p></r:error>
|
92
|
+
<label for="comment_filter_id">Filter: <r:filter_box_tag name="filter_id" value="Textile" /></label><br />
|
93
|
+
<r:text_area_tag name="content" class="required" rows="9" cols="40" /></p>
|
94
|
+
|
95
|
+
<r:if_comments_simple_spam_filter_enabled>
|
96
|
+
<r:snippet name="comment_spam_block" />
|
97
|
+
</r:if_comments_simple_spam_filter_enabled>
|
98
|
+
|
99
|
+
<r:submit_tag name="submit" value="Save Comment" />
|
100
|
+
|
101
|
+
</r:comments:form>
|
102
|
+
</r:if_enable_comments>
|
103
|
+
</r:page>
|
104
|
+
CONTENT
|
105
|
+
end.save
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.down
|
109
|
+
|
110
|
+
["comments", "comment", "comment_form", "comment_spam_block"].each do |snippet|
|
111
|
+
Snippet.find_by_name(snippet).destroy rescue p "Could not destroy snippet #{snippet}"
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# rake production radiant:extensions:comments:migrate
|
2
|
+
class AddApprovalColumns < ActiveRecord::Migration
|
3
|
+
def self.up
|
4
|
+
add_column :comments, :approved_at, :datetime
|
5
|
+
add_column :comments, :approved_by, :integer
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.down
|
9
|
+
remove_column :comments, :approved_by
|
10
|
+
remove_column :comments, :approved_at
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class MoveConfigToMigrations < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
if Radiant::Config.table_exists?
|
4
|
+
{ 'notification' => 'false',
|
5
|
+
'notification_from' => '',
|
6
|
+
'notification_to' => '',
|
7
|
+
'notification_site_name' => '',
|
8
|
+
'notify_creator' => 'true',
|
9
|
+
'notify_updater' => 'false',
|
10
|
+
'akismet_key' => '',
|
11
|
+
'akismet_url' => '',
|
12
|
+
'mollom_privatekey' => '',
|
13
|
+
'mollom_publickey' => '',
|
14
|
+
'filters_enabled' => 'true',
|
15
|
+
}.each{|k,v| Radiant::Config.create(:key => "comments.#{k}", :value => v) unless Radiant::Config["comments.#{k}"]}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.down
|
20
|
+
# not necessary
|
21
|
+
end
|
22
|
+
end
|