├── .gitignore ├── .ruby-gemset.example ├── .ruby-version.example ├── .rvmrc.example ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── app ├── assets │ ├── javascripts │ │ ├── the_comments.js.coffee │ │ └── the_comments_manage.js.coffee │ └── stylesheets │ │ └── the_comments.css.scss ├── controllers │ ├── _templates_ │ │ └── comments_controller.rb │ └── concerns │ │ └── the_comments │ │ ├── controller.rb │ │ └── view_token.rb ├── helpers │ └── render_comments_tree_helper.rb ├── models │ ├── _templates_ │ │ └── comment.rb │ └── concerns │ │ └── the_comments │ │ ├── comment.rb │ │ ├── comment_states.rb │ │ ├── commentable.rb │ │ └── user.rb └── views │ └── the_comments │ ├── _tree.html.erb │ ├── haml │ ├── _additional_info.html.haml │ ├── _comment.html.haml │ ├── _comment_body.html.haml │ ├── _comment_edit.html.haml │ ├── _form.html.haml │ ├── _guest_form.html.haml │ ├── _logined_form.html.haml │ ├── _manage_controls.html.haml │ ├── _sidebar.html.haml │ ├── _sidebar_admin.html.haml │ ├── _sidebar_backlink.html.haml │ ├── _sidebar_user.html.haml │ ├── _tree.html.haml │ └── manage.html.haml │ └── slim │ ├── _additional_info.html.slim │ ├── _comment.html.slim │ ├── _comment_body.html.slim │ ├── _comment_edit.html.slim │ ├── _form.html.slim │ ├── _guest_form.html.slim │ ├── _logined_form.html.slim │ ├── _manage_controls.html.slim │ ├── _sidebar.html.slim │ ├── _sidebar_admin.html.slim │ ├── _sidebar_backlink.html.slim │ ├── _sidebar_user.html.slim │ ├── _tree.html.slim │ ├── index.html.slim │ ├── manage.html.slim │ └── my_comments.html.slim ├── config ├── initializers │ └── the_comments.rb ├── locales │ ├── en.yml │ └── ru.yml └── routes.rb ├── db └── migrate │ ├── 20130101010101_the_comments_change_user.rb │ ├── 20130101010102_the_comments_create_comments.rb │ └── 20130101010103_the_comments_change_commentable.rb ├── docs ├── admin_ui_installation.md ├── advanced_installation.md ├── comment_api.md ├── commentable_api.md ├── config_file.md ├── content_preprocessors.md ├── customazation_of_views.md ├── denormalization_and_recent_comments.md ├── documentation.md ├── generators.md ├── pagination.md ├── routes.md ├── screencast.jpg ├── the_comments.jpg ├── the_comments_view_1.gif ├── the_comments_view_2.gif ├── the_comments_view_3.gif ├── the_comments_view_4.gif ├── the_comments_view_5.gif ├── user_api.md ├── what_is_comcoms.md ├── whats_wrong_with_other_gems.md └── where_is_example_application.md ├── gem_version.rb ├── lib ├── generators │ └── the_comments │ │ ├── USAGE │ │ ├── the_comments_generator.rb │ │ └── views_generator.rb ├── the_comments.rb └── the_comments │ ├── config.rb │ └── version.rb ├── spec └── dummy_app │ ├── .gitignore │ ├── .rspec │ ├── Gemfile │ ├── README.md │ ├── Rakefile │ ├── app │ ├── assets │ │ ├── images │ │ │ └── .keep │ │ ├── javascripts │ │ │ ├── admin_panel.js │ │ │ └── application.js │ │ └── stylesheets │ │ │ ├── admin_panel.css │ │ │ ├── app.css.scss │ │ │ └── application.css │ ├── controllers │ │ ├── application_controller.rb │ │ ├── comments_controller.rb │ │ ├── concerns │ │ │ └── .keep │ │ ├── posts_controller.rb │ │ └── users_controller.rb │ ├── helpers │ │ └── application_helper.rb │ ├── mailers │ │ └── .keep │ ├── models │ │ ├── .keep │ │ ├── comment.rb │ │ ├── concerns │ │ │ └── .keep │ │ ├── post.rb │ │ └── user.rb │ └── views │ │ ├── layouts │ │ ├── admin.html.haml │ │ └── application.html.haml │ │ └── posts │ │ ├── index.html.haml │ │ └── show.html.haml │ ├── bin │ ├── bundle │ ├── rails │ └── rake │ ├── config.ru │ ├── config │ ├── application.rb │ ├── boot.rb │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers │ │ ├── backtrace_silencers.rb │ │ ├── filter_parameter_logging.rb │ │ ├── inflections.rb │ │ ├── mime_types.rb │ │ ├── secret_token.rb │ │ ├── session_store.rb │ │ ├── sorcery.rb │ │ ├── the_comments.rb │ │ └── wrap_parameters.rb │ ├── locales │ │ └── en.yml │ └── routes.rb │ ├── db │ ├── migrate │ │ ├── 20130712061503_sorcery_core.rb │ │ ├── 20130712065951_create_posts.rb │ │ ├── 20131027185332_change_user.the_comments_engine.rb │ │ ├── 20131027185333_create_comments.the_comments_engine.rb │ │ └── 20131027185334_change_commentable.the_comments_engine.rb │ ├── schema.rb │ └── seeds.rb │ ├── lib │ ├── assets │ │ └── .keep │ └── tasks │ │ ├── .keep │ │ └── app_bootstrap.rake │ ├── log │ └── .keep │ ├── public │ ├── 404.html │ ├── 422.html │ ├── 500.html │ ├── favicon.ico │ └── robots.txt │ ├── spec │ ├── factories │ │ ├── post.rb │ │ └── user.rb │ ├── models │ │ └── user_counters_spec.rb │ └── spec_helper.rb │ ├── test │ ├── controllers │ │ └── .keep │ ├── fixtures │ │ └── .keep │ ├── helpers │ │ └── .keep │ ├── integration │ │ └── .keep │ ├── mailers │ │ └── .keep │ ├── models │ │ └── .keep │ └── test_helper.rb │ └── vendor │ └── assets │ ├── javascripts │ └── .keep │ └── stylesheets │ └── .keep ├── the_comments.gemspec ├── the_comments.yml.teamocil.example └── views_converter.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | 4 | .bundle 5 | .config 6 | .yardoc 7 | 8 | .rvmrc 9 | .ruby-gemset 10 | .ruby-version 11 | 12 | _yardoc 13 | coverage 14 | Gemfile.lock 15 | InstalledFiles 16 | lib/bundler/man 17 | spec/dummy_app/public/assets 18 | 19 | tmp 20 | doc 21 | pkg 22 | rdoc 23 | 24 | test/tmp 25 | spec/reports 26 | test/version_tmp 27 | 28 | .DS_Store 29 | .LSOverride 30 | .AppleDouble -------------------------------------------------------------------------------- /.ruby-gemset.example: -------------------------------------------------------------------------------- 1 | the_comments 2 | -------------------------------------------------------------------------------- /.ruby-version.example: -------------------------------------------------------------------------------- 1 | ruby-2.0.0-p353 2 | -------------------------------------------------------------------------------- /.rvmrc.example: -------------------------------------------------------------------------------- 1 | rvm use ruby-2.0.0-p353@the_comments --create 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.9.3 4 | gemfile: spec/dummy_app/Gemfile 5 | script: "cd spec/dummy_app && rake db:bootstrap RAILS_ENV=test && rspec --format documentation" -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in the_comments.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Ilya N. Zykin 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TheComments 2 | 3 | [![Gem Version](https://badge.fury.io/rb/the_comments.png)](http://badge.fury.io/rb/the_comments) | [![Build Status](https://travis-ci.org/the-teacher/the_comments.png?branch=master)](https://travis-ci.org/the-teacher/the_comments) | [![Code Climate](https://codeclimate.com/github/the-teacher/the_comments.png)](https://codeclimate.com/github/the-teacher/the_comments) | [(rubygems)](http://rubygems.org/gems/the_comments) 4 | 5 | TheComments - The best Rails gem for blog-style comments 6 | 7 | :question:   [Why is TheComments better than other gems?](docs/whats_wrong_with_other_gems.md#why-thecomments-is-better-than-others-gems) 8 | 9 | ### Features 10 | 11 | * Threaded comments 12 | * Useful cache counters 13 | * Admin UI for moderation 14 | * Mountable Engine.routes 15 | * Online Support via skype: **ilya.killich** 16 | * [Denormalization](docs/denormalization_and_recent_comments.md) for recent comments 17 | * Production-ready commenting system for Rails 4+ 18 | * Designed for preprocessors such as Sanitize, Textile, Markdown etc. 19 | 20 | ### :books:   [Documentation](docs/documentation.md) 21 | 22 | ## If you have any questions 23 | 24 | Please try playing around with the **[Dummy App](spec/dummy_app)** in the `spec` folder first. An example integration is often better than any documentation! Thanks. 25 | 26 | ## How to start the dummy app (screencast) 27 | 28 | [![Foo](https://raw.github.com/the-teacher/the_comments/master/docs/screencast.jpg)](http://vk.com/video_ext.php?oid=49225742&id=166578209&hash=10be1dba625149bb&hd=3) 29 | 30 | ## Quick Start Installation 31 | 32 | **NB: In the following examples, `Posts` is the model to which comments are being added. For your app, the model might be `Articles` or similar instead.** 33 | 34 | ### 1. Install Gems 35 | 36 | **Gemfile** 37 | 38 | ```ruby 39 | gem "the_comments", "~> 2.2.1" 40 | 41 | gem 'haml' # or gem 'slim' 42 | gem 'awesome_nested_set' # or same gem 43 | ``` 44 | 45 | **Bundle** 46 | 47 | ``` 48 | bundle 49 | ``` 50 | 51 | Don't forget to restart your server! 52 | 53 | ### 2. Add migrations 54 | 55 | ``` 56 | rake the_comments_engine:install:migrations 57 | ``` 58 | 59 | Will create: 60 | 61 | * xxxxx_change_user.rb 62 | * xxxxx_create_comments.rb 63 | * xxxxx_change_commentable.rb 64 | 65 | :warning:   **Open and change xxxxx_change_commentable.rb migration** 66 | 67 | ```ruby 68 | class ChangeCommentable < ActiveRecord::Migration 69 | def change 70 | # Additional fields to Commentable Models 71 | # [:posts, :articles, ... ] 72 | 73 | # There is only Post model is commentable 74 | [:posts].each do |table_name| 75 | change_table table_name do |t| 76 | t.integer :draft_comments_count, default: 0 77 | t.integer :published_comments_count, default: 0 78 | t.integer :deleted_comments_count, default: 0 79 | end 80 | end 81 | end 82 | end 83 | ``` 84 | 85 | :warning:   **Open and change xxxxx_change_user.rb migration** 86 | 87 | ```ruby 88 | class TheCommentsChangeUser < ActiveRecord::Migration 89 | def change 90 | #if you User class is not called User, you may want to change it. 91 | change_table :users do |t| 92 | # "Written by me" (cache counters) 93 | t.integer :my_draft_comments_count, default: 0 94 | t.integer :my_published_comments_count, default: 0 95 | t.integer :my_comments_count, default: 0 # my_draft_comments_count + my_published_comments_count 96 | 97 | # commentable's comments => comcoms (cache counters) 98 | # Relation through Comment#holder_id field 99 | t.integer :draft_comcoms_count, default: 0 100 | t.integer :published_comcoms_count, default: 0 101 | t.integer :deleted_comcoms_count, default: 0 102 | t.integer :spam_comcoms_count, default: 0 103 | end 104 | end 105 | end 106 | ``` 107 | 108 | **Invoke migrations** 109 | 110 | ``` 111 | rake db:migrate 112 | ``` 113 | 114 | ### 3. Code installation 115 | 116 | ```ruby 117 | rails g the_comments install 118 | ``` 119 | 120 | Will create: 121 | 122 | * config/initializers/the_comments.rb 123 | * app/controllers/comments_controller.rb 124 | * app/models/comment.rb 125 | 126 | :warning:   **Open each file and follow the instructions** 127 | 128 | ### 4. Models modifictions 129 | 130 | **app/models/user.rb** 131 | 132 | ```ruby 133 | class User < ActiveRecord::Base 134 | include TheComments::User 135 | 136 | has_many :posts 137 | 138 | # IT'S JUST AN EXAMPLE OF ANY ROLE SYSTEM 139 | def admin? 140 | self == User.first 141 | end 142 | 143 | # YOU HAVE TO IMPLEMENT YOUR ROLE POLICY FOR COMMENTS HERE 144 | def comments_admin? 145 | admin? 146 | end 147 | 148 | def comments_moderator? comment 149 | id == comment.holder_id 150 | end 151 | end 152 | ``` 153 | 154 | **app/models/post.rb** 155 | 156 | ```ruby 157 | class Post < ActiveRecord::Base 158 | include TheComments::Commentable 159 | 160 | belongs_to :user 161 | 162 | # Denormalization methods 163 | # Check the documentation for information on advanced usage 164 | def commentable_title 165 | "Undefined Post Title" 166 | end 167 | 168 | def commentable_url 169 | "#" 170 | end 171 | 172 | def commentable_state 173 | "published" 174 | end 175 | end 176 | ``` 177 | 178 | ### 5. Add routes 179 | 180 | **config/routes.rb** 181 | 182 | ```ruby 183 | MyApp::Application.routes.draw do 184 | root 'posts#index' 185 | resources :posts 186 | 187 | # ... 188 | 189 | # TheComments routes 190 | concern :user_comments, TheComments::UserRoutes.new 191 | concern :admin_comments, TheComments::AdminRoutes.new 192 | resources :comments, concerns: [:user_comments, :admin_comments] 193 | end 194 | ``` 195 | 196 | Refer to the [documentation](docs/documentation.md) for more information 197 | 198 | ### 6. Add to Application Controller 199 | 200 | **app/controllers/application_controller.rb** 201 | 202 | ```ruby 203 | class ApplicationController < ActionController::Base 204 | include TheComments::ViewToken 205 | 206 | # Prevent CSRF attacks by raising an exception. 207 | # For APIs, you may want to use :null_session instead. 208 | protect_from_forgery with: :exception 209 | end 210 | ``` 211 | 212 | ### 7. Install assets 213 | 214 | **app/assets/stylesheets/application.css** 215 | 216 | ```css 217 | /* 218 | *= require the_comments 219 | */ 220 | ``` 221 | 222 | **app/assets/javascripts/application.js** 223 | 224 | ```js 225 | //= require the_comments 226 | ``` 227 | 228 | ### 8. Example controller code 229 | 230 | **app/controllers/posts_controller.rb** 231 | 232 | ```ruby 233 | def show 234 | @post = Post.find params[:id] 235 | @comments = @post.comments.with_state([:draft, :published]) 236 | end 237 | ``` 238 | 239 | ### 9. Example view code 240 | 241 | **app/views/posts/show.html.haml** 242 | 243 | ```haml 244 | = render partial: 'the_comments/tree', locals: { commentable: @post, comments_tree: @comments } 245 | ``` 246 | 247 |
248 | 249 | ### Common problems 250 | 251 | For error with `unpermitted parameters` in webserver output. 252 | 253 | Example: 254 | 255 | Unpermitted parameters: commentable_type, commentable_id 256 | 257 | User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = 1 ORDER BY "users"."id" ASC LIMIT 1 258 | 259 | Completed 500 Internal Server Error in 8ms 260 | 261 | Add the following to your Comments Controller. 262 | 263 | def comment_params 264 | params 265 | .require(:comment) 266 | .permit(:title, :contacts, :raw_content, :parent_id, :commentable_type, :commentable_id) 267 | .merge(denormalized_fields) 268 | .merge(request_data_for_comment) 269 | .merge(tolerance_time: params[:tolerance_time].to_i) 270 | .merge(user: current_user, view_token: comments_view_token) 271 | end 272 | 273 | See [here](https://github.com/the-teacher/the_comments/issues/34). 274 | 275 |
276 | 277 | For errors with `around_validation`. 278 | 279 | Example: 280 | 281 | NoMethodError - protected method `around_validation' called for #: 282 | 283 | Create a new file `config/state_machine.rb`. 284 | 285 | # Rails 4.1.0.rc1 and StateMachine don't play nice 286 | # https://github.com/pluginaweek/state_machine/issues/295 287 | 288 | require 'state_machine/version' 289 | 290 | unless StateMachine::VERSION == '1.2.0' 291 | # If you see this message, please test removing this file 292 | # If it's still required, please bump up the version above 293 | Rails.logger.warn "Please remove me, StateMachine version has changed" 294 | end 295 | 296 | module StateMachine::Integrations::ActiveModel 297 | public :around_validation 298 | end 299 | 300 | See [here](https://github.com/pluginaweek/state_machine/issues/295). 301 | 302 |
303 | 304 | ### Feedback 305 | 306 | :speech_balloon:   My twitter: [@iam_teacher](https://twitter.com/iam_teacher)       hashtag: **#the_comments** 307 | 308 | ### Acknowledgments 309 | 310 | * Anna Nechaeva (my wife) - for love and my happy life 311 | * @tanraya (Andrew Kozlov) - for code review 312 | * @solenko (Anton Petrunich) - for mountable routes 313 | * @pyromaniac (Arkadiy Zabazhanov) - for consulting 314 | 315 |
316 | 317 | ### MIT License 318 | 319 | Copyright (c) 2013 Ilya N. Zykin 320 | 321 | Permission is hereby granted, free of charge, to any person obtaining 322 | a copy of this software and associated documentation files (the 323 | "Software"), to deal in the Software without restriction, including 324 | without limitation the rights to use, copy, modify, merge, publish, 325 | distribute, sublicense, and/or sell copies of the Software, and to 326 | permit persons to whom the Software is furnished to do so, subject to 327 | the following conditions: 328 | 329 | The above copyright notice and this permission notice shall be 330 | included in all copies or substantial portions of the Software. 331 | 332 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 333 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 334 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 335 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 336 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 337 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 338 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 339 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | -------------------------------------------------------------------------------- /app/assets/javascripts/the_comments.js.coffee: -------------------------------------------------------------------------------- 1 | # ERROR MSG BUILDER 2 | @comments_errors_builder = (errors) -> 3 | error_msgs = '' 4 | for field, errs of errors 5 | for err in errs 6 | error_msgs += "

#{ field }: #{ err }

" 7 | error_msgs 8 | 9 | # FORM CLEANER 10 | @clear_comment_form = -> 11 | $('.error_notifier', '#new_comment, .comments_tree').hide() 12 | $("input[name='comment[title]']").val('') 13 | $("textarea[name='comment[raw_content]']").val('') 14 | 15 | # NOTIFIER 16 | @comments_error_notifier = (form, text) -> 17 | form.children('.error_notifier').empty().hide().append(text).show() 18 | 19 | # TIME HELPER 20 | @unixsec = (t) -> Math.round(t.getTime() / 1000) 21 | 22 | # HIGHTLIGHT ANCHOR 23 | @highlight_anchor = -> 24 | hash = document.location.hash 25 | if hash.match('#comment_') 26 | $(hash).addClass 'highlighted' 27 | 28 | $ -> 29 | window.tolerance_time_start = unixsec(new Date) 30 | tolerance_time = $('[data-comments-tolarance-time]').first().data('comments-tolarance-time') 31 | 32 | # Button Click => AJAX Before Send 33 | submits = '#new_comment input[type=submit], .reply_comments_form input[type=submit]' 34 | 35 | $(document).on 'click', submits, (e) -> 36 | button = $ e.target 37 | form = button.parents('form').first() 38 | time_diff = unixsec(new Date) - window.tolerance_time_start 39 | 40 | if tolerance_time && (time_diff < tolerance_time) 41 | delta = tolerance_time - time_diff 42 | error_msgs = comments_errors_builder({ delay: ["Please wait #{delta} secs"] }) 43 | comments_error_notifier(form, error_msgs) 44 | return false 45 | 46 | $('.tolerance_time').val time_diff 47 | button.hide() 48 | true 49 | 50 | ################ COMMENTS FORMS ################ 51 | comment_forms = "#new_comment, .reply_comments_form" 52 | 53 | # ERROR 54 | $(document).on 'ajax:error', comment_forms, (request, response, status) -> 55 | form = $ @ 56 | $('input[type=submit]', form).show() 57 | error_msgs = comments_errors_builder({ "Server Error: ": [response.status] }) 58 | comments_error_notifier(form, error_msgs) 59 | 60 | # SUCCESS 61 | $(document).on 'ajax:success', comment_forms, (request, response, status) -> 62 | form = $ @ 63 | $('input[type=submit]', form).show() 64 | 65 | if typeof(response) is 'string' 66 | anchor = $(response).find('.comment').attr('id') 67 | clear_comment_form() 68 | form.hide() 69 | $('.parent_id').val('') 70 | $('#new_comment').fadeIn() 71 | tree = form.parent().siblings('.nested_set') 72 | tree = $('ol.comments_tree') if tree.length is 0 73 | tree.append(response) 74 | document.location.hash = anchor 75 | else 76 | error_msgs = comments_errors_builder(response.errors) 77 | comments_error_notifier(form, error_msgs) 78 | 79 | # NEW ROOT BUTTON 80 | $(document).on 'click', '#new_root_comment', -> 81 | $('.reply_comments_form').hide() 82 | $('.parent_id').val('') 83 | $('#new_comment').fadeIn() 84 | false 85 | 86 | # REPLY BUTTON 87 | $(document).on 'click', '.reply_link', -> 88 | link = $ @ 89 | comment = link.parent().parent().parent() 90 | 91 | $(comment_forms).hide() 92 | form = $('#new_comment').clone().removeAttr('id').addClass('reply_comments_form') 93 | 94 | comment_id = comment.data('comment-id') 95 | $('.parent_id', form).val comment_id 96 | 97 | comment.siblings('.form_holder').html(form) 98 | $('.error_notifier', form).empty().hide() 99 | form.fadeIn() 100 | false 101 | 102 | $ -> 103 | # ANCHOR HIGHLIGHT 104 | highlight_anchor() 105 | 106 | $(window).on 'hashchange', -> 107 | $('.comment.highlighted').removeClass 'highlighted' 108 | highlight_anchor() 109 | -------------------------------------------------------------------------------- /app/assets/javascripts/the_comments_manage.js.coffee: -------------------------------------------------------------------------------- 1 | $ -> 2 | hide_comment_panel = (btn) -> $(btn).parents('.panel').slideUp() 3 | 4 | comments = $ '.comments' 5 | 6 | # CONTROLS 7 | comments.on 'click', 'a.additional_info', -> 8 | btn = $ @ 9 | holder = btn.parents('.panel-body') 10 | holder.find('div.additional_info').slideToggle() 11 | false 12 | 13 | comments.on 'click', 'a.edit', -> 14 | btn = $ @ 15 | holder = btn.parents('.panel-body') 16 | holder.find('.edit_form, .comment_body, a.edit').toggle() 17 | false 18 | 19 | comments.on 'ajax:success', '.to_published, .to_draft, .to_spam, .to_deleted', -> 20 | hide_comment_panel @ 21 | 22 | # Edit form 23 | comments.on 'ajax:success', '.edit_comment', (request, response, status) -> 24 | form = $ @ 25 | holder = form.parents('.panel-body') 26 | holder.find('.edit_form, .comment_body, a.edit').toggle() 27 | holder.find('.comment_body').replaceWith response -------------------------------------------------------------------------------- /app/assets/stylesheets/the_comments.css.scss: -------------------------------------------------------------------------------- 1 | .comments_tree, .comments_list{ 2 | font-family: Arial; 3 | 4 | margin:0; 5 | padding:0; 6 | margin-bottom: 30px; 7 | 8 | *{ margin: 0; padding: 0; font-size: inherit; } 9 | 10 | a{ text-decoration: none; } 11 | a:hover{ text-decoration: underline; } 12 | 13 | ol{ 14 | margin: 0; 15 | padding: 0 0 0 20px; 16 | list-style: none outside none; 17 | } 18 | li{ 19 | margin-bottom: 5px; 20 | position: relative; 21 | list-style: none outside none; 22 | } 23 | } 24 | 25 | .action_btns a{ margin-right: 15px; } 26 | 27 | .comments, .comments_tree{ 28 | font-family: Arial; 29 | font-size: 13px; 30 | 31 | h3{ font-size: 1.6em; } 32 | 33 | .error_notifier{ 34 | background-color: #F2DEDE; 35 | border: 1px solid #B94A48; 36 | color: #B94A48; 37 | 38 | border-radius: 4px; 39 | margin: 0 0 15px 0; 40 | padding: 10px 10px 0 10px; 41 | overflow: hidden; 42 | 43 | p{ margin: 0 0 10px 0; } 44 | } 45 | form{ 46 | 47 | background: #e0e4f5; 48 | 49 | border: 1px solid #c6cff5; 50 | border-radius: 5px; 51 | padding: 10px; 52 | 53 | p{ margin: 0 0 10px 0; } 54 | 55 | input[type=text]{ 56 | border: 1px solid gray; 57 | padding: 4px; 58 | width: 75%; 59 | } 60 | label{ font-size: 15px; } 61 | textarea{ 62 | border: 1px solid gray; 63 | font-family: Arial; 64 | font-size: 13px; 65 | height: 150px; 66 | padding: 4px; 67 | width: 75%; 68 | } 69 | .trap{ 70 | margin: 0; padding: 0; 71 | filter: alpha(opacity=0.001); 72 | height: 0.1px; 73 | opacity: 0.001; 74 | overflow: hidden; 75 | } 76 | } 77 | } 78 | 79 | .comments_tree{ 80 | .nested_set{ 81 | border-left: 1px dotted lightGray; 82 | } 83 | 84 | li{ 85 | .comment.draft{ 86 | border: 1px solid gray; 87 | background: #eff5f3; 88 | padding: 10px; 89 | } 90 | } 91 | 92 | .form_holder{ margin-left: 40px; } 93 | 94 | .edit, .delete{ 95 | margin-bottom: 3px; 96 | text-align:center; 97 | line-height: 130%; 98 | background: #336; 99 | color: white; 100 | padding: 1px; 101 | } 102 | .delete{ background: gray; } 103 | 104 | .comment{ 105 | overflow: hidden; zoom: 1; 106 | 107 | .userpic{ 108 | overflow: hidden; zoom: 1; 109 | float: left; 110 | width: 50px; 111 | 112 | img{ margin-bottom: 10px; width: 42px; height: 42px; } 113 | } 114 | .userbar, .cbody{ 115 | margin: 0 0 5px 55px; 116 | padding: 3px; 117 | } 118 | .userbar{ 119 | background: #eff5f3; 120 | border-radius: 3px; 121 | padding-left: 7px; 122 | } 123 | &.draft{ 124 | .userbar{ background: #ffa768; } 125 | .to_draft{ display: none; } 126 | } 127 | &.published{ 128 | .to_published{ display: none; } 129 | } 130 | .cbody{ 131 | font-size: 15px; 132 | border-bottom: 1px solid #eee; 133 | padding-bottom: 10px; 134 | line-height: 135%; 135 | margin-bottom: 3px; 136 | overflow: hidden; 137 | } 138 | .reply{ 139 | margin: 0 0 5px 55px; 140 | font-size: 12px; 141 | } 142 | } 143 | 144 | .controls{ 145 | position: absolute; 146 | top: 53px; left: 5px; 147 | 148 | a{ 149 | font-size:11px; 150 | display:block; 151 | } 152 | } 153 | 154 | .comment{ 155 | margin-bottom: 10px; 156 | 157 | &.published, &.draft{ 158 | margin-bottom: 10px; 159 | border-radius: 3px; 160 | padding: 5px; 161 | } 162 | &.highlighted{ border: 1px dashed #ff6633 !important; } 163 | 164 | } 165 | 166 | .form_holder{ 167 | form{ margin: 10px 0; } 168 | } 169 | } 170 | 171 | .new_comment{ 172 | .btn{ 173 | padding: 7px; 174 | margin-top: 5px; 175 | cursor: pointer; 176 | } 177 | } 178 | 179 | .comments_list{ 180 | li{ 181 | margin-bottom: 20px; 182 | 183 | .item{ 184 | border: 1px solid gray; 185 | border-radius: 5px; 186 | padding: 10px; 187 | } 188 | 189 | .draft{ 190 | border-left: 5px solid orange; 191 | 192 | .controls{ 193 | a.to_draft{ display: none } 194 | } 195 | } 196 | .published{ 197 | border-left: 5px solid green; 198 | 199 | .controls{ 200 | a.to_published{ display: none; } 201 | } 202 | } 203 | 204 | .deleted{ 205 | border-left: 5px solid red; 206 | 207 | .controls{ 208 | a.to_deleted, a.to_spam{ display: none; } 209 | } 210 | } 211 | 212 | .comment{ 213 | div{ margin: 0 0 10px 0; } 214 | 215 | label{ 216 | width: 75px; 217 | font-weight: bold; 218 | display: inline-block; 219 | } 220 | input[type=text]{ 221 | width: 70%; 222 | padding: 4px; 223 | } 224 | input[type=submit]{ 225 | padding: 5px; 226 | cursor: pointer; 227 | } 228 | textarea{ 229 | width: 70%; 230 | padding: 4px; 231 | height: 150px; 232 | } 233 | .content{ 234 | line-height: 130%; 235 | background: #ddd; 236 | padding: 10px; 237 | } 238 | .commentable{ 239 | margin-bottom: 10px; 240 | } 241 | .controls{ 242 | background: lightgray; 243 | padding: 3px; 244 | a{ margin-right: 15px; } 245 | } 246 | } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /app/controllers/_templates_/comments_controller.rb: -------------------------------------------------------------------------------- 1 | class CommentsController < ApplicationController 2 | # layout 'admin' 3 | 4 | # Define your restrict methods and use them like this: 5 | # 6 | # before_action :user_required, except: %w[index create] 7 | # before_action :owner_required, except: %w[index create] 8 | # before_action :admin_required, only: %w[total_draft total_published total_deleted total_spam] 9 | 10 | include TheComments::Controller 11 | 12 | # >>> include TheComments::Controller <<< 13 | # (!) Almost all methods based on *current_user* method 14 | # 15 | # 1. Controller's public methods list: 16 | # You can redifine it for your purposes 17 | # public 18 | # %w[ manage index create edit update ] 19 | # %w[ my_comments my_draft my_published ] 20 | # %w[ draft published deleted spam ] 21 | # %w[ to_draft to_published to_deleted to_spam ] 22 | # %w[ total_draft total_published total_deleted total_spam ] 23 | # 24 | # 25 | # 2. Controller's private methods list: 26 | # You can redifine it for your purposes 27 | # 28 | # private 29 | # %w[ comment_template comment_partial ] 30 | # %w[ denormalized_fields request_data_for_comment define_commentable ] 31 | # %w[ comment_params patch_comment_params ] 32 | # %w[ ajax_requests_required cookies_required ] 33 | # %w[ empty_trap_required tolerance_time_required ] 34 | 35 | # KAMINARI pagination: 36 | # following methods based on gem "kaminari" 37 | # You should redefine them if you use something else 38 | # 39 | # public 40 | # %w[ manage index edit ] 41 | # %w[ draft published deleted spam ] 42 | # %w[ my_comments my_draft my_published ] 43 | # %w[ total_draft total_published total_deleted total_spam ] 44 | end -------------------------------------------------------------------------------- /app/controllers/concerns/the_comments/controller.rb: -------------------------------------------------------------------------------- 1 | module TheComments 2 | # Base functionality of Comments Controller 3 | # class CommentsController < ApplicationController 4 | # include TheComments::Controller 5 | # end 6 | module Controller 7 | extend ActiveSupport::Concern 8 | 9 | included do 10 | include TheComments::ViewToken 11 | 12 | # Attention! We should not set TheComments cookie before create 13 | skip_before_action :set_the_comments_cookies, only: [:create] 14 | 15 | # Spam protection 16 | before_action -> { @errors = [] }, only: [:create] 17 | 18 | before_action :ajax_requests_required, only: [:create] 19 | before_action :cookies_required, only: [:create] 20 | 21 | before_action :empty_trap_required, only: [:create], if: -> { TheComments.config.empty_trap_protection } 22 | before_action :tolerance_time_required, only: [:create], if: -> { TheComments.config.tolerance_time_protection } 23 | 24 | # preparation 25 | before_action :define_commentable, only: [:create] 26 | 27 | # raise an errors 28 | before_action -> { return render(json: { errors: @errors }) unless @errors.blank? }, only: [:create] 29 | end 30 | 31 | # App side methods (you can overwrite them) 32 | 33 | def manage 34 | @comments = current_user.comcoms.with_users.active.recent.page(params[:page]) 35 | render comment_template(:manage) 36 | end 37 | 38 | def my_comments 39 | @comments = current_user.my_comments.with_users.active.recent.page(params[:page]) 40 | render comment_template(:manage) 41 | end 42 | 43 | # Methods based on *current_user* helper 44 | # Methods for admin 45 | %w[draft published deleted].each do |state| 46 | define_method "#{state}" do 47 | @comments = current_user.comcoms.with_users.with_state(state).recent.page(params[:page]) 48 | render comment_template(:manage) 49 | end 50 | 51 | define_method "total_#{state}" do 52 | @comments = ::Comment.with_state(state).with_users.recent.page(params[:page]) 53 | render comment_template(:manage) 54 | end 55 | 56 | define_method "my_#{state}" do 57 | @comments = current_user.my_comments.with_users.with_state(state).recent.page(params[:page]) 58 | render comment_template(:manage) 59 | end 60 | end 61 | 62 | def spam 63 | @comments = current_user.comcoms.with_users.where(spam: true).recent.page(params[:page]) 64 | render comment_template(:manage) 65 | end 66 | 67 | def my_spam 68 | @comments = current_user.my_comments.with_users.where(spam: true).recent.page(params[:page]) 69 | render comment_template(:manage) 70 | end 71 | 72 | def total_spam 73 | @comments = ::Comment.where(spam: true).with_users.recent.page(params[:page]) 74 | render comment_template(:manage) 75 | end 76 | 77 | # BASE METHODS 78 | 79 | # Public methods 80 | 81 | def create 82 | @comment = @commentable.comments.new comment_params 83 | if @comment.valid? 84 | @comment.save 85 | return render layout: false, partial: comment_partial(:comment), locals: { tree: @comment } 86 | end 87 | render json: { errors: @comment.errors } 88 | end 89 | 90 | # Restricted area 91 | 92 | def edit 93 | @comments = current_user.comcoms.where(id: params[:id]).page(params[:page]) 94 | render comment_template(:manage) 95 | end 96 | 97 | def update 98 | comment = ::Comment.find(params[:id]) 99 | comment.update_attributes!(patch_comment_params) 100 | render(layout: false, partial: comment_partial(:comment_body), locals: { comment: comment }) 101 | end 102 | 103 | %w[draft published deleted].each do |state| 104 | define_method "to_#{state}" do 105 | ::Comment.find(params[:id]).try "to_#{state}" 106 | render nothing: true 107 | end 108 | end 109 | 110 | def to_spam 111 | comment = ::Comment.find(params[:id]) 112 | comment.to_spam 113 | comment.to_deleted 114 | render nothing: true 115 | end 116 | 117 | private 118 | 119 | def comment_template template 120 | { template: "the_comments/#{TheComments.config.template_engine}/#{template}" } 121 | end 122 | 123 | def comment_partial partial 124 | "the_comments/#{TheComments.config.template_engine}/#{partial}" 125 | end 126 | 127 | def denormalized_fields 128 | title = @commentable.commentable_title 129 | url = @commentable.commentable_url 130 | @commentable ? { commentable_title: title, commentable_url: url } : {} 131 | end 132 | 133 | def request_data_for_comment 134 | r = request 135 | { ip: r.ip, referer: CGI::unescape(r.referer || 'direct_visit'), user_agent: r.user_agent } 136 | end 137 | 138 | def define_commentable 139 | commentable_klass = params[:comment][:commentable_type].constantize 140 | commentable_id = params[:comment][:commentable_id] 141 | 142 | @commentable = commentable_klass.find(commentable_id) 143 | return render(json: { errors: [t('the_comments.undefined_commentable')] }) unless @commentable 144 | end 145 | 146 | def comment_params 147 | params 148 | .require(:comment) 149 | .permit(:title, :contacts, :raw_content, :parent_id) 150 | .merge(denormalized_fields) 151 | .merge(request_data_for_comment) 152 | .merge(tolerance_time: params[:tolerance_time].to_i) 153 | .merge(user: current_user, view_token: comments_view_token) 154 | end 155 | 156 | def patch_comment_params 157 | params 158 | .require(:comment) 159 | .permit(:title, :contacts, :raw_content, :parent_id) 160 | end 161 | 162 | # Protection hooks 163 | def ajax_requests_required 164 | unless request.xhr? 165 | return render(text: t('the_comments.ajax_requests_required')) 166 | end 167 | end 168 | 169 | def cookies_required 170 | if cookies[:the_comment_cookies] != TheComments::COMMENTS_COOKIES_TOKEN 171 | @errors << [t('the_comments.cookies'), t('the_comments.cookies_required')].join(': ') 172 | end 173 | end 174 | 175 | # TODO: 176 | # 1) inject ? 177 | # 2) fields can be removed on client side 178 | def empty_trap_required 179 | is_human = true 180 | params.slice(*TheComments.config.empty_inputs).values.each{|v| is_human = (is_human && v.blank?) } 181 | 182 | if !is_human 183 | @errors << [t('the_comments.trap'), t('the_comments.trap_message')].join(': ') 184 | end 185 | end 186 | 187 | def tolerance_time_required 188 | this_time = params[:tolerance_time].to_i 189 | min_time = TheComments.config.tolerance_time.to_i 190 | 191 | if this_time < min_time 192 | tdiff = min_time - this_time 193 | @errors << [t('the_comments.tolerance_time'), t('the_comments.tolerance_time_message', time: tdiff )].join(': ') 194 | end 195 | end 196 | end 197 | end 198 | -------------------------------------------------------------------------------- /app/controllers/concerns/the_comments/view_token.rb: -------------------------------------------------------------------------------- 1 | module TheComments 2 | # Cookies and View token for spam protection 3 | # include TheComments::ViewToken 4 | module ViewToken 5 | extend ActiveSupport::Concern 6 | 7 | included { before_action :set_the_comments_cookies } 8 | 9 | def comments_view_token 10 | cookies[:comments_view_token] 11 | end 12 | 13 | private 14 | 15 | def set_the_comments_cookies 16 | cookies[:the_comment_cookies] = { value: TheComments::COMMENTS_COOKIES_TOKEN, expires: 1.year.from_now } 17 | cookies[:comments_view_token] = { value: SecureRandom.hex, expires: 7.days.from_now } unless cookies[:comments_view_token] 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/helpers/render_comments_tree_helper.rb: -------------------------------------------------------------------------------- 1 | # coding: UTF-8 2 | # DOC: 3 | # We use Helper Methods for tree building, 4 | # because it's faster than View Templates and Partials 5 | 6 | # SECURITY note 7 | # Prepare your data on server side for rendering 8 | # or use h.html_escape(node.content) 9 | # for escape potentially dangerous content 10 | module RenderCommentsTreeHelper 11 | module Render 12 | class << self 13 | attr_accessor :h, :options 14 | 15 | # Main Helpers 16 | def controller 17 | @options[:controller] 18 | end 19 | 20 | def t str 21 | controller.t str 22 | end 23 | 24 | # Render Helpers 25 | def visible_draft? 26 | controller.try(:comments_view_token) == @comment.view_token 27 | end 28 | 29 | def moderator? 30 | controller.try(:current_user).try(:comments_moderator?, @comment) 31 | end 32 | 33 | # Render Methods 34 | def render_node(h, options) 35 | @h, @options = h, options 36 | @comment = options[:node] 37 | 38 | @max_reply_depth = options[:max_reply_depth] || TheComments.config.max_reply_depth 39 | 40 | if @comment.draft? 41 | draft_comment 42 | else @comment.published? 43 | published_comment 44 | end 45 | end 46 | 47 | def draft_comment 48 | if visible_draft? || moderator? 49 | published_comment 50 | else 51 | "
  • 52 |
    53 | #{ t('the_comments.waiting_for_moderation') } 54 | #{ h.link_to '#', '#comment_' + @comment.anchor } 55 |
    56 | #{ children } 57 |
  • " 58 | end 59 | end 60 | 61 | def published_comment 62 | "
  • 63 |
    64 |
    65 | #{ avatar } 66 | #{ userbar } 67 |
    #{ @comment.content }
    68 | #{ reply } 69 |
    70 |
    71 | 72 |
    73 | #{ children } 74 |
  • " 75 | end 76 | 77 | def avatar 78 | "
    79 | userpic 80 | #{ controls } 81 |
    " 82 | end 83 | 84 | def userbar 85 | anchor = h.link_to('#', '#comment_' + @comment.anchor) 86 | title = @comment.title.blank? ? t('the_comments.guest_name') : @comment.title 87 | "
    #{ title } #{ anchor }
    " 88 | end 89 | 90 | def moderator_controls 91 | if moderator? 92 | h.link_to(t('the_comments.edit'), h.edit_comment_url(@comment), class: :edit) 93 | end 94 | end 95 | 96 | def reply 97 | if @comment.depth < (@max_reply_depth - 1) 98 | "

    #{ t('the_comments.reply') }" 99 | end 100 | end 101 | 102 | def controls 103 | "

    #{ moderator_controls }
    " 104 | end 105 | 106 | def children 107 | "
      #{ options[:children] }
    " 108 | end 109 | end 110 | end 111 | end -------------------------------------------------------------------------------- /app/models/_templates_/comment.rb: -------------------------------------------------------------------------------- 1 | class Comment < ActiveRecord::Base 2 | include TheComments::Comment 3 | # --------------------------------------------------- 4 | # Define comment's avatar url 5 | # Usually we use Comment#user (owner of comment) to define avatar 6 | # @blog.comments.includes(:user) <= use includes(:user) to decrease queries count 7 | # comment#user.avatar_url 8 | # --------------------------------------------------- 9 | 10 | # public 11 | # --------------------------------------------------- 12 | # Simple way to define avatar url 13 | # 14 | # def avatar_url 15 | # src = id.to_s 16 | # src = title unless title.blank? 17 | # src = contacts if !contacts.blank? && /@/ =~ contacts 18 | # hash = Digest::MD5.hexdigest(src) 19 | # "https://2.gravatar.com/avatar/#{hash}?s=42&d=https://identicons.github.com/#{hash}.png" 20 | # end 21 | # --------------------------------------------------- 22 | 23 | # private 24 | # --------------------------------------------------- 25 | # Define your content filters 26 | # gem 'RedCloth' 27 | # gem 'sanitize' 28 | # gem 'MySmilesProcessor' 29 | # 30 | # def prepare_content 31 | # text = self.raw_content 32 | # text = RedCloth.new(text).to_html 33 | # text = MySmilesProcessor.new(text) 34 | # text = Sanitize.clean(text, Sanitize::Config::RELAXED) 35 | # self.content = text 36 | # end 37 | # --------------------------------------------------- 38 | end -------------------------------------------------------------------------------- /app/models/concerns/the_comments/comment.rb: -------------------------------------------------------------------------------- 1 | module TheComments 2 | module Comment 3 | extend ActiveSupport::Concern 4 | 5 | included do 6 | scope :active, -> { with_state [:draft, :published] } 7 | scope :with_users, -> { includes(:user) } 8 | 9 | # Nested Set 10 | acts_as_nested_set scope: [:commentable_type, :commentable_id] 11 | 12 | # simple sort scopes 13 | include ::TheSimpleSort::Base 14 | 15 | # TheSortableTree 16 | include ::TheSortableTree::Scopes 17 | 18 | # Comments State Machine 19 | include TheComments::CommentStates 20 | 21 | validates :raw_content, presence: true 22 | 23 | # relations 24 | belongs_to :user 25 | belongs_to :holder, class_name: :User 26 | belongs_to :commentable, polymorphic: true 27 | 28 | # callbacks 29 | before_create :define_holder, :define_default_state, :define_anchor, :denormalize_commentable 30 | after_create :update_cache_counters 31 | before_save :prepare_content 32 | end 33 | 34 | def header_title 35 | title.present? ? title : I18n.t('the_comments.guest_name') 36 | end 37 | 38 | def user_name 39 | user.try(:username) || user.try(:login) || header_title 40 | end 41 | 42 | def avatar_url 43 | src = id.to_s 44 | src = title unless title.blank? 45 | src = contacts if !contacts.blank? && /@/ =~ contacts 46 | hash = Digest::MD5.hexdigest(src) 47 | "https://2.gravatar.com/avatar/#{hash}?s=42&d=https://identicons.github.com/#{hash}.png" 48 | end 49 | 50 | def mark_as_spam 51 | count = self_and_descendants.update_all({spam: true}) 52 | update_spam_counter 53 | count 54 | end 55 | 56 | def mark_as_not_spam 57 | count = self_and_descendants.update_all({spam: false}) 58 | update_spam_counter 59 | count 60 | end 61 | 62 | def to_spam 63 | mark_as_spam 64 | end 65 | 66 | private 67 | 68 | def update_spam_counter 69 | holder.try :update_comcoms_spam_counter 70 | end 71 | 72 | def define_anchor 73 | self.anchor = SecureRandom.hex[0..5] 74 | end 75 | 76 | def define_holder 77 | c = self.commentable 78 | self.holder = c.is_a?(User) ? c : c.try(:user) 79 | end 80 | 81 | def define_default_state 82 | self.state = TheComments.config.default_owner_state if user && user == holder 83 | end 84 | 85 | def denormalize_commentable 86 | self.commentable_title = commentable.try :commentable_title 87 | self.commentable_state = commentable.try :commentable_state 88 | self.commentable_url = commentable.try :commentable_url 89 | end 90 | 91 | def prepare_content 92 | self.content = self.raw_content 93 | end 94 | 95 | # Warn: increment! doesn't call validation => 96 | # before_validation filters doesn't work => 97 | # We have few unuseful requests 98 | # I impressed that I found it and reduce DB requests 99 | # Awesome logic pazzl! I'm really pedant :D 100 | def update_cache_counters 101 | user.try :recalculate_my_comments_counter! 102 | 103 | if holder 104 | holder.send :try, :define_denormalize_flags 105 | holder.increment! "#{ state }_comcoms_count" 106 | # holder.class.increment_counter("#{ state }_comcoms_count", holder.id) 107 | end 108 | 109 | if commentable 110 | commentable.send :define_denormalize_flags 111 | commentable.increment! "#{ state }_comments_count" 112 | # holder.class.increment_counter("#{ state }_comments_count", holder.id) 113 | end 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /app/models/concerns/the_comments/comment_states.rb: -------------------------------------------------------------------------------- 1 | module TheComments 2 | module CommentStates 3 | extend ActiveSupport::Concern 4 | 5 | included do 6 | # :draft | :published | :deleted 7 | state_machine :state, :initial => TheComments.config.default_state do 8 | 9 | # events 10 | event :to_draft do 11 | transition all - :draft => :draft 12 | end 13 | 14 | event :to_published do 15 | transition all - :published => :published 16 | end 17 | 18 | event :to_deleted do 19 | transition any - :deleted => :deleted 20 | end 21 | 22 | # transition callbacks 23 | after_transition any => any do |comment| 24 | @comment = comment 25 | @owner = comment.user 26 | @holder = comment.holder 27 | @commentable = comment.commentable 28 | end 29 | 30 | # between draft and published 31 | after_transition [:draft, :published] => [:draft, :published] do |comment, transition| 32 | from = transition.from_name 33 | to = transition.to_name 34 | 35 | if @holder 36 | @holder.send :try, :define_denormalize_flags 37 | @holder.increment! "#{to}_comcoms_count" 38 | @holder.decrement! "#{from}_comcoms_count" 39 | end 40 | 41 | if @commentable 42 | @commentable.send :define_denormalize_flags 43 | @commentable.increment! "#{to}_comments_count" 44 | @commentable.decrement! "#{from}_comments_count" 45 | end 46 | end 47 | 48 | # to deleted (cascade like query) 49 | after_transition [:draft, :published] => :deleted do |comment| 50 | ids = comment.self_and_descendants.map(&:id) 51 | ::Comment.where(id: ids).update_all(state: :deleted) 52 | 53 | @owner.try :recalculate_my_comments_counter! 54 | @holder.try :recalculate_comcoms_counters! 55 | @commentable.try :recalculate_comments_counters! 56 | end 57 | 58 | # from deleted 59 | after_transition :deleted => [:draft, :published] do |comment, transition| 60 | to = transition.to_name 61 | comment.mark_as_not_spam 62 | 63 | @owner.try :recalculate_my_comments_counter! 64 | 65 | if @holder 66 | @holder.send :try, :define_denormalize_flags 67 | @holder.decrement! :deleted_comcoms_count 68 | @holder.increment! "#{to}_comcoms_count" 69 | end 70 | 71 | if @commentable 72 | @commentable.send :define_denormalize_flags 73 | @commentable.decrement! :deleted_comments_count 74 | @commentable.increment! "#{to}_comments_count" 75 | end 76 | end 77 | end 78 | end 79 | end 80 | end -------------------------------------------------------------------------------- /app/models/concerns/the_comments/commentable.rb: -------------------------------------------------------------------------------- 1 | module TheComments 2 | module Commentable 3 | 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | has_many :comments, as: :commentable 8 | 9 | # *define_denormalize_flags* - should be placed before title or url builder filters 10 | before_validation :define_denormalize_flags 11 | after_save :denormalize_for_comments, if: -> { !id_changed? } 12 | end 13 | 14 | # Default Denormalization methods 15 | # Overwrite it with your Application 16 | def commentable_title 17 | # My first blog post 18 | try(:title) || TheComments.config.default_title 19 | end 20 | 21 | def commentable_url 22 | # /posts/1 23 | ['', self.class.to_s.tableize, self.to_param].join('/') 24 | end 25 | 26 | def commentable_state 27 | # 'draft' 28 | try(:state) 29 | end 30 | 31 | # Helper methods 32 | def comments_sum 33 | published_comments_count + draft_comments_count 34 | end 35 | 36 | def recalculate_comments_counters! 37 | update_attributes!({ 38 | draft_comments_count: comments.with_state(:draft).count, 39 | published_comments_count: comments.with_state(:published).count, 40 | deleted_comments_count: comments.with_state(:deleted).count 41 | }) 42 | end 43 | 44 | private 45 | 46 | def define_denormalize_flags 47 | @trackable_commentable_title = commentable_title 48 | @trackable_commentable_state = commentable_state 49 | @trackable_commentable_url = commentable_url 50 | end 51 | 52 | def denormalization_fields_changed? 53 | a = @trackable_commentable_title != commentable_title 54 | b = @trackable_commentable_state != commentable_state 55 | c = @trackable_commentable_url != commentable_url 56 | a || b || c 57 | end 58 | 59 | def denormalize_for_comments 60 | if denormalization_fields_changed? 61 | comments.update_all({ 62 | commentable_title: commentable_title, 63 | commentable_state: commentable_state, 64 | commentable_url: commentable_url 65 | }) 66 | end 67 | end 68 | end 69 | end -------------------------------------------------------------------------------- /app/models/concerns/the_comments/user.rb: -------------------------------------------------------------------------------- 1 | module TheComments 2 | module User 3 | extend ActiveSupport::Concern 4 | 5 | included do 6 | has_many :comcoms, class_name: :Comment, foreign_key: :holder_id 7 | end 8 | 9 | def my_comments; ::Comment.where(user: self); end 10 | 11 | %w[draft published deleted].each do |state| 12 | define_method "my_#{state}_comments" do 13 | my_comments.with_state state 14 | end 15 | 16 | define_method "#{state}_comcoms" do 17 | comcoms.with_state state 18 | end 19 | end 20 | 21 | def my_spam_comments 22 | my_comments.where(spam: true) 23 | end 24 | 25 | # I think we shouldn't to have my_deleted_comments cache counter 26 | def recalculate_my_comments_counter! 27 | dcount = my_draft_comments.count 28 | pcount = my_published_comments.count 29 | update_attributes!({ 30 | my_draft_comments_count: dcount, 31 | my_published_comments_count: pcount, 32 | my_comments_count: dcount + pcount 33 | }) 34 | end 35 | 36 | def recalculate_comcoms_counters! 37 | update_attributes!({ 38 | draft_comcoms_count: draft_comcoms.count, 39 | published_comcoms_count: published_comcoms.count, 40 | deleted_comcoms_count: deleted_comcoms.count 41 | }) 42 | end 43 | 44 | def update_comcoms_spam_counter 45 | update!(spam_comcoms_count: comcoms.where(spam: true).count) 46 | end 47 | 48 | def comments_sum 49 | published_comments_count + draft_comments_count 50 | end 51 | 52 | def comcoms_sum 53 | published_comcoms_count + draft_comcoms_count 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /app/views/the_comments/_tree.html.erb: -------------------------------------------------------------------------------- 1 | <%= render partial: "the_comments/#{TheComments.config.template_engine}/tree", 2 | locals: { commentable: commentable, comments_tree: comments_tree } 3 | %> -------------------------------------------------------------------------------- /app/views/the_comments/haml/_additional_info.html.haml: -------------------------------------------------------------------------------- 1 | .additional_info{ style: "display:none" } 2 | %br 3 | %table.table.table-striped.table-hover 4 | %tr 5 | %th Tolerance time: 6 | %th IP: 7 | %th User Agent: 8 | %th Referer: 9 | %tr 10 | %td= comment.tolerance_time || :none 11 | %td= comment.ip 12 | %td= comment.user_agent 13 | %td= comment.referer 14 | -------------------------------------------------------------------------------- /app/views/the_comments/haml/_comment.html.haml: -------------------------------------------------------------------------------- 1 | = build_server_tree(tree, render_module: RenderCommentsTreeHelper, controller: controller) -------------------------------------------------------------------------------- /app/views/the_comments/haml/_comment_body.html.haml: -------------------------------------------------------------------------------- 1 | %table.comment_body.table.table-striped.table-hover 2 | %tr 3 | %td{ style: "width: 120px;"} 4 | %b= comment.commentable_type 5 | → 6 | %td 7 | = link_to comment.commentable_title, comment.commentable_url 8 | (#{comment.try(:commentable_state)}) 9 | %tr 10 | %td 11 | %b= t('the_comments.title') 12 | %td 13 | - if comment.try(:user) 14 | = link_to comment.user_name, comment.user 15 | - else 16 | = comment.header_title 17 | %tr 18 | %td 19 | %b= t('the_comments.contacts') 20 | %td= comment.contacts 21 | %tr.success 22 | %td 23 | %b= t('the_comments.content') 24 | %td{ style: 'word-break: break-all;' }= raw comment.content 25 | 26 | -------------------------------------------------------------------------------- /app/views/the_comments/haml/_comment_edit.html.haml: -------------------------------------------------------------------------------- 1 | .edit_form{ style: "display:none" } 2 | = form_for comment, remote: true, html:{ role: :form } do |f| 3 | %table.table.table-striped.table-hover 4 | %tr{ style: "width: 100px;"} 5 | %td 6 | %b= comment.commentable_type 7 | → 8 | %td 9 | = link_to comment.commentable_title, comment.commentable_url 10 | (#{comment.try(:commentable_state)}) 11 | %tr 12 | %td 13 | %b= t('the_comments.title') 14 | %td= f.text_field :title, class: "form-control" 15 | %tr 16 | %td 17 | %b= t('the_comments.contacts') 18 | %td= f.text_field :contacts, class: "form-control" 19 | %tr 20 | %td 21 | %b= t('the_comments.content') 22 | %td= f.text_area :raw_content, class: "form-control", rows: 7 23 | %tr 24 | %td 25 | %td= f.submit t('the_comments.update'), class: "btn btn-success" 26 | %hr 27 | -------------------------------------------------------------------------------- /app/views/the_comments/haml/_form.html.haml: -------------------------------------------------------------------------------- 1 | %h3 2 | = link_to t('the_comments.new'), '#', id: :new_root_comment 3 | 4 | = form_for Comment.new, remote: true, authenticity_token: true do |f| 5 | - if current_user 6 | = render partial: 'the_comments/haml/logined_form', locals: { f: f, commentable: commentable } 7 | - else 8 | = render partial: 'the_comments/haml/guest_form', locals: { f: f, commentable: commentable } 9 | -------------------------------------------------------------------------------- /app/views/the_comments/haml/_guest_form.html.haml: -------------------------------------------------------------------------------- 1 | %label= t('the_comments.form.title') 2 | %p= f.text_field :title, class: 'form-control' 3 | 4 | %label= t('the_comments.form.contacts') 5 | %p= f.text_field :contacts, class: 'form-control' 6 | 7 | %label= t('the_comments.form.content') 8 | %p= f.text_area :raw_content, class: 'form-control' 9 | 10 | %p.trap 11 | - TheComments.config.empty_inputs.each do |name| 12 | = text_field_tag name, nil, autocomplete: :off, tabindex: -1, id: nil 13 | 14 | = hidden_field_tag :tolerance_time, 0, id: nil, class: :tolerance_time 15 | 16 | = f.hidden_field :commentable_type, value: commentable.class 17 | = f.hidden_field :commentable_id, value: commentable.id 18 | = f.hidden_field :parent_id, class: :parent_id 19 | 20 | %p 21 | = f.submit t('the_comments.form.create'), class: :btn 22 | = t('the_comments.form.thank_you') 23 | -------------------------------------------------------------------------------- /app/views/the_comments/haml/_logined_form.html.haml: -------------------------------------------------------------------------------- 1 | .logined_comment_form 2 | .user_data 3 | = link_to user_path(current_user), nopin: :nopin do 4 | = image_tag current_user.try(:avatar).try(:url, :thumb) 5 | .comment_data 6 | = hidden_field_tag :tolerance_time, 0, id: nil, class: :tolerance_time 7 | = f.hidden_field :commentable_type, value: commentable.class 8 | = f.hidden_field :commentable_id, value: commentable.id 9 | = f.hidden_field :parent_id, class: :parent_id 10 | 11 | .user_name 12 | %b= current_user.username.present? ? current_user.username : current_user.login 13 | %label= t('the_comments.form.content') 14 | %p= f.text_area :raw_content 15 | 16 | %p 17 | = f.submit t('the_comments.form.create'), class: :btn 18 | = t('the_comments.form.thank_you') 19 | -------------------------------------------------------------------------------- /app/views/the_comments/haml/_manage_controls.html.haml: -------------------------------------------------------------------------------- 1 | - hidden = "display:none" 2 | 3 | .row.controls 4 | .col-md-9.action_btns 5 | = link_to '#', class: "edit btn btn-success" do 6 | = t('the_comments.edit') 7 | 8 | = link_to '#', class: "btn btn-warning edit", style: "display:none" do 9 | = t('the_comments.cancel') 10 | 11 | - unless to_hide = comment.published? ? hidden : nil 12 | - opts = { remote: true, style: to_hide, method: :post } 13 | = link_to [:to_published, comment], opts.merge(class: "btn btn-primary to_published") do 14 | = t('the_comments.to_published') 15 | 16 | - unless to_hide = comment.draft? ? hidden : nil 17 | - opts = { remote: true, style: to_hide, method: :post } 18 | = link_to [:to_draft, comment], opts.merge(class: "btn btn-warning to_draft") do 19 | = t('the_comments.to_draft') 20 | 21 | - unless to_hide = comment.deleted? ? hidden : nil 22 | - opts = { remote: true, style: to_hide, method: :delete, data: { confirm: t('the_comments.delete_confirm') } } 23 | = link_to [:to_deleted, comment], opts.merge(class: "btn btn-danger to_deleted") do 24 | = t('the_comments.to_deleted') 25 | 26 | - opts = { remote: true, method: :post} 27 | = link_to [:to_spam, comment], opts.merge(class: "btn btn-danger to_spam") do 28 | = t('the_comments.to_spam') 29 | .col-md-3.text-right 30 | = link_to t('the_comments.additional_info'), "#", class: "additional_info btn btn-info" 31 | -------------------------------------------------------------------------------- /app/views/the_comments/haml/_sidebar.html.haml: -------------------------------------------------------------------------------- 1 | .panel.panel-default 2 | .panel-heading= t "the_comments.nav.header" 3 | .panel-body 4 | = render partial: 'the_comments/haml/sidebar_backlink' 5 | 6 | - if current_user.comments_admin? 7 | = render partial: 'the_comments/haml/sidebar_admin' 8 | - else 9 | = render partial: 'the_comments/haml/sidebar_user' 10 | -------------------------------------------------------------------------------- /app/views/the_comments/haml/_sidebar_admin.html.haml: -------------------------------------------------------------------------------- 1 | - cuser = current_user 2 | 3 | = render partial: 'the_comments/haml/sidebar_user' 4 | 5 | - if cuser.comments_admin? 6 | %br 7 | %h5= t 'the_comments.in_system', num: Comment.count 8 | %p= link_to t("the_comments.published_comments", num: Comment.with_state(:published).count), [:total_published, :comments], class: 'btn btn-success btn-sm' 9 | %p= link_to t("the_comments.draft_comments", num: Comment.with_state(:draft).count), [:total_draft, :comments], class: 'btn btn-info btn-sm' 10 | %p 11 | = link_to t("the_comments.deleted_comments", num: Comment.with_state(:deleted).count), [:total_deleted, :comments], class: 'btn btn-default btn-sm' 12 | = link_to t("the_comments.spam_comments", num: Comment.where(spam: true).count), [:total_spam, :comments], class: 'btn btn-default btn-sm' 13 | -------------------------------------------------------------------------------- /app/views/the_comments/haml/_sidebar_backlink.html.haml: -------------------------------------------------------------------------------- 1 | %p= link_to t('the_comments.nav.to_root'), root_path 2 | %p= link_to t('the_comments.nav.all_incoming'), manage_comments_url 3 | %hr 4 | -------------------------------------------------------------------------------- /app/views/the_comments/haml/_sidebar_user.html.haml: -------------------------------------------------------------------------------- 1 | - cuser = current_user 2 | 3 | %h5=t 'the_comments.written_by_me', num: cuser.my_comments.count 4 | 5 | %p= link_to t("the_comments.published_comments", num: cuser.my_published_comments.count), [:my_published, :comments], class: 'btn btn-success btn-sm' 6 | %p= link_to t("the_comments.draft_comments", num: cuser.my_draft_comments.count), [:my_draft, :comments], class: 'btn btn-info btn-sm' 7 | 8 | %p 9 | - if cuser.comments_admin? 10 | = link_to t("the_comments.deleted_comments", num: cuser.my_deleted_comments.count), [:my_deleted, :comments], class: 'btn btn-default btn-sm' 11 | = link_to t("the_comments.spam_comments", num: cuser.my_spam_comments.count), [:my_spam, :comments], class: 'btn btn-default btn-sm' 12 | - else 13 | %span.btn.btn-default.btn-sm= t("the_comments.deleted_comments", num: cuser.my_deleted_comments.count) 14 | %span.btn.btn-default.btn-sm= t("the_comments.spam_comments", num: cuser.my_spam_comments.count) 15 | 16 | %br 17 | %h5= t 'the_comments.for_my_posts', num: cuser.comcoms.count 18 | 19 | %p= link_to t("the_comments.published_comments", num: cuser.published_comcoms_count), [:published, :comments], class: 'btn btn-success btn-sm' 20 | %p= link_to t("the_comments.draft_comments", num: cuser.draft_comcoms_count), [:draft, :comments], class: 'btn btn-info btn-sm' 21 | %p 22 | - if cuser.comments_admin? 23 | = link_to t("the_comments.deleted_comments", num: cuser.deleted_comcoms_count), [:deleted, :comments], class: 'btn btn-default btn-sm' 24 | = link_to t("the_comments.spam_comments", num: cuser.spam_comcoms_count), [:spam, :comments], class: 'btn btn-default btn-sm' 25 | - else 26 | %span.btn.btn-default.btn-sm= t("the_comments.deleted_comments", num: cuser.deleted_comcoms_count) 27 | %span.btn.btn-default.btn-sm= t("the_comments.spam_comments", num: cuser.spam_comcoms_count) 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/views/the_comments/haml/_tree.html.haml: -------------------------------------------------------------------------------- 1 | - if commentable.try(:comments_on?) || true 2 | %h4.comments_sum 3 | - if commentable.comments_sum.zero? 4 | Вы можете стать первым, кто оставит комментарий! 5 | - else 6 | Комментариев: #{ commentable.comments_sum } 7 | 8 | - unless current_user 9 | .comments_description 10 | %p — Комментарий можно оставить без регистрации, для этого достаточно заполнить одно обязательное поле Текст комментария. Анонимные комментарии проходят модерацию и до момента одобрения видны только в браузере автора 11 | %p — Комментарии зарегистрированных пользователей публикуются сразу после создания 12 | 13 | .comments#comments 14 | %ol.comments_tree{ data: { comments: { tolarance_time: TheComments.config.tolerance_time } } } 15 | = render partial: 'the_comments/haml/comment', locals: { tree: comments_tree } 16 | = render partial: 'the_comments/haml/form', locals: { commentable: commentable } 17 | -------------------------------------------------------------------------------- /app/views/the_comments/haml/manage.html.haml: -------------------------------------------------------------------------------- 1 | - content_for :title do 2 | = t "the_comments.management" 3 | 4 | - content_for :comments_sidebar do 5 | = render partial: 'the_comments/haml/sidebar' 6 | 7 | - content_for :comments_main do 8 | = paginate @comments 9 | 10 | - if @comments.blank? 11 | .alert.alert-info= t 'the_comments.no_comments_here' 12 | 13 | .comments 14 | - @comments.each do |comment| 15 | - klass = { published: :success, draft: :info, deleted: :danger }[comment.state.to_sym] 16 | .panel{ class: "panel-#{klass}" } 17 | .panel-heading= comment.header_title 18 | .panel-body 19 | = render partial: 'the_comments/haml/comment_body', locals: { comment: comment } 20 | 21 | - if current_user.comments_admin? 22 | = render partial: 'the_comments/haml/comment_edit', locals: { comment: comment } 23 | = render partial: 'the_comments/haml/manage_controls', locals: { comment: comment } 24 | = render partial: 'the_comments/haml/additional_info', locals: { comment: comment } 25 | 26 | = paginate @comments 27 | -------------------------------------------------------------------------------- /app/views/the_comments/slim/_additional_info.html.slim: -------------------------------------------------------------------------------- 1 | .additional_info style: "display:none" 2 | br 3 | table.table.table-striped.table-hover 4 | tr 5 | th Tolerance time: 6 | th IP: 7 | th User Agent: 8 | th Referer: 9 | tr 10 | td= comment.tolerance_time || :none 11 | td= comment.ip 12 | td= comment.user_agent 13 | td= comment.referer 14 | -------------------------------------------------------------------------------- /app/views/the_comments/slim/_comment.html.slim: -------------------------------------------------------------------------------- 1 | = build_server_tree(tree, render_module: RenderCommentsTreeHelper, controller: controller) 2 | -------------------------------------------------------------------------------- /app/views/the_comments/slim/_comment_body.html.slim: -------------------------------------------------------------------------------- 1 | table.comment_body.table.table-striped.table-hover 2 | tr 3 | td style: "width: 120px;" 4 | b= comment.commentable_type 5 | | → 6 | td 7 | = link_to comment.commentable_title, comment.commentable_url 8 | | (#{comment.try(:commentable_state)}) 9 | tr 10 | td 11 | b= t('the_comments.title') 12 | td 13 | - if comment.try(:user) 14 | = link_to comment.user_name, comment.user 15 | - else 16 | = comment.header_title 17 | tr 18 | td 19 | b= t('the_comments.contacts') 20 | td= comment.contacts 21 | tr.success 22 | td 23 | b= t('the_comments.content') 24 | td= comment.content 25 | -------------------------------------------------------------------------------- /app/views/the_comments/slim/_comment_edit.html.slim: -------------------------------------------------------------------------------- 1 | .edit_form style: "display:none" 2 | = form_for comment, remote: true, html:{ role: :form } do |f| 3 | table.table.table-striped.table-hover 4 | tr style: "width: 100px;" 5 | td 6 | b= comment.commentable_type 7 | | → 8 | td 9 | = link_to comment.commentable_title, comment.commentable_url 10 | | (#{comment.try(:commentable_state)}) 11 | tr 12 | td 13 | b= t('the_comments.title') 14 | td= f.text_field :title, class: "form-control" 15 | tr 16 | td 17 | b= t('the_comments.contacts') 18 | td= f.text_field :contacts, class: "form-control" 19 | tr 20 | td 21 | b= t('the_comments.content') 22 | td= f.text_area :raw_content, class: "form-control", rows: 7 23 | tr 24 | td 25 | td= f.submit t('the_comments.update'), class: "btn btn-success" 26 | hr 27 | -------------------------------------------------------------------------------- /app/views/the_comments/slim/_form.html.slim: -------------------------------------------------------------------------------- 1 | h3 2 | = link_to t('the_comments.new'), '#', id: :new_root_comment 3 | 4 | = form_for Comment.new, remote: true, authenticity_token: true do |f| 5 | - if current_user 6 | = render partial: 'the_comments/slim/logined_form', locals: { f: f, commentable: commentable } 7 | - else 8 | = render partial: 'the_comments/slim/guest_form', locals: { f: f, commentable: commentable } 9 | -------------------------------------------------------------------------------- /app/views/the_comments/slim/_guest_form.html.slim: -------------------------------------------------------------------------------- 1 | label= t('the_comments.form.title') 2 | p= f.text_field :title, class: 'form-control' 3 | 4 | label= t('the_comments.form.contacts') 5 | p= f.text_field :contacts, class: 'form-control' 6 | 7 | label= t('the_comments.form.content') 8 | p= f.text_area :raw_content, class: 'form-control' 9 | 10 | p.trap 11 | - TheComments.config.empty_inputs.each do |name| 12 | = text_field_tag name, nil, autocomplete: :off, tabindex: -1, id: nil 13 | 14 | = hidden_field_tag :tolerance_time, 0, id: nil, class: :tolerance_time 15 | 16 | = f.hidden_field :commentable_type, value: commentable.class 17 | = f.hidden_field :commentable_id, value: commentable.id 18 | = f.hidden_field :parent_id, class: :parent_id 19 | 20 | p 21 | = f.submit t('the_comments.form.create'), class: :btn 22 | = t('the_comments.form.thank_you') 23 | -------------------------------------------------------------------------------- /app/views/the_comments/slim/_logined_form.html.slim: -------------------------------------------------------------------------------- 1 | .logined_comment_form 2 | .user_data 3 | = link_to user_path(current_user), nopin: :nopin do 4 | = image_tag current_user.avatar.url(:thumb) 5 | .comment_data 6 | = hidden_field_tag :tolerance_time, 0, id: nil, class: :tolerance_time 7 | = f.hidden_field :commentable_type, value: commentable.class 8 | = f.hidden_field :commentable_id, value: commentable.id 9 | = f.hidden_field :parent_id, class: :parent_id 10 | 11 | .user_name 12 | b= current_user.username.present? ? current_user.username : current_user.login 13 | label= t('the_comments.form.content') 14 | p= f.text_area :raw_content 15 | 16 | p 17 | = f.submit t('the_comments.form.create'), class: :btn 18 | = t('the_comments.form.thank_you') 19 | -------------------------------------------------------------------------------- /app/views/the_comments/slim/_manage_controls.html.slim: -------------------------------------------------------------------------------- 1 | - hidden = "display:none" 2 | 3 | .row.controls 4 | .col-md-9.action_btns 5 | = link_to '#', class: "edit btn btn-success" do 6 | = t('the_comments.edit') 7 | 8 | = link_to '#', class: "btn btn-warning edit", style: "display:none" do 9 | = t('the_comments.cancel') 10 | 11 | - unless to_hide = comment.published? ? hidden : nil 12 | - opts = { remote: true, style: to_hide, method: :post } 13 | = link_to [:to_published, comment], opts.merge(class: "btn btn-primary to_published") do 14 | = t('the_comments.to_published') 15 | 16 | - unless to_hide = comment.draft? ? hidden : nil 17 | - opts = { remote: true, style: to_hide, method: :post } 18 | = link_to [:to_draft, comment], opts.merge(class: "btn btn-warning to_draft") do 19 | = t('the_comments.to_draft') 20 | 21 | - unless to_hide = comment.deleted? ? hidden : nil 22 | - opts = { remote: true, style: to_hide, method: :delete, data: { confirm: t('the_comments.delete_confirm') } } 23 | = link_to [:to_deleted, comment], opts.merge(class: "btn btn-danger to_deleted") do 24 | = t('the_comments.to_deleted') 25 | 26 | - opts = { remote: true, method: :post} 27 | = link_to [:to_spam, comment], opts.merge(class: "btn btn-danger to_spam") do 28 | = t('the_comments.to_spam') 29 | .col-md-3.text-right 30 | = link_to t('the_comments.additional_info'), "#", class: "additional_info btn btn-info" 31 | -------------------------------------------------------------------------------- /app/views/the_comments/slim/_sidebar.html.slim: -------------------------------------------------------------------------------- 1 | .panel.panel-default 2 | .panel-heading= t "the_comments.nav.header" 3 | .panel-body 4 | = render partial: 'the_comments/slim/sidebar_backlink' 5 | 6 | - if current_user.comments_admin? 7 | = render partial: 'the_comments/slim/sidebar_admin' 8 | - else 9 | = render partial: 'the_comments/slim/sidebar_user' 10 | -------------------------------------------------------------------------------- /app/views/the_comments/slim/_sidebar_admin.html.slim: -------------------------------------------------------------------------------- 1 | - cuser = current_user 2 | 3 | = render partial: 'the_comments/slim/sidebar_user' 4 | 5 | - if cuser.comments_admin? 6 | br 7 | h5= t 'the_comments.in_system', num: Comment.count 8 | p= link_to t("the_comments.published_comments", num: Comment.with_state(:published).count), [:total_published, :comments], class: 'btn btn-success btn-sm' 9 | p= link_to t("the_comments.draft_comments", num: Comment.with_state(:draft).count), [:total_draft, :comments], class: 'btn btn-info btn-sm' 10 | p 11 | = link_to t("the_comments.deleted_comments", num: Comment.with_state(:deleted).count), [:total_deleted, :comments], class: 'btn btn-default btn-sm' 12 | = link_to t("the_comments.spam_comments", num: Comment.where(spam: true).count), [:total_spam, :comments], class: 'btn btn-default btn-sm' 13 | -------------------------------------------------------------------------------- /app/views/the_comments/slim/_sidebar_backlink.html.slim: -------------------------------------------------------------------------------- 1 | p= link_to t('the_comments.nav.to_root'), root_path 2 | p= link_to t('the_comments.nav.all_incoming'), manage_comments_url 3 | hr 4 | -------------------------------------------------------------------------------- /app/views/the_comments/slim/_sidebar_user.html.slim: -------------------------------------------------------------------------------- 1 | - cuser = current_user 2 | 3 | h5=t 'the_comments.written_by_me', num: cuser.my_comments.count 4 | 5 | p= link_to t("the_comments.published_comments", num: cuser.my_published_comments.count), [:my_published, :comments], class: 'btn btn-success btn-sm' 6 | p= link_to t("the_comments.draft_comments", num: cuser.my_draft_comments.count), [:my_draft, :comments], class: 'btn btn-info btn-sm' 7 | 8 | p 9 | - if cuser.comments_admin? 10 | = link_to t("the_comments.deleted_comments", num: cuser.my_deleted_comments.count), [:my_deleted, :comments], class: 'btn btn-default btn-sm' 11 | = link_to t("the_comments.spam_comments", num: cuser.my_spam_comments.count), [:my_spam, :comments], class: 'btn btn-default btn-sm' 12 | - else 13 | span.btn.btn-default.btn-sm= t("the_comments.deleted_comments", num: cuser.my_deleted_comments.count) 14 | span.btn.btn-default.btn-sm= t("the_comments.spam_comments", num: cuser.my_spam_comments.count) 15 | 16 | br 17 | h5= t 'the_comments.for_my_posts', num: cuser.comcoms.count 18 | 19 | p= link_to t("the_comments.published_comments", num: cuser.published_comcoms_count), [:published, :comments], class: 'btn btn-success btn-sm' 20 | p= link_to t("the_comments.draft_comments", num: cuser.draft_comcoms_count), [:draft, :comments], class: 'btn btn-info btn-sm' 21 | p 22 | - if cuser.comments_admin? 23 | = link_to t("the_comments.deleted_comments", num: cuser.deleted_comcoms_count), [:deleted, :comments], class: 'btn btn-default btn-sm' 24 | = link_to t("the_comments.spam_comments", num: cuser.spam_comcoms_count), [:spam, :comments], class: 'btn btn-default btn-sm' 25 | - else 26 | span.btn.btn-default.btn-sm= t("the_comments.deleted_comments", num: cuser.deleted_comcoms_count) 27 | span.btn.btn-default.btn-sm= t("the_comments.spam_comments", num: cuser.spam_comcoms_count) 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/views/the_comments/slim/_tree.html.slim: -------------------------------------------------------------------------------- 1 | - if commentable.comments_on? 2 | h4.comments_sum 3 | - if commentable.comments_sum.zero? 4 | | Вы можете стать первым, кто оставит комментарий! 5 | - else 6 | | Комментариев: #{ commentable.comments_sum } 7 | 8 | - unless current_user 9 | .comments_description 10 | p — Комментарий можно оставить без регистрации, для этого достаточно заполнить одно обязательное поле Текст комментария. Анонимные комментарии проходят модерацию и до момента одобрения видны только в браузере автора 11 | p — Комментарии зарегистрированных пользователей публикуются сразу после создания 12 | 13 | .comments#comments 14 | ol.comments_tree data: { comments: { tolarance_time: TheComments.config.tolerance_time } } 15 | = render partial: 'the_comments/slim/comment', locals: { tree: comments_tree } 16 | = render partial: 'the_comments/slim/form', locals: { commentable: commentable } 17 | -------------------------------------------------------------------------------- /app/views/the_comments/slim/index.html.slim: -------------------------------------------------------------------------------- 1 | - content_for :title do 2 | = t "the_comments.management" 3 | 4 | - content_for :comments_sidebar do 5 | = render partial: 'the_comments/slim/sidebar' 6 | 7 | - content_for :comments_main do 8 | = paginate @comments 9 | 10 | .comments 11 | - @comments.each do |comment| 12 | - klass = { published: :success, draft: :info, deleted: :danger }[comment.state.to_sym] 13 | .panel class: "panel-#{klass}" 14 | .panel-heading= comment.header_title 15 | .panel-body 16 | = render partial: 'the_comments/slim/comment_body', locals: { comment: comment } 17 | 18 | = paginate @comments 19 | -------------------------------------------------------------------------------- /app/views/the_comments/slim/manage.html.slim: -------------------------------------------------------------------------------- 1 | - content_for :title do 2 | = t "the_comments.management" 3 | 4 | - content_for :comments_sidebar do 5 | = render partial: 'the_comments/slim/sidebar' 6 | 7 | - content_for :comments_main do 8 | = paginate @comments 9 | 10 | - if @comments.blank? 11 | .alert.alert-info= t 'the_comments.no_comments_here' 12 | 13 | .comments 14 | - @comments.each do |comment| 15 | - klass = { published: :success, draft: :info, deleted: :danger }[comment.state.to_sym] 16 | .panel class: "panel-#{klass}" 17 | .panel-heading= comment.header_title 18 | .panel-body 19 | = render partial: 'the_comments/slim/comment_body', locals: { comment: comment } 20 | 21 | - if current_user.comments_admin? 22 | = render partial: 'the_comments/slim/comment_edit', locals: { comment: comment } 23 | = render partial: 'the_comments/slim/manage_controls', locals: { comment: comment } 24 | = render partial: 'the_comments/slim/additional_info', locals: { comment: comment } 25 | 26 | = paginate @comments 27 | -------------------------------------------------------------------------------- /app/views/the_comments/slim/my_comments.html.slim: -------------------------------------------------------------------------------- 1 | - cuser = current_user 2 | 3 | - content_for :title do 4 | = t "the_comments.management" 5 | 6 | - content_for :comments_sidebar do 7 | = render partial: 'the_comments/slim/sidebar' 8 | 9 | - content_for :comments_main do 10 | = paginate @comments 11 | 12 | - if @comments.blank? 13 | .alert.alert-info= t 'the_comments.no_comments_here' 14 | 15 | .comments 16 | - @comments.each do |comment| 17 | - klass = { published: :primary, draft: :warning, deleted: :danger }[comment.state.to_sym] 18 | .panel class: "panel-#{klass}" 19 | .panel-heading= comment.title 20 | .panel-body 21 | = render partial: 'the_comments/slim/comment_body', locals: { comment: comment } 22 | 23 | - if cuser.comments_admin? 24 | = render partial: 'the_comments/slim/comment_edit', locals: { comment: comment } 25 | = render partial: 'the_comments/slim/manage_controls', locals: { comment: comment } 26 | = render partial: 'the_comments/slim/additional_info', locals: { comment: comment } 27 | 28 | = paginate @comments 29 | -------------------------------------------------------------------------------- /config/initializers/the_comments.rb: -------------------------------------------------------------------------------- 1 | # TheComments.config.param_name => value 2 | 3 | TheComments.configure do |config| 4 | config.max_reply_depth = 3 # comments tree depth 5 | config.tolerance_time = 10 # sec - after this delay user can post a comment 6 | config.default_state = :draft # default state for comment 7 | config.default_owner_state = :published # default state for comment for Moderator 8 | config.empty_inputs = [:commentBody] # array of spam trap fields 9 | config.default_title = 'Undefined title' # default commentable_title for denormalization 10 | config.template_engine = :haml 11 | 12 | config.empty_trap_protection = true 13 | config.tolerance_time_protection = true 14 | end 15 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | activerecord: 3 | attributes: 4 | comment: 5 | raw_content: Content 6 | errors: 7 | models: 8 | comment: 9 | attributes: 10 | raw_content: 11 | blank: 'should not be empty' 12 | # blank: '%{attribute} should not be empty' 13 | 14 | the_comments: 15 | incoming: "Incoming:" 16 | in_system: "In system:" 17 | written_by_me: "Written by me:" 18 | back_to_root: "← To root" 19 | 20 | my_comments: "My comments (%{num})" 21 | draft_comments: "New (%{num})" 22 | published_comments: "Published (%{num})" 23 | deleted_comments: "Deleted (%{num})" 24 | spam_comments: "Spam (%{num})" 25 | 26 | management: Comments management 27 | new: "New comment" 28 | update: "Update comment" 29 | additional_info: "Additional info" 30 | cancel: "Cancel" 31 | 32 | title: "Title on name:" 33 | contacts: "Contacts:" 34 | content: "Message:" 35 | 36 | guest_name: Guest 37 | reply: Reply to this comment 38 | edit: Edit 39 | to_spam: Spam! 40 | to_draft: Draft 41 | to_published: Publicate 42 | to_deleted: Delete 43 | 44 | no_comments_here: No comments here 45 | 46 | nav: 47 | header: "Navigation" 48 | 49 | form: 50 | title: "Your name:" 51 | contacts: "Contacts (only admin can see this):" 52 | content: "Comment* :" 53 | create: "Submit comment" 54 | thank_you: "Thank you!" 55 | 56 | trap: Trap 57 | trap_message: should be empty 58 | 59 | tolerance_time: Page view time 60 | tolerance_time_message: "Please wait %{time} seconds before send a comment and try again" 61 | 62 | delete_confirm: Are you sure? 63 | 64 | cookies: Cookies 65 | cookies_required: 'Please enable cookies and try to reload page' 66 | ajax_requests_required: 'Sorry, JavaScript/Ajax Requests required' 67 | waiting_for_moderation: Waiting for moderation 68 | undefined_commentable: Commentable object is undefined -------------------------------------------------------------------------------- /config/locales/ru.yml: -------------------------------------------------------------------------------- 1 | ru: 2 | activerecord: 3 | attributes: 4 | comment: 5 | raw_content: Содержимое 6 | errors: 7 | models: 8 | comment: 9 | attributes: 10 | raw_content: 11 | blank: 'Не может быть пустым' 12 | 13 | the_comments: 14 | incoming: "Входящие:" 15 | in_system: "Все в системе: %{num}" 16 | written_by_me: "Написаны мной: %{num}" 17 | for_my_posts: "К моим постам: %{num}" 18 | back_to_root: "← На главную" 19 | 20 | my_comments: "Мои комментарии %{num}" 21 | draft_comments: "На модерации: %{num}" 22 | published_comments: "Опубликованы: %{num}" 23 | deleted_comments: "Удалены: %{num}" 24 | spam_comments: "Спам: %{num}" 25 | 26 | waiting_for_moderation: Комментарий ожидает проверки 27 | management: Управление комментариями 28 | new: "Написать новый комментарий" 29 | update: "Обновить" 30 | additional_info: "Подробнее" 31 | cancel: "Отмена" 32 | 33 | title: "Имя или тема:" 34 | contacts: "Контакты:" 35 | content: "Сообщение:" 36 | 37 | guest_name: Гость 38 | reply: Ответить на этот комментарий 39 | edit: Править 40 | to_spam: Спам! 41 | to_draft: Черновик 42 | to_published: Публиковать 43 | to_deleted: Удалить 44 | 45 | no_comments_here: Здесь нет комментариев 46 | 47 | nav: 48 | header: Комментарии 49 | to_root: На главную 50 | all_incoming: Все входящие 51 | 52 | form: 53 | title: "Ваше имя:" 54 | contacts: "Контакты (не отображаются на сайте):" 55 | content: "Текст комментария* :" 56 | create: "Отправить комментарий" 57 | thank_you: "Спaсибо!" 58 | 59 | trap: Ловушка 60 | trap_message: Должна быть пустой 61 | 62 | tolerance_time: Просмотр времени 63 | tolerance_time_message: "Пожалуйста ожидайте %{time} сек. перед отправкой сообщения" 64 | 65 | cookies: Куки 66 | cookies_required: 'Включите куки и перезагрузите страницу' 67 | 68 | delete_confirm: Вы уверены? 69 | waiting_for_moderation: Ожидает модерации 70 | ajax_requests_required: 'Извините, ожидается JavaScript/Ajax запрос' 71 | undefined_commentable: Комментируемый объект не определен 72 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | module TheComments 2 | class UserRoutes 3 | def call mapper, options = {} 4 | mapper.collection do 5 | mapper.get :manage 6 | mapper.get :my_comments 7 | 8 | mapper.get :my_draft 9 | mapper.get :my_published 10 | mapper.get :my_deleted 11 | mapper.get :my_spam 12 | 13 | mapper.get :draft 14 | mapper.get :published 15 | mapper.get :deleted 16 | mapper.get :spam 17 | end 18 | 19 | mapper.member do 20 | mapper.post :to_spam 21 | mapper.post :to_draft 22 | mapper.post :to_published 23 | mapper.delete :to_deleted 24 | end 25 | end 26 | end 27 | 28 | class AdminRoutes 29 | def call mapper, options = {} 30 | mapper.collection do 31 | mapper.get :total_draft 32 | mapper.get :total_published 33 | mapper.get :total_deleted 34 | mapper.get :total_spam 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /db/migrate/20130101010101_the_comments_change_user.rb: -------------------------------------------------------------------------------- 1 | # null: false => de-facto db-level validation 2 | class TheCommentsChangeUser < ActiveRecord::Migration 3 | def change 4 | change_table :users do |t| 5 | # "Written by me" (cache counters) 6 | t.integer :my_draft_comments_count, default: 0 7 | t.integer :my_published_comments_count, default: 0 8 | t.integer :my_comments_count, default: 0 # my_draft_comments_count + my_published_comments_count 9 | 10 | # commentable's comments => comcoms (cache counters) 11 | # Relation through Comment#holder_id field 12 | t.integer :draft_comcoms_count, default: 0 13 | t.integer :published_comcoms_count, default: 0 14 | t.integer :deleted_comcoms_count, default: 0 15 | t.integer :spam_comcoms_count, default: 0 16 | end 17 | end 18 | end -------------------------------------------------------------------------------- /db/migrate/20130101010102_the_comments_create_comments.rb: -------------------------------------------------------------------------------- 1 | class TheCommentsCreateComments < ActiveRecord::Migration 2 | def change 3 | create_table :comments do |t| 4 | # relations 5 | t.integer :user_id 6 | t.integer :holder_id 7 | 8 | # polymorphic, commentable object 9 | t.integer :commentable_id 10 | t.string :commentable_type 11 | 12 | # denormalization 13 | t.string :commentable_url 14 | t.string :commentable_title 15 | t.string :commentable_state 16 | 17 | # comment 18 | t.string :anchor 19 | 20 | t.string :title 21 | t.string :contacts 22 | 23 | t.text :raw_content 24 | t.text :content 25 | 26 | # moderation token 27 | t.string :view_token 28 | 29 | # state machine => :draft | :published | :deleted 30 | t.string :state, default: :draft 31 | 32 | # base user data (BanHammer power) 33 | t.string :ip, default: :undefined 34 | t.string :referer, default: :undefined 35 | t.string :user_agent, default: :undefined 36 | t.integer :tolerance_time 37 | 38 | # unusable: for future versions 39 | t.boolean :spam, default: false 40 | 41 | # nested set 42 | t.integer :parent_id 43 | t.integer :lft 44 | t.integer :rgt 45 | t.integer :depth, default: 0 46 | 47 | t.timestamps 48 | end 49 | end 50 | end -------------------------------------------------------------------------------- /db/migrate/20130101010103_the_comments_change_commentable.rb: -------------------------------------------------------------------------------- 1 | class TheCommentsChangeCommentable < ActiveRecord::Migration 2 | def change 3 | # Uncomment this. Add fields to Commentable Models 4 | # 5 | # [:users, :posts, :blogs, :articles, :pages].each do |table_name| 6 | # change_table table_name do |t| 7 | # t.integer :draft_comments_count, default: 0 8 | # t.integer :published_comments_count, default: 0 9 | # t.integer :deleted_comments_count, default: 0 10 | # end 11 | # end 12 | end 13 | end -------------------------------------------------------------------------------- /docs/admin_ui_installation.md: -------------------------------------------------------------------------------- 1 | ←   [documentation](documentation.md) 2 | 3 | ## Admin UI installation 4 | 5 | ### 1. Gems install 6 | 7 | **Gemfile** 8 | 9 | ```ruby 10 | # TheComments base 11 | gem 'the_comments', "~> 2.0" 12 | 13 | gem 'haml' # or gem 'slim' 14 | gem 'awesome_nested_set' # or same gem 15 | 16 | # TheComments Admin UI gems 17 | 18 | # pagination 19 | gem 'kaminari' 20 | 21 | # bootstrap 3 22 | gem 'bootstrap-sass', github: 'thomas-mcdonald/bootstrap-sass' 23 | ``` 24 | 25 | **Bundle** 26 | 27 | ``` 28 | bundle 29 | ``` 30 | 31 | ### 2. Assets install 32 | 33 | **app/assets/stylesheets/admin_ui.css** 34 | 35 | ```css 36 | /* 37 | *= require bootstrap 38 | */ 39 | ``` 40 | 41 | **app/assets/javascripts/admin_ui.js** 42 | 43 | ```js 44 | //= require jquery 45 | //= require jquery_ujs 46 | 47 | //= require bootstrap 48 | //= require the_comments_manage 49 | ``` 50 | 51 | ### 3. Admin layout 52 | 53 | You can use following yields to insert TheComments management tools in your Layout. 54 | 55 | ```haml 56 | = yield :comments_sidebar 57 | = yield :comments_main 58 | ``` 59 | 60 | For example: 61 | 62 | ```haml 63 | !!! 5 64 | %html(lang="en") 65 | %head 66 | %meta(charset="utf-8") 67 | %meta(http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1") 68 | %meta(name="viewport" content="width=device-width, initial-scale=1.0") 69 | %title= content_for?(:title) ? yield(:title) : "Admin Panel" 70 | %link(href="favicon.ico" rel="shortcut icon") 71 | 72 | = stylesheet_link_tag :admin_ui 73 | = javascript_include_tag :admin_ui 74 | = csrf_meta_tags 75 | 76 | %body 77 | .container 78 | .row 79 | .col-md-12 80 | %h3= content_for?(:title) ? yield(:title) : "Admin Panel" 81 | .row 82 | .col-md-3= yield :comments_sidebar 83 | .col-md-9= yield :comments_main 84 | 85 | = stylesheet_link_tag "//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.min.css" 86 | ``` 87 | 88 | ### 4. Comments controller modifications 89 | 90 | by default your comments controller looks like this: 91 | 92 | **app/controllers/comments_controller.rb** 93 | 94 | ```ruby 95 | class CommentsController < ApplicationController 96 | # layout 'admin' 97 | 98 | # Define your restrict methods and use them like this: 99 | # 100 | # before_action :user_required, except: %w[index create] 101 | # before_action :owner_required, except: %w[index create] 102 | # before_action :admin_required, only: %w[total_draft total_published total_deleted total_spam] 103 | 104 | include TheComments::Controller 105 | 106 | # >>> include TheComments::Controller <<< 107 | # (!) Almost all methods based on *current_user* method 108 | # 109 | # 1. Controller's public methods list: 110 | # You can redifine it for your purposes 111 | # public 112 | # %w[ manage index create edit update ] 113 | # %w[ my_comments my_draft my_published ] 114 | # %w[ draft published deleted spam ] 115 | # %w[ to_draft to_published to_deleted to_spam ] 116 | # %w[ total_draft total_published total_deleted total_spam ] 117 | # 118 | # 119 | # 2. Controller's private methods list: 120 | # You can redifine it for your purposes 121 | # 122 | # private 123 | # %w[ comment_template comment_partial ] 124 | # %w[ denormalized_fields request_data_for_comment define_commentable ] 125 | # %w[ comment_params patch_comment_params ] 126 | # %w[ ajax_requests_required cookies_required ] 127 | # %w[ empty_trap_required tolerance_time_required ] 128 | 129 | # KAMINARI pagination: 130 | # following methods based on gem "kaminari" 131 | # You should redefine them if you use something else 132 | # 133 | # public 134 | # %w[ manage index edit ] 135 | # %w[ draft published deleted spam ] 136 | # %w[ my_comments my_draft my_published ] 137 | # %w[ total_draft total_published total_deleted total_spam ] 138 | end 139 | ``` 140 | 141 | You must define protection methods to restrict access to Admin UI for regular users. 142 | 143 | ### 5. Visit Admin UI 144 | 145 | **localhost:3000/comments/manage** 146 | -------------------------------------------------------------------------------- /docs/advanced_installation.md: -------------------------------------------------------------------------------- 1 | ←   [documentation](documentation.md) 2 | 3 | ## Advanced Installation 4 | 5 | ### 1. Gems install 6 | 7 | **Gemfile** 8 | 9 | ```ruby 10 | gem 'the_comments', "~> 2.0" 11 | 12 | gem 'haml' # or gem 'slim' 13 | gem 'awesome_nested_set' # or same gem 14 | ``` 15 | 16 | **Bundle** 17 | 18 | ``` 19 | bundle 20 | ``` 21 | 22 | ### 2. Migrations install 23 | 24 | **Copy migrations** 25 | 26 | ``` 27 | rake the_comments_engine:install:migrations 28 | ``` 29 | 30 | Will create: 31 | 32 | * xxxxx_change_user.rb 33 | * xxxxx_create_comments.rb 34 | * xxxxx_change_commentable.rb 35 | 36 | :warning:   **Open and change xxxxx_change_commentable.rb migration** 37 | 38 | ```ruby 39 | class ChangeCommentable < ActiveRecord::Migration 40 | def change 41 | # Additional fields to Commentable Models 42 | # [:posts, :articles, ... ] 43 | 44 | # There is only Post model is commentable 45 | [:posts].each do |table_name| 46 | change_table table_name do |t| 47 | t.integer :draft_comments_count, default: 0 48 | t.integer :published_comments_count, default: 0 49 | t.integer :deleted_comments_count, default: 0 50 | end 51 | end 52 | end 53 | end 54 | ``` 55 | 56 | **Invoke migrations** 57 | 58 | ``` 59 | rake db:migrate 60 | ``` 61 | 62 | ### 3. Code install 63 | 64 | ```ruby 65 | rails g the_comments install 66 | ``` 67 | 68 | Will create: 69 | 70 | * config/initializers/the_comments.rb 71 | * app/controllers/comments_controller.rb 72 | * app/models/comment.rb 73 | 74 | :warning:   **Open each file and follow an instructions** 75 | 76 | ### 4. Models modifictions 77 | 78 | **app/models/user.rb** 79 | 80 | ```ruby 81 | class User < ActiveRecord::Base 82 | include TheComments::User 83 | 84 | has_many :posts 85 | 86 | # Your way to define privileged users 87 | def admin? 88 | self == User.first 89 | end 90 | 91 | # Required TheComments methods for users restrictions 92 | def comments_admin? 93 | admin? 94 | end 95 | 96 | def comments_moderator? comment 97 | id == comment.holder_id 98 | end 99 | end 100 | ``` 101 | 102 | **app/models/post.rb** 103 | 104 | ```ruby 105 | class Post < ActiveRecord::Base 106 | include TheComments::Commentable 107 | 108 | belongs_to :user 109 | 110 | # Denormalization methods 111 | # Migration: t.string :title 112 | # => "My new awesome post" 113 | def commentable_title 114 | try(:title) || "Undefined post title" 115 | end 116 | 117 | # => your way to build URL 118 | # => "/posts/254" 119 | def commentable_url 120 | ['', self.class.to_s.tableize, id].join('/') 121 | end 122 | 123 | # gem 'state_machine' 124 | # Migration: t.string :state 125 | # => "published" | "draft" | "deleted" 126 | def commentable_state 127 | try(:state) || "published" 128 | end 129 | end 130 | ``` 131 | 132 | ### 5. Mount Engine routes 133 | 134 | **config/routes.rb** 135 | 136 | ```ruby 137 | MyApp::Application.routes.draw do 138 | root 'posts#index' 139 | resources :posts 140 | 141 | # ... 142 | 143 | # TheComments routes 144 | concern :user_comments, TheComments::UserRoutes.new 145 | concern :admin_comments, TheComments::AdminRoutes.new 146 | resources :comments, concerns: [:user_comments, :admin_comments] 147 | end 148 | ``` 149 | 150 | Please, read [documentation](docs/documentation.md) to learn more 151 | 152 | ### 6. Assets install 153 | 154 | **app/assets/stylesheets/application.css** 155 | 156 | ```css 157 | /* 158 | *= require the_comments 159 | */ 160 | ``` 161 | 162 | **app/assets/javascripts/application.js** 163 | 164 | ```js 165 | //= require the_comments 166 | ``` 167 | 168 | ### 7. Controller code example 169 | 170 | **app/controllers/posts_controllers.rb** 171 | 172 | ```ruby 173 | def show 174 | @post = Post.find params[:id] 175 | @comments = @post.comments.with_state([:draft, :published]) 176 | end 177 | ``` 178 | 179 | ### 8. View code example 180 | 181 | **app/views/posts/show.html.haml** 182 | 183 | ```haml 184 | = render partial: 'the_comments/tree', locals: { commentable: @post, comments_tree: @comments } 185 | ``` 186 | -------------------------------------------------------------------------------- /docs/comment_api.md: -------------------------------------------------------------------------------- 1 | ←   [documentation](documentation.md) 2 | 3 | ### Comment API 4 | 5 | ```ruby 6 | @comment = Comment.last 7 | 8 | # Comment creator, can be nil (for Guest) 9 | @comment.user # => User 10 | 11 | # Comment holder 12 | # Owner of commentable object 13 | # shouldn't be nil, should be defined on create 14 | @comment.holder # => User 15 | 16 | # Commentable object 17 | @comment.commentable # => Post 18 | 19 | # Raw user input 20 | @comment.raw_content 21 | 22 | # Processed user input 23 | # method *prepare_content* should be redefined by developer 24 | @comment.content 25 | 26 | # Denormalization fields 27 | @comment.commentable_title # => "Harum sint error odit." 28 | @comment.commentable_url # => "/posts/7" 29 | @comment.commentable_state # => "published" 30 | 31 | # Stat info from request 32 | # Can be used for spam detection 33 | @comment.user_agent # => Opera/9.80 (Windows NT 5.1; U; en) Presto/2.2.15 Version/10.10 34 | @comment.tolerance_time # => 5 (secs) 35 | @comment.referer # => localhost:3000/post/7 36 | @comment.ip # => 192.168.0.12 37 | 38 | # State 39 | @comment.state # => draft | published | deleted 40 | 41 | # Spam flag 42 | @comment.spam # => true 43 | 44 | # Alias for *mark_as_spam* 45 | @comment.to_spam 46 | 47 | # mark this comment and all descendants as spam/not spam 48 | @comment.mark_as_spam 49 | @comment.mark_as_not_spam 50 | 51 | # Comment's creator avatar 52 | # this method can be redefined by developer 53 | @comment.avatar_url # => "https://2.gravatar.com/avatar/015e ... 2f05?s=42&d=https://identicons.github.com/AVATAR.png" 54 | 55 | # Anchor of comment 56 | # this method can be redefined by developer 57 | @comment.anchor # => b58020 58 | ``` -------------------------------------------------------------------------------- /docs/commentable_api.md: -------------------------------------------------------------------------------- 1 | ←   [documentation](documentation.md) 2 | 3 | ### Commentable API 4 | 5 | ```ruby 6 | class Post < ActiveRecord::Base 7 | include TheCommentsCommentable 8 | 9 | belongs_to :user 10 | 11 | def commentable_title 12 | try(:title) || "Undefined title" 13 | end 14 | 15 | def commentable_url 16 | ['', self.class.to_s.tableize, id].join('/') 17 | end 18 | 19 | def commentable_state 20 | try(:state) || "published" 21 | end 22 | end 23 | ``` 24 | 25 | ```ruby 26 | @post = Post.last 27 | 28 | # Post owner 29 | @post.user # => User 30 | 31 | # All comments for commentable object 32 | @post.comments # => ActiveRecord:Collection 33 | 34 | # Cache counters 35 | @post.draft_comments_count # => 1 36 | @post.published_comments_count # => 2 37 | @post.deleted_comments_count # => 0 38 | 39 | # equal values with direct request to database 40 | @post.comments.with_state([:draft]).count # => 1 41 | @post.comments.with_state([:published]).count # => 2 42 | @post.comments.with_state([:deleted]).count # => 0 43 | 44 | # Alias for: 45 | # draft_comments_count + published_comments_count 46 | @post.comments_sum # => 3 47 | 48 | # Spam comments 49 | @post.comments.where(spam: true) # => ActiveRecord::Relation 50 | 51 | # recalculate cache counters 52 | @post.recalculate_comments_counters! 53 | 54 | # Default Denormalization methods 55 | # should be redefined by developer 56 | @post.commentable_title => "Maiores eos rerum numquam aut." 57 | @post.commentable_url => "/posts/9" 58 | @post.commentable_state => "published" 59 | ``` -------------------------------------------------------------------------------- /docs/config_file.md: -------------------------------------------------------------------------------- 1 | ←   [documentation](documentation.md) 2 | 3 | ### TheComments config 4 | 5 | Following rails generator will copy default config file into your application 6 | 7 | ```ruby 8 | bundle exec rails g the_comments config 9 | ``` 10 | 11 | **config/initializers/the_comments.rb** 12 | 13 | ```ruby 14 | # TheComments.config.param_name => value 15 | 16 | TheComments.configure do |config| 17 | config.max_reply_depth = 3 # comments tree depth 18 | config.tolerance_time = 5 # sec - after this delay user can post a comment 19 | config.default_state = :draft # default state for comment 20 | config.default_owner_state = :published # default state for comment for Moderator 21 | config.empty_inputs = [:commentBody] # array of spam trap fields 22 | config.default_title = 'Undefined title' # default commentable_title for denormalization 23 | 24 | config.empty_trap_protection = true 25 | config.tolerance_time_protection = true 26 | end 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/content_preprocessors.md: -------------------------------------------------------------------------------- 1 | ←   [documentation](documentation.md) 2 | 3 | ### Text preprocessors 4 | 5 | TheComments designed for using with text preprocessors: Textile, Markdown, Sanitize, Coderay etc. 6 | 7 | That is why Comment model has 2 fields for user input: **raw_content** and **content** 8 | 9 | ```ruby 10 | class CreateComments < ActiveRecord::Migration 11 | def change 12 | create_table :comments do |t| 13 | # ... 14 | 15 | t.text :raw_content 16 | t.text :content 17 | 18 | # ... 19 | end 20 | end 21 | end 22 | ``` 23 | 24 | **raw_content** - field with original user's input 25 | 26 | **content** - field with processed user's input 27 | 28 |
    29 | 30 | **before_save :prepare_content** - provides processing of raw user's input 31 | 32 | By default **prepare_content** looks like this: 33 | 34 | ```ruby 35 | def prepare_content 36 | self.content = self.raw_content 37 | end 38 | ``` 39 | 40 | I think every developer should redefine this behaviour. To do this you should to use following instructions. 41 | 42 | ### Comment Model customization 43 | 44 | invoke TheComments generator 45 | 46 | ```ruby 47 | bundle exec rails g the_comments models 48 | ``` 49 | 50 | This will create **app/models/comment.rb** 51 | 52 | ```ruby 53 | class Comment < ActiveRecord::Base 54 | include TheCommentsBase 55 | 56 | # --------------------------------------------------- 57 | # Define your filters for content 58 | # Expample for: gem 'RedCloth', gem 'sanitize' 59 | # your personal SmilesProcessor 60 | 61 | # def prepare_content 62 | # text = self.raw_content 63 | # text = RedCloth.new(text).to_html 64 | # text = SmilesProcessor.new(text) 65 | # text = Sanitize.clean(text, Sanitize::Config::RELAXED) 66 | # self.content = text 67 | # end 68 | # --------------------------------------------------- 69 | end 70 | ``` 71 | 72 | Just redefine **prepare_content** for your purposes 73 | 74 | -------------------------------------------------------------------------------- /docs/customazation_of_views.md: -------------------------------------------------------------------------------- 1 | ←   [documentation](documentation.md) 2 | 3 | ## Customization 4 | 5 | You can use **rails generators** for copy files into your Application. After that you can customize almost everything 6 | 7 | Generators list: 8 | 9 | ```ruby 10 | bundle exec rails g the_comments --help 11 | ``` 12 | 13 | ### Customization of views 14 | 15 | Copy View files for customization: 16 | 17 | ```ruby 18 | bundle exec rails g the_comments:views assets 19 | bundle exec rails g the_comments:views views 20 | ``` 21 | 22 | ### Customization of comments tree 23 | 24 | Copy Helper file for tree customization: 25 | 26 | ```ruby 27 | bundle exec rails g the_comments:views helper 28 | ``` 29 | 30 | For more info read [TheSortableTree doc](https://github.com/the-teacher/the_sortable_tree) 31 | -------------------------------------------------------------------------------- /docs/denormalization_and_recent_comments.md: -------------------------------------------------------------------------------- 1 | ←   [documentation](documentation.md) 2 | 3 | ## Denormalization 4 | 5 | For building of Recent comments list (for polymorphic relationship) we need to have many additional requests to database. It's classic problem of polymorphic comments. 6 | 7 | I use denormalization of commentable objects to solve this problem. 8 | 9 | My practice shows - We need 3 denormalized fields into comment for (request-free) building of recent comments list: 10 | 11 | the_comments 12 | 13 | * **Comment#commentable_title** - for example: "My first post about Ruby On Rails" 14 | * **Comment#commentable_url** - for example: "/posts/1-my-first-post-about-ruby-on-rails" 15 | * **Comment#commentable_state** - for example: "draft" 16 | 17 | That is why any **Commentable Model should have few methods** to provide denormalization for Comments. 18 | 19 | ## Recent comments building 20 | 21 | Denormalization makes building of Recent comments (for polymorphic relationship) very easy! 22 | 23 | Controller: 24 | 25 | ```ruby 26 | @comments = Comment.with_state(:published) 27 | .where(commentable_state: [:published]) 28 | .order('created_at DESC') 29 | .page(params[:page]) 30 | ``` 31 | 32 | View: 33 | 34 | ```ruby 35 | - @comments.each do |comment| 36 | %div 37 | %p= comment.commentable_title 38 | %p= link_to comment.commentable_title, comment.commentable_url 39 | %p= comment.content 40 | ``` 41 | -------------------------------------------------------------------------------- /docs/documentation.md: -------------------------------------------------------------------------------- 1 | #### INSTALLATION 2 | * :white_check_mark:   [ADVANCED INSTALLATION](advanced_installation.md) 3 | * :white_check_mark:   [ADMIN UI INSTALLATION](admin_ui_installation.md) 4 | * :white_check_mark:   [Routing](routes.md) 5 | * :white_check_mark:   [Generators](generators.md) 6 | 7 | #### API 8 | * :white_check_mark:   [User API](user_api.md) 9 | * :white_check_mark:   [Comment API](comment_api.md) 10 | * :white_check_mark:   [Commentable API](commentable_api.md) 11 | 12 | #### Understanding 13 | * :white_check_mark:   [What is ComComs?](what_is_comcoms.md) 14 | * :white_check_mark:   [Denormalization and Recent comments](denormalization_and_recent_comments.md) 15 | * :white_check_mark:   [What's wrong with other gems?](whats_wrong_with_other_gems.md) 16 | * :white_check_mark:   [Why TheComments is better than others gems?](whats_wrong_with_other_gems.md#why-thecomments-is-better-than-others-gems) 17 | 18 | #### Customazation 19 | * :white_check_mark:   [Views](customazation_of_views.md) 20 | * :white_check_mark:   [Text Preprocessors - Sanitize, Markdown etc.](content_preprocessors.md) 21 | 22 | #### Configuration 23 | * :white_check_mark:   [the_comments.rb config file](config_file.md) 24 | 25 | #### Q&A 26 | * :white_check_mark:   [I want not to use kaminari for Admin UI](pagination.md) 27 | * :white_check_mark:   [Where is example application?](where_is_example_application.md) 28 | * :white_check_mark:   [How can I run tests?](where_is_example_application.md#run-tests) 29 | 30 | -------------------------------------------------------------------------------- /docs/generators.md: -------------------------------------------------------------------------------- 1 | ←   [documentation](documentation.md) 2 | 3 | ## Generators 4 | 5 | ```ruby 6 | rails g the_comments NAME 7 | rails g the_comments:views NAME 8 | ``` 9 | 10 | #### Migrations 11 | 12 | ```ruby 13 | rake the_comments_engine:install:migrations 14 | ``` 15 | 16 | ### Full list 17 | 18 | ```ruby 19 | rails g the_comments --help 20 | ``` 21 | 22 | #### Main 23 | 24 | ```ruby 25 | rails g the_comments install 26 | ``` 27 | 28 | This will create: 29 | 30 | * config/initializers/the_comments.rb 31 | * app/controllers/comments_controller.rb 32 | * app/models/comment.rb 33 | 34 | #### Controllers 35 | 36 | ```ruby 37 | rails g the_comments controllers 38 | ``` 39 | 40 | This will create: 41 | 42 | * app/controllers/comments_controller.rb 43 | 44 | #### Models 45 | 46 | ```ruby 47 | rails g the_comments models 48 | ``` 49 | 50 | This will create: 51 | 52 | * app/models/comment.rb 53 | 54 | #### Config 55 | 56 | ```ruby 57 | rails g the_comments config 58 | ``` 59 | 60 | #### Locals 61 | 62 | ```ruby 63 | rails g the_comments locales 64 | ``` 65 | 66 | #### Views 67 | 68 | ```ruby 69 | rails g the_comments:views js 70 | rails g the_comments:views css 71 | rails g the_comments:views assets 72 | rails g the_comments:views helper 73 | rails g the_comments:views views 74 | ``` 75 | -------------------------------------------------------------------------------- /docs/pagination.md: -------------------------------------------------------------------------------- 1 | ←   [documentation](documentation.md) 2 | 3 | ## I want not to use kaminari for Admin UI 4 | 5 | By default we use Kaminari pagination for Admin UI. 6 | 7 | But you can change this. For example, your **comments_controller.rb** looks like this: 8 | 9 | **app/controllers/comments_controller.rb** 10 | 11 | ```ruby 12 | class CommentsController < ApplicationController 13 | # layout 'admin' 14 | 15 | # Define your restrict methods and use them like this: 16 | # 17 | # before_action :user_required, except: %w[index create] 18 | # before_action :owner_required, except: %w[index create] 19 | # before_action :admin_required, only: %w[total_draft total_published total_deleted total_spam] 20 | 21 | include TheComments::Controller 22 | 23 | # >>> include TheComments::Controller <<< 24 | # (!) Almost all methods based on *current_user* method 25 | # 26 | # 1. Controller's public methods list: 27 | # You can redifine it for your purposes 28 | # public 29 | # %w[ manage index create edit update ] 30 | # %w[ my_comments my_draft my_published ] 31 | # %w[ draft published deleted spam ] 32 | # %w[ to_draft to_published to_deleted to_spam ] 33 | # %w[ total_draft total_published total_deleted total_spam ] 34 | # 35 | # 36 | # 2. Controller's private methods list: 37 | # You can redifine it for your purposes 38 | # 39 | # private 40 | # %w[ comment_template comment_partial ] 41 | # %w[ denormalized_fields request_data_for_comment define_commentable ] 42 | # %w[ comment_params patch_comment_params ] 43 | # %w[ ajax_requests_required cookies_required ] 44 | # %w[ empty_trap_required tolerance_time_required ] 45 | 46 | # KAMINARI pagination: 47 | # following methods based on gem "kaminari" 48 | # You should redefine them if you use something else 49 | # 50 | # public 51 | # %w[ manage index edit ] 52 | # %w[ draft published deleted spam ] 53 | # %w[ my_comments my_draft my_published ] 54 | # %w[ total_draft total_published total_deleted total_spam ] 55 | end 56 | ``` 57 | 58 | There is we can see comments about kaminari. So, we can try to change it. 59 | 60 | There is example how it can be in your real app: 61 | 62 | ```ruby 63 | class CommentsController < ApplicationController 64 | layout 'admin' 65 | 66 | before_action :user_required, except: %w[index create] 67 | before_action :owner_required, except: %w[index create] 68 | before_action :admin_required, only: %w[total_draft total_published total_deleted total_spam] 69 | 70 | include TheComments::Controller 71 | 72 | public 73 | 74 | def index 75 | @comments = ::Comment.with_state(:published).recent.super_paginator(params) 76 | render comment_template(:index) 77 | end 78 | 79 | def manage 80 | @comments = current_user.comcoms.active.recent.super_paginator(params) 81 | render comment_template(:manage) 82 | end 83 | 84 | def my_comments 85 | @comments = current_user.my_comments.active.recent.super_paginator(params) 86 | render comment_template(:my_comments) 87 | end 88 | 89 | def edit 90 | @comments = current_user.comcoms.where(id: params[:id]).super_paginator(params) 91 | render comment_template(:manage) 92 | end 93 | 94 | %w[draft published deleted].each do |state| 95 | define_method "#{state}" do 96 | @comments = current_user.comcoms.with_state(state).recent.super_paginator(params) 97 | render comment_template(:manage) 98 | end 99 | 100 | define_method "total_#{state}" do 101 | @comments = ::Comment.with_state(state).recent.super_paginator(params) 102 | render comment_template(:manage) 103 | end 104 | 105 | unless state == 'deleted' 106 | define_method "my_#{state}" do 107 | @comments = current_user.my_comments.with_state(state).recent.super_paginator(params) 108 | render comment_template(:my_comments) 109 | end 110 | end 111 | end 112 | 113 | def spam 114 | @comments = current_user.comcoms.where(spam: true).recent.super_paginator(params) 115 | render comment_template(:manage) 116 | end 117 | 118 | def total_spam 119 | @comments = ::Comment.where(spam: true).recent.super_paginator(params) 120 | render comment_template(:manage) 121 | end 122 | end 123 | ``` -------------------------------------------------------------------------------- /docs/routes.md: -------------------------------------------------------------------------------- 1 | ←   [documentation](documentation.md) 2 | 3 | ## TheComments Routes 4 | 5 | **config/routes.rb** 6 | 7 | ```ruby 8 | MyApp::Application.routes.draw do 9 | root 'posts#index' 10 | resources :posts 11 | 12 | # ... 13 | 14 | # TheComments routes 15 | concern :user_comments, TheComments::UserRoutes.new 16 | concern :admin_comments, TheComments::AdminRoutes.new 17 | resources :comments, concerns: [:user_comments, :admin_comments] 18 | end 19 | ``` 20 | 21 | And after that you can see routes: 22 | 23 | ```ruby 24 | rake routes | grep comments 25 | ``` 26 | 27 | ```ruby 28 | comments / TheComments::Engine 29 | to_spam_comment POST /comments/:id/to_spam(.:format) comments#to_spam 30 | to_draft_comment POST /comments/:id/to_draft(.:format) comments#to_draft 31 | to_published_comment POST /comments/:id/to_published(.:format) comments#to_published 32 | to_deleted_comment DELETE /comments/:id/to_deleted(.:format) comments#to_deleted 33 | manage_comments GET /comments/manage(.:format) comments#manage 34 | my_draft_comments GET /comments/my_draft(.:format) comments#my_draft 35 | my_published_comments GET /comments/my_published(.:format) comments#my_published 36 | my_comments_comments GET /comments/my_comments(.:format) comments#my_comments 37 | total_draft_comments GET /comments/total_draft(.:format) comments#total_draft 38 | total_published_comments GET /comments/total_published(.:format) comments#total_published 39 | total_deleted_comments GET /comments/total_deleted(.:format) comments#total_deleted 40 | total_spam_comments GET /comments/total_spam(.:format) comments#total_spam 41 | draft_comments GET /comments/draft(.:format) comments#draft 42 | published_comments GET /comments/published(.:format) comments#published 43 | deleted_comments GET /comments/deleted(.:format) comments#deleted 44 | spam_comments GET /comments/spam(.:format) comments#spam 45 | comments GET /comments(.:format) comments#index 46 | POST /comments(.:format) comments#create 47 | new_comment GET /comments/new(.:format) comments#new 48 | edit_comment GET /comments/:id/edit(.:format) comments#edit 49 | comment GET /comments/:id(.:format) comments#show 50 | PATCH /comments/:id(.:format) comments#update 51 | PUT /comments/:id(.:format) comments#update 52 | DELETE /comments/:id(.:format) comments#destroy 53 | ``` 54 | 55 | And now you can use url helpers with 2 ways: 56 | 57 | ### Way 1. Url Helpers 58 | 59 | ```ruby 60 | = link_to 'link', comments_path 61 | = link_to 'link', manage_comments_path 62 | = link_to 'link', new_comment_path 63 | 64 | = link_to 'link', comment_path(@comment) 65 | = link_to 'link', to_spam_comment_path(@comment) 66 | ``` 67 | 68 | ### Way 2. Array notation 69 | 70 | ```ruby 71 | = link_to 'link', [:index, :comments] 72 | = link_to 'link', [:manage, :comments] 73 | = link_to 'link', [:draft, :comments] 74 | 75 | = link_to 'link', [@comment] 76 | = link_to 'link', [:to_spam, @comment] 77 | ``` -------------------------------------------------------------------------------- /docs/screencast.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-trash/the_comments/5992b861f88100ed49058afad623b0d744fcc055/docs/screencast.jpg -------------------------------------------------------------------------------- /docs/the_comments.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-trash/the_comments/5992b861f88100ed49058afad623b0d744fcc055/docs/the_comments.jpg -------------------------------------------------------------------------------- /docs/the_comments_view_1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-trash/the_comments/5992b861f88100ed49058afad623b0d744fcc055/docs/the_comments_view_1.gif -------------------------------------------------------------------------------- /docs/the_comments_view_2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-trash/the_comments/5992b861f88100ed49058afad623b0d744fcc055/docs/the_comments_view_2.gif -------------------------------------------------------------------------------- /docs/the_comments_view_3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-trash/the_comments/5992b861f88100ed49058afad623b0d744fcc055/docs/the_comments_view_3.gif -------------------------------------------------------------------------------- /docs/the_comments_view_4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-trash/the_comments/5992b861f88100ed49058afad623b0d744fcc055/docs/the_comments_view_4.gif -------------------------------------------------------------------------------- /docs/the_comments_view_5.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-trash/the_comments/5992b861f88100ed49058afad623b0d744fcc055/docs/the_comments_view_5.gif -------------------------------------------------------------------------------- /docs/user_api.md: -------------------------------------------------------------------------------- 1 | ←   [documentation](documentation.md) 2 | 3 | ### User API 4 | 5 | **When User is not commentable model** 6 | 7 | ```ruby 8 | class User < ActiveRecord::Base 9 | include TheCommentsUser 10 | 11 | has_many :posts # commentable model 12 | has_many :products # commentable model 13 | end 14 | ``` 15 | 16 | :warning:   Please, read this: [What is ComComs?](what_is_comcoms.md) 17 | 18 | 19 | We can use following methods 20 | 21 | ```ruby 22 | @user = User.first 23 | 24 | @user.comcoms #=> all comments for posts and products, where user is owner 25 | 26 | # cache counters 27 | @user.draft_comcoms_count # => 1 28 | @user.published_comcoms_count # => 5 29 | @user.deleted_comcoms_count # => 3 30 | @user.spam_comcoms_count # => 2 31 | 32 | # equal values, but with request to database 33 | @user.comcoms.with_state([:draft]).count # => 1 34 | @user.comcoms.with_state([:published]).count # => 5 35 | @user.comcoms.with_state([:deleted]).count # => 3 36 | @user.comcoms.where(spam: true).count # => 2 37 | 38 | # draft and published comments 39 | # written by this user 40 | @user.my_comments # => ActiveRecord::Relation 41 | 42 | # cache counters for comments 43 | # written by this user 44 | # there is no cache counter for deleted state! 45 | @user.my_draft_comments_count # => 3 46 | @user.my_published_comments_count # => 7 47 | 48 | # equal values, but with request to database 49 | @user.my_draft_comments.count # => 3 50 | @user.my_published_comments.count # => 7 51 | @user.my_deleted_comments.count # => 1 52 | 53 | # helper methods to get comments 54 | # written by this user 55 | @user.my_draft_comments # => ActiveRecord::Relation 56 | @user.my_published_comments # => ActiveRecord::Relation 57 | @user.my_deleted_comments # => ActiveRecord::Relation 58 | 59 | # recalculate cache counters 60 | @user.recalculate_my_comments_counter! 61 | ``` 62 | 63 | **When User is commentable model** 64 | 65 | ```ruby 66 | class User < ActiveRecord::Base 67 | include TheCommentsUser 68 | include TheCommentsCommentable 69 | 70 | has_many :posts # commentable model 71 | has_many :products # commentable model 72 | end 73 | ``` 74 | 75 | you should to use following instruction [Commentable API](commentable_api.md) 76 | -------------------------------------------------------------------------------- /docs/what_is_comcoms.md: -------------------------------------------------------------------------------- 1 | ←   [documentation](documentation.md) 2 | 3 | ### What is ComComs? 4 | 5 | :warning:   **comcoms** - is main method to get all comments related with user's commentable models. 6 | 7 | :warning:   **comments** - is main method to get comments related with any commentable model. 8 | 9 | **ComComs** - **com**ments of **com**mentable models 10 | 11 | ComComs are all incoming comments for all models, where this user is owner. 12 | 13 | For example, some user **has_many :posts**, and **has_many :products** - all comments for all user's posts and all user's products called as **comcoms**. 14 | 15 | #### Why we need ComComs? 16 | 17 | User model can be commentable too. For example to build user's "public wall" (like tweets list for current user). 18 | 19 | And we should to separate **comments** attached to user model (tweets) and comments attached to any another user's model. 20 | 21 | That is why User model in-fact has following relationship declarations: 22 | 23 | ```ruby 24 | class User < ActiveRecord::Base 25 | has_many :comcoms, class_name: :Comment, foreign_key: :holder_id 26 | 27 | # and if User model is commentable model 28 | # has_many :comments, as: :commentable 29 | 30 | has_many :posts 31 | has_many :products 32 | end 33 | ``` 34 | 35 | in real application it should be described like this: 36 | 37 | ```ruby 38 | class User < ActiveRecord::Base 39 | include TheCommentsUser 40 | include TheCommentsCommentable 41 | 42 | has_many :posts 43 | has_many :products 44 | end 45 | ``` 46 | 47 | But in most popular situation User model should not be commentable, and you should use only **comcoms** method to get all comments related with this user: 48 | 49 | ```ruby 50 | class User < ActiveRecord::Base 51 | include TheCommentsUser 52 | 53 | has_many :posts 54 | has_many :products 55 | end 56 | ``` 57 | 58 | and later in your application 59 | 60 | ```ruby 61 | @user = User.find params[:id] 62 | @user.comcoms.count # => 42 63 | ``` 64 | -------------------------------------------------------------------------------- /docs/whats_wrong_with_other_gems.md: -------------------------------------------------------------------------------- 1 | ←   [documentation](documentation.md) 2 | 3 | ### What's wrong with other gems? 4 | 5 | Take a look at [Ruby-Toolbox](https://www.ruby-toolbox.com/categories/rails_comments). What can we see? 6 | 7 | * [Acts as commentable with threading](https://github.com/elight/acts_as_commentable_with_threading) - Where is the render helper for the tree? There is no helper! Am I supposed to write a render helper for the tree myself? Nooooo!!! I'm sorry, I can't use this gem. 8 | * [acts_as_commentable](https://github.com/jackdempsey/acts_as_commentable) - I can see the code for models. But I can't see the code for controllers and views. Unfortunately, there is no threading. This isn't enough for me. 9 | * [opinio](https://github.com/Draiken/opinio) - Better, but still no threading. I can do better! 10 | * [has_threaded_comments](https://github.com/aarongough/has_threaded_comments) - A solid gem. Has model, controller and view helpers for tree rendering! **But** last activity was 2 years ago, it still needs a few features - I can do better. 11 | 12 | ### Why is TheComments better than other gems? 13 | 14 | 1. TheComments allows for threaded comments 15 | 2. **Only TheComments has special helper for tree rendering** (based on [TheSortableTree](https://github.com/the-teacher/the_sortable_tree)). 16 | 3. TheComments is designed to reduce database requests. Helpful for cache counters. 17 | 4. TheComments has a solution for [building Recent Comments](https://github.com/the-teacher/the_comments/blob/master/docs/denormalization_and_recent_comments.md) (for polymorphic relations) 18 | 5. TheComments is designed for text preprocessors (Textile, Markdown, Sanitize, Coderay etc.) 19 | 6. TheComments has an admin UI based on bootstrap 3 20 | 7. TheComments is an "all-in-one" solution.
    21 | It has: Models and Controllers logic (via concerns), Generators, Views, Helper for fast Tree rendering and Admin UI. 22 | 8. If you have problems with TheComments, I'll try to help you via skype: **ilya.killich** 23 | 24 | ### TheComments based on: 25 | 26 | 1. [AwesomeNestedSet](https://github.com/collectiveidea/awesome_nested_set) - for comments threading 27 | 2. [TheSortableTree](https://github.com/the-teacher/the_sortable_tree) - for fast rendering of comments tree 28 | 3. [State Machine](https://github.com/pluginaweek/state_machine) - to provide easy and correct recalculation cache counters on states transitions -------------------------------------------------------------------------------- /docs/where_is_example_application.md: -------------------------------------------------------------------------------- 1 | ←   [documentation](documentation.md) 2 | 3 | ### Dummy Application 4 | 5 | TheComments repository contains a dummy application for development and testing. 6 | 7 | It's here: [Dummy App](https://github.com/the-teacher/the_comments/tree/master/spec/dummy_app) 8 | 9 | To run the dummy app: 10 | 11 | ```ruby 12 | git clone https://github.com/the-teacher/the_comments.git 13 | 14 | cd the_comments/spec/dummy_app/ 15 | 16 | bundle 17 | 18 | rake db:bootstrap_and_seed 19 | 20 | rails s -p 3000 -b localhost 21 | ``` 22 | 23 | ### Run tests 24 | 25 | To run the RSPEC tests: 26 | 27 | ```ruby 28 | git clone https://github.com/the-teacher/the_comments.git 29 | 30 | cd the_comments/spec/dummy_app/ 31 | 32 | bundle 33 | 34 | rake db:bootstrap RAILS_ENV=test 35 | 36 | rspec --format documentation 37 | ``` 38 | -------------------------------------------------------------------------------- /gem_version.rb: -------------------------------------------------------------------------------- 1 | module TheComments 2 | VERSION = "2.3.1" 3 | end 4 | -------------------------------------------------------------------------------- /lib/generators/the_comments/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | TheComments: generators will copy files for organize comments tree in your web project 3 | 4 | Usage: [bundle exec] rails g the_comments NAME 5 | Usage: [bundle exec] rails g the_comments:views NAME 6 | 7 | # This text: 8 | > rails g the_comments --help 9 | 10 | # Main generators: 11 | > rails g the_comments install 12 | 13 | This will create: 14 | config/initializers/the_comments.rb 15 | app/controllers/comments_controller.rb 16 | app/models/comment.rb 17 | 18 | # Controller generators: 19 | > rails g the_comments controllers 20 | 21 | This will create: 22 | app/controllers/comments_controller.rb 23 | 24 | # Model generators: 25 | > rails g the_comments models 26 | 27 | This will create: 28 | app/models/comment.rb 29 | 30 | # Config generators: 31 | > rails g the_comments config 32 | 33 | # Locals generators: 34 | > rails g the_comments locales 35 | 36 | # View Generators: 37 | > rails g the_comments:views js 38 | > rails g the_comments:views css 39 | > rails g the_comments:views assets 40 | > rails g the_comments:views helper 41 | > rails g the_comments:views views 42 | 43 | # Migrations: 44 | > rake the_comments_engine:install:migrations -------------------------------------------------------------------------------- /lib/generators/the_comments/the_comments_generator.rb: -------------------------------------------------------------------------------- 1 | class TheCommentsGenerator < Rails::Generators::NamedBase 2 | source_root File.expand_path('../templates', __FILE__) 3 | # argument :xname, type: :string, default: :xname 4 | 5 | # > rails g the_comments NAME 6 | def generate_controllers 7 | case gen_name 8 | when 'locales' 9 | cp_locales 10 | when 'models' 11 | cp_models 12 | when 'controllers' 13 | cp_controllers 14 | when 'config' 15 | cp_config 16 | when 'install' 17 | cp_config 18 | cp_models 19 | cp_controllers 20 | else 21 | puts 'TheComments Generator - wrong Name' 22 | puts 'Try to use [ install | config | controllers | models ]' 23 | end 24 | end 25 | 26 | private 27 | 28 | def root_path; TheComments::Engine.root; end 29 | 30 | def gen_name 31 | name.to_s.downcase 32 | end 33 | 34 | def cp_config 35 | copy_file "#{root_path}/config/initializers/the_comments.rb", 36 | "config/initializers/the_comments.rb" 37 | end 38 | 39 | def cp_models 40 | copy_file "#{root_path}/app/models/_templates_/comment.rb", 41 | "app/models/comment.rb" 42 | end 43 | 44 | def cp_controllers 45 | copy_file "#{root_path}/app/controllers/_templates_/comments_controller.rb", 46 | "app/controllers/comments_controller.rb" 47 | end 48 | 49 | def cp_locales 50 | copy_file "#{root_path}/config/locales/en.yml", 51 | "config/locales/en.the_comments.yml" 52 | 53 | copy_file "#{root_path}/config/locales/ru.yml", 54 | "config/locales/ru.the_comments.yml" 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/generators/the_comments/views_generator.rb: -------------------------------------------------------------------------------- 1 | module TheComments 2 | module Generators 3 | class ViewsGenerator < Rails::Generators::NamedBase 4 | source_root TheComments::Engine.root 5 | 6 | def self.banner 7 | <<-BANNER.chomp 8 | 9 | USAGE: [bundle exec] rails g the_comments:views NAME 10 | 11 | > rails g the_comments:views js 12 | > rails g the_comments:views css 13 | > rails g the_comments:views assets 14 | 15 | > rails g the_comments:views views 16 | > rails g the_comments:views helper 17 | 18 | > rails g the_comments:views all 19 | 20 | BANNER 21 | end 22 | 23 | def copy_sortable_tree_files 24 | copy_gem_files 25 | end 26 | 27 | private 28 | 29 | def param_name 30 | name.downcase 31 | end 32 | 33 | def copy_gem_files 34 | case param_name 35 | when 'js' 36 | js_copy 37 | when 'css' 38 | css_copy 39 | when 'assets' 40 | js_copy; css_copy 41 | when 'views' 42 | views_copy 43 | when 'helper' 44 | helper_copy 45 | when 'all' 46 | js_copy 47 | css_copy 48 | views_copy 49 | helper_copy 50 | else 51 | puts 'TheComments View Generator - wrong Name' 52 | puts "Wrong params - use only [ js | css | assets | views | helper | all ] values" 53 | end 54 | end 55 | 56 | def js_copy 57 | f1 = "app/assets/javascripts/the_comments.js.coffee" 58 | f2 = "app/assets/javascripts/the_comments_manage.js.coffee" 59 | copy_file f1, f1 60 | copy_file f2, f2 61 | end 62 | 63 | def css_copy 64 | f1 = "app/assets/stylesheets/the_comments.css.scss" 65 | copy_file f1, f1 66 | end 67 | 68 | def views_copy 69 | d1 = "app/views/the_comments" 70 | directory d1, d1 71 | end 72 | 73 | def helper_copy 74 | f1 = "app/helpers/render_comments_tree_helper.rb" 75 | copy_file f1, f1 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/the_comments.rb: -------------------------------------------------------------------------------- 1 | require 'state_machine' 2 | require 'state_machine/version' 3 | 4 | require 'the_simple_sort' 5 | require 'the_sortable_tree' 6 | 7 | require 'the_comments/config' 8 | require 'the_comments/version' 9 | 10 | module TheComments 11 | COMMENTS_COOKIES_TOKEN = 'JustTheCommentsCookies' 12 | 13 | class Engine < Rails::Engine 14 | config.autoload_paths += Dir["#{config.root}/app/controllers/concerns/**/"] 15 | config.autoload_paths += Dir["#{config.root}/app/models/concerns/**/"] 16 | end 17 | end 18 | 19 | # Loading of concerns 20 | _root_ = File.expand_path('../../', __FILE__) 21 | require "#{_root_}/config/routes.rb" 22 | 23 | if StateMachine::VERSION.to_f <= 1.2 24 | puts '~' * 50 25 | puts 'TheComments' 26 | puts '~' * 50 27 | puts 'WARNING!' 28 | puts 'StateMachine patch for Rails4 will be applied' 29 | puts 30 | puts '> private method *around_validation* from StateMachine::Integrations::ActiveModel will be public' 31 | puts 32 | puts 'https://github.com/pluginaweek/state_machine/issues/295' 33 | puts 'https://github.com/pluginaweek/state_machine/issues/251' 34 | puts '~' * 50 35 | module StateMachine::Integrations::ActiveModel 36 | public :around_validation 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/the_comments/config.rb: -------------------------------------------------------------------------------- 1 | module TheComments 2 | def self.configure(&block) 3 | yield @config ||= TheComments::Configuration.new 4 | end 5 | 6 | def self.config 7 | @config 8 | end 9 | 10 | # Configuration class 11 | class Configuration 12 | include ActiveSupport::Configurable 13 | 14 | config_accessor :max_reply_depth, 15 | :tolerance_time, 16 | :default_state, 17 | :default_owner_state, 18 | :empty_inputs, 19 | :default_title, 20 | :template_engine, 21 | :empty_trap_protection, 22 | :tolerance_time_protection 23 | end 24 | 25 | configure do |config| 26 | config.max_reply_depth = 3 27 | config.tolerance_time = 5 28 | config.default_state = :draft 29 | config.default_owner_state = :published 30 | config.empty_inputs = [:message] 31 | config.default_title = 'Undefined title' 32 | config.template_engine = :haml 33 | 34 | config.empty_trap_protection = true 35 | config.tolerance_time_protection = true 36 | end 37 | end -------------------------------------------------------------------------------- /lib/the_comments/version.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../../gem_version', __FILE__) -------------------------------------------------------------------------------- /spec/dummy_app/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | .rvmrc 10 | 11 | # Ignore the default SQLite database. 12 | /db/*.db 13 | /db/*.sqlite3 14 | /db/*.sqlite3-journal 15 | 16 | # Ignore all logfiles and tempfiles. 17 | /log/*.log 18 | /tmp 19 | -------------------------------------------------------------------------------- /spec/dummy_app/.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /spec/dummy_app/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Base gems 4 | gem 'rails', '4.1.4 ' #github: 'rails/rails', branch: 'master' 5 | gem 'sqlite3' 6 | 7 | gem 'jquery-rails' 8 | gem 'jbuilder', '~> 1.2' 9 | 10 | gem 'uglifier', '>= 1.3.0' 11 | gem 'sass-rails', '~> 4.0.0' 12 | gem 'coffee-rails', '~> 4.0.0' 13 | 14 | gem 'thin', group: [:development] 15 | # gem 'therubyracer', platforms: :ruby 16 | gem 'sdoc', require: false, group: [:doc] 17 | 18 | # App gems 19 | gem 'haml' 20 | gem 'sorcery' 21 | gem 'kaminari' 22 | gem 'haml-rails' 23 | 24 | # TheComments requires 25 | gem 'the_comments', 26 | path: '../../' 27 | 28 | gem 'bootstrap-sass', 29 | github: 'thomas-mcdonald/bootstrap-sass' 30 | 31 | gem 'awesome_nested_set', 32 | git: 'https://github.com/collectiveidea/awesome_nested_set.git', 33 | branch: 'master' 34 | 35 | # Test gems 36 | group :development, :test do 37 | gem 'faker' 38 | gem 'factory_girl' 39 | gem "factory_girl_rails" 40 | gem 'rspec-rails', '~> 2.0' 41 | 42 | # gem 'database_cleaner' 43 | end 44 | -------------------------------------------------------------------------------- /spec/dummy_app/README.md: -------------------------------------------------------------------------------- 1 | ## TheComments Dummy App 2 | 3 | ### First step 4 | 5 | ``` 6 | git clone https://github.com/the-teacher/the_comments.git 7 | 8 | cd the_comments/spec/dummy_app/ 9 | 10 | bundle 11 | ``` 12 | 13 | ### App start 14 | 15 | ``` 16 | rake db:bootstrap_and_seed 17 | 18 | rails s -p 3000 -b localhost 19 | ``` 20 | 21 | Browser 22 | 23 | ``` 24 | http://localhost:3000/ 25 | ``` 26 | 27 | ### Tests start 28 | 29 | ``` 30 | rake db:bootstrap RAILS_ENV=test 31 | 32 | rspec --format documentation 33 | ``` 34 | -------------------------------------------------------------------------------- /spec/dummy_app/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | App::Application.load_tasks 7 | -------------------------------------------------------------------------------- /spec/dummy_app/app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-trash/the_comments/5992b861f88100ed49058afad623b0d744fcc055/spec/dummy_app/app/assets/images/.keep -------------------------------------------------------------------------------- /spec/dummy_app/app/assets/javascripts/admin_panel.js: -------------------------------------------------------------------------------- 1 | //= require jquery 2 | //= require jquery_ujs 3 | 4 | //= require bootstrap 5 | //= require the_comments_manage -------------------------------------------------------------------------------- /spec/dummy_app/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require jquery 14 | //= require jquery_ujs 15 | 16 | //= require the_comments -------------------------------------------------------------------------------- /spec/dummy_app/app/assets/stylesheets/admin_panel.css: -------------------------------------------------------------------------------- 1 | /* 2 | *= require bootstrap 3 | */ -------------------------------------------------------------------------------- /spec/dummy_app/app/assets/stylesheets/app.css.scss: -------------------------------------------------------------------------------- 1 | .body{ 2 | margin: auto; 3 | width: 800px; 4 | } -------------------------------------------------------------------------------- /spec/dummy_app/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the top of the 9 | * compiled file, but it's generally better to create a new file per style scope. 10 | * 11 | 12 | *= require_self 13 | *= require app 14 | *= require the_comments 15 | 16 | */ 17 | -------------------------------------------------------------------------------- /spec/dummy_app/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | include TheComments::ViewToken 3 | 4 | # Prevent CSRF attacks by raising an exception. 5 | # For APIs, you may want to use :null_session instead. 6 | protect_from_forgery with: :exception 7 | end 8 | -------------------------------------------------------------------------------- /spec/dummy_app/app/controllers/comments_controller.rb: -------------------------------------------------------------------------------- 1 | class CommentsController < ApplicationController 2 | # Define your restrict methods and use them like this: 3 | # 4 | # before_action :user_required, except: [:index, :create] 5 | # 6 | # before_action :owner_required, only: [:my, :incoming, :edit, :trash] 7 | # before_action :moderator_required, only: [:update, :to_published, :to_draft, :to_spam, :to_trash] 8 | 9 | layout 'admin' 10 | 11 | include TheComments::Controller 12 | 13 | # Public methods: 14 | # 15 | # [:index, :create] 16 | 17 | # Application side methods: 18 | # Overwrite following default methods if it's need 19 | # Following methods based on *current_user* helper method 20 | # Look here: https://github.com/the-teacher/the_comments/blob/master/app/controllers/concerns/the_comments_controller.rb#L62 21 | # 22 | # [:my, :incoming, :edit, :trash] 23 | 24 | # You must protect following methods 25 | # Only comments moderator (holder or admin) can invoke following actions 26 | # 27 | # [:update, :to_published, :to_draft, :to_spam, :to_trash] 28 | end -------------------------------------------------------------------------------- /spec/dummy_app/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-trash/the_comments/5992b861f88100ed49058afad623b0d744fcc055/spec/dummy_app/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /spec/dummy_app/app/controllers/posts_controller.rb: -------------------------------------------------------------------------------- 1 | class PostsController < ApplicationController 2 | def index 3 | @posts = Post.all 4 | @recent_comments = Comment.with_state(:published) 5 | .where(commentable_state: [:published]) 6 | .recent.page(params[:page]) 7 | end 8 | 9 | def show 10 | @post = Post.find params[:id] 11 | @comments = @post.comments.with_state([:draft, :published]).nested_set 12 | end 13 | end -------------------------------------------------------------------------------- /spec/dummy_app/app/controllers/users_controller.rb: -------------------------------------------------------------------------------- 1 | class UsersController < ApplicationController 2 | def autologin 3 | user = User.find params[:id] 4 | auto_login user if user 5 | redirect_to request.referrer || root_path 6 | end 7 | end -------------------------------------------------------------------------------- /spec/dummy_app/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /spec/dummy_app/app/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-trash/the_comments/5992b861f88100ed49058afad623b0d744fcc055/spec/dummy_app/app/mailers/.keep -------------------------------------------------------------------------------- /spec/dummy_app/app/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-trash/the_comments/5992b861f88100ed49058afad623b0d744fcc055/spec/dummy_app/app/models/.keep -------------------------------------------------------------------------------- /spec/dummy_app/app/models/comment.rb: -------------------------------------------------------------------------------- 1 | class Comment < ActiveRecord::Base 2 | include TheComments::Comment 3 | # --------------------------------------------------- 4 | # Define comment's avatar url 5 | # Usually we use Comment#user (owner of comment) to define avatar 6 | # @blog.comments.includes(:user) <= use includes(:user) to decrease queries count 7 | # comment#user.avatar_url 8 | # --------------------------------------------------- 9 | 10 | # --------------------------------------------------- 11 | # Simple way to define avatar url 12 | 13 | # def avatar_url 14 | # hash = Digest::MD5.hexdigest self.id.to_s 15 | # "http://www.gravatar.com/avatar/#{hash}?s=30&d=identicon" 16 | # end 17 | # --------------------------------------------------- 18 | 19 | # --------------------------------------------------- 20 | # Define your filters for content 21 | # Expample for: gem 'RedCloth', gem 'sanitize' 22 | # your personal SmilesProcessor 23 | 24 | # def prepare_content 25 | # text = self.raw_content 26 | # text = RedCloth.new(text).to_html 27 | # text = SmilesProcessor.new(text) 28 | # text = Sanitize.clean(text, Sanitize::Config::RELAXED) 29 | # self.content = text 30 | # end 31 | # --------------------------------------------------- 32 | end -------------------------------------------------------------------------------- /spec/dummy_app/app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-trash/the_comments/5992b861f88100ed49058afad623b0d744fcc055/spec/dummy_app/app/models/concerns/.keep -------------------------------------------------------------------------------- /spec/dummy_app/app/models/post.rb: -------------------------------------------------------------------------------- 1 | class Post < ActiveRecord::Base 2 | include TheComments::Commentable 3 | 4 | belongs_to :user 5 | 6 | def commentable_title 7 | title 8 | end 9 | 10 | def commentable_url 11 | ['', self.class.to_s.tableize, id].join('/') 12 | end 13 | 14 | def commentable_state 15 | :published.to_s 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/dummy_app/app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | include TheComments::User 3 | include TheComments::Commentable 4 | 5 | authenticates_with_sorcery! 6 | 7 | has_many :posts 8 | 9 | # can be replaced to TheCommentsUser as default 10 | def admin? 11 | self == User.first 12 | end 13 | 14 | def comments_admin? 15 | admin? 16 | end 17 | 18 | def comments_moderator? comment 19 | id == comment.holder_id 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/dummy_app/app/views/layouts/admin.html.haml: -------------------------------------------------------------------------------- 1 | !!! 5 2 | %html(lang="ru") 3 | %head 4 | %meta(charset="utf-8") 5 | %meta(http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1") 6 | %meta(name="viewport" content="width=device-width, initial-scale=1.0") 7 | %title= content_for?(:title) ? yield(:title) : "Admin Panel" 8 | %link(href="favicon.ico" rel="shortcut icon") 9 | 10 | = stylesheet_link_tag :admin_panel 11 | = javascript_include_tag :admin_panel 12 | = csrf_meta_tags 13 | 14 | %body 15 | .container 16 | .row 17 | .col-md-12 18 | %h3= content_for?(:title) ? yield(:title) : "Admin Panel" 19 | .row 20 | .col-md-3 21 | = yield :comments_sidebar 22 | .col-md-9 23 | = yield :comments_main 24 | 25 | = stylesheet_link_tag "//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.min.css" -------------------------------------------------------------------------------- /spec/dummy_app/app/views/layouts/application.html.haml: -------------------------------------------------------------------------------- 1 | !!! 2 | %html 3 | %head 4 | %title TheComments Dummy App 5 | = stylesheet_link_tag :application, media: :all 6 | = javascript_include_tag :application 7 | = csrf_meta_tags 8 | 9 | %body 10 | .body 11 | %p 12 | = link_to 'Home', root_path 13 | \| 14 | - if current_user 15 | User: #{current_user.username} 16 | \| 17 | = link_to "Comments Manage (+#{current_user.draft_comcoms_count})", manage_comments_path 18 | - else 19 | Guest 20 | = yield -------------------------------------------------------------------------------- /spec/dummy_app/app/views/posts/index.html.haml: -------------------------------------------------------------------------------- 1 | %h3 Posts: 2 | %ul 3 | - @posts.each do |post| 4 | %li 5 | = link_to post_url(post) do 6 | = "#{post.title} (#{post.comments_sum})" 7 | 8 | %h3 Autologin Users: 9 | %ul 10 | - User.all.each do |user| 11 | %li 12 | = link_to autologin_url(user) do 13 | - admin = user.comments_admin? ? '[Admin]' : nil 14 | = "#{user.username} (#{user.draft_comcoms_count}) #{admin}" 15 | 16 | %h3 Recent Comments: 17 | %ul 18 | - @recent_comments.each do |comment| 19 | %p 20 | %b= link_to comment.commentable_title, comment.commentable_url 21 | %br 22 | %i= comment.content 23 | -------------------------------------------------------------------------------- /spec/dummy_app/app/views/posts/show.html.haml: -------------------------------------------------------------------------------- 1 | %h3= @post.title 2 | %p= @post.content 3 | 4 | %p 5 | Comments count: #{@post.comments_sum} 6 | 7 | = render partial: 'the_comments/tree', locals: { commentable: @post, comments_tree: @comments } -------------------------------------------------------------------------------- /spec/dummy_app/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /spec/dummy_app/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../../config/application', __FILE__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /spec/dummy_app/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /spec/dummy_app/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /spec/dummy_app/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(:default, Rails.env) 8 | 9 | module App 10 | class Application < Rails::Application 11 | # Settings in config/environments/* take precedence over those specified here. 12 | # Application configuration should go into files in config/initializers 13 | # -- all .rb files in that directory are automatically loaded. 14 | 15 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 16 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 17 | # config.time_zone = 'Central Time (US & Canada)' 18 | 19 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 20 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 21 | # config.i18n.default_locale = :de 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/dummy_app/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | 4 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) 5 | -------------------------------------------------------------------------------- /spec/dummy_app/config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: sqlite3 3 | database: db/development.db 4 | pool: 5 5 | timeout: 5000 6 | 7 | test: 8 | adapter: sqlite3 9 | database: db/test.db 10 | pool: 5 11 | timeout: 5000 -------------------------------------------------------------------------------- /spec/dummy_app/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | App::Application.initialize! 6 | -------------------------------------------------------------------------------- /spec/dummy_app/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | App::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | # Raise an error on page load if there are pending migrations 23 | config.active_record.migration_error = :page_load 24 | 25 | # Debug mode disables concatenation and preprocessing of assets. 26 | # This option may cause significant delays in view rendering with a large 27 | # number of complex assets. 28 | config.assets.debug = true 29 | end 30 | -------------------------------------------------------------------------------- /spec/dummy_app/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | App::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both thread web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. 20 | # config.action_dispatch.rack_cache = true 21 | 22 | # Disable Rails's static asset server (Apache or nginx will already do this). 23 | config.serve_static_assets = false 24 | 25 | # Compress JavaScripts and CSS. 26 | config.assets.js_compressor = :uglifier 27 | # config.assets.css_compressor = :sass 28 | 29 | # Do not fallback to assets pipeline if a precompiled asset is missed. 30 | config.assets.compile = false 31 | 32 | # Generate digests for assets URLs. 33 | config.assets.digest = true 34 | 35 | # Version of your assets, change this if you want to expire all your assets. 36 | config.assets.version = '1.0' 37 | 38 | # Specifies the header that your server uses for sending files. 39 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 40 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 41 | 42 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 43 | # config.force_ssl = true 44 | 45 | # Set to :debug to see everything in the log. 46 | config.log_level = :info 47 | 48 | # Prepend all log lines with the following tags. 49 | # config.log_tags = [ :subdomain, :uuid ] 50 | 51 | # Use a different logger for distributed setups. 52 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 53 | 54 | # Use a different cache store in production. 55 | # config.cache_store = :mem_cache_store 56 | 57 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 58 | # config.action_controller.asset_host = "http://assets.example.com" 59 | 60 | # Precompile additional assets. 61 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 62 | # config.assets.precompile += %w( search.js ) 63 | 64 | # Ignore bad email addresses and do not raise email delivery errors. 65 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 66 | # config.action_mailer.raise_delivery_errors = false 67 | 68 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 69 | # the I18n.default_locale when a translation can not be found). 70 | config.i18n.fallbacks = true 71 | 72 | # Send deprecation notices to registered listeners. 73 | config.active_support.deprecation = :notify 74 | 75 | # Disable automatic flushing of the log to improve performance. 76 | # config.autoflush_log = false 77 | 78 | # Use default logging formatter so that PID and timestamp are not suppressed. 79 | config.log_formatter = ::Logger::Formatter.new 80 | end 81 | -------------------------------------------------------------------------------- /spec/dummy_app/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | App::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static asset server for tests with Cache-Control for performance. 16 | config.serve_static_assets = true 17 | config.static_cache_control = "public, max-age=3600" 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Print deprecation notices to the stderr. 35 | config.active_support.deprecation = :stderr 36 | end 37 | -------------------------------------------------------------------------------- /spec/dummy_app/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /spec/dummy_app/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /spec/dummy_app/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /spec/dummy_app/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /spec/dummy_app/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure your secret_key_base is kept private 11 | # if you're sharing your code publicly. 12 | App::Application.config.secret_key_base = '27b186570a6fd4b0fbbb16c50a61dc99cb695db28beadceed09ee5717dbb1cd1b75042e548cc595fd1a2465a5313006f54f27b169856fb4db1db0ff50c9ae756' 13 | -------------------------------------------------------------------------------- /spec/dummy_app/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | App::Application.config.session_store :cookie_store, key: '_app_session' 4 | -------------------------------------------------------------------------------- /spec/dummy_app/config/initializers/sorcery.rb: -------------------------------------------------------------------------------- 1 | # The first thing you need to configure is which modules you need in your app. 2 | # The default is nothing which will include only core features (password encryption, login/logout). 3 | # Available submodules are: :user_activation, :http_basic_auth, :remember_me, 4 | # :reset_password, :session_timeout, :brute_force_protection, :activity_logging, :external 5 | Rails.application.config.sorcery.submodules = [:reset_password] 6 | 7 | # Here you can configure each submodule's features. 8 | Rails.application.config.sorcery.configure do |config| 9 | # -- core -- 10 | # What controller action to call for non-authenticated users. You can also 11 | # override the 'not_authenticated' method of course. 12 | # Default: `:not_authenticated` 13 | # 14 | # config.not_authenticated_action = 15 | 16 | 17 | # When a non logged in user tries to enter a page that requires login, save 18 | # the URL he wanted to reach, and send him there after login, using 'redirect_back_or_to'. 19 | # Default: `true` 20 | # 21 | # config.save_return_to_url = 22 | 23 | 24 | # Set domain option for cookies; Useful for remember_me submodule. 25 | # Default: `nil` 26 | # 27 | # config.cookie_domain = 28 | 29 | 30 | # -- session timeout -- 31 | # How long in seconds to keep the session alive. 32 | # Default: `3600` 33 | # 34 | # config.session_timeout = 35 | 36 | 37 | # Use the last action as the beginning of session timeout. 38 | # Default: `false` 39 | # 40 | # config.session_timeout_from_last_action = 41 | 42 | 43 | # -- http_basic_auth -- 44 | # What realm to display for which controller name. For example {"My App" => "Application"} 45 | # Default: `{"application" => "Application"}` 46 | # 47 | # config.controller_to_realm_map = 48 | 49 | 50 | # -- activity logging -- 51 | # will register the time of last user login, every login. 52 | # Default: `true` 53 | # 54 | # config.register_login_time = 55 | 56 | 57 | # will register the time of last user logout, every logout. 58 | # Default: `true` 59 | # 60 | # config.register_logout_time = 61 | 62 | 63 | # will register the time of last user action, every action. 64 | # Default: `true` 65 | # 66 | # config.register_last_activity_time = 67 | 68 | 69 | # -- external -- 70 | # What providers are supported by this app, i.e. [:twitter, :facebook, :github, :linkedin, :xing, :google, :liveid] . 71 | # Default: `[]` 72 | # 73 | # config.external_providers = 74 | 75 | 76 | # You can change it by your local ca_file. i.e. '/etc/pki/tls/certs/ca-bundle.crt' 77 | # Path to ca_file. By default use a internal ca-bundle.crt. 78 | # Default: `'path/to/ca_file'` 79 | # 80 | # config.ca_file = 81 | 82 | 83 | # For information about LinkedIn API: 84 | # - user info fields go to https://developer.linkedin.com/documents/profile-fields 85 | # - access permissions go to https://developer.linkedin.com/documents/authentication#granting 86 | # 87 | # config.linkedin.key = "" 88 | # config.linkedin.secret = "" 89 | # config.linkedin.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=linkedin" 90 | # config.linkedin.user_info_fields = ['first-name', 'last-name'] 91 | # config.linkedin.user_info_mapping = {first_name: "firstName", last_name: "lastName"} 92 | # config.linkedin.access_permissions = ['r_basicprofile'] 93 | # 94 | # 95 | # For information about XING API: 96 | # - user info fields go to https://dev.xing.com/docs/get/users/me 97 | # 98 | # config.xing.key = "" 99 | # config.xing.secret = "" 100 | # config.xing.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=xing" 101 | # config.xing.user_info_mapping = {first_name: "first_name", last_name: "last_name"} 102 | # 103 | # 104 | # Twitter wil not accept any requests nor redirect uri containing localhost, 105 | # make sure you use 0.0.0.0:3000 to access your app in development 106 | # 107 | # config.twitter.key = "" 108 | # config.twitter.secret = "" 109 | # config.twitter.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=twitter" 110 | # config.twitter.user_info_mapping = {:email => "screen_name"} 111 | # 112 | # config.facebook.key = "" 113 | # config.facebook.secret = "" 114 | # config.facebook.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=facebook" 115 | # config.facebook.user_info_mapping = {:email => "name"} 116 | # config.facebook.access_permissions = ["email", "publish_stream"] 117 | # 118 | # config.github.key = "" 119 | # config.github.secret = "" 120 | # config.github.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=github" 121 | # config.github.user_info_mapping = {:email => "name"} 122 | # 123 | # config.google.key = "" 124 | # config.google.secret = "" 125 | # config.google.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=google" 126 | # config.google.user_info_mapping = {:email => "email", :username => "name"} 127 | # 128 | # config.vk.key = "" 129 | # config.vk.secret = "" 130 | # config.vk.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=vk" 131 | # config.vk.user_info_mapping = {:login => "domain", :name => "full_name"} 132 | # 133 | # To use liveid in development mode you have to replace mydomain.com with 134 | # a valid domain even in development. To use a valid domain in development 135 | # simply add your domain in your /etc/hosts file in front of 127.0.0.1 136 | # 137 | # config.liveid.key = "" 138 | # config.liveid.secret = "" 139 | # config.liveid.callback_url = "http://mydomain.com:3000/oauth/callback?provider=liveid" 140 | # config.liveid.user_info_mapping = {:username => "name"} 141 | 142 | 143 | # --- user config --- 144 | config.user_config do |user| 145 | # -- core -- 146 | # specify username attributes, for example: [:username, :email]. 147 | # Default: `[:username]` 148 | # 149 | # user.username_attribute_names = 150 | 151 | 152 | # change *virtual* password attribute, the one which is used until an encrypted one is generated. 153 | # Default: `:password` 154 | # 155 | # user.password_attribute_name = 156 | 157 | 158 | # downcase the username before trying to authenticate, default is false 159 | # Default: `false` 160 | # 161 | # user.downcase_username_before_authenticating = 162 | 163 | 164 | # change default email attribute. 165 | # Default: `:email` 166 | # 167 | # user.email_attribute_name = 168 | 169 | 170 | # change default crypted_password attribute. 171 | # Default: `:crypted_password` 172 | # 173 | # user.crypted_password_attribute_name = 174 | 175 | 176 | # what pattern to use to join the password with the salt 177 | # Default: `""` 178 | # 179 | # user.salt_join_token = 180 | 181 | 182 | # change default salt attribute. 183 | # Default: `:salt` 184 | # 185 | # user.salt_attribute_name = 186 | 187 | 188 | # how many times to apply encryption to the password. 189 | # Default: `nil` 190 | # 191 | # user.stretches = 192 | 193 | 194 | # encryption key used to encrypt reversible encryptions such as AES256. 195 | # WARNING: If used for users' passwords, changing this key will leave passwords undecryptable! 196 | # Default: `nil` 197 | # 198 | # user.encryption_key = 199 | 200 | 201 | # use an external encryption class. 202 | # Default: `nil` 203 | # 204 | # user.custom_encryption_provider = 205 | 206 | 207 | # encryption algorithm name. See 'encryption_algorithm=' for available options. 208 | # Default: `:bcrypt` 209 | # 210 | # user.encryption_algorithm = 211 | 212 | 213 | # make this configuration inheritable for subclasses. Useful for ActiveRecord's STI. 214 | # Default: `false` 215 | # 216 | # user.subclasses_inherit_config = 217 | 218 | 219 | # -- remember_me -- 220 | # allow the remember_me cookie to settable through AJAX 221 | # Default: `true` 222 | # 223 | # user.remember_me_httponly = 224 | 225 | # How long in seconds the session length will be 226 | # Default: `604800` 227 | # 228 | # user.remember_me_for = 229 | 230 | 231 | # -- user_activation -- 232 | # the attribute name to hold activation state (active/pending). 233 | # Default: `:activation_state` 234 | # 235 | # user.activation_state_attribute_name = 236 | 237 | 238 | # the attribute name to hold activation code (sent by email). 239 | # Default: `:activation_token` 240 | # 241 | # user.activation_token_attribute_name = 242 | 243 | 244 | # the attribute name to hold activation code expiration date. 245 | # Default: `:activation_token_expires_at` 246 | # 247 | # user.activation_token_expires_at_attribute_name = 248 | 249 | 250 | # how many seconds before the activation code expires. nil for never expires. 251 | # Default: `nil` 252 | # 253 | # user.activation_token_expiration_period = 254 | 255 | 256 | # your mailer class. Required. 257 | # Default: `nil` 258 | # 259 | # user.user_activation_mailer = 260 | 261 | 262 | # when true sorcery will not automatically 263 | # email activation details and allow you to 264 | # manually handle how and when email is sent. 265 | # Default: `false` 266 | # 267 | # user.activation_mailer_disabled = 268 | 269 | 270 | # activation needed email method on your mailer class. 271 | # Default: `:activation_needed_email` 272 | # 273 | # user.activation_needed_email_method_name = 274 | 275 | 276 | # activation success email method on your mailer class. 277 | # Default: `:activation_success_email` 278 | # 279 | # user.activation_success_email_method_name = 280 | 281 | 282 | # do you want to prevent or allow users that did not activate by email to login? 283 | # Default: `true` 284 | # 285 | # user.prevent_non_active_users_to_login = 286 | 287 | 288 | # -- reset_password -- 289 | # reset password code attribute name. 290 | # Default: `:reset_password_token` 291 | # 292 | # user.reset_password_token_attribute_name = 293 | 294 | 295 | # expires at attribute name. 296 | # Default: `:reset_password_token_expires_at` 297 | # 298 | # user.reset_password_token_expires_at_attribute_name = 299 | 300 | 301 | # when was email sent, used for hammering protection. 302 | # Default: `:reset_password_email_sent_at` 303 | # 304 | # user.reset_password_email_sent_at_attribute_name = 305 | 306 | 307 | # mailer class. Needed. 308 | # Default: `nil` 309 | # 310 | # user.reset_password_mailer = ResetPasswordMailer 311 | 312 | 313 | # reset password email method on your mailer class. 314 | # Default: `:reset_password_email` 315 | # 316 | # user.reset_password_email_method_name = 317 | 318 | 319 | # when true sorcery will not automatically 320 | # email password reset details and allow you to 321 | # manually handle how and when email is sent 322 | # Default: `false` 323 | # 324 | user.reset_password_mailer_disabled = true 325 | 326 | 327 | # how many seconds before the reset request expires. nil for never expires. 328 | # Default: `nil` 329 | # 330 | # user.reset_password_expiration_period = 331 | 332 | 333 | # hammering protection, how long to wait before allowing another email to be sent. 334 | # Default: `5 * 60` 335 | # 336 | # user.reset_password_time_between_emails = 337 | 338 | 339 | # -- brute_force_protection -- 340 | # Failed logins attribute name. 341 | # Default: `:failed_logins_count` 342 | # 343 | # user.failed_logins_count_attribute_name = 344 | 345 | 346 | # This field indicates whether user is banned and when it will be active again. 347 | # Default: `:lock_expires_at` 348 | # 349 | # user.lock_expires_at_attribute_name = 350 | 351 | 352 | # How many failed logins allowed. 353 | # Default: `50` 354 | # 355 | # user.consecutive_login_retries_amount_limit = 356 | 357 | 358 | # How long the user should be banned. in seconds. 0 for permanent. 359 | # Default: `60 * 60` 360 | # 361 | # user.login_lock_time_period = 362 | 363 | # Unlock token attribute name 364 | # Default: `:unlock_token` 365 | # 366 | # user.unlock_token_attribute_name = 367 | 368 | # Unlock token mailer method 369 | # Default: `:send_unlock_token_email` 370 | # 371 | # user.unlock_token_email_method_name = 372 | 373 | # when true sorcery will not automatically 374 | # send email with unlock token 375 | # Default: `false` 376 | # 377 | # user.unlock_token_mailer_disabled = true 378 | 379 | # Unlock token mailer class 380 | # Default: `nil` 381 | # 382 | # user.unlock_token_mailer = UserMailer 383 | 384 | # -- activity logging -- 385 | # Last login attribute name. 386 | # Default: `:last_login_at` 387 | # 388 | # user.last_login_at_attribute_name = 389 | 390 | 391 | # Last logout attribute name. 392 | # Default: `:last_logout_at` 393 | # 394 | # user.last_logout_at_attribute_name = 395 | 396 | 397 | # Last activity attribute name. 398 | # Default: `:last_activity_at` 399 | # 400 | # user.last_activity_at_attribute_name = 401 | 402 | 403 | # How long since last activity is he user defined logged out? 404 | # Default: `10 * 60` 405 | # 406 | # user.activity_timeout = 407 | 408 | 409 | # -- external -- 410 | # Class which holds the various external provider data for this user. 411 | # Default: `nil` 412 | # 413 | # user.authentications_class = 414 | 415 | 416 | # User's identifier in authentications class. 417 | # Default: `:user_id` 418 | # 419 | # user.authentications_user_id_attribute_name = 420 | 421 | 422 | # Provider's identifier in authentications class. 423 | # Default: `:provider` 424 | # 425 | # user.provider_attribute_name = 426 | 427 | 428 | # User's external unique identifier in authentications class. 429 | # Default: `:uid` 430 | # 431 | # user.provider_uid_attribute_name = 432 | end 433 | 434 | # This line must come after the 'user config' block. 435 | # Define which model authenticates with sorcery. 436 | config.user_class = "User" 437 | end 438 | -------------------------------------------------------------------------------- /spec/dummy_app/config/initializers/the_comments.rb: -------------------------------------------------------------------------------- 1 | # TheComments.config.param_name => value 2 | 3 | TheComments.configure do |config| 4 | config.max_reply_depth = 3 # comments tree depth 5 | config.tolerance_time = 5 # sec - after this delay user can post a comment 6 | config.default_state = :draft # default state for comment 7 | config.default_owner_state = :published # default state for comment for Moderator 8 | config.empty_inputs = [:commentBody] # array of spam trap fields 9 | config.default_title = 'Undefined title' # default commentable_title for denormalization 10 | config.template_engine = :haml 11 | 12 | config.empty_trap_protection = true 13 | config.tolerance_time_protection = true 14 | end 15 | -------------------------------------------------------------------------------- /spec/dummy_app/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /spec/dummy_app/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /spec/dummy_app/config/routes.rb: -------------------------------------------------------------------------------- 1 | App::Application.routes.draw do 2 | root 'posts#index' 3 | 4 | get "autologin/:id" => "users#autologin", as: :autologin 5 | 6 | # Login system 7 | get "login" => "sessions#new", as: :login 8 | delete "logout" => "sessions#destroy", as: :logout 9 | get "signup" => "users#new", as: :signup 10 | post "sessions" => "sessions#create", as: :sessions 11 | 12 | resources :posts 13 | resources :users 14 | 15 | # TheComments routes 16 | concern :user_comments, TheComments::UserRoutes.new 17 | concern :admin_comments, TheComments::AdminRoutes.new 18 | resources :comments, concerns: [:user_comments, :admin_comments] 19 | end 20 | -------------------------------------------------------------------------------- /spec/dummy_app/db/migrate/20130712061503_sorcery_core.rb: -------------------------------------------------------------------------------- 1 | class SorceryCore < ActiveRecord::Migration 2 | def self.up 3 | create_table :users do |t| 4 | t.string :username, :null => false # if you use another field as a username, for example email, you can safely remove this field. 5 | t.string :email, :default => nil # if you use this field as a username, you might want to make it :null => false. 6 | t.string :crypted_password, :default => nil 7 | t.string :salt, :default => nil 8 | 9 | t.timestamps 10 | end 11 | end 12 | 13 | def self.down 14 | drop_table :users 15 | end 16 | end -------------------------------------------------------------------------------- /spec/dummy_app/db/migrate/20130712065951_create_posts.rb: -------------------------------------------------------------------------------- 1 | class CreatePosts < ActiveRecord::Migration 2 | def change 3 | create_table :posts do |t| 4 | t.integer :user_id 5 | t.string :title 6 | t.text :content 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/dummy_app/db/migrate/20131027185332_change_user.the_comments_engine.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from the_comments_engine (originally 20130101010101) 2 | # null: false => de-facto db-level validation 3 | class ChangeUser < ActiveRecord::Migration 4 | def change 5 | change_table :users do |t| 6 | # "Written by me" (cache counters) 7 | t.integer :my_draft_comments_count, default: 0 8 | t.integer :my_published_comments_count, default: 0 9 | t.integer :my_comments_count, default: 0 # my_draft_comments_count + my_published_comments_count 10 | 11 | # commentable's comments => comcoms (cache counters) 12 | # Relation through Comment#holder_id field 13 | t.integer :draft_comcoms_count, default: 0 14 | t.integer :published_comcoms_count, default: 0 15 | t.integer :deleted_comcoms_count, default: 0 16 | t.integer :spam_comcoms_count, default: 0 17 | end 18 | end 19 | end -------------------------------------------------------------------------------- /spec/dummy_app/db/migrate/20131027185333_create_comments.the_comments_engine.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from the_comments_engine (originally 20130101010102) 2 | class CreateComments < ActiveRecord::Migration 3 | def change 4 | create_table :comments do |t| 5 | # relations 6 | t.integer :user_id 7 | t.integer :holder_id 8 | 9 | # polymorphic, commentable object 10 | t.integer :commentable_id 11 | t.string :commentable_type 12 | 13 | # denormalization 14 | t.string :commentable_url 15 | t.string :commentable_title 16 | t.string :commentable_state 17 | 18 | # comment 19 | t.string :anchor 20 | 21 | t.string :title 22 | t.string :contacts 23 | 24 | t.text :raw_content 25 | t.text :content 26 | 27 | # moderation token 28 | t.string :view_token 29 | 30 | # state machine => :draft | :published | :deleted 31 | t.string :state, default: :draft 32 | 33 | # base user data (BanHammer power) 34 | t.string :ip, default: :undefined 35 | t.string :referer, default: :undefined 36 | t.string :user_agent, default: :undefined 37 | t.integer :tolerance_time 38 | 39 | # unusable: for future versions 40 | t.boolean :spam, default: false 41 | 42 | # nested set 43 | t.integer :parent_id 44 | t.integer :lft 45 | t.integer :rgt 46 | t.integer :depth, default: 0 47 | 48 | t.timestamps 49 | end 50 | end 51 | end -------------------------------------------------------------------------------- /spec/dummy_app/db/migrate/20131027185334_change_commentable.the_comments_engine.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from the_comments_engine (originally 20130101010103) 2 | class ChangeCommentable < ActiveRecord::Migration 3 | def change 4 | # Uncomment this. Add fields to Commentable Models 5 | # 6 | [:users, :posts].each do |table_name| 7 | change_table table_name do |t| 8 | t.integer :draft_comments_count, default: 0 9 | t.integer :published_comments_count, default: 0 10 | t.integer :deleted_comments_count, default: 0 11 | end 12 | end 13 | end 14 | end -------------------------------------------------------------------------------- /spec/dummy_app/db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended that you check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(version: 20131027185334) do 15 | 16 | create_table "comments", force: true do |t| 17 | t.integer "user_id" 18 | t.integer "holder_id" 19 | t.integer "commentable_id" 20 | t.string "commentable_type" 21 | t.string "commentable_url" 22 | t.string "commentable_title" 23 | t.string "commentable_state" 24 | t.string "anchor" 25 | t.string "title" 26 | t.string "contacts" 27 | t.text "raw_content" 28 | t.text "content" 29 | t.string "view_token" 30 | t.string "state", default: "draft" 31 | t.string "ip", default: "undefined" 32 | t.string "referer", default: "undefined" 33 | t.string "user_agent", default: "undefined" 34 | t.integer "tolerance_time" 35 | t.boolean "spam", default: false 36 | t.integer "parent_id" 37 | t.integer "lft" 38 | t.integer "rgt" 39 | t.integer "depth", default: 0 40 | t.datetime "created_at" 41 | t.datetime "updated_at" 42 | end 43 | 44 | create_table "posts", force: true do |t| 45 | t.integer "user_id" 46 | t.string "title" 47 | t.text "content" 48 | t.datetime "created_at" 49 | t.datetime "updated_at" 50 | t.integer "draft_comments_count", default: 0 51 | t.integer "published_comments_count", default: 0 52 | t.integer "deleted_comments_count", default: 0 53 | end 54 | 55 | create_table "users", force: true do |t| 56 | t.string "username", null: false 57 | t.string "email" 58 | t.string "crypted_password" 59 | t.string "salt" 60 | t.datetime "created_at" 61 | t.datetime "updated_at" 62 | t.integer "my_draft_comments_count", default: 0 63 | t.integer "my_published_comments_count", default: 0 64 | t.integer "my_comments_count", default: 0 65 | t.integer "draft_comcoms_count", default: 0 66 | t.integer "published_comcoms_count", default: 0 67 | t.integer "deleted_comcoms_count", default: 0 68 | t.integer "spam_comcoms_count", default: 0 69 | t.integer "draft_comments_count", default: 0 70 | t.integer "published_comments_count", default: 0 71 | t.integer "deleted_comments_count", default: 0 72 | end 73 | 74 | end 75 | -------------------------------------------------------------------------------- /spec/dummy_app/db/seeds.rb: -------------------------------------------------------------------------------- 1 | puts 'Users' 2 | 3 | 3.times do 4 | User.create!(username: Faker::Name.name) 5 | print '.' 6 | end 7 | 8 | puts 9 | puts 'Posts' 10 | 11 | User.all.each do |user| 12 | 3.times do 13 | user.posts.create!( 14 | title: Faker::Lorem.sentence, 15 | content: Faker::Lorem.paragraphs(3).join 16 | ) 17 | print '.' 18 | end 19 | end 20 | 21 | puts 22 | puts 'Comments' 23 | 24 | users = User.all 25 | posts = Post.all 26 | 27 | 20.times do 28 | user = users.sample 29 | post = posts.sample 30 | 31 | Comment.create!( 32 | user: user, 33 | commentable: post, 34 | title: Faker::Lorem.sentence, 35 | raw_content: Faker::Lorem.paragraphs(3).join, 36 | state: :published 37 | ) 38 | 39 | print '.' 40 | end 41 | 42 | puts -------------------------------------------------------------------------------- /spec/dummy_app/lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-trash/the_comments/5992b861f88100ed49058afad623b0d744fcc055/spec/dummy_app/lib/assets/.keep -------------------------------------------------------------------------------- /spec/dummy_app/lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-trash/the_comments/5992b861f88100ed49058afad623b0d744fcc055/spec/dummy_app/lib/tasks/.keep -------------------------------------------------------------------------------- /spec/dummy_app/lib/tasks/app_bootstrap.rake: -------------------------------------------------------------------------------- 1 | namespace :db do 2 | desc "DB: drop, create, migrate" 3 | task bootstrap: :environment do 4 | Rake::Task["db:drop"].invoke 5 | Rake::Task["db:create"].invoke 6 | Rake::Task["db:migrate"].invoke 7 | end 8 | 9 | # rake db:bootstrap_and_seed 10 | desc "Reset DB and seed" 11 | task bootstrap_and_seed: :environment do 12 | Rake::Task["db:bootstrap"].invoke 13 | Rake::Task["db:seed"].invoke 14 | end 15 | end -------------------------------------------------------------------------------- /spec/dummy_app/log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-trash/the_comments/5992b861f88100ed49058afad623b0d744fcc055/spec/dummy_app/log/.keep -------------------------------------------------------------------------------- /spec/dummy_app/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 48 | 49 | 50 | 51 | 52 |
    53 |

    The page you were looking for doesn't exist.

    54 |

    You may have mistyped the address or the page may have moved.

    55 |
    56 |

    If you are the application owner check the logs for more information.

    57 | 58 | 59 | -------------------------------------------------------------------------------- /spec/dummy_app/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 48 | 49 | 50 | 51 | 52 |
    53 |

    The change you wanted was rejected.

    54 |

    Maybe you tried to change something you didn't have access to.

    55 |
    56 |

    If you are the application owner check the logs for more information.

    57 | 58 | 59 | -------------------------------------------------------------------------------- /spec/dummy_app/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 48 | 49 | 50 | 51 | 52 |
    53 |

    We're sorry, but something went wrong.

    54 |
    55 |

    If you are the application owner check the logs for more information.

    56 | 57 | 58 | -------------------------------------------------------------------------------- /spec/dummy_app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-trash/the_comments/5992b861f88100ed49058afad623b0d744fcc055/spec/dummy_app/public/favicon.ico -------------------------------------------------------------------------------- /spec/dummy_app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /spec/dummy_app/spec/factories/post.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :post, class: Post do 3 | sequence(:title) { Faker::Lorem.sentence } 4 | sequence(:content) { Faker::Lorem.sentence } 5 | end 6 | end -------------------------------------------------------------------------------- /spec/dummy_app/spec/factories/user.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :user, class: User do 3 | sequence(:username) { Faker::Name.name } 4 | sequence(:email) { Faker::Internet.email } 5 | end 6 | end -------------------------------------------------------------------------------- /spec/dummy_app/spec/models/user_counters_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | def destroy_all 4 | User.destroy_all 5 | 3.times{ begin; Comment.destroy_all; rescue; end; } 6 | end 7 | 8 | # -------------------------------------- 9 | # Helpers 10 | # -------------------------------------- 11 | def my_comments_count_assert user, count 12 | user.my_comments.active.count.should eq count 13 | user.my_comments_count.should eq count 14 | end 15 | 16 | def comments_count_assert(obj, values) 17 | obj.comments.with_state(:draft).count.should eq values[0] 18 | obj.comments.with_state(:published).count.should eq values[1] 19 | obj.comments.with_state(:deleted).count.should eq values[2] 20 | end 21 | 22 | def comments_counters_assert(obj, values) 23 | obj.draft_comments_count.should eq values[0] 24 | obj.published_comments_count.should eq values[1] 25 | obj.deleted_comments_count.should eq values[2] 26 | end 27 | 28 | def comcoms_count_assert(obj, values) 29 | obj.comcoms.with_state(:draft).count.should eq values[0] 30 | obj.comcoms.with_state(:published).count.should eq values[1] 31 | obj.comcoms.with_state(:deleted).count.should eq values[2] 32 | end 33 | 34 | def comcoms_counters_assert(obj, values) 35 | obj.draft_comcoms_count.should eq values[0] 36 | obj.published_comcoms_count.should eq values[1] 37 | obj.deleted_comcoms_count.should eq values[2] 38 | end 39 | 40 | # -------------------------------------- 41 | # init functions 42 | # -------------------------------------- 43 | def create_users_and_post 44 | @user = FactoryGirl.create(:user) 45 | @post_holder = FactoryGirl.create(:user) 46 | @post = FactoryGirl.create(:post, user: @post_holder) 47 | end 48 | 49 | def base_test_situation 50 | create_users_and_post 51 | 52 | 3.times do 53 | @comment = Comment.create!( 54 | user: @user, 55 | commentable: @post, 56 | title: Faker::Lorem.sentence, 57 | raw_content: Faker::Lorem.paragraphs(3).join 58 | ) 59 | end 60 | 61 | @user.reload 62 | @post_holder.reload 63 | end 64 | 65 | def tree_test_situation 66 | create_users_and_post 67 | 68 | # LEVEL 1 69 | 3.times do 70 | Comment.create!( 71 | user: @user, 72 | commentable: @post, 73 | title: Faker::Lorem.sentence, 74 | raw_content: Faker::Lorem.paragraphs(3).join, 75 | state: :published 76 | ) 77 | end 78 | # LEVEL 2 79 | parent_comment = Comment.first 80 | 3.times do 81 | Comment.create!( 82 | user: @user, 83 | commentable: @post, 84 | parent_id: parent_comment.id, 85 | title: Faker::Lorem.sentence, 86 | raw_content: Faker::Lorem.paragraphs(3).join, 87 | state: :published 88 | ) 89 | end 90 | # LEVEL 3 91 | parent_comment = Comment.first.children.first 92 | 3.times do 93 | Comment.create!( 94 | user: @user, 95 | commentable: @post, 96 | parent_id: parent_comment.id, 97 | title: Faker::Lorem.sentence, 98 | raw_content: Faker::Lorem.paragraphs(3).join, 99 | state: :published 100 | ) 101 | end 102 | 103 | @user.reload 104 | @post_holder.reload 105 | end 106 | # -------------------------------------- 107 | # ~ init functions 108 | # -------------------------------------- 109 | 110 | describe User do 111 | context 'User leave comment to the post' do 112 | after(:all){ destroy_all } 113 | 114 | before(:all){ create_users_and_post } 115 | 116 | it 'should be User' do 117 | @user.should be_instance_of User 118 | @post_holder.should be_instance_of User 119 | end 120 | 121 | it 'Post should has owner' do 122 | @post.user.should eq @post_holder 123 | end 124 | 125 | it 'should be Post' do 126 | @post.should be_instance_of Post 127 | end 128 | 129 | it 'Create new comment' do 130 | @comment = Comment.create!( 131 | user: @user, 132 | commentable: @post, 133 | title: Faker::Lorem.sentence, 134 | raw_content: Faker::Lorem.paragraphs(3).join 135 | ) 136 | 137 | @comment.user.should eq @user 138 | @comment.holder.should eq @post_holder 139 | end 140 | end 141 | 142 | context "Written by me counters" do 143 | after(:all) { destroy_all } 144 | before(:all){ create_users_and_post } 145 | it 'should has correct My counters values' do 146 | @comment = Comment.create!( 147 | user: @user, 148 | commentable: @post, 149 | title: Faker::Lorem.sentence, 150 | raw_content: Faker::Lorem.paragraphs(3).join 151 | ) 152 | @user.reload 153 | @user.my_comments_count.should eq 1 154 | @user.my_draft_comments_count.should eq 1 155 | @user.my_published_comments_count.should eq 0 156 | end 157 | end 158 | 159 | context "Commentable Denormalization" do 160 | after(:all) { destroy_all } 161 | before(:all) do 162 | base_test_situation 163 | end 164 | 165 | it 'should have denormalized fields' do 166 | title = "New Title!" 167 | @post.update_attribute(:title, title) 168 | @post.title.should eq title 169 | @comment = @post.comments.first 170 | @comment.commentable_title.should eq title 171 | end 172 | end 173 | 174 | context 'User leave 3 comments and Instances has expectable counter values' do 175 | after(:all) { destroy_all } 176 | 177 | before(:all){ base_test_situation } 178 | 179 | describe 'User counters' do 180 | it 'my_comments counter' do 181 | my_comments_count_assert(@user, 3) 182 | end 183 | 184 | it 'Comcoms counters' do 185 | @user.comcoms.count.should eq 0 186 | 187 | comcoms_counters_assert(@user, [0,0,0]) 188 | end 189 | end 190 | 191 | describe 'Holder counters' do 192 | it 'my_comments counter' do 193 | my_comments_count_assert(@post_holder, 0) 194 | end 195 | 196 | it 'Comcoms counters' do 197 | @post_holder.comcoms.count.should eq 3 198 | comcoms_counters_assert(@post_holder, [3,0,0]) 199 | end 200 | end 201 | end 202 | 203 | context 'User leave 3 comments, 1 Comment DRAFT => PUBLISHED' do 204 | after(:all) { destroy_all } 205 | 206 | before(:all) do 207 | base_test_situation 208 | @comment = Comment.first 209 | @comment.to_published 210 | 211 | @user.reload 212 | @post.reload 213 | @comment.reload 214 | @post_holder.reload 215 | end 216 | 217 | describe 'User counters' do 218 | it 'has expectable values' do 219 | comments_counters_assert @user, [0,0,0] 220 | comments_count_assert @user, [0,0,0] 221 | comcoms_counters_assert @user, [0,0,0] 222 | comcoms_count_assert @user, [0,0,0] 223 | my_comments_count_assert @user, 3 224 | end 225 | end 226 | 227 | describe 'Holder counters' do 228 | it 'has expectable values' do 229 | @post_holder.comcoms.count.should eq 3 230 | 231 | my_comments_count_assert(@post_holder, 0) 232 | comcoms_count_assert @post_holder, [2,1,0] 233 | comcoms_counters_assert @post_holder, [2,1,0] 234 | end 235 | end 236 | end 237 | 238 | context '3 comments, 1 Comment DRAFT => DELETE' do 239 | after(:all) { destroy_all } 240 | 241 | before(:all) do 242 | base_test_situation 243 | @comment = Comment.first 244 | @comment.to_published 245 | @comment.to_deleted 246 | 247 | @user.reload 248 | @post.reload 249 | @comment.reload 250 | @post_holder.reload 251 | end 252 | 253 | it 'has correct counters values' do 254 | comments_counters_assert @user, [0,0,0] 255 | comments_count_assert @user, [0,0,0] 256 | comcoms_count_assert @user, [0,0,0] 257 | my_comments_count_assert @user, 2 258 | @user.my_comments.count.should eq 3 259 | 260 | comments_counters_assert @post_holder, [0,0,0] 261 | comments_count_assert @post_holder, [0,0,0] 262 | comcoms_count_assert @post_holder, [2,0,1] 263 | my_comments_count_assert @post_holder, 0 264 | @post_holder.my_comments.count.should eq 0 265 | 266 | comments_count_assert @post, [2,0,1] 267 | comments_counters_assert @post, [2,0,1] 268 | end 269 | end 270 | 271 | context 'Comments tree, 1 branch to deleted' do 272 | after(:each) { destroy_all } 273 | before(:each){ tree_test_situation } 274 | 275 | it 'has correct counters values before deleting' do 276 | Comment.count.should eq 9 277 | 278 | my_comments_count_assert @user, 9 279 | comments_count_assert @user, [0,0,0] 280 | 281 | my_comments_count_assert @post_holder, 0 282 | comments_count_assert @post_holder, [0,0,0] 283 | 284 | comcoms_count_assert @post_holder, [0,9,0] 285 | comcoms_counters_assert @post_holder, [0,9,0] 286 | 287 | comments_count_assert @post, [0,9,0] 288 | comments_counters_assert @post, [0,9,0] 289 | end 290 | 291 | it 'has correct counters values after branch deleting' do 292 | @comment = Comment.first.children.first 293 | @comment.to_deleted 294 | 295 | @user.reload 296 | @post.reload 297 | @post_holder.reload 298 | 299 | Comment.with_state(:published).count.should eq 5 300 | Comment.with_state(:deleted).count.should eq 4 301 | 302 | my_comments_count_assert @user, 5 303 | comments_count_assert @user, [0,0,0] 304 | 305 | my_comments_count_assert @post_holder, 0 306 | comments_count_assert @post_holder, [0,0,0] 307 | 308 | comcoms_count_assert @post_holder, [0,5,4] 309 | comcoms_counters_assert @post_holder, [0,5,4] 310 | 311 | comments_count_assert @post, [0,5,4] 312 | comments_counters_assert @post, [0,5,4] 313 | end 314 | 315 | # This code helps to me to catch logic bugs 316 | # there is no any asserts 317 | # context "callbacks catch" do 318 | # after(:all) { destroy_all } 319 | # it "before validation callback with increment! must be call manually" do 320 | # @user = FactoryGirl.create(:user) 321 | # @post_holder = FactoryGirl.create(:user) 322 | # @post = FactoryGirl.create(:post, user: @post_holder) 323 | 324 | # Comment.create!( 325 | # user: @user, 326 | # commentable: @post, 327 | # title: Faker::Lorem.sentence, 328 | # raw_content: Faker::Lorem.paragraphs(3).join, 329 | # state: :deleted 330 | # ) 331 | # Comment.first.to_published 332 | # end 333 | # end 334 | 335 | # it 'has correct counters after comment destroy' do 336 | # pending("has correct counters after comment destroy") 337 | # end 338 | end 339 | end 340 | -------------------------------------------------------------------------------- /spec/dummy_app/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] ||= 'test' 2 | require File.expand_path("../../config/environment", __FILE__) 3 | 4 | require 'rspec/rails' 5 | require 'rspec/autorun' 6 | 7 | Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } 8 | ActiveRecord::Migration.check_pending! if defined?(ActiveRecord::Migration) 9 | 10 | RSpec.configure do |config| 11 | config.infer_base_class_for_anonymous_controllers = false 12 | config.fixture_path = "#{::Rails.root}/spec/fixtures" 13 | config.use_transactional_fixtures = true 14 | config.order = "random" 15 | 16 | # DatabaseCleaner 17 | # config.before(:suite) do 18 | # DatabaseCleaner.strategy = :transaction 19 | # DatabaseCleaner.clean_with(:truncation) 20 | # end 21 | 22 | # config.before(:each) do 23 | # DatabaseCleaner.start 24 | # end 25 | 26 | # config.after(:each) do 27 | # DatabaseCleaner.clean 28 | # end 29 | end 30 | -------------------------------------------------------------------------------- /spec/dummy_app/test/controllers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-trash/the_comments/5992b861f88100ed49058afad623b0d744fcc055/spec/dummy_app/test/controllers/.keep -------------------------------------------------------------------------------- /spec/dummy_app/test/fixtures/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-trash/the_comments/5992b861f88100ed49058afad623b0d744fcc055/spec/dummy_app/test/fixtures/.keep -------------------------------------------------------------------------------- /spec/dummy_app/test/helpers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-trash/the_comments/5992b861f88100ed49058afad623b0d744fcc055/spec/dummy_app/test/helpers/.keep -------------------------------------------------------------------------------- /spec/dummy_app/test/integration/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-trash/the_comments/5992b861f88100ed49058afad623b0d744fcc055/spec/dummy_app/test/integration/.keep -------------------------------------------------------------------------------- /spec/dummy_app/test/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-trash/the_comments/5992b861f88100ed49058afad623b0d744fcc055/spec/dummy_app/test/mailers/.keep -------------------------------------------------------------------------------- /spec/dummy_app/test/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-trash/the_comments/5992b861f88100ed49058afad623b0d744fcc055/spec/dummy_app/test/models/.keep -------------------------------------------------------------------------------- /spec/dummy_app/test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] ||= "test" 2 | require File.expand_path('../../config/environment', __FILE__) 3 | require 'rails/test_help' 4 | 5 | class ActiveSupport::TestCase 6 | ActiveRecord::Migration.check_pending! 7 | 8 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 9 | # 10 | # Note: You'll currently still have to declare fixtures explicitly in integration tests 11 | # -- they do not yet inherit this setting 12 | fixtures :all 13 | 14 | # Add more helper methods to be used by all tests here... 15 | end 16 | -------------------------------------------------------------------------------- /spec/dummy_app/vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-trash/the_comments/5992b861f88100ed49058afad623b0d744fcc055/spec/dummy_app/vendor/assets/javascripts/.keep -------------------------------------------------------------------------------- /spec/dummy_app/vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-trash/the_comments/5992b861f88100ed49058afad623b0d744fcc055/spec/dummy_app/vendor/assets/stylesheets/.keep -------------------------------------------------------------------------------- /the_comments.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'the_comments/version' 5 | 6 | Gem::Specification.new do |gem| 7 | gem.name = "the_comments" 8 | gem.version = TheComments::VERSION 9 | gem.authors = ["Ilya N. Zykin"] 10 | gem.email = ["zykin-ilya@ya.ru"] 11 | gem.description = %q{ Comments with threading for Rails 4 } 12 | gem.summary = %q{ the_comments by the-teacher } 13 | gem.homepage = "https://github.com/the-teacher/the_comments" 14 | 15 | gem.files = `git ls-files`.split($/) 16 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 17 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 18 | gem.require_paths = ["lib"] 19 | 20 | gem.add_dependency 'state_machine', '~> 1.2.0' 21 | gem.add_dependency 'the_sortable_tree', '~> 2.5.0' 22 | gem.add_dependency 'the_simple_sort', '~> 0.0.2' 23 | 24 | # gem.add_dependency 'rails', '>= 4.0' 25 | end 26 | -------------------------------------------------------------------------------- /the_comments.yml.teamocil.example: -------------------------------------------------------------------------------- 1 | windows: 2 | - name: "TheComments" 3 | root: "~/rails/opensource/the_comments" 4 | filters: 5 | before: "rvm use ruby-2.0.0-p353; rvm gemset use the_comments --create" 6 | layout: tiled 7 | panes: 8 | - cmd: "clear;" 9 | - cmd: "clear;" 10 | - cmd: "cd spec/dummy_app; clear" 11 | - cmd: "cd spec/dummy_app; clear; rake db:bootstrap RAILS_ENV=test; rspec" -------------------------------------------------------------------------------- /views_converter.rb: -------------------------------------------------------------------------------- 1 | # gem install haml2slim --no-ri --no-rdoc 2 | # ruby views_converter.rb 3 | 4 | from = "./app/views/the_comments/haml" 5 | to = "./app/views/the_comments/slim" 6 | 7 | `haml2slim #{from} #{to} --trace` 8 | 9 | Dir.glob("#{to}/*.slim") do |slim_file| 10 | content = File.read slim_file 11 | content = content.gsub "haml", "slim" 12 | 13 | file = File.open slim_file, "w" 14 | file.write content 15 | file.close 16 | end 17 | --------------------------------------------------------------------------------