├── .github └── workflows │ └── tests.yml ├── .gitignore ├── Gemfile ├── MIT-LICENSE ├── README.md ├── Rakefile ├── app ├── assets │ ├── config │ │ └── commontator │ │ │ └── manifest.js │ ├── images │ │ └── commontator │ │ │ ├── downvote.png │ │ │ ├── downvote_active.png │ │ │ ├── downvote_disabled.png │ │ │ ├── downvote_hover.png │ │ │ ├── upvote.png │ │ │ ├── upvote_active.png │ │ │ ├── upvote_disabled.png │ │ │ └── upvote_hover.png │ ├── javascripts │ │ └── commontator │ │ │ ├── application.js │ │ │ └── mentions.js │ └── stylesheets │ │ └── commontator │ │ ├── application.scss │ │ └── comments.scss ├── controllers │ └── commontator │ │ ├── application_controller.rb │ │ ├── comments_controller.rb │ │ ├── security_transgression.rb │ │ ├── subscriptions_controller.rb │ │ └── threads_controller.rb ├── helpers │ └── commontator │ │ ├── application_helper.rb │ │ └── link_renderer.rb ├── mailers │ └── commontator │ │ └── subscriptions_mailer.rb ├── models │ └── commontator │ │ ├── collection.rb │ │ ├── comment.rb │ │ ├── json_array_coder.rb │ │ ├── subscription.rb │ │ └── thread.rb └── views │ └── commontator │ ├── comments │ ├── _body.html.erb │ ├── _form.html.erb │ ├── _list.html.erb │ ├── _show.html.erb │ ├── _show.js.erb │ ├── _votes.html.erb │ ├── cancel.js.erb │ ├── create.js.erb │ ├── delete.js.erb │ ├── edit.js.erb │ ├── new.js.erb │ ├── show.js.erb │ ├── update.js.erb │ └── vote.js.erb │ ├── shared │ └── _thread.html.erb │ ├── subscriptions │ ├── _link.html.erb │ └── subscribe.js.erb │ ├── subscriptions_mailer │ └── comment_created.html.erb │ └── threads │ ├── _hide_show_links.js.erb │ ├── _reply.html.erb │ ├── _show.html.erb │ ├── _show.js.erb │ └── show.js.erb ├── arrow.xcf ├── commontator.gemspec ├── config ├── initializers │ └── commontator.rb ├── locales │ ├── de.yml │ ├── en.yml │ ├── pt-BR.yml │ ├── ru.yml │ └── zh.yml └── routes.rb ├── db └── migrate │ ├── 10_install_commontator.rb │ └── 11_add_replying_to_comments.rb ├── lib ├── commontator.rb ├── commontator │ ├── acts_as_commontable.rb │ ├── acts_as_commontator.rb │ ├── build_thread.rb │ ├── commontable_config.rb │ ├── commontator_config.rb │ ├── config.rb │ ├── controllers.rb │ ├── engine.rb │ ├── shared_helper.rb │ └── version.rb └── tasks │ └── commontator_tasks.rake ├── script └── rails ├── spec ├── controllers │ └── commontator │ │ ├── comments_controller_spec.rb │ │ ├── subscriptions_controller_spec.rb │ │ └── threads_controller_spec.rb ├── dummy │ ├── README.md │ ├── Rakefile │ ├── app │ │ ├── assets │ │ │ ├── config │ │ │ │ └── manifest.js │ │ │ ├── javascripts │ │ │ │ └── application.js │ │ │ └── stylesheets │ │ │ │ └── application.css │ │ ├── controllers │ │ │ ├── application_controller.rb │ │ │ ├── dummy_api_controller.rb │ │ │ └── dummy_models_controller.rb │ │ ├── models │ │ │ ├── dummy_dependent_model.rb │ │ │ ├── dummy_model.rb │ │ │ └── dummy_user.rb │ │ └── views │ │ │ ├── dummy_models │ │ │ └── show.html.erb │ │ │ └── layouts │ │ │ └── application.html.erb │ ├── bin │ │ ├── rails │ │ ├── rake │ │ └── setup │ ├── config.ru │ ├── config │ │ ├── application.rb │ │ ├── boot.rb │ │ ├── cable.yml │ │ ├── database.yml │ │ ├── environment.rb │ │ ├── environments │ │ │ ├── development.rb │ │ │ ├── production.rb │ │ │ └── test.rb │ │ ├── initializers │ │ │ ├── application_controller_renderer.rb │ │ │ ├── assets.rb │ │ │ ├── backtrace_silencers.rb │ │ │ ├── commontator.rb │ │ │ ├── content_security_policy.rb │ │ │ ├── cookies_serializer.rb │ │ │ ├── default_url_options.rb │ │ │ ├── filter_parameter_logging.rb │ │ │ ├── inflections.rb │ │ │ ├── mime_types.rb │ │ │ ├── secret_token.rb │ │ │ ├── session_store.rb │ │ │ └── wrap_parameters.rb │ │ ├── locales │ │ │ └── en.yml │ │ ├── routes.rb │ │ └── storage.yml │ ├── db │ │ ├── migrate │ │ │ ├── 00_create_dummy_models.rb │ │ │ ├── 01_create_dummy_users.rb │ │ │ ├── 02_create_dummy_dependent_models.rb │ │ │ └── 03_acts_as_votable_migration.rb │ │ ├── schema.rb │ │ └── seeds.rb │ ├── lib │ │ └── .keep │ ├── public │ │ ├── 404.html │ │ ├── 422.html │ │ ├── 500.html │ │ └── favicon.ico │ └── script │ │ └── rails ├── helpers │ └── commontator │ │ └── application_helper_spec.rb ├── lib │ ├── commontator │ │ ├── acts_as_commontable_spec.rb │ │ ├── acts_as_commontator_spec.rb │ │ ├── commontable_config_spec.rb │ │ ├── commontator_config_spec.rb │ │ ├── controllers_spec.rb │ │ └── shared_helper_spec.rb │ └── commontator_spec.rb ├── mailers │ └── commontator │ │ └── subscriptions_mailer_spec.rb ├── models │ └── commontator │ │ ├── comment_spec.rb │ │ ├── subscription_spec.rb │ │ └── thread_spec.rb ├── rails_helper.rb └── spec_helper.rb └── vendor └── assets ├── javascripts ├── mentionsInput │ └── jquery.mentionsInput.js └── underscore │ └── underscore.js └── stylesheets └── mentionsInput └── jquery.mentionsInput.css /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | schedule: 9 | - cron: '0 0 * * 0' # weekly 10 | jobs: 11 | tests: 12 | timeout-minutes: 30 13 | runs-on: ubuntu-20.04 14 | services: 15 | postgres: 16 | image: postgres:13 17 | ports: 18 | - 5432:5432 19 | options: >- 20 | --health-cmd pg_isready 21 | --health-interval 10s 22 | --health-timeout 5s 23 | --health-retries 5 24 | env: 25 | POSTGRES_USER: postgres 26 | POSTGRES_DB: ci_test 27 | POSTGRES_PASSWORD: postgres 28 | strategy: 29 | matrix: 30 | include: 31 | - RUBY_VERSION: 3.1 32 | RAILS_VERSION: 7.0.1 33 | - RUBY_VERSION: 2.7 34 | RAILS_VERSION: 6.1.4.4 35 | env: 36 | RAILS_VERSION: ${{ matrix.RAILS_VERSION }} 37 | steps: 38 | - uses: actions/checkout@v2 39 | - uses: ruby/setup-ruby@v1 40 | with: 41 | ruby-version: ${{ matrix.RUBY_VERSION }} 42 | bundler-cache: true 43 | - name: Setup database 44 | run: bundle exec rake --trace db:create db:schema:load db:seed 45 | - name: Run tests 46 | run: bundle exec rake spec 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.log 3 | *.sqlite3 4 | *~ 5 | .DS_Store 6 | .byebug_history 7 | Gemfile.lock 8 | 9 | .bundle/ 10 | coverage/ 11 | log/ 12 | pkg/ 13 | tmp/ 14 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Declare your gem's dependencies in commontator.gemspec. 4 | # Bundler will treat runtime dependencies like base dependencies, and 5 | # development dependencies will be added by default to the :development group. 6 | gemspec 7 | 8 | # Declare any dependencies that are still in development here instead of in 9 | # your gemspec. These might include edge Rails or gems from your path or 10 | # Git. Remember to move these dependencies to your gemspec before releasing 11 | # your gem to rubygems.org. 12 | 13 | # To use debugger 14 | gem 'byebug' 15 | 16 | # Specify the Rails version for testing 17 | gem *[ 'rails', ENV['RAILS_VERSION'] ].compact 18 | 19 | # Reduces boot times through caching; required in spec/dummy/config/boot.rb 20 | gem 'bootsnap', '>= 1.4.4', require: false 21 | 22 | # Database adapters 23 | gem 'sqlite3', require: false 24 | gem 'mysql2', require: false 25 | gem 'pg', require: false 26 | 27 | # Code coverage 28 | gem 'codeclimate-test-reporter', require: false 29 | gem 'simplecov', require: false 30 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2012-19 Rice University 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Commontator 2 | 3 | [![Gem Version](https://badge.fury.io/rb/commontator.svg)](https://badge.fury.io/rb/commontator) 4 | [![Tests](https://github.com/lml/commontator/workflows/Tests/badge.svg)](https://github.com/lml/commontator/actions?query=workflow:Tests) 5 | [![Code Climate](https://codeclimate.com/github/lml/commontator/badges/gpa.svg)](https://codeclimate.com/github/lml/commontator) 6 | [![Code Coverage](https://codeclimate.com/github/lml/commontator/badges/coverage.svg)](https://codeclimate.com/github/lml/commontator) 7 | 8 | Commontator is a Rails engine for comments. It is compatible with Rails 5.2+. 9 | Being an engine means it is fully functional as soon as you install and 10 | configure the gem, providing models, views and controllers of its own. 11 | At the same time, almost anything about it can be configured or customized to suit your needs. 12 | 13 | ## Installation 14 | 15 | Follow the steps below to install Commontator: 16 | 17 | 1. Gem 18 | 19 | Add this line to your application's Gemfile: 20 | 21 | ```rb 22 | gem 'commontator' 23 | ``` 24 | 25 | You will also need jquery and a sass compiler, which can be either be installed through 26 | the webpacker gem and yarn/npm/bower or through the jquery-rails and sass[c]-rails gems: 27 | 28 | ```rb 29 | gem 'jquery-rails' 30 | gem 'sassc-rails' 31 | ``` 32 | 33 | Then execute: 34 | 35 | ```sh 36 | $ bundle install 37 | ``` 38 | 39 | 2. Initializer and Migrations 40 | 41 | Run the following command to copy Commontator's initializer and migrations to your app: 42 | 43 | ```sh 44 | $ rake commontator:install 45 | ``` 46 | 47 | Or alternatively: 48 | 49 | ```sh 50 | $ rake commontator:install:initializers 51 | 52 | $ rake commontator:install:migrations 53 | ``` 54 | 55 | And then execute: 56 | 57 | ```sh 58 | $ rails db:migrate 59 | ``` 60 | 61 | 3. Configuration 62 | 63 | Change Commontator's configurations to suit your needs by editing `config/initializers/commontator.rb`. 64 | Make sure to check that your configuration file is up to date every time you update the gem, as available options can change with each minor version. 65 | If you have deprecated options in your initializer, Commontator will issue warnings (usually printed to your console). 66 | 67 | Commontator relies on Rails's `sanitize` helper method to sanitize user input before display. 68 | The default allowed tags and attributes are very permissive, basically 69 | only blocking tags, attributes and attribute values that could be used for XSS. 70 | [Read more about configuring the Rails sanitize helper.](https://edgeapi.rubyonrails.org/classes/ActionView/Helpers/SanitizeHelper.html). 71 | 72 | 4. Routes 73 | 74 | Add this line to your Rails application's `routes.rb` file: 75 | 76 | ```rb 77 | mount Commontator::Engine => '/commontator' 78 | ``` 79 | 80 | You can change the mount path if you would like a different one. 81 | 82 | ### Assets 83 | 84 | 1. Javascripts 85 | 86 | Make sure your application.js requires jquery and rails-ujs or jquery-ujs: 87 | 88 | Rails 5.1+: 89 | ```js 90 | //= require jquery 91 | //= require rails-ujs 92 | ``` 93 | 94 | Rails 5.0: 95 | ```js 96 | //= require jquery 97 | // If jquery-ujs was installed through jquery-rails 98 | //= require jquery_ujs 99 | // If jquery-ujs was installed through webpacker and yarn/npm/bower 100 | //= require jquery-ujs 101 | ``` 102 | 103 | If using Commontator's mentions functionality, also require Commontator's application.js: 104 | 105 | ```js 106 | //= require commontator/application 107 | ``` 108 | 109 | 2. Stylesheets 110 | 111 | In order to display comment threads properly, you must 112 | require Commontator's application.scss in your `application.[s]css`: 113 | 114 | ```css 115 | *= require commontator/application 116 | ``` 117 | 118 | #### Sprockets 4+ 119 | 120 | You must require Commontator's manifest.js in your app's manifest.js for images to work properly: 121 | 122 | ```js 123 | //= link commontator/manifest.js 124 | ``` 125 | 126 | You also need to either add the necessary link tag commands to your layout to load 127 | commontator/application.js and commontator/application.css or require them in your app's 128 | application.js and application.css like in Sprockets 3. 129 | 130 | ## Usage 131 | 132 | Follow the steps below to add Commontator to your models and views: 133 | 134 | 1. Models 135 | 136 | Add this line to your user model(s) (or any models that should be able to post comments): 137 | 138 | ```rb 139 | acts_as_commontator 140 | ``` 141 | 142 | Add this line to any models you want to be able to comment on (i.e. models that have comment threads): 143 | 144 | ```rb 145 | acts_as_commontable 146 | ``` 147 | if you want the thread and all its comments removed when your commontable model is destroyed pass 148 | :destroy as the :dependent option to`acts_as_commontable`: 149 | 150 | ```rb 151 | acts_as_commontable dependent: :destroy 152 | ``` 153 | 154 | instead of `:destroy` you may use any other supported `:dependent` option from rails `has_one` 155 | association. 156 | 157 | 2. Views 158 | 159 | In the following instructions, `@commontable` is an instance of a model that `acts_as_commontable`. 160 | You must supply this variable to the views that will use Commontator. 161 | 162 | Wherever you would like to display comments, call `commontator_thread(@commontable)`: 163 | 164 | ```erb 165 | <%= commontator_thread(@commontable) %> 166 | ``` 167 | 168 | This will create a link that can be clicked to display the comment thread. 169 | 170 | Note that model's record must already exist in the database, so do not use this in `new.html.erb`, `_form.html.erb` or similar views. 171 | We recommend you use this in the model's `show.html.erb` view or the equivalent for your app. 172 | 173 | 3. Controllers 174 | 175 | By default, the `commontator_thread` method only provides a link to the desired comment thread. 176 | Sometimes it may be desirable to have the thread display right away when the corresponding page is loaded. 177 | In that case, just add the following method call to the controller action that displays the page in question: 178 | 179 | ```rb 180 | commontator_thread_show(@commontable) 181 | ``` 182 | 183 | Note that the call to `commontator_thread` in the view is still necessary in either case. 184 | 185 | The `commontator_thread_show` method checks the current user's read permission on the thread and will display the thread if the user is allowed to read it, according to the options in the initializer. 186 | 187 | That's it! Commontator is now ready for use. 188 | 189 | ## Emails 190 | 191 | When you enable subscriptions, emails are sent automatically by Commontator. 192 | If sending emails, remember to add your host URL's to your environment files 193 | (test.rb, development.rb and production.rb): 194 | 195 | ```rb 196 | config.action_mailer.default_url_options = { host: "https://www.example.com" } 197 | ``` 198 | 199 | Sometimes you may need to subscribe (commontator) users automatically when some event happens. 200 | You can call `object.commontator_thread.subscribe(user)` to subscribe users programmatically. 201 | Batch sending through Mailgun is also supported and automatically detected. 202 | Read the Customization section to see how to customize subscription emails. 203 | 204 | ## Voting 205 | 206 | You can allow users to vote on each others' comments 207 | by adding the `acts_as_votable` gem to your Gemfile: 208 | 209 | ```rb 210 | gem 'acts_as_votable' 211 | ``` 212 | 213 | And enabling the relevant option in Commontator's initializer: 214 | 215 | ```rb 216 | config.comment_voting = :ld # See the initializer for available options 217 | ``` 218 | 219 | ## Mentions 220 | 221 | You can allow users to mention other users in the comments. 222 | Mentioned users are automatically subscribed to the thread and receive email notifications. 223 | 224 | First make sure you required Commontator's application.js 225 | in your `application.js` as explained in the Javascripts section. 226 | Then enable mentions in Commontator's initializer: 227 | 228 | ```rb 229 | config.mentions_enabled = true 230 | ``` 231 | 232 | Finally configure the user_mentions_proc, which receives the current user, the current thread, 233 | and the search query inputted by that user and should return a relation containing the users 234 | that can be mentioned and match the query string: 235 | 236 | ```rb 237 | config.user_mentions_proc = ->(current_user, thread, query) { ... } 238 | ``` 239 | 240 | Please be aware that with mentions enabled, any registered user 241 | can use the `user_mentions_proc` to search for other users. 242 | Make sure to properly escape SQL in this proc and do not allow searches on sensitive fields. 243 | 244 | Use '@' with at least three other characters to mention someone in a new/edited comment. 245 | 246 | The mentions script assumes that Commontator is mounted at `/commontator`, 247 | so make sure that is indeed the case if you plan to use mentions. 248 | 249 | ## Browser Support 250 | 251 | Commontator should work properly on any of the major browsers. 252 | The mentions functionality won't work with IE before version 8. 253 | To function properly, this gem requires that visitors to the site have javascript enabled. 254 | 255 | ## Customization 256 | 257 | Copy Commontator's files to your app using any of the following commands: 258 | 259 | ```sh 260 | $ rake commontator:copy:locales 261 | 262 | $ rake commontator:copy:images 263 | $ rake commontator:copy:javascripts 264 | $ rake commontator:copy:stylesheets 265 | 266 | $ rake commontator:copy:views 267 | $ rake commontator:copy:helpers 268 | 269 | $ rake commontator:copy:controllers 270 | $ rake commontator:copy:mailers 271 | 272 | $ rake commontator:copy:models 273 | ``` 274 | 275 | You are now free to modify them and have any changes made manifest in your application. 276 | You can safely remove files you do not want to customize. 277 | 278 | You can customize subscription emails (mailer views) with `rake commontator:copy:views`. 279 | 280 | If copying Commontator's locales, please note that by default Rails will not autoload locales in 281 | subfolders of `config/locales` (like ours) unless you add the following to your `application.rb`: 282 | 283 | ```rb 284 | config.i18n.load_path += Dir[root.join('config', 'locales', '**', '*.yml')] 285 | ``` 286 | 287 | ## Contributing 288 | 289 | 1. Fork the lml/commontator repo on Github 290 | 2. Create a feature or bugfix branch (`git checkout -b my-new-feature`) 291 | 3. Write tests for the feature/bugfix 292 | 4. Implement the new feature/bugfix 293 | 5. Make sure both new and old tests pass (`rake`) 294 | 6. Commit your changes (`git commit -am 'Added some feature'`) 295 | 7. Push the branch (`git push origin my-new-feature`) 296 | 8. Create a new Pull Request to lml/commontator on Github 297 | 298 | ## Development Environment Setup 299 | 300 | 1. Use bundler to install all dependencies: 301 | 302 | ```sh 303 | $ bundle install 304 | ``` 305 | 306 | 2. Setup the database: 307 | 308 | ```sh 309 | $ rails db:setup 310 | ``` 311 | 312 | ## Testing 313 | 314 | To run all existing tests for Commontator, simply execute the following from the main folder: 315 | 316 | ```sh 317 | $ rake 318 | ``` 319 | 320 | ## License 321 | 322 | This gem is distributed under the terms of the MIT license. 323 | See the MIT-LICENSE file for details. 324 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # https://viget.com/extend/rails-engine-testing-with-rspec-capybara-and-factorygirl 2 | begin 3 | require 'bundler/setup' 4 | rescue LoadError 5 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 6 | end 7 | 8 | APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__) 9 | load 'rails/tasks/engine.rake' 10 | 11 | Bundler::GemHelper.install_tasks 12 | 13 | Dir[File.join(__dir__, 'tasks/**/*.rake')].each { |file| load file } 14 | 15 | require 'rspec/core' 16 | require 'rspec/core/rake_task' 17 | 18 | desc 'Run all specs in spec directory (excluding plugin specs)' 19 | RSpec::Core::RakeTask.new(spec: 'app:db:test:prepare') 20 | 21 | task default: :spec 22 | -------------------------------------------------------------------------------- /app/assets/config/commontator/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../../images 2 | //= link_directory ../../javascripts .js 3 | //= link_directory ../../stylesheets .css 4 | -------------------------------------------------------------------------------- /app/assets/images/commontator/downvote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lml/commontator/729fc34968d131bcb5d31325f3e5536e4ee491d4/app/assets/images/commontator/downvote.png -------------------------------------------------------------------------------- /app/assets/images/commontator/downvote_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lml/commontator/729fc34968d131bcb5d31325f3e5536e4ee491d4/app/assets/images/commontator/downvote_active.png -------------------------------------------------------------------------------- /app/assets/images/commontator/downvote_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lml/commontator/729fc34968d131bcb5d31325f3e5536e4ee491d4/app/assets/images/commontator/downvote_disabled.png -------------------------------------------------------------------------------- /app/assets/images/commontator/downvote_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lml/commontator/729fc34968d131bcb5d31325f3e5536e4ee491d4/app/assets/images/commontator/downvote_hover.png -------------------------------------------------------------------------------- /app/assets/images/commontator/upvote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lml/commontator/729fc34968d131bcb5d31325f3e5536e4ee491d4/app/assets/images/commontator/upvote.png -------------------------------------------------------------------------------- /app/assets/images/commontator/upvote_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lml/commontator/729fc34968d131bcb5d31325f3e5536e4ee491d4/app/assets/images/commontator/upvote_active.png -------------------------------------------------------------------------------- /app/assets/images/commontator/upvote_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lml/commontator/729fc34968d131bcb5d31325f3e5536e4ee491d4/app/assets/images/commontator/upvote_disabled.png -------------------------------------------------------------------------------- /app/assets/images/commontator/upvote_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lml/commontator/729fc34968d131bcb5d31325f3e5536e4ee491d4/app/assets/images/commontator/upvote_hover.png -------------------------------------------------------------------------------- /app/assets/javascripts/commontator/application.js: -------------------------------------------------------------------------------- 1 | //= require underscore/underscore 2 | //= require mentionsInput/jquery.mentionsInput 3 | //= require commontator/mentions 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/commontator/mentions.js: -------------------------------------------------------------------------------- 1 | window.Commontator = {}; 2 | Commontator._ = window._.noConflict(); 3 | Commontator.initMentions = function() { 4 | $('.commontator .field textarea:not(.mentions-added)').each(function(_index, textarea){ 5 | $textarea = $(textarea); 6 | $form = $textarea.parents('form'); 7 | threadId = $textarea.parents('.thread').attr('id').match(/[\d]+/)[0]; 8 | $textarea.addClass('mentions-added'); 9 | currentValue = $textarea.val(); 10 | $textarea.mentionsInput({ 11 | elastic: false, 12 | showAvatars: false, 13 | allowRepeat: true, 14 | minChars: 3, 15 | onDataRequest:function (mode, query, callback) { 16 | $.getJSON('/commontator/threads/' + threadId + '/mentions.json', {q: query}, function(responseData) { 17 | callback.call(this, responseData.mentions); 18 | }); 19 | } 20 | }); 21 | $textarea.val(currentValue); 22 | $textarea.on('focusout', function(){ 23 | $textarea.mentionsInput('getMentions', function(mentions){ 24 | $form.find('input[name="mentioned_ids[]"]').remove(); 25 | $(mentions).each(function(_index, mention){ 26 | $input = $('', { type: 'hidden', name: 'mentioned_ids[]', value: mention.id }); 27 | $form.append($input) 28 | }); 29 | }); 30 | }); 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /app/assets/stylesheets/commontator/application.scss: -------------------------------------------------------------------------------- 1 | /* 2 | *= require mentionsInput/jquery.mentionsInput 3 | *= require_self 4 | *= require_tree . 5 | */ 6 | 7 | .commontator { 8 | *:not(input):not(textarea) { 9 | margin: 0; 10 | border: none; 11 | padding: 0; 12 | } 13 | 14 | .commontator-hidden { 15 | display: none; 16 | } 17 | 18 | .header { 19 | display: block; 20 | 21 | overflow: hidden; 22 | } 23 | 24 | .actions { 25 | float: right; 26 | 27 | margin-left: 10px; 28 | } 29 | 30 | .status { 31 | font-size: 120%; 32 | font-weight: bold; 33 | font-style: normal; 34 | } 35 | 36 | .comment-list { 37 | background-color: #FFF; 38 | } 39 | 40 | .children { 41 | padding-left: 15px; 42 | } 43 | 44 | /* Modified from: https://www.strangerstudios.com/sandbox/pagination/diggstyle.php */ 45 | .page-entries-info, .will-paginate { 46 | margin: 10px 0; 47 | } 48 | 49 | .pagination { 50 | a { 51 | margin-right: 4px; 52 | padding: 2px 5px 2px 5px; 53 | border: 1px solid #999; 54 | 55 | color: #666; 56 | text-decoration: none; 57 | 58 | &:hover, &:active { 59 | border: 1px solid #555; 60 | 61 | color: #000; 62 | } 63 | } 64 | 65 | em { 66 | margin-right: 4px; 67 | border: 1px solid #555; 68 | padding: 2px 5px 2px 5px; 69 | 70 | background-color: #555; 71 | 72 | font-weight: bold; 73 | color: #FFF; 74 | } 75 | 76 | .name { 77 | margin: 2px 9px 2px 0; 78 | } 79 | 80 | .disabled { 81 | margin-right: 4px; 82 | border: 1px solid #EEE; 83 | padding: 2px 5px 2px 5px; 84 | 85 | color: #DDD; 86 | } 87 | } 88 | 89 | .mentions { 90 | left: -8px; 91 | top: -8px; 92 | 93 | font-family: monospace; 94 | font-size: 13.3333px; 95 | } 96 | 97 | .field textarea { 98 | width: 100%; 99 | 100 | padding: 0; 101 | 102 | overflow: auto; 103 | } 104 | 105 | .submit { 106 | margin: 10px 0; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /app/assets/stylesheets/commontator/comments.scss: -------------------------------------------------------------------------------- 1 | .commontator .comment { 2 | &, .body blockquote { 3 | border: none; 4 | border-top: 1px solid; 5 | border-left: 1px solid; 6 | 7 | font-weight: normal; 8 | color: #000; 9 | } 10 | 11 | & { 12 | clear: both; 13 | 14 | margin: 15px 0; 15 | border-color: #DDD; 16 | padding: 0; 17 | 18 | font-style: normal; 19 | } 20 | 21 | .section { 22 | clear: both; 23 | 24 | margin: 5px 7px; 25 | } 26 | 27 | .author { 28 | font-size: 110%; 29 | } 30 | 31 | .actions { 32 | font-size: 90%; 33 | } 34 | 35 | .avatar { 36 | float: left; 37 | 38 | margin-right: 7px; 39 | } 40 | 41 | .votes { 42 | float: right; 43 | 44 | margin-left: 7px; 45 | 46 | text-align: center; 47 | 48 | input, img { 49 | background-color: transparent; 50 | } 51 | } 52 | 53 | .upvote, .downvote { 54 | display: block; 55 | 56 | height: 18px; 57 | 58 | margin: 5px 0; 59 | } 60 | 61 | .like { 62 | float: left; 63 | 64 | margin: 0 5px; 65 | } 66 | 67 | .vote-count { 68 | font-size: 110%; 69 | } 70 | 71 | .body { 72 | overflow: hidden; 73 | 74 | blockquote { 75 | border-color: #CCC; 76 | padding: 5px 7px; 77 | 78 | background-color: #EEE; 79 | 80 | font-style: italic; 81 | } 82 | } 83 | 84 | .reply, .timestamp { 85 | font-size: 90%; 86 | } 87 | 88 | .timestamp { 89 | color: #666; 90 | } 91 | 92 | .error-explanation { 93 | margin-bottom: 10px; 94 | 95 | h3 { 96 | margin-top: 5px; 97 | } 98 | 99 | ul { 100 | list-style-position: inside; 101 | } 102 | } 103 | 104 | .pagination { 105 | margin: 10px 7px; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /app/controllers/commontator/application_controller.rb: -------------------------------------------------------------------------------- 1 | class Commontator::ApplicationController < ActionController::Base 2 | before_action :commontator_set_user, :ensure_user 3 | 4 | rescue_from Commontator::SecurityTransgression, with: -> { head(:forbidden) } 5 | 6 | helper Commontator::ApplicationHelper 7 | 8 | protected 9 | 10 | def security_transgression_unless(check) 11 | raise Commontator::SecurityTransgression unless check 12 | end 13 | 14 | def ensure_user 15 | security_transgression_unless(@commontator_user && @commontator_user.is_commontator) 16 | end 17 | 18 | def set_thread 19 | @commontator_thread = params[:thread_id].blank? ? 20 | Commontator::Thread.find(params[:id]) : 21 | Commontator::Thread.find(params[:thread_id]) 22 | 23 | security_transgression_unless @commontator_thread.can_be_read_by? @commontator_user 24 | end 25 | 26 | def commontable_url 27 | main_app.polymorphic_url(@commontator_thread.commontable) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/controllers/commontator/comments_controller.rb: -------------------------------------------------------------------------------- 1 | class Commontator::CommentsController < Commontator::ApplicationController 2 | skip_before_action :ensure_user, only: :show 3 | before_action :set_thread, only: [ :new, :create ] 4 | before_action :set_comment_and_thread, except: [ :new, :create ] 5 | before_action :commontator_set_thread_variables, only: [ :show, :update, :delete, :undelete ] 6 | 7 | # GET /comments/1 8 | def show 9 | respond_to do |format| 10 | format.html { redirect_to commontable_url } 11 | format.js 12 | end 13 | end 14 | 15 | # GET /threads/1/comments/new 16 | def new 17 | @comment = Commontator::Comment.new(thread: @commontator_thread, creator: @commontator_user) 18 | parent_id = params.dig(:comment, :parent_id) 19 | unless parent_id.blank? 20 | parent = Commontator::Comment.find parent_id 21 | @comment.parent = parent 22 | @comment.body = "
#{ 23 | Commontator.commontator_name(parent.creator) 24 | }\n#{ 25 | parent.body 26 | }\n
\n" if [ :q, :b ].include? @commontator_thread.config.comment_reply_style 27 | end 28 | security_transgression_unless @comment.can_be_created_by?(@commontator_user) 29 | 30 | respond_to do |format| 31 | format.html { redirect_to commontable_url } 32 | format.js 33 | end 34 | end 35 | 36 | # POST /threads/1/comments 37 | def create 38 | @comment = Commontator::Comment.new( 39 | thread: @commontator_thread, creator: @commontator_user, body: params.dig(:comment, :body) 40 | ) 41 | parent_id = params.dig(:comment, :parent_id) 42 | @comment.parent = Commontator::Comment.find(parent_id) unless parent_id.blank? 43 | security_transgression_unless @comment.can_be_created_by?(@commontator_user) 44 | 45 | respond_to do |format| 46 | if params[:cancel].blank? 47 | if @comment.save 48 | sub = @commontator_thread.config.thread_subscription.to_sym 49 | @commontator_thread.subscribe(@commontator_user) if sub == :a || sub == :b 50 | subscribe_mentioned if @commontator_thread.config.mentions_enabled 51 | Commontator::Subscription.comment_created(@comment) 52 | @commontator_page = @commontator_thread.new_comment_page( 53 | @comment.parent_id, @commontator_show_all 54 | ) 55 | 56 | format.js 57 | else 58 | format.js { render :new } 59 | end 60 | else 61 | format.js { render :cancel } 62 | end 63 | 64 | format.html { redirect_to commontable_url } 65 | end 66 | end 67 | 68 | # GET /comments/1/edit 69 | def edit 70 | @comment.editor = @commontator_user 71 | security_transgression_unless @comment.can_be_edited_by?(@commontator_user) 72 | 73 | respond_to do |format| 74 | format.html { redirect_to commontable_url } 75 | format.js 76 | end 77 | end 78 | 79 | # PUT /comments/1 80 | def update 81 | @comment.editor = @commontator_user 82 | @comment.body = params.dig(:comment, :body) 83 | security_transgression_unless @comment.can_be_edited_by?(@commontator_user) 84 | 85 | respond_to do |format| 86 | if params[:cancel].blank? 87 | if @comment.save 88 | subscribe_mentioned if @commontator_thread.config.mentions_enabled 89 | 90 | format.js 91 | else 92 | format.js { render :edit } 93 | end 94 | else 95 | @comment.restore_attributes 96 | 97 | format.js { render :cancel } 98 | end 99 | 100 | format.html { redirect_to commontable_url } 101 | end 102 | end 103 | 104 | # PUT /comments/1/delete 105 | def delete 106 | security_transgression_unless @comment.can_be_deleted_by?(@commontator_user) 107 | 108 | @comment.errors.add(:base, t('commontator.comment.errors.already_deleted')) \ 109 | unless @comment.delete_by(@commontator_user) 110 | 111 | respond_to do |format| 112 | format.html { redirect_to commontable_url } 113 | format.js { render :delete } 114 | end 115 | end 116 | 117 | # PUT /comments/1/undelete 118 | def undelete 119 | security_transgression_unless @comment.can_be_deleted_by?(@commontator_user) 120 | 121 | @comment.errors.add(:base, t('commontator.comment.errors.not_deleted')) \ 122 | unless @comment.undelete_by(@commontator_user) 123 | 124 | respond_to do |format| 125 | format.html { redirect_to commontable_url } 126 | format.js { render :delete } 127 | end 128 | end 129 | 130 | # PUT /comments/1/upvote 131 | def upvote 132 | security_transgression_unless @comment.can_be_voted_on_by?(@commontator_user) 133 | 134 | @comment.upvote_from @commontator_user 135 | 136 | respond_to do |format| 137 | format.html { redirect_to commontable_url } 138 | format.js { render :vote } 139 | end 140 | end 141 | 142 | # PUT /comments/1/downvote 143 | def downvote 144 | security_transgression_unless @comment.can_be_voted_on_by?(@commontator_user) &&\ 145 | @comment.thread.config.comment_voting.to_sym == :ld 146 | 147 | @comment.downvote_from @commontator_user 148 | 149 | respond_to do |format| 150 | format.html { redirect_to commontable_url } 151 | format.js { render :vote } 152 | end 153 | end 154 | 155 | # PUT /comments/1/unvote 156 | def unvote 157 | security_transgression_unless @comment.can_be_voted_on_by?(@commontator_user) 158 | 159 | @comment.unvote voter: @commontator_user 160 | 161 | respond_to do |format| 162 | format.html { redirect_to commontable_url } 163 | format.js { render :vote } 164 | end 165 | end 166 | 167 | protected 168 | 169 | def set_comment_and_thread 170 | @comment = Commontator::Comment.find(params[:id]) 171 | @commontator_thread = @comment.thread 172 | end 173 | 174 | def subscribe_mentioned 175 | Commontator.commontator_mentions(@commontator_user, @commontator_thread, '') 176 | .where(id: params[:mentioned_ids]) 177 | .each do |user| 178 | @commontator_thread.subscribe(user) 179 | end 180 | end 181 | end 182 | -------------------------------------------------------------------------------- /app/controllers/commontator/security_transgression.rb: -------------------------------------------------------------------------------- 1 | class Commontator::SecurityTransgression < StandardError 2 | end 3 | -------------------------------------------------------------------------------- /app/controllers/commontator/subscriptions_controller.rb: -------------------------------------------------------------------------------- 1 | class Commontator::SubscriptionsController < Commontator::ApplicationController 2 | before_action :set_thread 3 | 4 | # PUT /threads/1/subscribe 5 | def subscribe 6 | security_transgression_unless @commontator_thread.can_subscribe?(@commontator_user) 7 | 8 | @commontator_thread.errors.add(:base, t('commontator.subscription.errors.already_subscribed')) \ 9 | unless @commontator_thread.subscribe(@commontator_user) 10 | 11 | respond_to do |format| 12 | format.html { redirect_to commontable_url } 13 | format.js { render :subscribe } 14 | end 15 | 16 | end 17 | 18 | # PUT /threads/1/unsubscribe 19 | def unsubscribe 20 | security_transgression_unless @commontator_thread.can_subscribe?(@commontator_user) 21 | 22 | @commontator_thread.errors.add(:base, t('commontator.subscription.errors.not_subscribed')) \ 23 | unless @commontator_thread.unsubscribe(@commontator_user) 24 | 25 | respond_to do |format| 26 | format.html { redirect_to commontable_url } 27 | format.js { render :subscribe } 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/controllers/commontator/threads_controller.rb: -------------------------------------------------------------------------------- 1 | class Commontator::ThreadsController < Commontator::ApplicationController 2 | skip_before_action :ensure_user, only: :show 3 | before_action :set_thread 4 | before_action :commontator_set_thread_variables, except: :mentions 5 | before_action :commontator_set_new_comment, only: [ :show, :reopen ] 6 | 7 | # GET /threads/1 8 | def show 9 | respond_to do |format| 10 | format.html { redirect_to commontable_url } 11 | format.js { commontator_thread_show(@commontator_thread.commontable) } 12 | end 13 | end 14 | 15 | # PUT /threads/1/close 16 | def close 17 | security_transgression_unless @commontator_thread.can_be_edited_by?(@commontator_user) 18 | 19 | @commontator_thread.errors.add(:base, t('commontator.thread.errors.already_closed')) \ 20 | unless @commontator_thread.close(@commontator_user) 21 | 22 | respond_to do |format| 23 | format.html { redirect_to commontable_url } 24 | format.js do 25 | commontator_thread_show(@commontator_thread.commontable) 26 | render :show 27 | end 28 | end 29 | end 30 | 31 | # PUT /threads/1/reopen 32 | def reopen 33 | security_transgression_unless @commontator_thread.can_be_edited_by?(@commontator_user) 34 | 35 | @commontator_thread.errors.add(:base, t('commontator.thread.errors.not_closed')) \ 36 | unless @commontator_thread.reopen 37 | 38 | respond_to do |format| 39 | format.html { redirect_to commontable_url } 40 | format.js do 41 | commontator_thread_show(@commontator_thread.commontable) 42 | render :show 43 | end 44 | end 45 | end 46 | 47 | # GET /threads/1/mentions.json 48 | def mentions 49 | security_transgression_unless @commontator_thread.can_be_read_by?(@commontator_user) && 50 | @commontator_thread.config.mentions_enabled 51 | 52 | respond_to do |format| 53 | format.json do 54 | query = params[:q].to_s 55 | 56 | if query.size < 3 57 | render json: { errors: ['Query string is too short (minimum 3 characters)'] }, 58 | status: :unprocessable_entity 59 | else 60 | render json: { 61 | mentions: Commontator.commontator_mentions( 62 | @commontator_user, @commontator_thread, query 63 | ).map { |user| { id: user.id, name: Commontator.commontator_name(user), type: 'user' } } 64 | } 65 | end 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /app/helpers/commontator/application_helper.rb: -------------------------------------------------------------------------------- 1 | module Commontator::ApplicationHelper 2 | def javascript_proc 3 | Commontator.javascript_proc.call(self).html_safe 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/helpers/commontator/link_renderer.rb: -------------------------------------------------------------------------------- 1 | require 'will_paginate/view_helpers/link_renderer' 2 | require 'will_paginate/view_helpers/action_view' 3 | 4 | 5 | class Commontator::LinkRenderer < WillPaginate::ActionView::LinkRenderer 6 | protected 7 | 8 | def html_container(html) 9 | html = "#{@options[:name]}#{html}" if @options[:name] 10 | tag(:div, html, container_attributes) 11 | end 12 | 13 | def url(page) 14 | @base_url_params ||= begin 15 | url_params = merge_get_params(default_url_params) 16 | merge_optional_params(url_params) 17 | end 18 | 19 | url_params = @base_url_params.dup 20 | add_current_page_param(url_params, page) 21 | 22 | @template.commontator.url_for(url_params) 23 | end 24 | 25 | private 26 | 27 | def link(text, target, attributes = {}) 28 | attributes = attributes.merge('data-remote' => true) if @options[:remote] 29 | super 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/mailers/commontator/subscriptions_mailer.rb: -------------------------------------------------------------------------------- 1 | class Commontator::SubscriptionsMailer < ActionMailer::Base 2 | helper Commontator::SharedHelper 3 | 4 | def comment_created(comment, recipients) 5 | setup_variables(comment, recipients) 6 | 7 | mail(@mail_params).tap do |message| 8 | message.mailgun_recipient_variables = @mailgun_recipient_variables if @using_mailgun 9 | end 10 | end 11 | 12 | protected 13 | 14 | def setup_variables(comment, recipients) 15 | @comment = comment 16 | @thread = @comment.thread 17 | @creator = @comment.creator 18 | 19 | @mail_params = { from: @thread.config.email_from_proc.call(@thread) } 20 | 21 | @recipient_emails = recipients.map do |recipient| 22 | Commontator.commontator_email(recipient, self) 23 | end 24 | 25 | @using_mailgun = Rails.application.config.action_mailer.delivery_method == :mailgun 26 | 27 | if @using_mailgun 28 | @recipients_header = :to 29 | @mailgun_recipient_variables = {}.tap do |mailgun_recipient_variables| 30 | @recipient_emails.each { |email| mailgun_recipient_variables[email] = {} } 31 | end 32 | else 33 | @recipients_header = :bcc 34 | end 35 | 36 | @mail_params[@recipients_header] = @recipient_emails 37 | 38 | @creator_name = Commontator.commontator_name(@creator) 39 | @commontable_name = Commontator.commontable_name(@thread) 40 | @comment_url = Commontator.comment_url(@comment, main_app) 41 | 42 | @mail_params[:subject] = t( 43 | 'commontator.email.comment_created.subject', 44 | creator_name: @creator_name, 45 | commontable_name: @commontable_name, 46 | comment_url: @comment_url 47 | ) 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /app/models/commontator/collection.rb: -------------------------------------------------------------------------------- 1 | class Commontator::Collection < WillPaginate::Collection 2 | attr_reader :root_per_page 3 | 4 | # This method determines if we are in a shorter version of the first page, which we call page 0 5 | def page_zero? 6 | total_entries > @per_page && @per_page < @root_per_page 7 | end 8 | 9 | def initialize(array, count, root_per_page, per_page, page = 1) 10 | self.total_entries = count 11 | @root_per_page = root_per_page 12 | @per_page = per_page 13 | @current_page = page_zero? ? 0 : WillPaginate::PageNumber(page) 14 | @first_call = true 15 | 16 | replace(array) 17 | end 18 | 19 | # We return 2 total_pages under certain conditions to trick will_paginate 20 | # into rendering the pagination controls when it otherwise wouldn't 21 | def total_pages 22 | min_total_pages = page_zero? && @first_call ? 2 : 1 23 | @first_call = false 24 | [ (total_entries.to_f/@root_per_page).ceil, min_total_pages ].max 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/models/commontator/comment.rb: -------------------------------------------------------------------------------- 1 | class Commontator::Comment < ActiveRecord::Base 2 | belongs_to :creator, polymorphic: true 3 | belongs_to :editor, polymorphic: true, optional: true 4 | belongs_to :thread, inverse_of: :comments 5 | belongs_to :parent, optional: true, class_name: name, inverse_of: :children 6 | 7 | has_many :children, class_name: name, foreign_key: :parent_id, inverse_of: :parent 8 | 9 | validates :editor, presence: true, on: :update 10 | validates :body, presence: true, uniqueness: { 11 | scope: [ :creator_type, :creator_id, :thread_id, :deleted_at ], message: :double_posted 12 | } 13 | validate :parent_is_not_self, :parent_belongs_to_the_same_thread, if: :parent 14 | 15 | cattr_accessor :is_votable 16 | self.is_votable = begin 17 | require 'acts_as_votable' 18 | acts_as_votable 19 | 20 | true 21 | rescue LoadError 22 | false 23 | end 24 | 25 | def self.is_votable? 26 | is_votable 27 | end 28 | 29 | def is_modified? 30 | !editor.nil? 31 | end 32 | 33 | def is_latest? 34 | thread.latest_comment(false) == self 35 | end 36 | 37 | def get_vote_by(user) 38 | return nil unless self.class.is_votable? && !user.nil? && user.is_commontator 39 | 40 | # Preloaded with a condition in thread#nested_comments_for 41 | votes_for.to_a.find { |vote| vote.voter_id == user.id && vote.voter_type == user.class.name } 42 | end 43 | 44 | def update_cached_votes(vote_scope = nil) 45 | self.update_column(:cached_votes_up, count_votes_up(true)) 46 | self.update_column(:cached_votes_down, count_votes_down(true)) 47 | end 48 | 49 | def is_deleted? 50 | !deleted_at.nil? 51 | end 52 | 53 | def delete_by(user) 54 | return false if is_deleted? 55 | 56 | self.deleted_at = Time.now 57 | self.editor = user 58 | self.save 59 | end 60 | 61 | def undelete_by(user) 62 | return false unless is_deleted? 63 | 64 | self.deleted_at = nil 65 | self.editor = user 66 | self.save 67 | end 68 | 69 | def body 70 | is_deleted? ? I18n.t( 71 | 'commontator.comment.status.deleted_by', deleter_name: Commontator.commontator_name(editor) 72 | ) : super 73 | end 74 | 75 | def created_timestamp 76 | I18n.t 'commontator.comment.status.created_at', 77 | created_at: I18n.l(created_at, format: :commontator) 78 | end 79 | 80 | def updated_timestamp 81 | I18n.t 'commontator.comment.status.updated_at', 82 | editor_name: Commontator.commontator_name(editor || creator), 83 | updated_at: I18n.l(updated_at, format: :commontator) 84 | end 85 | 86 | ################## 87 | # Access Control # 88 | ################## 89 | 90 | def can_be_created_by?(user) 91 | user == creator && !user.nil? && user.is_commontator && 92 | !thread.is_closed? && thread.can_be_read_by?(user) 93 | end 94 | 95 | def can_be_edited_by?(user) 96 | return true if thread.can_be_edited_by?(user) && 97 | thread.config.moderator_permissions.to_sym == :e 98 | 99 | comment_edit = thread.config.comment_editing.to_sym 100 | !thread.is_closed? && !is_deleted? && user == creator && (editor.nil? || user == editor) && 101 | comment_edit != :n && (is_latest? || comment_edit == :a) && thread.can_be_read_by?(user) 102 | end 103 | 104 | def can_be_deleted_by?(user) 105 | mod_perm = thread.config.moderator_permissions.to_sym 106 | return true if thread.can_be_edited_by?(user) && (mod_perm == :e || mod_perm == :d) 107 | 108 | comment_del = thread.config.comment_deletion.to_sym 109 | !thread.is_closed? && user == creator && (!is_deleted? || editor == user) && 110 | comment_del != :n && (is_latest? || comment_del == :a) && thread.can_be_read_by?(user) 111 | end 112 | 113 | def can_be_voted_on? 114 | !thread.is_closed? && !is_deleted? && thread.is_votable? && self.class.is_votable? 115 | end 116 | 117 | def can_be_voted_on_by?(user) 118 | !user.nil? && user.is_commontator && user != creator && 119 | thread.can_be_read_by?(user) && can_be_voted_on? 120 | end 121 | 122 | protected 123 | 124 | # These 2 validation messages are not currently translated because end users should never see them 125 | def parent_is_not_self 126 | return if parent != self 127 | errors.add :parent, 'must be a different comment' 128 | throw :abort 129 | end 130 | 131 | def parent_belongs_to_the_same_thread 132 | return if parent.thread_id == thread_id 133 | errors.add :parent, 'must belong to the same thread' 134 | throw :abort 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /app/models/commontator/json_array_coder.rb: -------------------------------------------------------------------------------- 1 | module Commontator::JsonArrayCoder 2 | def self.load(data) 3 | obj = JSON.load(data) 4 | obj.is_a?(Array) ? obj : [] 5 | end 6 | 7 | def self.dump(obj) 8 | obj.is_a?(Array) ? JSON.dump(obj) : '[]' 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/models/commontator/subscription.rb: -------------------------------------------------------------------------------- 1 | class Commontator::Subscription < ActiveRecord::Base 2 | belongs_to :subscriber, polymorphic: true 3 | belongs_to :thread, inverse_of: :subscriptions 4 | 5 | validates :thread, presence: true, uniqueness: { scope: [ :subscriber_type, :subscriber_id ] } 6 | 7 | def self.comment_created(comment) 8 | recipients = comment.thread.subscribers.reject { |sub| sub == comment.creator } 9 | return if recipients.empty? 10 | 11 | mail = Commontator::SubscriptionsMailer.comment_created(comment, recipients) 12 | mail.respond_to?(:deliver_later) ? mail.deliver_later : mail.deliver 13 | end 14 | 15 | def unread_comments(show_all) 16 | created_at = Commontator::Comment.arel_table[:created_at] 17 | thread.filtered_comments(show_all).where(created_at.gteq(updated_at)) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/models/commontator/thread.rb: -------------------------------------------------------------------------------- 1 | class Commontator::Thread < ActiveRecord::Base 2 | belongs_to :closer, polymorphic: true, optional: true 3 | belongs_to :commontable, polymorphic: true, optional: true, inverse_of: :commontator_thread 4 | 5 | has_many :comments, dependent: :destroy, inverse_of: :thread 6 | has_many :subscriptions, dependent: :destroy, inverse_of: :thread 7 | 8 | validates :commontable, presence: true, unless: :is_closed? 9 | validates :commontable_id, uniqueness: { scope: :commontable_type, allow_nil: true } 10 | 11 | RAILS_7_PRELOADER = Rails::VERSION::MAJOR >= 7 12 | 13 | def config 14 | @config ||= commontable.try(:commontable_config) || Commontator 15 | end 16 | 17 | def is_votable? 18 | config.comment_voting.to_sym != :n 19 | end 20 | 21 | def is_filtered? 22 | !config.comment_filter.nil? 23 | end 24 | 25 | def filtered_comments(show_all) 26 | return comments if show_all 27 | 28 | cf = config.comment_filter 29 | return comments if cf.nil? 30 | 31 | comments.where(cf) 32 | end 33 | 34 | def ordered_comments(show_all) 35 | fc = filtered_comments(show_all) 36 | cc = Commontator::Comment.arel_table 37 | 38 | # ID is used as a tie-breaker because MySQL lacks sub-second timestamp resolution 39 | case config.comment_order.to_sym 40 | when :l 41 | fc.order(cc[:created_at].desc, cc[:id].desc) 42 | when :ve 43 | fc.order( 44 | Arel::Nodes::Descending.new(cc[:cached_votes_up] - cc[:cached_votes_down]), 45 | cc[:created_at].asc, 46 | cc[:id].asc 47 | ) 48 | when :vl 49 | fc.order( 50 | Arel::Nodes::Descending.new(cc[:cached_votes_up] - cc[:cached_votes_down]), 51 | cc[:created_at].desc, 52 | cc[:id].desc 53 | ) 54 | else 55 | fc.order(cc[:created_at].asc, cc[:id].asc) 56 | end 57 | end 58 | 59 | def latest_comment(show_all) 60 | @latest_comment ||= ordered_comments(show_all).last 61 | end 62 | 63 | def comments_with_parent_id(parent_id, show_all) 64 | oc = ordered_comments(show_all) 65 | 66 | if [ :i, :b ].include?(config.comment_reply_style) 67 | # Filter comments by parent_id so we display nested comments 68 | oc.where(parent_id: parent_id) 69 | elsif parent_id.nil? 70 | # Parent is the thread itself and nesting is disabled, so include all comments 71 | oc 72 | else 73 | # Parent is some comment and nesting is disabled, so return nothing 74 | oc.none 75 | end 76 | end 77 | 78 | def paginated_comments(page, parent_id, show_all) 79 | cp = comments_with_parent_id(parent_id, show_all) 80 | 81 | cp.paginate(page: page, per_page: config.comments_per_page[0]) 82 | end 83 | 84 | def nest_comments( 85 | comments, root_per_page, per_page_by_parent_id, count_by_parent_id, children_by_parent_id 86 | ) 87 | comments.map do |comment| 88 | # Delete is used to ensure loops don't cause stack overflow 89 | children = children_by_parent_id.delete(comment.id) || [] 90 | count = count_by_parent_id.delete(comment.id) || 0 91 | per_page = per_page_by_parent_id.delete(comment.id) || 0 92 | nested_children = nest_comments( 93 | children, root_per_page, per_page_by_parent_id, count_by_parent_id, children_by_parent_id 94 | ) 95 | 96 | [ comment, Commontator::Collection.new(nested_children, count, root_per_page, per_page) ] 97 | end 98 | end 99 | 100 | def nested_comments_for(user, comments, show_all) 101 | includes = [ :thread, :creator, :editor ] 102 | total_entries = comments.total_entries 103 | root_per_page = config.comments_per_page[0] 104 | current_page = comments.current_page.to_i 105 | comments = comments.includes(includes).to_a 106 | count_by_parent_id = {} 107 | per_page_by_parent_id = {} 108 | children_by_parent_id = Hash.new { |hash, key| hash[key] = [] } 109 | 110 | if [ :i, :b ].include? config.comment_reply_style 111 | all_parent_ids = comments.map(&:id) 112 | (config.comments_per_page[1..-1] + [ 0 ]).each_with_index do |per_page, index| 113 | filtered_comments(show_all).where(parent_id: all_parent_ids) 114 | .group(:parent_id) 115 | .count 116 | .each do |parent_id, count| 117 | count_by_parent_id[parent_id] = count 118 | per_page_by_parent_id[parent_id] = per_page 119 | end 120 | 121 | next if per_page == 0 122 | 123 | children = all_parent_ids.empty? ? [] : Commontator::Comment.find_by_sql( 124 | all_parent_ids.map do |parent_id| 125 | Commontator::Comment.select(Arel.star).from( 126 | Arel::Nodes::TableAlias.new( 127 | Arel::Nodes::Grouping.new( 128 | Arel::Nodes::SqlLiteral.new( 129 | ordered_comments(show_all).where(parent_id: parent_id).limit(per_page).to_sql 130 | ) 131 | ), :commontator_comments 132 | ) 133 | ).to_sql 134 | end.reduce { |memo, sql| memo.nil? ? sql : "#{memo} UNION ALL #{sql}" } 135 | ) 136 | children.each { |comment| children_by_parent_id[comment.parent_id] << comment } 137 | all_parent_ids = children.map(&:id) 138 | end 139 | end 140 | 141 | Commontator::Collection.new( 142 | nest_comments( 143 | comments, root_per_page, per_page_by_parent_id, count_by_parent_id, children_by_parent_id 144 | ), 145 | total_entries, 146 | root_per_page, 147 | root_per_page, 148 | current_page 149 | ).tap do |nested_comments| 150 | next unless is_votable? 151 | 152 | if RAILS_7_PRELOADER 153 | ActiveRecord::Associations::Preloader.new( 154 | records: nested_comments.flatten, 155 | associations: :votes_for, 156 | scope: ActsAsVotable::Vote.where(voter: user) 157 | ).call 158 | else 159 | ActiveRecord::Associations::Preloader.new.preload( 160 | nested_comments.flatten, :votes_for, ActsAsVotable::Vote.where(voter: user) 161 | ) 162 | end 163 | end 164 | end 165 | 166 | def new_comment_page(parent_id, show_all) 167 | per_page = config.comments_per_page[0].to_i 168 | return 1 if per_page <= 0 169 | 170 | comment_order = config.comment_order.to_sym 171 | return 1 if comment_order == :l 172 | 173 | cp = comments_with_parent_id(parent_id, show_all) 174 | cc = Commontator::Comment.arel_table 175 | comment_index = case config.comment_order.to_sym 176 | when :l 177 | 1 # First comment 178 | when :ve 179 | # Last comment with rating == 0 180 | cp.where((cc[:cached_votes_up] - cc[:cached_votes_down]).gteq(0)).count 181 | when :vl 182 | # First comment with rating == 0 183 | cp.where((cc[:cached_votes_up] - cc[:cached_votes_down]).gt(0)).count + 1 184 | else 185 | cp.count # Last comment 186 | end 187 | 188 | (comment_index.to_f/per_page).ceil 189 | end 190 | 191 | def is_closed? 192 | !closed_at.nil? 193 | end 194 | 195 | def close(user = nil) 196 | return false if is_closed? 197 | 198 | self.closed_at = Time.now 199 | self.closer = user 200 | save 201 | end 202 | 203 | def reopen 204 | return false unless is_closed? && !commontable.nil? 205 | 206 | self.closed_at = nil 207 | save 208 | end 209 | 210 | def subscribers 211 | subscriptions.map(&:subscriber) 212 | end 213 | 214 | def subscription_for(subscriber) 215 | return nil if !subscriber || !subscriber.is_commontator 216 | 217 | subscriber.commontator_subscriptions.find_by(thread_id: self.id) 218 | end 219 | 220 | def subscribe(subscriber) 221 | return false unless subscriber.is_commontator && !subscription_for(subscriber) 222 | 223 | subscription = Commontator::Subscription.new 224 | subscription.subscriber = subscriber 225 | subscription.thread = self 226 | subscription.save 227 | end 228 | 229 | def unsubscribe(subscriber) 230 | subscription = subscription_for(subscriber) 231 | return false unless subscription 232 | 233 | subscription.destroy 234 | end 235 | 236 | def mark_as_read_for(subscriber) 237 | subscription = subscription_for(subscriber) 238 | return false unless subscription 239 | 240 | subscription.touch 241 | end 242 | 243 | # Creates a new empty thread and assigns it to the commontable 244 | # The old thread is kept in the database for archival purposes 245 | def clear 246 | return if commontable.nil? || !is_closed? 247 | 248 | new_thread = Commontator::Thread.new 249 | new_thread.commontable = commontable 250 | 251 | with_lock do 252 | self.commontable = nil 253 | save! 254 | new_thread.save! 255 | Commontator::Subscription.where(thread: self).update_all(thread_id: new_thread.id) 256 | end 257 | end 258 | 259 | ################## 260 | # Access Control # 261 | ################## 262 | 263 | # Reader capabilities (user can be nil or false) 264 | def can_be_read_by?(user) 265 | return true if can_be_edited_by?(user) 266 | 267 | !commontable.nil? && config.thread_read_proc.call(self, user) 268 | end 269 | 270 | # Thread moderator capabilities 271 | def can_be_edited_by?(user) 272 | !commontable.nil? && !user.nil? && user.is_commontator && 273 | config.thread_moderator_proc.call(self, user) 274 | end 275 | 276 | def can_subscribe?(user) 277 | thread_sub = config.thread_subscription.to_sym 278 | !is_closed? && !user.nil? && user.is_commontator && 279 | (thread_sub == :m || thread_sub == :b) && can_be_read_by?(user) 280 | end 281 | end 282 | -------------------------------------------------------------------------------- /app/views/commontator/comments/_body.html.erb: -------------------------------------------------------------------------------- 1 | <%# 2 | Views that use this partial must provide the following variable: 3 | comment 4 | %> 5 | 6 | <%= commontator_simple_format comment.body %> 7 | -------------------------------------------------------------------------------- /app/views/commontator/comments/_form.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | # Views that use this partial must provide the following variable: 3 | # comment 4 | # 5 | 6 | new_record = comment.new_record? 7 | 8 | # Optionally, they can also supply the following variables: 9 | thread ||= new_record ? comment.thread : nil 10 | no_remote ||= false 11 | %> 12 | 13 | <% config = comment.thread.config %> 14 | 15 | <% if comment.errors.any? %> 16 |
17 |

<%= t "commontator.comment.errors.#{new_record ? 'create' : 'update'}" %>

18 | 19 | 24 |
25 | <% end %> 26 | 27 | <%= form_for([commontator, thread, comment], remote: !no_remote) do |form| %> 28 | <%= form.hidden_field :parent_id %> 29 | 30 | <% unless comment.parent.nil? %> 31 |
32 | <%= t('commontator.comment.status.replying', 33 | creator_name: Commontator.commontator_name(comment.parent.creator)) %> 34 |
35 | <% end %> 36 | 37 |
38 | <%= 39 | form.text_area :body, rows: '7', id: new_record ? 40 | comment.parent.nil? ? "commontator-thread-#{@commontator_thread.id}-new-comment-body" : 41 | "commontator-comment-#{comment.parent.id}-reply" : 42 | "commontator-comment-#{comment.id}-edit-body" 43 | %> 44 | <%= javascript_tag('Commontator.initMentions();') if config.mentions_enabled %> 45 |
46 | 47 |
48 | <%= form.submit t("commontator.comment.actions.#{new_record ? 'create' : 'update'}") %> 49 | <%= 50 | form.submit( 51 | t('commontator.comment.actions.cancel'), name: 'cancel' 52 | ) unless config.new_comment_style == :t && new_record && comment.parent.nil? 53 | %> 54 |
55 | <% end %> 56 | -------------------------------------------------------------------------------- /app/views/commontator/comments/_list.html.erb: -------------------------------------------------------------------------------- 1 | <%# 2 | Controllers that use this partial must supply the following variables: 3 | user 4 | nested_comments 5 | %> 6 | 7 | <% nested_comments.each do |comment, nested_children| %> 8 |
9 | <%= 10 | render partial: 'commontator/comments/show', formats: [ :html ], locals: { 11 | user: user, comment: comment, nested_children: nested_children 12 | } 13 | %> 14 |
15 | <% end %> 16 | -------------------------------------------------------------------------------- /app/views/commontator/comments/_show.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | # Controllers that use this partial must supply the following variables: 3 | # user 4 | # comment 5 | # nested_children or page 6 | # show_all 7 | 8 | thread = comment.thread 9 | nested_children ||= begin 10 | children = thread.paginated_comments(page, comment.id, show_all) 11 | thread.nested_comments_for(user, children, show_all) 12 | end 13 | 14 | creator = comment.creator 15 | link = Commontator.commontator_link(creator, main_app) 16 | name = Commontator.commontator_name(creator) || '' 17 | %> 18 | 19 |
20 | 21 | <%= link.blank? ? name : link_to(name, link) %> 22 | 23 | 24 | 25 | <%= 26 | link_to( 27 | t('commontator.comment.actions.edit'), 28 | commontator.edit_comment_path(comment), 29 | id: "commontator-comment-#{comment.id}-edit", 30 | class: 'edit', 31 | remote: true 32 | ) if comment.can_be_edited_by?(user) 33 | %> 34 | 35 | <% if comment.can_be_deleted_by?(user) %> 36 | <% is_deleted = comment.is_deleted? %> 37 | <% del_string = is_deleted ? 'undelete' : 'delete' %> 38 | <%= link_to t("commontator.comment.actions.#{del_string}"), 39 | commontator.polymorphic_path([del_string.to_sym, comment]), 40 | data: is_deleted ? {} : { confirm: t('commontator.comment.actions.confirm_delete') }, 41 | method: :put, 42 | id: "commontator-comment-#{comment.id}-#{del_string}", 43 | class: del_string, 44 | remote: true %> 45 | <% end %> 46 | 47 |
48 | 49 |
50 | 51 | <%= Commontator.commontator_avatar(creator, self) %> 52 | 53 | 54 | 55 | <%= render partial: 'commontator/comments/votes', locals: { comment: comment, user: user } %> 56 | 57 | 58 |
59 | <%= render partial: 'commontator/comments/body', locals: { comment: comment } %> 60 |
61 |
62 | 63 |
64 | <% unless comment.is_deleted? %> 65 | 66 | <%= 67 | link_to( 68 | t('commontator.comment.actions.reply'), 69 | commontator.new_thread_comment_path(thread, comment: { parent_id: comment.id }), 70 | remote: true 71 | ) if thread.config.comment_reply_style != :n && !thread.is_closed? 72 | %> 73 | 74 | <% end %> 75 | 76 | 77 | <%= comment.created_timestamp %> 78 | 79 | 80 | 81 | <%= comment.updated_timestamp if comment.is_modified? %> 82 | 83 |
84 | 85 |
86 | <% if thread.config.comment_order == :l %> 87 |
88 | <% end %> 89 | 90 | <%= render partial: 'commontator/comments/list', 91 | locals: { user: user, nested_comments: nested_children } %> 92 | 93 | <% if thread.config.comment_order != :l %> 94 |
95 | <% end %> 96 |
97 | 98 | 109 | -------------------------------------------------------------------------------- /app/views/commontator/comments/_show.js.erb: -------------------------------------------------------------------------------- 1 | <% 2 | # Views that use this partial must supply the following variables: 3 | # user 4 | # thread 5 | # comment 6 | # page 7 | # show_all 8 | %> 9 | 10 | $("#commontator-comment-<%= comment.id %>").html("<%= escape_javascript( 11 | render partial: 'commontator/comments/show', formats: [ :html ], locals: { 12 | user: user, thread: thread, comment: comment, page: page, show_all: show_all 13 | } 14 | ) %>"); 15 | -------------------------------------------------------------------------------- /app/views/commontator/comments/_votes.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | # Controllers that use this partial must provide the following variables: 3 | # comment 4 | # user 5 | 6 | thread = comment.thread 7 | %> 8 | 9 | <% if comment.can_be_voted_on? %> 10 | <% can_vote = comment.can_be_voted_on_by?(user) %> 11 | <% vote = comment.get_vote_by(user) %> 12 | <% comment_voting = thread.config.comment_voting.to_sym %> 13 | 14 | <% if comment_voting == :ld || comment_voting == :l %> 15 | <% vtype = (comment_voting == :ld) ? 'upvote' : 'like' %> 16 | 17 | <% if can_vote && (vote.blank? || !vote.vote_flag) %> 18 | <%= form_tag commontator.upvote_comment_path(comment), 19 | method: :put, 20 | remote: true do %> 21 | <%= image_submit_tag "commontator/upvote.png", 22 | onmouseover: "this.src='#{image_path("commontator/upvote_hover.png")}'", 23 | onmouseout: "this.src='#{image_path("commontator/upvote.png")}'" %> 24 | <% end %> 25 | <% elsif can_vote %> 26 | <%= form_tag commontator.unvote_comment_path(comment), 27 | method: :put, 28 | remote: true do %> 29 | <%= image_submit_tag "commontator/upvote_active.png", 30 | onmouseover: "this.src='#{image_path("commontator/upvote.png")}'", 31 | onmouseout: "this.src='#{image_path("commontator/upvote_active.png")}'" 32 | %> 33 | <% end %> 34 | <% else %> 35 | <%= image_tag "commontator/upvote_disabled.png" %> 36 | <% end %> 37 | 38 | <% end %> 39 | 40 | 41 | <% config = thread.config %> 42 | <%= config.vote_count_proc.call(thread, comment.cached_votes_up, comment.cached_votes_down) %> 43 | 44 | 45 | <% if comment_voting == :ld %> 46 | 47 | <% if can_vote && (vote.blank? || vote.vote_flag) %> 48 | <%= form_tag commontator.downvote_comment_path(comment), 49 | method: :put, 50 | remote: true do %> 51 | <%= image_submit_tag "commontator/downvote.png", 52 | onmouseover: "this.src='#{image_path("commontator/downvote_hover.png")}'", 53 | onmouseout: "this.src='#{image_path("commontator/downvote.png")}'" %> 54 | <% end %> 55 | <% elsif can_vote %> 56 | <%= form_tag commontator.unvote_comment_path(comment), 57 | method: :put, 58 | remote: true do %> 59 | <%= image_submit_tag "commontator/downvote_active.png", 60 | onmouseover: "this.src='#{image_path("commontator/downvote.png")}'", 61 | onmouseout: "this.src='#{image_path("commontator/downvote_active.png")}'" 62 | %> 63 | <% end %> 64 | <% else %> 65 | <%= image_tag "commontator/downvote_disabled.png" %> 66 | <% end %> 67 | 68 | <% end %> 69 | <% end %> 70 | -------------------------------------------------------------------------------- /app/views/commontator/comments/cancel.js.erb: -------------------------------------------------------------------------------- 1 | <% if @comment.nil? || @comment.new_record? %> 2 | <% 3 | id = @comment.nil? || @comment.parent.nil? ? 4 | "commontator-thread-#{@commontator_thread.id}-new-comment" : 5 | "commontator-comment-#{@comment.parent.id}-reply" 6 | %> 7 | 8 | $("#<%= id %>").hide(); 9 | 10 | $("#<%= id %>-link").fadeIn(); 11 | <% else %> 12 | $("#commontator-comment-<%= @comment.id %>-body").html("<%= escape_javascript( 13 | render partial: 'body', locals: { comment: @comment } 14 | ) %>"); 15 | <% end %> 16 | 17 | <%= javascript_proc %> 18 | -------------------------------------------------------------------------------- /app/views/commontator/comments/create.js.erb: -------------------------------------------------------------------------------- 1 | <%= 2 | if @comment.parent.nil? 3 | partial = 'threads' 4 | extra_locals = {} 5 | id = "commontator-thread-#{@commontator_thread.id}-new-comment" 6 | else 7 | partial = 'comments' 8 | extra_locals = { comment: @comment.parent } 9 | id = "commontator-comment-#{@comment.parent.id}-reply" 10 | end 11 | 12 | render partial: "commontator/#{partial}/show", locals: extra_locals.merge( 13 | user: @commontator_user, 14 | thread: @commontator_thread, 15 | page: @commontator_page, 16 | show_all: @commontator_show_all 17 | ) 18 | %> 19 | 20 | <% if @commontator_new_comment.nil? %> 21 | $("#<%= id %>").hide(); 22 | 23 | $("#<%= id %>-link").fadeIn(); 24 | <% else %> 25 | $("#<%= id %>").html("<%= escape_javascript( 26 | render partial: 'form', locals: { 27 | comment: @commontator_new_comment, thread: @commontator_thread 28 | } 29 | ) %>"); 30 | <% end %> 31 | 32 | var commontatorComment = $("#commontator-comment-<%= @comment.id %>").hide().fadeIn(); 33 | $('html, body').animate( 34 | { scrollTop: commontatorComment.offset().top - window.innerHeight/2 }, 'fast' 35 | ); 36 | 37 | <%= javascript_proc %> 38 | -------------------------------------------------------------------------------- /app/views/commontator/comments/delete.js.erb: -------------------------------------------------------------------------------- 1 | <%= 2 | render partial: 'show', locals: { 3 | user: @commontator_user, 4 | thread: @commontator_thread, 5 | comment: @comment, 6 | page: @commontator_page, 7 | show_all: @commontator_show_all 8 | } 9 | %> 10 | 11 | <%= javascript_proc %> 12 | -------------------------------------------------------------------------------- /app/views/commontator/comments/edit.js.erb: -------------------------------------------------------------------------------- 1 | $("#commontator-comment-<%= @comment.id %>-body").html("<%= escape_javascript( 2 | render partial: 'form', locals: { comment: @comment } 3 | ) %>"); 4 | 5 | $('#commontator-comment-<%= @comment.id %>-edit-body').focus(); 6 | 7 | <%= javascript_proc %> 8 | -------------------------------------------------------------------------------- /app/views/commontator/comments/new.js.erb: -------------------------------------------------------------------------------- 1 | <% 2 | id = @comment.parent.nil? ? "commontator-thread-#{@commontator_thread.id}-new-comment" : 3 | "commontator-comment-#{@comment.parent.id}-reply" 4 | %> 5 | 6 | var commontatorForm = $("#<%= id %>").html("<%= escape_javascript( 7 | render partial: 'form', locals: { comment: @comment, thread: @commontator_thread } 8 | ) %>").hide().fadeIn(); 9 | $('html, body').animate({ scrollTop: commontatorForm.offset().top - window.innerHeight/2 }, 'fast'); 10 | 11 | $("#<%= id %>-link").hide(); 12 | 13 | $('#<%= id %>-body').focus(); 14 | 15 | <%= javascript_proc %> 16 | -------------------------------------------------------------------------------- /app/views/commontator/comments/show.js.erb: -------------------------------------------------------------------------------- 1 | var commontatorOldCommentIds = $("#commontator-comment-<%= 2 | @comment.id 3 | %>-children").children().map(function() { 4 | return '#' + $(this).attr('id'); 5 | }).toArray().join(','); 6 | 7 | <%= 8 | render partial: 'show', locals: { 9 | user: @commontator_user, 10 | thread: @commontator_thread, 11 | comment: @comment, 12 | page: @commontator_page, 13 | show_all: @commontator_show_all 14 | } 15 | %> 16 | 17 | var commontatorNewComments = $("#commontator-comment-<%= 18 | @comment.id 19 | %>-children").children().not(commontatorOldCommentIds).hide().fadeIn(); 20 | $('html, body').animate( 21 | { scrollTop: commontatorNewComments.offset().top - window.innerHeight/2 }, 'fast' 22 | ); 23 | 24 | <%= javascript_proc %> 25 | -------------------------------------------------------------------------------- /app/views/commontator/comments/update.js.erb: -------------------------------------------------------------------------------- 1 | <%= 2 | render partial: 'show', locals: { 3 | user: @commontator_user, 4 | thread: @commontator_thread, 5 | comment: @comment, 6 | page: @commontator_page, 7 | show_all: @commontator_show_all 8 | } 9 | %> 10 | 11 | <%= javascript_proc %> 12 | -------------------------------------------------------------------------------- /app/views/commontator/comments/vote.js.erb: -------------------------------------------------------------------------------- 1 | $("#commontator-comment-<%= @comment.id %>-votes").html("<%= escape_javascript( 2 | render partial: 'votes', locals: { comment: @comment, user: @commontator_user } 3 | ) %>"); 4 | 5 | <%= javascript_proc %> 6 | -------------------------------------------------------------------------------- /app/views/commontator/shared/_thread.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | # Controllers that use this partial must supply the following variables: 3 | # thread 4 | # user 5 | # page 6 | # show_all 7 | # Additionally, they may override the following variable: 8 | @commontator_thread_show ||= false 9 | %> 10 | 11 | <% if !thread.nil? && thread.can_be_read_by?(user) %> 12 |
13 | <% if @commontator_thread_show %> 14 | <%= 15 | render partial: 'commontator/threads/show', locals: { 16 | thread: thread, user: user, page: page, show_all: show_all 17 | } 18 | %> 19 | <% else %> 20 | <% subscription = thread.subscription_for(user) %> 21 | <%= link_to "#{t 'commontator.thread.actions.show'} (#{ 22 | (subscription.unread_comments(show_all).count.to_s + '/') if subscription 23 | }#{thread.filtered_comments(show_all).count.to_s})", 24 | commontator.thread_path(thread), 25 | remote: true %> 26 | <% end %> 27 |
28 | <% end %> 29 | -------------------------------------------------------------------------------- /app/views/commontator/subscriptions/_link.html.erb: -------------------------------------------------------------------------------- 1 | <%# 2 | Views that use this partial must supply the following variables: 3 | thread 4 | user 5 | %> 6 | 7 | <% is_subscribed = !!thread.subscription_for(user) %> 8 | <% sub_string = is_subscribed ? 'unsubscribe' : 'subscribe' %> 9 | <%= link_to t("commontator.subscription.actions.#{sub_string}"), 10 | commontator.polymorphic_path([sub_string.to_sym, thread]), 11 | method: :put, 12 | id: "commontator-thread-#{thread.id}-#{sub_string}", 13 | class: sub_string, 14 | remote: true %> 15 | -------------------------------------------------------------------------------- /app/views/commontator/subscriptions/subscribe.js.erb: -------------------------------------------------------------------------------- 1 | $("#commontator-thread-<%= @commontator_thread.id %>-subscription").html("<%= escape_javascript( 2 | render partial: 'link', locals: { thread: @commontator_thread, user: @commontator_user } 3 | ) %>"); 4 | 5 | <%= javascript_proc %> 6 | -------------------------------------------------------------------------------- /app/views/commontator/subscriptions_mailer/comment_created.html.erb: -------------------------------------------------------------------------------- 1 |

2 | <%= t 'commontator.email.comment_created.body', 3 | creator_name: @creator_name, commontable_name: @commontable_name %> 4 |

5 | 6 | <%= render partial: 'commontator/comments/body', locals: { comment: @comment } %> 7 | 8 |

9 | <%= t 'commontator.email.thread_link_html', 10 | comment_url: @comment_url, commontable_name: @commontable_name %> 11 |

12 | -------------------------------------------------------------------------------- /app/views/commontator/threads/_hide_show_links.js.erb: -------------------------------------------------------------------------------- 1 | <%# 2 | Views that use this partial must supply the following variables: 3 | thread 4 | %> 5 | 6 | $("#commontator-thread-<%= thread.id %>-hide-link").click(function() { 7 | $("#commontator-thread-<%= thread.id %>-content").hide(); 8 | 9 | var commontatorLink = $("#commontator-thread-<%= thread.id %>-show").fadeIn(); 10 | $('html, body').animate( 11 | { scrollTop: commontatorLink.offset().top - window.innerHeight/2 }, 'fast' 12 | ); 13 | return false; 14 | }); 15 | 16 | $("#commontator-thread-<%= thread.id %>-show-link").click(function() { 17 | var commontatorThread = $("#commontator-thread-<%= thread.id %>-content").fadeIn(); 18 | $('html, body').animate( 19 | { scrollTop: commontatorThread.offset().top - window.innerHeight/2 }, 'fast' 20 | ); 21 | 22 | $("#commontator-thread-<%= thread.id %>-show").hide(); 23 | return false; 24 | }); 25 | 26 | $("#commontator-thread-<%= thread.id %>-hide").show(); 27 | -------------------------------------------------------------------------------- /app/views/commontator/threads/_reply.html.erb: -------------------------------------------------------------------------------- 1 | <%# 2 | Views that use this partial must supply the following variables: 3 | thread 4 | user 5 | %> 6 | 7 |
8 | <% if thread.is_closed? %> 9 |

<%= t 'commontator.thread.status.cannot_post' %>

10 | <% elsif !user %> 11 |

<%= t 'commontator.require_login' %>

12 | <% else %> 13 | <% if @commontator_new_comment.nil? %> 14 | 19 | <% end %> 20 | 21 |
23 | <% unless @commontator_new_comment.nil? %> 24 | <%= 25 | render partial: 'commontator/comments/form', locals: { 26 | comment: @commontator_new_comment, thread: thread 27 | } 28 | %> 29 | <% end %> 30 |
31 | <% end %> 32 |
33 | -------------------------------------------------------------------------------- /app/views/commontator/threads/_show.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | # Views that use this partial must supply the following variables: 3 | # user 4 | # thread 5 | # page 6 | # show_all 7 | 8 | can_subscribe = thread.can_subscribe?(user) 9 | can_edit = thread.can_be_edited_by?(user) 10 | comments = thread.paginated_comments(page, nil, show_all) 11 | nested_comments = thread.nested_comments_for(user, comments, show_all) 12 | %> 13 | 14 |
15 | <%= link_to t('commontator.thread.actions.show'), 16 | '#', 17 | id: "commontator-thread-#{thread.id}-show-link" %> 18 |
19 | 20 |
21 |
22 | 23 | 24 | <%= link_to t('commontator.thread.actions.hide'), 25 | '#', 26 | id: "commontator-thread-#{thread.id}-hide-link" %> 27 | 28 | 29 | <% if can_subscribe %> 30 | 31 | <%= render partial: 'commontator/subscriptions/link', 32 | locals: { thread: thread, user: user } %> 33 | 34 | <% end %> 35 | 36 | <% if can_edit %> 37 | <% 38 | if show_all 39 | filter_class = filter_string = 'filter' 40 | else 41 | filter_string = 'show_all' 42 | filter_class = 'show-all' 43 | end 44 | is_closed = thread.is_closed? 45 | close_string = is_closed ? 'reopen' : 'close' 46 | %> 47 | 48 | <% if thread.is_filtered? %> 49 | <%= link_to t("commontator.thread.actions.#{filter_string}"), 50 | commontator.thread_path(thread, show_all: (show_all ? nil : true)), 51 | id: "commontator-thread-#{thread.id}-#{filter_class}-link", 52 | class: filter_class, 53 | remote: true %> 54 | <% end %> 55 | 56 | <%= link_to t("commontator.thread.actions.#{close_string}"), 57 | commontator.polymorphic_path([close_string.to_sym, thread]), 58 | data: is_closed ? {} : 59 | { confirm: t('commontator.thread.actions.confirm_close') }, 60 | method: :put, 61 | id: "commontator-thread-#{thread.id}-#{close_string}-link", 62 | class: close_string, 63 | remote: true %> 64 | <% end %> 65 | 66 | 67 | 68 | <%= t "commontator.thread.status.#{thread.is_closed? ? 'closed' : 'open'}", 69 | closer_name: (thread.is_closed? ? Commontator.commontator_name(thread.closer) : '') %> 70 | 71 |
72 | 73 | <% if thread.config.comment_order == :l %> 74 | <%= render partial: 'commontator/threads/reply', locals: { thread: thread, user: user } %> 75 | <% end %> 76 | 77 |
78 | <%= 79 | render partial: 'commontator/comments/list', locals: { 80 | user: user, nested_comments: nested_comments 81 | } 82 | %> 83 |
84 | 85 | 106 | 107 | <% if thread.config.comment_order != :l %> 108 | <%= render partial: 'commontator/threads/reply', locals: { thread: thread, user: user } %> 109 | <% end %> 110 |
111 | 112 | 115 | -------------------------------------------------------------------------------- /app/views/commontator/threads/_show.js.erb: -------------------------------------------------------------------------------- 1 | <%# 2 | Views that use this partial must supply the following variables: 3 | user 4 | thread 5 | page 6 | show_all 7 | %> 8 | 9 | 10 | $("#commontator-thread-<%= thread.id %>").html("<%= escape_javascript( 11 | render partial: 'commontator/threads/show', formats: [ :html ], locals: { 12 | user: user, thread: thread, page: page, show_all: show_all 13 | } 14 | ) %>"); 15 | -------------------------------------------------------------------------------- /app/views/commontator/threads/show.js.erb: -------------------------------------------------------------------------------- 1 | <%= 2 | render partial: 'show', locals: { 3 | user: @commontator_user, 4 | thread: @commontator_thread, 5 | page: @commontator_page, 6 | show_all: @commontator_show_all 7 | } 8 | %> 9 | 10 | $("#commontator-thread-<%= @commontator_thread.id %>-comment-list").hide().fadeIn(); 11 | 12 | <%= javascript_proc %> 13 | -------------------------------------------------------------------------------- /arrow.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lml/commontator/729fc34968d131bcb5d31325f3e5536e4ee491d4/arrow.xcf -------------------------------------------------------------------------------- /commontator.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path('lib', __dir__) 2 | 3 | # Maintain your gem's version: 4 | require 'commontator/version' 5 | 6 | # Describe your gem and declare its dependencies: 7 | Gem::Specification.new do |gem| 8 | gem.name = 'commontator' 9 | gem.version = COMMONTATOR_VERSION 10 | gem.authors = [ 'Dante Soares' ] 11 | gem.email = [ 'dante.m.soares@rice.edu' ] 12 | gem.homepage = 'https://github.com/lml/commontator' 13 | gem.license = 'MIT' 14 | gem.summary = 'Allows users to comment on any model in your application.' 15 | gem.description = 'A Rails engine for comments.' 16 | 17 | gem.files = Dir['{app,config,db,lib,vendor}/**/*'] + [ 'MIT-LICENSE', 'README.md' ] 18 | 19 | gem.add_dependency 'rails', '>= 6.0' 20 | gem.add_dependency 'sprockets-rails' 21 | gem.add_dependency 'will_paginate' 22 | 23 | gem.add_development_dependency 'jquery-rails' 24 | gem.add_development_dependency 'sassc-rails' 25 | gem.add_development_dependency 'rails-i18n' 26 | gem.add_development_dependency 'rspec-rails' 27 | gem.add_development_dependency 'rails-controller-testing' 28 | gem.add_development_dependency 'listen' 29 | gem.add_development_dependency 'acts_as_votable' 30 | gem.add_development_dependency 'mailgun_rails' 31 | end 32 | -------------------------------------------------------------------------------- /config/locales/de.yml: -------------------------------------------------------------------------------- 1 | --- 2 | de: 3 | activerecord: 4 | attributes: 5 | commontator/comment: 6 | body: Kommentar 7 | creator: Ersteller 8 | editor: Bearbeiter 9 | thread: Diskussion 10 | parent: Elternkommentar 11 | commontator/subscription: 12 | subscriber: Subscriber 13 | thread: Diskussion 14 | commontator/thread: 15 | commontable: Commontable 16 | errors: 17 | models: 18 | commontator/comment: 19 | attributes: 20 | body: 21 | double_posted: ist ein Duplikat. 22 | models: 23 | commontator/comment: 24 | one: Kommentar 25 | other: Kommentare 26 | commontator/subscription: 27 | one: Subscription 28 | other: Subscriptions 29 | commontator/thread: 30 | one: Diskussion 31 | other: Diskussionen 32 | commontator: 33 | anonymous: Anonymous 34 | comment: 35 | actions: 36 | cancel: Abbrechen 37 | confirm_delete: Sind Sie sicher, dass sie diesen Kommentar löschen wollen? 38 | create: Kommentar speichern 39 | delete: Löschen 40 | edit: Bearbeiten 41 | new: Neuer Kommentar 42 | undelete: Löschen rückgängig 43 | update: Kommentar bearbeiten 44 | reply: Auf Kommentar antworten. 45 | errors: 46 | already_deleted: Dieser Kommentar wurde bereits gelöscht. 47 | create: "Kommentar kann nicht veröffentlicht werden:" 48 | not_deleted: Dieser Kommentar wurde nicht gelöscht.. 49 | update: "Kommentar kann nicht bearbeitet werden:" 50 | status: 51 | created_at: Veröffentlich am %{created_at}. 52 | deleted_by: Kommentar gelöscht von %{deleter_name}. 53 | updated_at: Bearbeitet von %{editor_name} am %{updated_at}. 54 | replying: "%{creator_name} Antworten" 55 | reply_pages: "Antwortseiten:" 56 | email: 57 | comment_created: 58 | body: "%{creator_name} kommentierte zu %{commontable_name}:" 59 | subject: "%{creator_name} veröffentlichte einen Kommentar zu %{commontable_name}" 60 | thread_link_html: Hier klicken um alle Kommentare zu %{commontable_name} zu sehen. 61 | require_login: Sie müssen sich anmelden bevor sie einen Kommentar veröffentlichen können. 62 | subscription: 63 | actions: 64 | subscribe: Subscribe 65 | unsubscribe: Unsubscribe 66 | errors: 67 | already_subscribed: Sie erhalten zu der Diskussion bereits Aktualisierungen 68 | not_subscribed: Sie erhalten zu der Diskussion keine Nachrichten. 69 | thread: 70 | actions: 71 | show_all: Alle Kommentare anzeigen 72 | filter: Kommentare filtern 73 | close: Diskussion beenden 74 | confirm_close: Sind Sie sicher, dass sie die Diskussion schliessen wollen? 75 | reopen: Diskussion wieder eröffnen 76 | show: Kommentare anzeigen 77 | hide: Kommentare ausblenden 78 | errors: 79 | already_closed: Diese Diskussion ist bereits geschlossen. 80 | not_closed: Diese Diskussion ist nicht geschlossen. 81 | status: 82 | cannot_post: Zurzeit können keine neuen Kommentare verfasst werden. 83 | closed: Kommentare (geschlossen von %{closer_name}) 84 | open: Kommentare 85 | pages: "Kommentarseiten:" 86 | time: 87 | formats: 88 | commontator: "%d. %B %Y um %H:%M %Z" 89 | -------------------------------------------------------------------------------- /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 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at https://guides.rubyonrails.org/i18n.html. 31 | 32 | --- 33 | en: 34 | activerecord: 35 | attributes: 36 | commontator/comment: 37 | body: Comment 38 | creator: Creator 39 | editor: Editor 40 | thread: Discussion 41 | parent: Parent Comment 42 | commontator/subscription: 43 | subscriber: Subscriber 44 | thread: Discussion 45 | commontator/thread: 46 | commontable: Commontable 47 | errors: 48 | models: 49 | commontator/comment: 50 | attributes: 51 | body: 52 | double_posted: is a duplicate of another comment. 53 | models: 54 | commontator/comment: 55 | one: comment 56 | other: comments 57 | commontator/subscription: 58 | one: subscription 59 | other: subscriptions 60 | commontator/thread: 61 | one: thread 62 | other: threads 63 | commontator: 64 | anonymous: Anonymous 65 | comment: 66 | actions: 67 | cancel: Cancel 68 | confirm_delete: Are you sure you want to delete this comment? 69 | create: Post Comment 70 | delete: Delete 71 | edit: Edit 72 | new: New Comment 73 | undelete: Undelete 74 | update: Modify Comment 75 | reply: Reply to Comment. 76 | errors: 77 | already_deleted: This comment has already been deleted. 78 | create: "This comment could not be posted because:" 79 | not_deleted: This comment is not deleted. 80 | update: "This comment could not be modified because:" 81 | status: 82 | created_at: Posted on %{created_at}. 83 | deleted_by: Comment deleted by %{deleter_name}. 84 | updated_at: Last modified by %{editor_name} on %{updated_at}. 85 | replying: Replying to %{creator_name} 86 | reply_pages: "Reply pages:" 87 | email: 88 | comment_created: 89 | body: "%{creator_name} commented on %{commontable_name}:" 90 | subject: "%{creator_name} posted a comment on %{commontable_name}" 91 | thread_link_html: Click here to view all comments on %{commontable_name}. 92 | require_login: You must login before you can post a comment. 93 | subscription: 94 | actions: 95 | subscribe: Subscribe 96 | unsubscribe: Unsubscribe 97 | errors: 98 | already_subscribed: You are already subscribed to this discussion. 99 | not_subscribed: You are not subscribed to this discussion. 100 | thread: 101 | actions: 102 | show_all: Show All Comments 103 | filter: Filter Comments 104 | close: Close Discussion 105 | confirm_close: Are you sure you want to close this discussion? 106 | reopen: Reopen Discussion 107 | show: Show Comments 108 | hide: Hide Comments 109 | errors: 110 | already_closed: This discussion has already been closed. 111 | not_closed: This discussion is not closed. 112 | status: 113 | cannot_post: New comments cannot be posted at this time. 114 | closed: Comments (Closed by %{closer_name}) 115 | open: Comments 116 | pages: "Comment pages:" 117 | time: 118 | formats: 119 | commontator: "%b %d %Y at %I:%M%p %Z" 120 | -------------------------------------------------------------------------------- /config/locales/pt-BR.yml: -------------------------------------------------------------------------------- 1 | --- 2 | pt-BR: 3 | activerecord: 4 | attributes: 5 | commontator/comment: 6 | body: Comentário 7 | creator: Criador 8 | editor: Editor 9 | thread: Discussão 10 | parent: Comentário pai 11 | commontator/subscription: 12 | subscriber: Assinante 13 | thread: Discussão 14 | commontator/thread: 15 | commontable: Comentável 16 | errors: 17 | models: 18 | commontator/comment: 19 | attributes: 20 | body: 21 | double_posted: é um comentário duplicado. 22 | models: 23 | commontator/comment: 24 | one: comentário 25 | other: comentários 26 | commontator/subscription: 27 | one: inscrição 28 | other: incrições 29 | commontator/thread: 30 | one: tópico 31 | other: tópicos 32 | commontator: 33 | anonymous: Anônimo 34 | comment: 35 | actions: 36 | cancel: Cancelar 37 | confirm_delete: Tem certeza que deseja remover este comentário? 38 | create: Postar comentário 39 | delete: Deletar 40 | edit: Editar 41 | new: Novo comentário 42 | undelete: Desfazer 43 | update: Editar comentário 44 | reply: Responder ao comentário. 45 | errors: 46 | already_deleted: Este comentário já foi removido. 47 | create: "Este comentário não pôde ser postado porque:" 48 | not_deleted: Este comentário não foi removido. 49 | update: "Este comentário não pôde ser editado porque:" 50 | status: 51 | created_at: Postado em %{created_at}. 52 | deleted_by: Comentário removido por %{deleter_name}. 53 | updated_at: Editado por %{editor_name} em %{updated_at}. 54 | replying: Respondendo a %{creator_name} 55 | reply_pages: "Páginas de respostas:" 56 | email: 57 | comment_created: 58 | body: "%{creator_name} comentou em %{commontable_name}:" 59 | subject: "%{creator_name} postou um comentário em %{commontable_name}" 60 | thread_link_html: Clique aqui para ver todos os comentários em %{commontable_name}. 61 | require_login: Você deve fazer login para postar um comentário. 62 | subscription: 63 | actions: 64 | subscribe: Inscrever-se 65 | unsubscribe: Cancelar inscrição 66 | errors: 67 | already_subscribed: Você já está inscrito nessa discussão. 68 | not_subscribed: Você não está inscrito nessa discussão. 69 | thread: 70 | actions: 71 | show_all: Mostrar todos os comentários 72 | filter: Filtrar comentários 73 | close: Fechar discussão 74 | confirm_close: Tem certeza que deseja fechar a discussão? 75 | reopen: Reabrir discussão 76 | show: Mostrar comentários 77 | hide: Esconder comentários 78 | errors: 79 | already_closed: Essa discussão já foi fechada. 80 | not_closed: Essa discussão não está fechada. 81 | status: 82 | cannot_post: Não é possível postar novos comentários. 83 | closed: Comentários (Fechado por %{closer_name}) 84 | open: Comentários 85 | pages: "Páginas de comentários:" 86 | time: 87 | formats: 88 | commontator: "%d de %B de %Y às %H:%M %Z" 89 | -------------------------------------------------------------------------------- /config/locales/ru.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ru: 3 | activerecord: 4 | attributes: 5 | commontator/comment: 6 | body: Комментарий 7 | creator: Создал 8 | editor: Редактировал 9 | thread: Обсуждение 10 | parent: Родительский Комментарий 11 | commontator/subscription: 12 | subscriber: Подписчик 13 | thread: Обсуждение 14 | commontator/thread: 15 | commontable: Комментируем 16 | errors: 17 | models: 18 | commontator/comment: 19 | attributes: 20 | body: 21 | double_posted: этот комментарий уже есть. 22 | models: 23 | commontator/comment: 24 | one: комментарий 25 | many: комментарии 26 | other: комментарии 27 | commontator/subscription: 28 | one: подписка 29 | many: подписки 30 | other: подписки 31 | commontator/thread: 32 | one: обсуждение 33 | many: обсуждения 34 | other: обсуждения 35 | commontator: 36 | anonymous: Аноним 37 | comment: 38 | actions: 39 | cancel: Отменить 40 | confirm_delete: Вы уверены, что хотите удалить комментарий? 41 | create: Оставить Комментарий 42 | delete: Удалить 43 | edit: Редактировать 44 | new: Новый Комментарий 45 | undelete: Восстановить 46 | update: Изменить Комментарий 47 | reply: Ответить на Комментарий. 48 | errors: 49 | already_deleted: Этот комментарий был удален. 50 | create: Комментарий нельзя оставить потому что 51 | not_deleted: Этот комментарий не удален. 52 | update: Этот комментарий нельзя изменить потому что 53 | status: 54 | created_at: Создан %{created_at}. 55 | deleted_by: Комментарий был удален %{deleter_name}. 56 | updated_at: Редактировал %{editor_name}, %{updated_at}. 57 | replying: Отвечая %{creator_name} 58 | reply_pages: "Страницы ответов:" 59 | email: 60 | comment_created: 61 | body: "%{creator_name} комментировал %{commontable_name}:" 62 | subject: "%{creator_name} оставил комментарий к %{commontable_name}" 63 | thread_link_html: Нажмите здесь чтобы посмотреть все комментарии к %{commontable_name}. 64 | require_login: Вы должны залогиниться прежде чем оставить комментарий. 65 | subscription: 66 | actions: 67 | subscribe: Подписаться 68 | unsubscribe: Отписаться 69 | errors: 70 | already_subscribed: Вы уже подписаны на это обсуждение. 71 | not_subscribed: Вы не подписаны на это обсуждение. 72 | thread: 73 | actions: 74 | show_all: Показать все комментарии 75 | filter: Фильтровать комментарии 76 | close: Закрыть обсуждение 77 | confirm_close: Вы уверены, что хотите закрыть обсуждение? 78 | reopen: Открыть заново обсуждение 79 | show: Показать комментарии 80 | hide: скрыть комментарии 81 | errors: 82 | already_closed: Это обсуждение было закрыто. 83 | not_closed: Это обсуждение не закрыто. 84 | status: 85 | cannot_post: Новый комментарии нельзя разместить в данный момент. 86 | closed: Комментарии (Закрыты %{closer_name}) 87 | open: Комментарии 88 | pages: "Страницы комментариев:" 89 | time: 90 | formats: 91 | commontator: "%d %b %Y в %H:%M %Z" 92 | -------------------------------------------------------------------------------- /config/locales/zh.yml: -------------------------------------------------------------------------------- 1 | --- 2 | zh: &zh 3 | activerecord: 4 | attributes: 5 | commontator/comment: 6 | body: 留言 7 | creator: 留言者 8 | editor: 編輯 9 | thread: 討論 10 | parent: 家长留言 11 | commontator/subscription: 12 | subscriber: 訂閱者 13 | thread: 討論 14 | commontator/thread: 15 | commontable: 可以留言 16 | errors: 17 | models: 18 | commontator/comment: 19 | attributes: 20 | body: 21 | double_posted: 已經有一筆相同的留言。 22 | models: 23 | commontator/comment: 24 | one: 留言 25 | other: 留言 26 | commontator/subscription: 27 | one: 訂閱 28 | other: 訂閱 29 | commontator/thread: 30 | one: 討論 31 | other: 討論 32 | commontator: 33 | anonymous: 匿名 34 | comment: 35 | actions: 36 | cancel: 取消 37 | confirm_delete: 確定要刪除這筆留言嗎? 38 | create: 發佈 39 | delete: 刪除 40 | edit: 編輯 41 | new: 新留言 42 | undelete: 未刪除 43 | update: 修改留言 44 | reply: 回复评论 45 | errors: 46 | already_deleted: 此留言已經被刪除了 47 | create: 此留言無法發佈因為: 48 | not_deleted: 此留言未被刪除 49 | update: 此留言無法被修改因為: 50 | status: 51 | created_at: 於%{created_at}發佈 52 | deleted_by: 被%{deleter_name}刪除 53 | updated_at: 最在於%{updated_at}被%{editor_name}修改 54 | replying: 回复%{creator_name} 55 | reply_pages: 回复页面: 56 | email: 57 | comment_created: 58 | body: "%{creator_name}留言於%{commontable_name}" 59 | subject: "%{creator_name}於%{commontable_name}發佈了一則留言" 60 | thread_link_html: 點此處看%{commontable_name}的所有留言 61 | require_login: 你需要先登入 62 | subscription: 63 | actions: 64 | subscribe: 訂閱 65 | unsubscribe: 取消訂閱 66 | errors: 67 | already_subscribed: 你已經訂閱過了 68 | not_subscribed: 你沒有訂閱過 69 | thread: 70 | actions: 71 | show_all: 列出所有留言 72 | filter: 過濾留言 73 | close: 關閉討論 74 | confirm_close: 你確定要關閉此討論嗎? 75 | reopen: 回復討論 76 | show: 打開留言 77 | hide: 关闭留言 78 | errors: 79 | already_closed: 此討論已經被關閉 80 | not_closed: 此討論尚未被關閉 81 | status: 82 | cannot_post: 現在還不能發佈 83 | closed: 留言已經被%{closer_name}關閉 84 | open: 留言 85 | pages: 留言页面: 86 | time: 87 | formats: 88 | commontator: "%Y年%m月%d日 於 %H:%M %Z" 89 | 90 | zh-CN: 91 | <<: *zh 92 | 93 | zh-HK: 94 | <<: *zh 95 | 96 | zh-TW: 97 | <<: *zh 98 | 99 | zh-YUE: 100 | <<: *zh 101 | time: 102 | formats: 103 | commontator: "%Y年%m月%d號 於 %H:%M %Z" 104 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Commontator::Engine.routes.draw do 2 | resources :threads, only: [ :show ] do 3 | resources :comments, except: [ :index, :destroy ], shallow: true do 4 | member do 5 | put 'upvote' 6 | put 'downvote' 7 | put 'unvote' 8 | 9 | put 'delete' 10 | put 'undelete' 11 | end 12 | end 13 | 14 | member do 15 | get 'mentions' if Commontator.mentions_enabled 16 | 17 | put 'subscribe', to: 'subscriptions#subscribe' 18 | put 'unsubscribe', to: 'subscriptions#unsubscribe' 19 | 20 | put 'close' 21 | put 'reopen' 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /db/migrate/10_install_commontator.rb: -------------------------------------------------------------------------------- 1 | class InstallCommontator < ActiveRecord::Migration[6.0] 2 | def change 3 | create_table :commontator_threads do |t| 4 | t.references :commontable, 5 | polymorphic: true, 6 | index: { unique: true, name: 'index_commontator_threads_on_c_id_and_c_type' } 7 | t.references :closer, polymorphic: true 8 | 9 | t.datetime :closed_at 10 | 11 | t.timestamps 12 | end 13 | 14 | create_table :commontator_comments do |t| 15 | t.references :thread, null: false, index: false, foreign_key: { 16 | to_table: :commontator_threads, on_update: :cascade, on_delete: :cascade 17 | } 18 | t.references :creator, polymorphic: true, null: false, index: false 19 | t.references :editor, polymorphic: true 20 | 21 | t.text :body, null: false 22 | t.datetime :deleted_at 23 | 24 | t.integer :cached_votes_up, default: 0, index: true 25 | t.integer :cached_votes_down, default: 0, index: true 26 | 27 | t.timestamps 28 | end 29 | 30 | add_index :commontator_comments, [ :creator_id, :creator_type, :thread_id ], 31 | name: 'index_commontator_comments_on_c_id_and_c_type_and_t_id' 32 | add_index :commontator_comments, [ :thread_id, :created_at ] 33 | 34 | create_table :commontator_subscriptions do |t| 35 | t.references :thread, null: false, foreign_key: { 36 | to_table: :commontator_threads, on_update: :cascade, on_delete: :cascade 37 | } 38 | t.references :subscriber, polymorphic: true, null: false, index: false 39 | 40 | t.timestamps 41 | end 42 | 43 | add_index :commontator_subscriptions, [ :subscriber_id, :subscriber_type, :thread_id ], 44 | unique: true, 45 | name: 'index_commontator_subscriptions_on_s_id_and_s_type_and_t_id' 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /db/migrate/11_add_replying_to_comments.rb: -------------------------------------------------------------------------------- 1 | class AddReplyingToComments < ActiveRecord::Migration[6.0] 2 | def change 3 | add_reference :commontator_comments, :parent, foreign_key: { 4 | to_table: :commontator_comments, on_update: :restrict, on_delete: :cascade 5 | } 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/commontator.rb: -------------------------------------------------------------------------------- 1 | require 'will_paginate' 2 | require 'commontator/version' 3 | 4 | module Commontator 5 | # These requires need the Commontator module to function properly 6 | require 'commontator/engine' 7 | require 'commontator/config' 8 | require 'commontator/controllers' 9 | require 'commontator/acts_as_commontator' 10 | require 'commontator/acts_as_commontable' 11 | 12 | VERSION = COMMONTATOR_VERSION 13 | 14 | include Commontator::Config 15 | end 16 | -------------------------------------------------------------------------------- /lib/commontator/acts_as_commontable.rb: -------------------------------------------------------------------------------- 1 | require_relative 'commontable_config' 2 | require_relative 'build_thread' 3 | 4 | module Commontator::ActsAsCommontable 5 | def self.included(base) 6 | base.class_attribute :is_commontable 7 | base.is_commontable = false 8 | base.extend(ClassMethods) 9 | end 10 | 11 | module ClassMethods 12 | def acts_as_commontable(options = {}) 13 | class_exec do 14 | association_options = options.extract!(:dependent) 15 | association_options[:dependent] ||= :nullify 16 | 17 | cattr_accessor :commontable_config 18 | self.commontable_config = Commontator::CommontableConfig.new(options) 19 | 20 | has_one :commontator_thread, **association_options.merge( 21 | as: :commontable, class_name: 'Commontator::Thread' 22 | ) 23 | 24 | prepend Commontator::BuildThread 25 | 26 | # Support creating acts_as_commontable records without a commontator_thread when migrating 27 | validates :commontator_thread, presence: true, if: -> { Commontator::Thread.table_exists? } 28 | 29 | self.is_commontable = true 30 | end 31 | end 32 | 33 | alias_method :acts_as_commentable, :acts_as_commontable 34 | end 35 | end 36 | 37 | ActiveSupport.on_load :active_record do 38 | include Commontator::ActsAsCommontable 39 | end 40 | -------------------------------------------------------------------------------- /lib/commontator/acts_as_commontator.rb: -------------------------------------------------------------------------------- 1 | require_relative 'commontator_config' 2 | 3 | module Commontator::ActsAsCommontator 4 | def self.included(base) 5 | base.class_attribute :is_commontator 6 | base.is_commontator = false 7 | base.extend(ClassMethods) 8 | end 9 | 10 | module ClassMethods 11 | def acts_as_commontator(options = {}) 12 | class_exec do 13 | association_options = options.extract!(:dependent) 14 | association_options[:dependent] ||= :destroy 15 | 16 | cattr_accessor :commontator_config 17 | self.commontator_config = Commontator::CommontatorConfig.new(options) 18 | 19 | has_many :commontator_comments, **association_options.merge( 20 | as: :creator, class_name: 'Commontator::Comment' 21 | ) 22 | has_many :commontator_subscriptions, **association_options.merge( 23 | as: :subscriber, class_name: 'Commontator::Subscription' 24 | ) 25 | 26 | self.is_commontator = true 27 | end 28 | end 29 | 30 | alias_method :acts_as_commonter, :acts_as_commontator 31 | alias_method :acts_as_commentator, :acts_as_commontator 32 | alias_method :acts_as_commenter, :acts_as_commontator 33 | end 34 | end 35 | 36 | ActiveSupport.on_load :active_record do 37 | include Commontator::ActsAsCommontator 38 | end 39 | -------------------------------------------------------------------------------- /lib/commontator/build_thread.rb: -------------------------------------------------------------------------------- 1 | module Commontator::BuildThread 2 | def commontator_thread 3 | @commontator_thread ||= super 4 | return @commontator_thread unless @commontator_thread.nil? 5 | 6 | @commontator_thread = build_commontator_thread.tap do |thread| 7 | thread.save if persisted? 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/commontator/commontable_config.rb: -------------------------------------------------------------------------------- 1 | require_relative 'config' 2 | 3 | class Commontator::CommontableConfig 4 | Commontator::Config::COMMONTABLE_ATTRIBUTES.each do |attribute| 5 | attr_accessor attribute 6 | end 7 | 8 | # For backwards-compatibility with Integer comments_per_page 9 | def comments_per_page=(cpp) 10 | @comments_per_page = [ cpp ].flatten 11 | end 12 | 13 | def initialize(options = {}) 14 | Commontator::Config::COMMONTABLE_ATTRIBUTES.each do |attribute| 15 | send "#{attribute}=", options[attribute] || Commontator.send(attribute) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/commontator/commontator_config.rb: -------------------------------------------------------------------------------- 1 | require_relative 'config' 2 | 3 | class Commontator::CommontatorConfig 4 | Commontator::Config::COMMONTATOR_ATTRIBUTES.each do |attribute| 5 | attr_accessor attribute 6 | end 7 | 8 | def initialize(options = {}) 9 | Commontator::Config::COMMONTATOR_ATTRIBUTES.each do |attribute| 10 | send "#{attribute}=", options[attribute] || Commontator.send(attribute) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/commontator/config.rb: -------------------------------------------------------------------------------- 1 | module Commontator::Config 2 | # Attributes 3 | 4 | # Can be set in initializer only 5 | ENGINE_ATTRIBUTES = [ 6 | :current_user_proc, 7 | :javascript_proc 8 | ] 9 | 10 | # Can be set in initializer or passed as an option to acts_as_commontator 11 | COMMONTATOR_ATTRIBUTES = [ 12 | :user_name_proc, 13 | :user_link_proc, 14 | :user_avatar_proc, 15 | :user_email_proc, 16 | :user_mentions_proc 17 | ] 18 | 19 | # Can be set in initializer or passed as an option to acts_as_commontable 20 | COMMONTABLE_ATTRIBUTES = [ 21 | :comment_filter, 22 | :thread_read_proc, 23 | :thread_moderator_proc, 24 | :comment_editing, 25 | :comment_deletion, 26 | :moderator_permissions, 27 | :comment_voting, 28 | :vote_count_proc, 29 | :comment_order, 30 | :thread_subscription, 31 | :email_from_proc, 32 | :commontable_name_proc, 33 | :comment_url_proc, 34 | :new_comment_style, 35 | :comment_reply_style, 36 | :comments_per_page, 37 | :mentions_enabled 38 | ] 39 | 40 | DEPRECATED_ATTRIBUTES = [ 41 | [:moderators_can_edit_comments, :moderator_permissions], 42 | [:hide_deleted_comments, :comment_filter], 43 | [:hide_closed_threads, :thread_read_proc], 44 | [:wp_link_renderer_proc], 45 | [:voting_text_proc, :vote_count_proc], 46 | [:user_name_clickable, :user_link_proc], 47 | [:user_admin_proc, :thread_moderator_proc], 48 | [:auto_subscribe_on_comment, :thread_subscription], 49 | [:can_edit_own_comments, :comment_editing], 50 | [:can_edit_old_comments, :comment_editing], 51 | [:can_delete_own_comments, :comment_deletion], 52 | [:can_delete_old_comments, :comment_deletion], 53 | [:can_subscribe_to_thread, :thread_subscription], 54 | [:can_vote_on_comments, :comment_voting], 55 | [:combine_upvotes_and_downvotes, :vote_count_proc], 56 | [:comments_order, :comment_order], 57 | [:closed_threads_are_readable, :thread_read_proc], 58 | [:deleted_comments_are_visible, :comment_filter], 59 | [:can_read_thread_proc, :thread_read_proc], 60 | [:can_edit_thread_proc, :thread_moderator_proc], 61 | [:admin_can_edit_comments, :moderator_permissions], 62 | [:subscription_email_enable_proc, :user_email_proc], 63 | [:comment_name, 'config/locales'], 64 | [:comment_create_verb_present, 'config/locales'], 65 | [:comment_create_verb_past, 'config/locales'], 66 | [:comment_edit_verb_present, 'config/locales'], 67 | [:comment_edit_verb_past, 'config/locales'], 68 | [:timestamp_format, 'config/locales'], 69 | [:subscription_email_to_proc, 'config/locales'], 70 | [:subscription_email_from_proc, :email_from_proc], 71 | [:subscription_email_subject_proc, 'config/locales'], 72 | [:comments_ordered_by_votes, :comment_order], 73 | [:current_user_method, :current_user_proc], 74 | [:user_missing_name, 'config/locales'], 75 | [:user_email_method, :user_email_proc], 76 | [:user_name_method, :user_name_proc], 77 | [:commontable_id_method], 78 | [:commontable_url_proc, :comment_url_proc] 79 | ] 80 | 81 | def self.included(base) 82 | (ENGINE_ATTRIBUTES + COMMONTATOR_ATTRIBUTES + COMMONTABLE_ATTRIBUTES).each do |attribute| 83 | base.mattr_accessor attribute 84 | end 85 | 86 | base.mattr_accessor :show_deprecation_warning 87 | DEPRECATED_ATTRIBUTES.each do |deprecated, replacement| 88 | base.define_singleton_method(deprecated) do 89 | base.show_deprecation_warning = true 90 | replacement_string = (replacement.nil? ? 'No replacement is available. You can safely remove it from your configuration file.' : "Use `#{replacement.to_s}` instead.") 91 | warn "\n[COMMONTATOR] Deprecation: `config.#{deprecated.to_s}` is deprecated and has been disabled. #{replacement_string}\n" 92 | end 93 | 94 | base.define_singleton_method("#{deprecated.to_s}=") { |obj| base.send(deprecated) } 95 | end 96 | 97 | base.extend ClassMethods 98 | end 99 | 100 | module ClassMethods 101 | def configure 102 | self.show_deprecation_warning = false 103 | 104 | yield self 105 | 106 | warn("\n[COMMONTATOR] We recommend that you backup the config/initializers/commontator.rb file, rename or remove it, run rake commontator:install:initializers to copy the new default one, then configure it to your liking.\n") if show_deprecation_warning 107 | end 108 | 109 | def commontator_config(user) 110 | (user && user.is_commontator) ? user.commontator_config : self 111 | end 112 | 113 | def commontable_config(obj) 114 | (obj && obj.is_commontable) ? obj.commontable_config : self 115 | end 116 | 117 | def commontator_name(user) 118 | commontator_config(user).user_name_proc.call(user) 119 | end 120 | 121 | def commontator_link(user, routing_proxy) 122 | commontator_config(user).user_link_proc.call(user, routing_proxy) 123 | end 124 | 125 | def commontator_email(user, mailer = nil) 126 | commontator_config(user).user_email_proc.call(user, mailer) 127 | end 128 | 129 | def commontator_avatar(user, view) 130 | commontator_config(user).user_avatar_proc.call(user, view) 131 | end 132 | 133 | def commontator_mentions(user, thread, search_phrase) 134 | commontator_config(user).user_mentions_proc.call(user, thread, search_phrase) 135 | end 136 | 137 | def commontable_name(commontable) 138 | commontable_config(commontable).commontable_name_proc.call(commontable) 139 | end 140 | 141 | def comment_url(comment, routing_proxy) 142 | commontable_config(comment.thread.commontable).comment_url_proc.call(comment, routing_proxy) 143 | end 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /lib/commontator/controllers.rb: -------------------------------------------------------------------------------- 1 | require_relative 'shared_helper' 2 | 3 | module Commontator::Controllers 4 | def commontator_set_thread_variables 5 | return if @commontator_thread.nil? || !@commontator_thread.can_be_read_by?(@commontator_user) 6 | 7 | @commontator_page = [params[:page].to_i, 1].max 8 | @commontator_show_all = !params[:show_all].blank? && 9 | @commontator_thread.can_be_edited_by?(@commontator_user) 10 | end 11 | 12 | def commontator_set_new_comment 13 | return unless @commontator_thread.config.new_comment_style == :t 14 | 15 | new_comment = Commontator::Comment.new(creator: @commontator_user, thread: @commontator_thread) 16 | @commontator_new_comment = new_comment if new_comment.can_be_created_by?(@commontator_user) 17 | end 18 | 19 | def commontator_thread_show(commontable) 20 | commontator_set_user 21 | commontator_set_thread(commontable) 22 | commontator_set_thread_variables 23 | commontator_set_new_comment 24 | 25 | @commontator_thread_show = true 26 | @commontator_thread.mark_as_read_for(@commontator_user) 27 | end 28 | end 29 | 30 | ActiveSupport.on_load :action_controller do 31 | include Commontator::Controllers 32 | end 33 | -------------------------------------------------------------------------------- /lib/commontator/engine.rb: -------------------------------------------------------------------------------- 1 | require 'commontator' 2 | require 'sprockets/railtie' 3 | 4 | class Commontator::Engine < ::Rails::Engine 5 | isolate_namespace Commontator 6 | 7 | # Files in installed gems don't change during development, 8 | # but still cause issues in Rails 7 if autoloaded in an initializer 9 | # To fix this, make sure they are autoloaded only once 10 | config.autoload_once_paths = config.autoload_paths + config.eager_load_paths 11 | 12 | config.assets.precompile += [ 'commontator/*.png' ] 13 | end 14 | -------------------------------------------------------------------------------- /lib/commontator/shared_helper.rb: -------------------------------------------------------------------------------- 1 | module Commontator::SharedHelper 2 | def commontator_set_user 3 | @commontator_user = Commontator.current_user_proc.call(self) 4 | end 5 | 6 | def commontator_set_thread(commontable) 7 | @commontator_thread = commontable.commontator_thread 8 | end 9 | 10 | def commontator_thread(commontable) 11 | commontator_set_user 12 | commontator_set_thread(commontable) 13 | 14 | render( 15 | partial: 'commontator/shared/thread', locals: { 16 | user: @commontator_user, 17 | thread: @commontator_thread, 18 | page: @commontator_page, 19 | show_all: @commontator_show_all 20 | } 21 | ).html_safe 22 | end 23 | 24 | def commontator_gravatar_image_tag(user, border = 1, options = {}) 25 | email = Commontator.commontator_email(user) || '' 26 | name = Commontator.commontator_name(user) || '' 27 | 28 | url = "https://secure.gravatar.com/avatar/#{Digest::MD5.hexdigest(email)}?#{options.to_query}" 29 | 30 | image_tag(url, alt: name, title: name, border: border) 31 | end 32 | 33 | # Unlike the Rails versions of split_paragraphs and simple_format, Commontator's: 34 | # - Split paragraphs on any number of newlines optionally adjacent to spaces 35 | # - Create all

tags (no
tags) 36 | # - Do not add

tags between other html tags 37 | def commontator_split_paragraphs(text) 38 | return [] if text.blank? 39 | 40 | text.to_str.gsub(/\r\n?/, "\n").gsub(/>\s*\n<").split(/\s*\n\s*/).reject(&:blank?) 41 | end 42 | 43 | def commontator_simple_format(text, html_options = {}, options = {}) 44 | wrapper_tag = options.fetch(:wrapper_tag, :p) 45 | 46 | text = sanitize(text) if options.fetch(:sanitize, true) 47 | paragraphs = commontator_split_paragraphs(text) 48 | 49 | if paragraphs.empty? 50 | content_tag(wrapper_tag, nil, html_options) 51 | else 52 | paragraphs.map! do |paragraph| 53 | paragraph.starts_with?('<') && paragraph.ends_with?('>') ? 54 | raw(paragraph) : content_tag(wrapper_tag, raw(paragraph), html_options) 55 | end.join("\n").html_safe 56 | end 57 | end 58 | end 59 | 60 | ActiveSupport.on_load :action_controller do 61 | include Commontator::SharedHelper 62 | end 63 | 64 | ActiveSupport.on_load :action_controller_base do 65 | helper Commontator::SharedHelper 66 | end 67 | -------------------------------------------------------------------------------- /lib/commontator/version.rb: -------------------------------------------------------------------------------- 1 | COMMONTATOR_VERSION = '7.0.1' 2 | -------------------------------------------------------------------------------- /lib/tasks/commontator_tasks.rake: -------------------------------------------------------------------------------- 1 | COMMONTATOR_COPY_TASKS = ['config/locales', 'app/assets/images', 2 | 'app/assets/stylesheets', 'app/views', 'app/mailers', 3 | 'app/helpers', 'app/controllers', 'app/models'] 4 | 5 | namespace :commontator do 6 | namespace :install do 7 | desc "Copy initializers from commontator to application" 8 | task :initializers do 9 | Dir.glob(File.expand_path('../../config/initializers/*.rb', __dir__)) do |file| 10 | if File.exist?(File.expand_path(File.basename(file), 'config/initializers')) 11 | print "NOTE: Initializer #{File.basename(file)} from commontator has been skipped. Initializer with the same name already exists.\n" 12 | else 13 | cp file, 'config/initializers', verbose: false 14 | print "Copied initializer #{File.basename(file)} from commontator\n" 15 | end 16 | end 17 | end 18 | end 19 | 20 | namespace :copy do 21 | COMMONTATOR_COPY_TASKS.each do |path| 22 | name = File.basename(path) 23 | desc "Copy #{name} from commontator to application" 24 | task name.to_sym do 25 | namespace = path.start_with?('app') ? '/commontator' : '' 26 | cp_r File.expand_path("../../#{path}#{namespace}", __dir__), path, verbose: false 27 | print "Copied #{name} from commontator\n" 28 | end 29 | end 30 | end 31 | 32 | desc "Copy initializers and migrations from commontator to application" 33 | task :install do 34 | Rake::Task["commontator:install:initializers"].invoke 35 | Rake::Task["commontator:install:migrations"].invoke 36 | end 37 | 38 | desc "Copy assets, views, mailers, helpers, controllers and models from commontator to application" 39 | task :copy do 40 | COMMONTATOR_COPY_TASKS.each do |path| 41 | Rake::Task["commontator:copy:#{File.basename(path)}"].invoke 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" from the root of your application. 3 | 4 | ENGINE_ROOT = File.expand_path('..', __dir__) 5 | ENGINE_PATH = File.expand_path('../lib/commontator/engine', __dir__) 6 | 7 | require 'rails/all' 8 | 9 | APP_PATH = File.expand_path('spec/dummy/config/application', ENGINE_ROOT) 10 | 11 | require 'rails/commands' 12 | -------------------------------------------------------------------------------- /spec/controllers/commontator/subscriptions_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Commontator::SubscriptionsController, type: :controller do 4 | routes { Commontator::Engine.routes } 5 | 6 | before { setup_controller_spec } 7 | 8 | context 'authorized' do 9 | before do 10 | @user.can_read = true 11 | Thread.current[:user] = @user 12 | end 13 | 14 | context 'PUT #subscribe' do 15 | it 'works' do 16 | put :subscribe, params: { id: @thread.id } 17 | expect(response).to redirect_to(@commontable_path) 18 | expect(assigns(:commontator_thread).errors).to be_empty 19 | expect(@thread.subscription_for(@user)).not_to be_nil 20 | 21 | @thread.unsubscribe(@user) 22 | @user.can_read = false 23 | @user.can_edit = true 24 | put :subscribe, params: { id: @thread.id } 25 | expect(response).to redirect_to(@commontable_path) 26 | expect(assigns(:commontator_thread).errors).to be_empty 27 | expect(@thread.subscription_for(@user)).not_to be_nil 28 | 29 | @thread.unsubscribe(@user) 30 | @user.can_edit = false 31 | @user.is_admin = true 32 | put :subscribe, params: { id: @thread.id } 33 | expect(response).to redirect_to(@commontable_path) 34 | expect(assigns(:commontator_thread).errors).to be_empty 35 | expect(@thread.subscription_for(@user)).not_to be_nil 36 | end 37 | end 38 | 39 | context 'PUT #unsubscribe' do 40 | it 'works' do 41 | @thread.subscribe(@user) 42 | put :unsubscribe, params: { id: @thread.id } 43 | expect(response).to redirect_to(@commontable_path) 44 | expect(assigns(:commontator_thread).errors).to be_empty 45 | expect(@thread.subscription_for(@user)).to be_nil 46 | 47 | @thread.subscribe(@user) 48 | @user.can_read = false 49 | @user.can_edit = true 50 | put :unsubscribe, params: { id: @thread.id } 51 | expect(response).to redirect_to(@commontable_path) 52 | expect(assigns(:commontator_thread).errors).to be_empty 53 | expect(@thread.subscription_for(@user)).to be_nil 54 | 55 | @thread.subscribe(@user) 56 | @user.can_edit = false 57 | @user.is_admin = true 58 | put :unsubscribe, params: { id: @thread.id } 59 | expect(response).to redirect_to(@commontable_path) 60 | expect(assigns(:commontator_thread).errors).to be_empty 61 | expect(@thread.subscription_for(@user)).to be_nil 62 | end 63 | end 64 | end 65 | 66 | context 'unauthorized' do 67 | context 'PUT #subscribe' do 68 | it 'returns 403 Forbidden' do 69 | put :subscribe, params: { id: @thread.id } 70 | expect(response).to have_http_status(:forbidden) 71 | expect(@thread.subscription_for(nil)).to be_nil 72 | expect(@thread.subscription_for(@user)).to be_nil 73 | 74 | Thread.current[:user] = @user 75 | put :subscribe, params: { id: @thread.id } 76 | expect(response).to have_http_status(:forbidden) 77 | expect(@thread.subscription_for(@user)).to be_nil 78 | 79 | @thread.subscribe(@user) 80 | @user.can_read = true 81 | put :subscribe, params: { id: @thread.id } 82 | expect(response).to redirect_to(@commontable_path) 83 | expect(assigns(:commontator_thread).errors).not_to be_empty 84 | end 85 | end 86 | 87 | context 'PUT #unsubscribe' do 88 | it 'returns 403 Forbidden' do 89 | @thread.subscribe(@user) 90 | put :unsubscribe, params: { id: @thread.id } 91 | expect(response).to have_http_status(:forbidden) 92 | expect(@thread.subscription_for(nil)).to be_nil 93 | expect(@thread.subscription_for(@user)).not_to be_nil 94 | 95 | Thread.current[:user] = @user 96 | put :unsubscribe, params: { id: @thread.id } 97 | expect(response).to have_http_status(:forbidden) 98 | expect(@thread.subscription_for(@user)).not_to be_nil 99 | 100 | @thread.unsubscribe(@user) 101 | @user.can_read = true 102 | put :unsubscribe, params: { id: @thread.id } 103 | expect(response).to redirect_to(@commontable_path) 104 | expect(assigns(:commontator_thread).errors).not_to be_empty 105 | end 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /spec/controllers/commontator/threads_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Commontator::ThreadsController, type: :controller do 4 | routes { Commontator::Engine.routes } 5 | 6 | before { setup_controller_spec } 7 | 8 | context 'authorized' do 9 | before do 10 | @user.can_read = true 11 | Thread.current[:user] = @user 12 | end 13 | 14 | context 'GET #show' do 15 | it 'works' do 16 | get :show, params: { id: @thread.id } 17 | expect(response).to redirect_to(@commontable_path) 18 | 19 | @user.can_read = false 20 | @user.can_edit = true 21 | get :show, params: { id: @thread.id } 22 | expect(response).to redirect_to(@commontable_path) 23 | 24 | @user.can_edit = false 25 | @user.is_admin = true 26 | get :show, params: { id: @thread.id } 27 | expect(response).to redirect_to(@commontable_path) 28 | end 29 | end 30 | 31 | context 'open' do 32 | context 'PUT #close' do 33 | it 'works' do 34 | @user.can_edit = true 35 | put :close, params: { id: @thread.id } 36 | expect(response).to redirect_to(@commontable_path) 37 | expect(assigns(:commontator_thread).errors).to be_empty 38 | expect(assigns(:commontator_thread).is_closed?).to eq true 39 | expect(assigns(:commontator_thread).closer).to eq @user 40 | 41 | expect(assigns(:commontator_thread).reopen).to eq true 42 | @user.can_edit = false 43 | @user.is_admin = true 44 | put :close, params: { id: @thread.id } 45 | expect(response).to redirect_to(@commontable_path) 46 | expect(assigns(:commontator_thread).errors).to be_empty 47 | expect(assigns(:commontator_thread).is_closed?).to eq true 48 | expect(assigns(:commontator_thread).closer).to eq @user 49 | end 50 | end 51 | 52 | context 'PUT #reopen' do 53 | it 'redirects to the thread and returns an error message' do 54 | @user.can_edit = true 55 | put :reopen, params: { id: @thread.id } 56 | expect(response).to redirect_to(@commontable_path) 57 | expect(assigns(:commontator_thread).errors).not_to be_empty 58 | expect(assigns(:commontator_thread).is_closed?).to eq false 59 | expect(assigns(:commontator_thread).closer).to be_nil 60 | 61 | @user.can_edit = false 62 | @user.is_admin = true 63 | put :reopen, params: { id: @thread.id } 64 | expect(response).to redirect_to(@commontable_path) 65 | expect(assigns(:commontator_thread).errors).not_to be_empty 66 | expect(assigns(:commontator_thread).is_closed?).to eq false 67 | expect(assigns(:commontator_thread).closer).to be_nil 68 | end 69 | end 70 | end 71 | 72 | context 'closed' do 73 | before { expect(@thread.close).to eq true } 74 | 75 | context 'PUT #reopen' do 76 | it 'works' do 77 | @user.can_edit = true 78 | put :reopen, params: { id: @thread.id } 79 | expect(response).to redirect_to(@commontable_path) 80 | expect(assigns(:commontator_thread).errors).to be_empty 81 | expect(assigns(:commontator_thread).is_closed?).to eq false 82 | 83 | expect(assigns(:commontator_thread).close).to eq true 84 | @user.can_edit = false 85 | @user.is_admin = true 86 | put :reopen, params: { id: @thread.id } 87 | expect(response).to redirect_to(@commontable_path) 88 | expect(assigns(:commontator_thread).errors).to be_empty 89 | expect(assigns(:commontator_thread).is_closed?).to eq false 90 | end 91 | end 92 | 93 | context 'PUT #close' do 94 | it 'redirects to the thread and returns an error message' do 95 | @user.can_edit = true 96 | put :close, params: { id: @thread.id } 97 | expect(response).to redirect_to(@commontable_path) 98 | expect(assigns(:commontator_thread).errors).not_to be_empty 99 | expect(assigns(:commontator_thread).is_closed?).to eq true 100 | expect(assigns(:commontator_thread).closer).to be_nil 101 | 102 | @user.can_edit = false 103 | @user.is_admin = true 104 | put :close, params: { id: @thread.id } 105 | expect(response).to redirect_to(@commontable_path) 106 | expect(assigns(:commontator_thread).errors).not_to be_empty 107 | expect(assigns(:commontator_thread).is_closed?).to eq true 108 | expect(assigns(:commontator_thread).closer).to be_nil 109 | end 110 | end 111 | end 112 | 113 | context 'GET #mentions' do 114 | let(:search_phrase) { nil } 115 | let(:call_request) do 116 | get :mentions, params: { id: @thread.id, format: :json, q: search_phrase } 117 | end 118 | 119 | let!(:other_user) { DummyUser.create } 120 | 121 | context 'mentions enabled' do 122 | context 'query is blank' do 123 | it 'returns a JSON error message' do 124 | call_request 125 | expect(response).to have_http_status(:unprocessable_entity) 126 | expect(JSON.parse(response.body)['errors']).to( 127 | include('Query string is too short (minimum 3 characters)') 128 | ) 129 | end 130 | end 131 | 132 | context 'query is too short' do 133 | let(:search_phrase) { 'Us' } 134 | 135 | it 'returns a JSON error message' do 136 | call_request 137 | expect(response).to have_http_status(:unprocessable_entity) 138 | expect(JSON.parse(response.body)['errors']).to( 139 | include('Query string is too short (minimum 3 characters)') 140 | ) 141 | end 142 | end 143 | 144 | context 'query is 3 characters or more' do 145 | let(:search_phrase) { 'User' } 146 | 147 | let(:valid_result) { [@user] } 148 | let(:valid_response) do 149 | { 150 | 'mentions' => valid_result.map do |user| 151 | { 'id' => user.id, 'name' => user.name, 'type' => 'user' } 152 | end 153 | } 154 | end 155 | 156 | it 'calls the user_mentions_proc and returns the result' do 157 | expect(Commontator.user_mentions_proc).to( 158 | receive(:call).with(@user, @thread, search_phrase).and_return(valid_result) 159 | ) 160 | 161 | call_request 162 | expect(response).to have_http_status(:success) 163 | 164 | response_body = JSON.parse(response.body) 165 | expect(response_body['errors']).to be_nil 166 | expect(response_body).to eq valid_response 167 | end 168 | end 169 | end 170 | 171 | context 'mentions disabled' do 172 | before { @thread.config.mentions_enabled = false } 173 | after { @thread.config.mentions_enabled = true } 174 | 175 | it 'returns 403 Forbidden' do 176 | call_request 177 | expect(response).to have_http_status(:forbidden) 178 | end 179 | end 180 | end 181 | end 182 | 183 | context 'unauthorized' do 184 | context 'GET #show' do 185 | it 'returns 403 Forbidden' do 186 | get :show, params: { id: @thread.id } 187 | expect(response).to have_http_status(:forbidden) 188 | 189 | Thread.current[:user] = @user 190 | get :show, params: { id: @thread.id } 191 | expect(response).to have_http_status(:forbidden) 192 | end 193 | end 194 | 195 | context 'PUT #close' do 196 | it 'returns 403 Forbidden' do 197 | put :close, params: { id: @thread.id } 198 | expect(response).to have_http_status(:forbidden) 199 | @thread.reload 200 | expect(@thread.is_closed?).to eq false 201 | 202 | Thread.current[:user] = @user 203 | put :close, params: { id: @thread.id } 204 | expect(response).to have_http_status(:forbidden) 205 | @thread.reload 206 | expect(@thread.is_closed?).to eq false 207 | 208 | @user.can_read = true 209 | put :close, params: { id: @thread.id } 210 | expect(response).to have_http_status(:forbidden) 211 | @thread.reload 212 | expect(@thread.is_closed?).to eq false 213 | 214 | @user.can_edit = true 215 | expect(@thread.close).to eq true 216 | put :close, params: { id: @thread.id } 217 | expect(response).to redirect_to(@commontable_path) 218 | expect(assigns(:commontator_thread).errors).not_to be_empty 219 | end 220 | end 221 | 222 | context 'PUT #reopen' do 223 | it 'returns 403 Forbidden' do 224 | expect(@thread.close).to eq true 225 | put :reopen, params: { id: @thread.id } 226 | expect(response).to have_http_status(:forbidden) 227 | @thread.reload 228 | expect(@thread.is_closed?).to eq true 229 | 230 | Thread.current[:user] = @user 231 | put :reopen, params: { id: @thread.id } 232 | expect(response).to have_http_status(:forbidden) 233 | @thread.reload 234 | expect(@thread.is_closed?).to eq true 235 | 236 | @user.can_read = true 237 | put :reopen, params: { id: @thread.id } 238 | expect(response).to have_http_status(:forbidden) 239 | @thread.reload 240 | expect(@thread.is_closed?).to eq true 241 | 242 | expect(@thread.reopen).to eq true 243 | @user.can_edit = true 244 | put :reopen, params: { id: @thread.id } 245 | expect(response).to redirect_to(@commontable_path) 246 | expect(assigns(:commontator_thread).errors).not_to be_empty 247 | end 248 | end 249 | 250 | context 'GET #mentions' do 251 | it 'returns 403 Forbidden' do 252 | get :mentions, params: { id: @thread.id, format: :json, q: 'User' } 253 | expect(response).to have_http_status(:forbidden) 254 | end 255 | end 256 | end 257 | end 258 | -------------------------------------------------------------------------------- /spec/dummy/README.md: -------------------------------------------------------------------------------- 1 | ## Dummy 2 | 3 | A dummy application used to test the Commontator engine. 4 | 5 | -------------------------------------------------------------------------------- /spec/dummy/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_relative 'config/application' 5 | 6 | Dummy::Application.load_tasks 7 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link commontator/manifest.js 2 | //= link_directory ../javascripts .js 3 | //= link_directory ../stylesheets .css 4 | -------------------------------------------------------------------------------- /spec/dummy/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 | // the compiled file. 9 | // 10 | // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD 11 | // GO AFTER THE REQUIRES BELOW. 12 | // 13 | //= require jquery 14 | //= require rails-ujs 15 | //= require commontator/application 16 | //= require_tree . 17 | -------------------------------------------------------------------------------- /spec/dummy/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 | *= require_self 12 | *= require commontator/application 13 | *= require_tree . 14 | */ 15 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | end 3 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/dummy_api_controller.rb: -------------------------------------------------------------------------------- 1 | class DummyApiController < ActionController::API 2 | before_action :get_dummy 3 | 4 | def show 5 | commontator_thread_show(@dummy_model) 6 | end 7 | 8 | def url_options 9 | return Hash.new if request.nil? 10 | super 11 | end 12 | 13 | protected 14 | 15 | def get_dummy 16 | @dummy_model = DummyModel.find_by(id: params[:id]) || DummyModel.first 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/dummy_models_controller.rb: -------------------------------------------------------------------------------- 1 | class DummyModelsController < ApplicationController 2 | before_action :get_dummy 3 | 4 | def show 5 | commontator_thread_show(@dummy_model) 6 | end 7 | 8 | def url_options 9 | return Hash.new if request.nil? 10 | super 11 | end 12 | 13 | protected 14 | 15 | def get_dummy 16 | @dummy_model = DummyModel.find_by(id: params[:id]) || DummyModel.first 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/dummy/app/models/dummy_dependent_model.rb: -------------------------------------------------------------------------------- 1 | class DummyDependentModel < ActiveRecord::Base 2 | acts_as_commontable dependent: :destroy 3 | end 4 | 5 | -------------------------------------------------------------------------------- /spec/dummy/app/models/dummy_model.rb: -------------------------------------------------------------------------------- 1 | class DummyModel < ActiveRecord::Base 2 | acts_as_commontable 3 | end 4 | 5 | -------------------------------------------------------------------------------- /spec/dummy/app/models/dummy_user.rb: -------------------------------------------------------------------------------- 1 | class DummyUser < ActiveRecord::Base 2 | acts_as_commontator 3 | 4 | attr_accessor :is_admin, :can_edit, :can_read 5 | 6 | def email 7 | "dummy_user#{id}@example.com" 8 | end 9 | 10 | def name 11 | "Dummy User ##{id}" 12 | end 13 | end 14 | 15 | -------------------------------------------------------------------------------- /spec/dummy/app/views/dummy_models/show.html.erb: -------------------------------------------------------------------------------- 1 | <%= commontator_thread(@dummy_model) %> 2 | -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= stylesheet_link_tag "application", media: "all" %> 6 | <%= javascript_include_tag "application" %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../config/application', __dir__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /spec/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /spec/dummy/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | 4 | # path to your application root. 5 | APP_ROOT = File.expand_path('..', __dir__) 6 | 7 | def system!(*args) 8 | system(*args) || abort("\n== Command #{args} failed ==") 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to setup or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at anytime and get an expectable outcome. 14 | # Add necessary setup steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | # puts "\n== Copying sample files ==" 21 | # unless File.exist?('config/database.yml') 22 | # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' 23 | # end 24 | 25 | puts "\n== Preparing database ==" 26 | system! 'bin/rails db:prepare' 27 | 28 | puts "\n== Removing old logs and tempfiles ==" 29 | system! 'bin/rails log:clear tmp:clear' 30 | 31 | puts "\n== Restarting application server ==" 32 | system! 'bin/rails restart' 33 | end 34 | -------------------------------------------------------------------------------- /spec/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /spec/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 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(*Rails.groups) 8 | 9 | module Dummy 10 | class Application < Rails::Application 11 | # Initialize configuration defaults for originally generated Rails version. 12 | config.load_defaults 6.1 13 | 14 | # Settings in config/environments/* take precedence over those specified here. 15 | # Application configuration can go into files in config/initializers 16 | # -- all .rb files in that directory are automatically loaded after loading 17 | # the framework and any gems in your application. 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | require 'bootsnap/setup' # Speed up boot time by caching expensive operations. 5 | -------------------------------------------------------------------------------- /spec/dummy/config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: test 6 | 7 | production: 8 | adapter: redis 9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 10 | channel_prefix: dummy_production 11 | -------------------------------------------------------------------------------- /spec/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | <% commontator_database_adapter = ENV.fetch('COMMONTATOR_DATABASE_ADAPTER', 'sqlite3') %> 7 | 8 | default: &default 9 | adapter: <%= commontator_database_adapter %> 10 | pool: <%= ENV.fetch('RAILS_MAX_THREADS') { 5 } %> 11 | host: localhost 12 | username: <%= ENV.fetch('COMMONTATOR_DATABASE_USERNAME', 'commontator') %> 13 | password: <%= ENV.fetch('COMMONTATOR_DATABASE_PASSWORD', 'commontator') %> 14 | 15 | development: 16 | <<: *default 17 | database: <%= ENV.fetch( 18 | 'COMMONTATOR_DEV_DATABASE', 19 | commontator_database_adapter == 'sqlite3' ? 'db/development.sqlite3' : 'commontator_dev' 20 | ) %> 21 | 22 | # Warning: The database defined as "test" will be erased and 23 | # re-generated from your development database when you run "rake". 24 | # Do not set this db to the same as development or production. 25 | test: 26 | <<: *default 27 | database: <%= ENV.fetch( 28 | 'COMMONTATOR_TEST_DATABASE', 29 | commontator_database_adapter == 'sqlite3' ? 'db/test.sqlite3' : 'commontator_test' 30 | ) %> 31 | -------------------------------------------------------------------------------- /spec/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.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. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable/disable caching. By default caching is disabled. 16 | # Run rails dev:cache to toggle caching. 17 | if Rails.root.join('tmp', 'caching-dev.txt').exist? 18 | config.action_controller.perform_caching = true 19 | config.action_controller.enable_fragment_cache_logging = true 20 | 21 | config.cache_store = :memory_store 22 | config.public_file_server.headers = { 23 | 'Cache-Control' => "public, max-age=#{2.days.to_i}" 24 | } 25 | else 26 | config.action_controller.perform_caching = false 27 | 28 | config.cache_store = :null_store 29 | end 30 | 31 | # Store uploaded files on the local file system (see config/storage.yml for options). 32 | config.active_storage.service = :local 33 | 34 | # Don't care if the mailer can't send. 35 | config.action_mailer.raise_delivery_errors = false 36 | 37 | config.action_mailer.perform_caching = false 38 | 39 | # Print deprecation notices to the Rails logger. 40 | config.active_support.deprecation = :log 41 | 42 | # Raise an error on page load if there are pending migrations. 43 | config.active_record.migration_error = :page_load 44 | 45 | # Highlight code that triggered database queries in logs. 46 | config.active_record.verbose_query_logs = true 47 | 48 | # Debug mode disables concatenation and preprocessing of assets. 49 | # This option may cause significant delays in view rendering with a large 50 | # number of complex assets. 51 | config.assets.debug = true 52 | 53 | # Suppress logger output for asset requests. 54 | config.assets.quiet = true 55 | 56 | # Raises error for missing translations. 57 | # config.action_view.raise_on_missing_translations = true 58 | 59 | # Use an evented file watcher to asynchronously detect changes in source code, 60 | # routes, locales, etc. This feature depends on the listen gem. 61 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 62 | end 63 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.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 threaded 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 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] 18 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). 19 | # config.require_master_key = true 20 | 21 | # Disable serving static files from the `/public` folder by default since 22 | # Apache or NGINX already handles this. 23 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 24 | 25 | # Compress CSS using a preprocessor. 26 | # config.assets.css_compressor = :sass 27 | 28 | # Do not fallback to assets pipeline if a precompiled asset is missed. 29 | config.assets.compile = false 30 | 31 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 32 | # config.action_controller.asset_host = 'http://assets.example.com' 33 | 34 | # Specifies the header that your server uses for sending files. 35 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 36 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 37 | 38 | # Store uploaded files on the local file system (see config/storage.yml for options). 39 | config.active_storage.service = :local 40 | 41 | # Mount Action Cable outside main process or domain. 42 | # config.action_cable.mount_path = nil 43 | # config.action_cable.url = 'wss://example.com/cable' 44 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 45 | 46 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 47 | # config.force_ssl = true 48 | 49 | # Use the lowest log level to ensure availability of diagnostic information 50 | # when problems arise. 51 | config.log_level = :debug 52 | 53 | # Prepend all log lines with the following tags. 54 | config.log_tags = [ :request_id ] 55 | 56 | # Use a different cache store in production. 57 | # config.cache_store = :mem_cache_store 58 | 59 | # Use a real queuing backend for Active Job (and separate queues per environment). 60 | # config.active_job.queue_adapter = :resque 61 | # config.active_job.queue_name_prefix = "dummy_production" 62 | 63 | config.action_mailer.perform_caching = false 64 | 65 | # Ignore bad email addresses and do not raise email delivery errors. 66 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 67 | # config.action_mailer.raise_delivery_errors = false 68 | 69 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 70 | # the I18n.default_locale when a translation cannot be found). 71 | config.i18n.fallbacks = true 72 | 73 | # Send deprecation notices to registered listeners. 74 | config.active_support.deprecation = :notify 75 | 76 | # Use default logging formatter so that PID and timestamp are not suppressed. 77 | config.log_formatter = ::Logger::Formatter.new 78 | 79 | # Use a different logger for distributed setups. 80 | # require 'syslog/logger' 81 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 82 | 83 | if ENV["RAILS_LOG_TO_STDOUT"].present? 84 | logger = ActiveSupport::Logger.new(STDOUT) 85 | logger.formatter = config.log_formatter 86 | config.logger = ActiveSupport::TaggedLogging.new(logger) 87 | end 88 | 89 | # Do not dump schema after migrations. 90 | config.active_record.dump_schema_after_migration = false 91 | 92 | # Inserts middleware to perform automatic connection switching. 93 | # The `database_selector` hash is used to pass options to the DatabaseSelector 94 | # middleware. The `delay` is used to determine how long to wait after a write 95 | # to send a subsequent read to the primary. 96 | # 97 | # The `database_resolver` class is used by the middleware to determine which 98 | # database is appropriate to use based on the time delay. 99 | # 100 | # The `database_resolver_context` class is used by the middleware to set 101 | # timestamps for the last write to the primary. The resolver uses the context 102 | # class timestamps to determine how long to wait before reading from the 103 | # replica. 104 | # 105 | # By default Rails will store a last write timestamp in the session. The 106 | # DatabaseSelector middleware is designed as such you can define your own 107 | # strategy for connection switching and pass that into the middleware through 108 | # these configuration options. 109 | # config.active_record.database_selector = { delay: 2.seconds } 110 | # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver 111 | # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session 112 | end 113 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # The test environment is used exclusively to run your application's 2 | # test suite. You never need to work with it otherwise. Remember that 3 | # your test database is "scratch space" for the test suite and is wiped 4 | # and recreated between test runs. Don't rely on the data there! 5 | 6 | Rails.application.configure do 7 | # Settings specified here will take precedence over those in config/application.rb. 8 | 9 | config.cache_classes = true 10 | 11 | # Do not eager load code on boot. This avoids loading your whole application 12 | # just for the purpose of running a single test. If you are using a tool that 13 | # preloads Rails for running tests, you may have to set it to true. 14 | config.eager_load = false 15 | 16 | # Configure public file server for tests with Cache-Control for performance. 17 | config.public_file_server.enabled = true 18 | config.public_file_server.headers = { 19 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}" 20 | } 21 | 22 | # Show full error reports and disable caching. 23 | config.consider_all_requests_local = true 24 | config.action_controller.perform_caching = false 25 | config.cache_store = :null_store 26 | 27 | # Raise exceptions instead of rendering exception templates. 28 | config.action_dispatch.show_exceptions = false 29 | 30 | # Disable request forgery protection in test environment. 31 | config.action_controller.allow_forgery_protection = false 32 | 33 | # Store uploaded files on the local file system in a temporary directory. 34 | config.active_storage.service = :test 35 | 36 | config.action_mailer.perform_caching = false 37 | 38 | # Tell Action Mailer not to deliver emails to the real world. 39 | # The :test delivery method accumulates sent emails in the 40 | # ActionMailer::Base.deliveries array. 41 | config.action_mailer.delivery_method = :test 42 | 43 | # Print deprecation notices to the stderr. 44 | config.active_support.deprecation = :stderr 45 | 46 | # Raises error for missing translations. 47 | # config.action_view.raise_on_missing_translations = true 48 | end 49 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in the app/assets 11 | # folder are already added. 12 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 13 | -------------------------------------------------------------------------------- /spec/dummy/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/config/initializers/commontator.rb: -------------------------------------------------------------------------------- 1 | # Dummy application configuration file 2 | Commontator.configure do |config| 3 | config.javascript_proc = ->(view) { '// Some javascript' } 4 | 5 | config.current_user_proc = ->(context) do 6 | user = Thread.current[:user] 7 | return user unless user.nil? && Rails.env.development? 8 | 9 | DummyUser.order(:created_at).last.tap do |user| 10 | user.can_read = true 11 | user.can_edit = true 12 | user.is_admin = true 13 | end 14 | end 15 | 16 | config.user_name_proc = ->(user) { user.try(:name) || 'Anonymous' } 17 | 18 | config.user_avatar_proc = ->(user, view) do 19 | view.commontator_gravatar_image_tag(user, 1, s: 60, d: 'mm') 20 | end 21 | 22 | config.thread_read_proc = ->(thread, user) { user && user.can_read } 23 | 24 | config.thread_moderator_proc = ->(thread, user) { user.is_admin || user.can_edit } 25 | 26 | config.comment_voting = :ld 27 | 28 | config.comment_order = :e 29 | 30 | config.new_comment_style = :l 31 | 32 | config.comment_reply_style = :b 33 | 34 | config.comments_per_page = [ 5, 3, 2 ] 35 | 36 | config.thread_subscription = :b 37 | 38 | config.mentions_enabled = true 39 | 40 | config.user_mentions_proc = ->(current_user, thread, query) do 41 | 'DummyUser'.include?(query) ? DummyUser.all : DummyUser.none 42 | end 43 | 44 | config.comment_filter = Commontator::Comment.arel_table[:body].does_not_match('%hidden%') 45 | end 46 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy 4 | # For further information see the following documentation 5 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 6 | 7 | # Rails.application.config.content_security_policy do |policy| 8 | # policy.default_src :self, :https 9 | # policy.font_src :self, :https, :data 10 | # policy.img_src :self, :https, :data 11 | # policy.object_src :none 12 | # policy.script_src :self, :https 13 | # policy.style_src :self, :https 14 | 15 | # # Specify URI for violation reports 16 | # # policy.report_uri "/csp-violation-report-endpoint" 17 | # end 18 | 19 | # If you are using UJS then enable automatic nonce generation 20 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } 21 | 22 | # Set the nonce only to specific directives 23 | # Rails.application.config.content_security_policy_nonce_directives = %w(script-src) 24 | 25 | # Report CSP violations to a specified URI 26 | # For further information see the following documentation: 27 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only 28 | # Rails.application.config.content_security_policy_report_only = true 29 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/default_url_options.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.action_mailer.default_url_options = { host: 'example.com' } 2 | -------------------------------------------------------------------------------- /spec/dummy/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/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/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 | -------------------------------------------------------------------------------- /spec/dummy/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 | Dummy::Application.config.secret_key_base = 'dummy' 13 | 14 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Dummy::Application.config.session_store :cookie_store, key: '_dummy_session' 4 | 5 | -------------------------------------------------------------------------------- /spec/dummy/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] 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/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 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at https://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /spec/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html 3 | root controller: :dummy_models, action: :show 4 | 5 | resources :dummy_models, only: :show 6 | 7 | mount Commontator::Engine => :commontator 8 | end 9 | -------------------------------------------------------------------------------- /spec/dummy/config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket 23 | 24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/00_create_dummy_models.rb: -------------------------------------------------------------------------------- 1 | class CreateDummyModels < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :dummy_models do |t| 4 | t.timestamps null: false 5 | end 6 | 7 | reversible do |dir| 8 | dir.up do 9 | DummyModel.create 10 | DummyModel.delete_all 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/01_create_dummy_users.rb: -------------------------------------------------------------------------------- 1 | class CreateDummyUsers < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :dummy_users do |t| 4 | t.timestamps null: false 5 | end 6 | 7 | reversible do |dir| 8 | dir.up do 9 | DummyUser.create 10 | DummyUser.delete_all 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/02_create_dummy_dependent_models.rb: -------------------------------------------------------------------------------- 1 | class CreateDummyDependentModels < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :dummy_dependent_models do |t| 4 | t.timestamps null: false 5 | end 6 | 7 | reversible do |dir| 8 | dir.up do 9 | DummyDependentModel.create 10 | DummyDependentModel.delete_all 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/03_acts_as_votable_migration.rb: -------------------------------------------------------------------------------- 1 | class ActsAsVotableMigration < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :votes do |t| 4 | t.references :votable, polymorphic: true 5 | t.references :voter, polymorphic: true 6 | 7 | t.boolean :vote_flag 8 | t.string :vote_scope 9 | t.integer :vote_weight 10 | 11 | t.timestamps null: false 12 | end 13 | 14 | add_index :votes, [:voter_id, :voter_type, :vote_scope] 15 | add_index :votes, [:votable_id, :votable_type, :vote_scope] 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/dummy/db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # This file is the source Rails uses to define your schema when running `bin/rails 6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to 7 | # be faster and is potentially less error prone than running all of your 8 | # migrations from scratch. Old migrations may fail to apply correctly if those 9 | # migrations use external dependencies or application code. 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 11) do 14 | 15 | create_table "commontator_comments", force: :cascade do |t| 16 | t.integer "thread_id", null: false 17 | t.string "creator_type", null: false 18 | t.integer "creator_id", null: false 19 | t.string "editor_type" 20 | t.integer "editor_id" 21 | t.text "body", null: false 22 | t.datetime "deleted_at" 23 | t.integer "cached_votes_up", default: 0 24 | t.integer "cached_votes_down", default: 0 25 | t.datetime "created_at", precision: 6, null: false 26 | t.datetime "updated_at", precision: 6, null: false 27 | t.integer "parent_id" 28 | t.index ["cached_votes_down"], name: "index_commontator_comments_on_cached_votes_down" 29 | t.index ["cached_votes_up"], name: "index_commontator_comments_on_cached_votes_up" 30 | t.index ["creator_id", "creator_type", "thread_id"], name: "index_commontator_comments_on_c_id_and_c_type_and_t_id" 31 | t.index ["editor_type", "editor_id"], name: "index_commontator_comments_on_editor_type_and_editor_id" 32 | t.index ["parent_id"], name: "index_commontator_comments_on_parent_id" 33 | t.index ["thread_id", "created_at"], name: "index_commontator_comments_on_thread_id_and_created_at" 34 | end 35 | 36 | create_table "commontator_subscriptions", force: :cascade do |t| 37 | t.bigint "thread_id", null: false 38 | t.string "subscriber_type", null: false 39 | t.bigint "subscriber_id", null: false 40 | t.datetime "created_at", precision: 6, null: false 41 | t.datetime "updated_at", precision: 6, null: false 42 | t.index ["subscriber_id", "subscriber_type", "thread_id"], name: "index_commontator_subscriptions_on_s_id_and_s_type_and_t_id", unique: true 43 | t.index ["thread_id"], name: "index_commontator_subscriptions_on_thread_id" 44 | end 45 | 46 | create_table "commontator_threads", force: :cascade do |t| 47 | t.string "commontable_type" 48 | t.bigint "commontable_id" 49 | t.string "closer_type" 50 | t.bigint "closer_id" 51 | t.datetime "closed_at" 52 | t.datetime "created_at", precision: 6, null: false 53 | t.datetime "updated_at", precision: 6, null: false 54 | t.index ["closer_type", "closer_id"], name: "index_commontator_threads_on_closer_type_and_closer_id" 55 | t.index ["commontable_type", "commontable_id"], name: "index_commontator_threads_on_c_id_and_c_type", unique: true 56 | end 57 | 58 | create_table "dummy_dependent_models", force: :cascade do |t| 59 | t.datetime "created_at", null: false 60 | t.datetime "updated_at", null: false 61 | end 62 | 63 | create_table "dummy_models", force: :cascade do |t| 64 | t.datetime "created_at", null: false 65 | t.datetime "updated_at", null: false 66 | end 67 | 68 | create_table "dummy_users", force: :cascade do |t| 69 | t.datetime "created_at", null: false 70 | t.datetime "updated_at", null: false 71 | end 72 | 73 | create_table "votes", force: :cascade do |t| 74 | t.string "votable_type" 75 | t.integer "votable_id" 76 | t.string "voter_type" 77 | t.integer "voter_id" 78 | t.boolean "vote_flag" 79 | t.string "vote_scope" 80 | t.integer "vote_weight" 81 | t.datetime "created_at", null: false 82 | t.datetime "updated_at", null: false 83 | t.index ["votable_id", "votable_type", "vote_scope"], name: "index_votes_on_votable_id_and_votable_type_and_vote_scope" 84 | t.index ["votable_type", "votable_id"], name: "index_votes_on_votable" 85 | t.index ["voter_id", "voter_type", "vote_scope"], name: "index_votes_on_voter_id_and_voter_type_and_vote_scope" 86 | t.index ["voter_type", "voter_id"], name: "index_votes_on_voter" 87 | end 88 | 89 | add_foreign_key "commontator_comments", "commontator_comments", column: "parent_id", on_update: :restrict, on_delete: :cascade 90 | add_foreign_key "commontator_comments", "commontator_threads", column: "thread_id", on_update: :cascade, on_delete: :cascade 91 | add_foreign_key "commontator_subscriptions", "commontator_threads", column: "thread_id", on_update: :cascade, on_delete: :cascade 92 | end 93 | -------------------------------------------------------------------------------- /spec/dummy/db/seeds.rb: -------------------------------------------------------------------------------- 1 | DummyUser.create! 2 | DummyModel.create! 3 | -------------------------------------------------------------------------------- /spec/dummy/lib/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lml/commontator/729fc34968d131bcb5d31325f3e5536e4ee491d4/spec/dummy/lib/.keep -------------------------------------------------------------------------------- /spec/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 17 | 18 | 19 | 20 | 21 |

22 |

The page you were looking for doesn't exist.

23 |

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

24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /spec/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The change you wanted was rejected.

23 |

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

24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /spec/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

We're sorry, but something went wrong.

23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /spec/dummy/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lml/commontator/729fc34968d131bcb5d31325f3e5536e4ee491d4/spec/dummy/public/favicon.ico -------------------------------------------------------------------------------- /spec/dummy/script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | APP_PATH = File.expand_path('../config/application', __dir__) 5 | require_relative '../config/boot' 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /spec/helpers/commontator/application_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Commontator::ApplicationHelper, type: :helper do 4 | it 'prints output of javascript proc' do 5 | expect(javascript_proc).to eq '// Some javascript' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/lib/commontator/acts_as_commontable_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Commontator::ActsAsCommontable, type: :lib do 4 | it 'adds methods to ActiveRecord and subclasses' do 5 | expect(ActiveRecord::Base).to respond_to(:acts_as_commontable) 6 | expect(ActiveRecord::Base).to respond_to(:is_commontable) 7 | expect(ActiveRecord::Base.is_commontable).to eq false 8 | expect(DummyModel).to respond_to(:acts_as_commontable) 9 | expect(DummyModel).to respond_to(:is_commontable) 10 | expect(DummyModel.is_commontable).to eq true 11 | expect(DummyUser).to respond_to(:acts_as_commontable) 12 | expect(DummyUser).to respond_to(:is_commontable) 13 | expect(DummyUser.is_commontable).to eq false 14 | end 15 | 16 | it 'modifies models that act_as_commontable' do 17 | dummy = DummyModel.create 18 | expect(dummy).to respond_to(:commontator_thread) 19 | expect(dummy).to respond_to(:commontable_config) 20 | expect(dummy.commontable_config).to be_a(Commontator::CommontableConfig) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/lib/commontator/acts_as_commontator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Commontator::ActsAsCommontator, type: :lib do 4 | it 'adds methods to ActiveRecord and subclasses' do 5 | expect(ActiveRecord::Base).to respond_to(:acts_as_commontator) 6 | expect(ActiveRecord::Base).to respond_to(:is_commontator) 7 | expect(ActiveRecord::Base.is_commontator).to eq false 8 | expect(DummyModel).to respond_to(:acts_as_commontator) 9 | expect(DummyModel).to respond_to(:is_commontator) 10 | expect(DummyModel.is_commontator).to eq false 11 | expect(DummyUser).to respond_to(:acts_as_commontator) 12 | expect(DummyUser).to respond_to(:is_commontator) 13 | expect(DummyUser.is_commontator).to eq true 14 | end 15 | 16 | it 'modifies models that act_as_commontator' do 17 | user = DummyUser.create 18 | expect(user).to respond_to(:commontator_comments) 19 | expect(user).to respond_to(:commontator_subscriptions) 20 | expect(user).to respond_to(:commontator_config) 21 | expect(user.commontator_config).to be_a(Commontator::CommontatorConfig) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/lib/commontator/commontable_config_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Commontator::CommontableConfig, type: :lib do 4 | it 'responds to commontable attributes' do 5 | config = described_class.new 6 | Commontator::Config::COMMONTABLE_ATTRIBUTES.each do |attribute| 7 | expect(config).to respond_to(attribute) 8 | end 9 | end 10 | 11 | it 'does not respond to engine or commontator attributes' do 12 | config = described_class.new 13 | ( 14 | Commontator::Config::ENGINE_ATTRIBUTES + Commontator::Config::COMMONTATOR_ATTRIBUTES 15 | ).each do |attribute| 16 | expect(config).not_to respond_to(attribute) 17 | end 18 | end 19 | 20 | it 'is configurable' do 21 | proc = ->(thread) { 'Some name' } 22 | proc2 = ->(thread) { 'Another name' } 23 | config = described_class.new(commontable_name_proc: proc) 24 | expect(config.commontable_name_proc).to eq proc 25 | config = described_class.new(commontable_name_proc: proc2) 26 | expect(config.commontable_name_proc).to eq proc2 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/lib/commontator/commontator_config_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Commontator::CommontatorConfig, type: :lib do 4 | it 'responds to commontator attributes' do 5 | config = described_class.new 6 | Commontator::Config::COMMONTATOR_ATTRIBUTES.each do |attribute| 7 | expect(config).to respond_to(attribute) 8 | end 9 | end 10 | 11 | it 'does not respond to engine or commontable attributes' do 12 | config = described_class.new 13 | ( 14 | Commontator::Config::ENGINE_ATTRIBUTES + Commontator::Config::COMMONTABLE_ATTRIBUTES 15 | ).each do |attribute| 16 | expect(config).not_to respond_to(attribute) 17 | end 18 | end 19 | 20 | it 'is configurable' do 21 | proc = ->(user) { 'Some name' } 22 | proc2 = ->(user) { 'Another name' } 23 | config = described_class.new(user_name_proc: proc) 24 | expect(config.user_name_proc).to eq proc 25 | config = described_class.new(user_name_proc: proc2) 26 | expect(config.user_name_proc).to eq proc2 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/lib/commontator/controllers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Commontator::Controllers, type: :lib do 4 | { 5 | ActionController::API => DummyApiController, 6 | ActionController::Base => DummyModelsController 7 | }.each do |klass, subclass| 8 | it "adds commontator_thread_show to #{klass.name} instances" do 9 | expect(klass.new.respond_to?(:commontator_thread_show, true)).to eq true 10 | expect(subclass.new.respond_to?(:commontator_thread_show, true)).to eq true 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/lib/commontator/shared_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Commontator::SharedHelper, type: :lib do 4 | let(:controller) { DummyModelsController.new } 5 | before { setup_controller_spec } 6 | 7 | { 8 | ActionController::API => DummyApiController, 9 | ActionController::Base => DummyModelsController 10 | }.each do |klass, subclass| 11 | it "adds commontator_thread to #{klass.name} instances" do 12 | expect(klass.new.respond_to?(:commontator_thread, true)).to eq true 13 | expect(subclass.new.respond_to?(:commontator_thread, true)).to eq true 14 | end 15 | end 16 | 17 | it 'adds itself as a helper to ActionController::Base and subclasses' do 18 | expect(ActionController::Base.helpers).to respond_to(:commontator_thread) 19 | expect(DummyModelsController.helpers).to respond_to(:commontator_thread) 20 | end 21 | 22 | it 'renders a commontator thread' do 23 | @user.can_read = true 24 | Thread.current[:user] = @user 25 | 26 | thread_link = controller.view_context.commontator_thread(DummyModel.create) 27 | expect(thread_link).not_to be_blank 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/lib/commontator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Commontator, type: :lib do 4 | it 'responds to all attributes' do 5 | ( 6 | Commontator::Config::ENGINE_ATTRIBUTES + 7 | Commontator::Config::COMMONTATOR_ATTRIBUTES + 8 | Commontator::Config::COMMONTABLE_ATTRIBUTES 9 | ).each do |attribute| 10 | expect(Commontator).to respond_to(attribute) 11 | end 12 | end 13 | 14 | it 'is configurable' do 15 | l1 = ->(controller) { Thread.current[:user] } 16 | l2 = ->(controller) { Thread.current[:user] } 17 | expect(l1).not_to eq l2 18 | Commontator.configure do |config| 19 | config.current_user_proc = l1 20 | end 21 | expect(Commontator.current_user_proc).to eq l1 22 | Commontator.configure do |config| 23 | config.current_user_proc = l2 24 | end 25 | expect(Commontator.current_user_proc).to eq l2 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/mailers/commontator/subscriptions_mailer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | require 'mailgun_rails' 3 | 4 | RSpec.describe Commontator::SubscriptionsMailer, type: :mailer do 5 | before do 6 | setup_mailer_spec 7 | @user2 = DummyUser.create 8 | @thread.subscribe(@user) 9 | @thread.subscribe(@user2) 10 | @comment = Commontator::Comment.new 11 | @comment.thread = @thread 12 | @comment.creator = @user 13 | @comment.body = 'Something' 14 | @comment.save! 15 | @recipients = @thread.subscribers.reject { |sub| sub == @user } 16 | end 17 | 18 | it 'creates deliverable mail' do 19 | mail = described_class.comment_created(@comment, @recipients) 20 | expect(mail.to).to be_nil 21 | expect(mail.cc).to be_nil 22 | expect(mail.bcc.size).to eq 1 23 | expect(mail.bcc).to include(@user2.email) 24 | expect(mail.subject).not_to be_empty 25 | expect(mail.body).not_to be_empty 26 | expect(mail.deliver_now).to eq mail 27 | end 28 | 29 | context 'with Mailgun' do 30 | let(:recipient_variables) do 31 | @recipients.each_with_object({}) { |user, memo| memo[user.email] = {} } 32 | end 33 | before do 34 | allow(Rails.application.config.action_mailer).to( 35 | receive(:delivery_method).and_return(:mailgun) 36 | ) 37 | end 38 | 39 | it 'creates deliverable mail' do 40 | mail = described_class.comment_created(@comment, @recipients) 41 | expect(mail.to.size).to eq 1 42 | expect(mail.to).to include(@user2.email) 43 | expect(mail.cc).to be_nil 44 | expect(mail.bcc).to be_nil 45 | expect(mail.subject).not_to be_empty 46 | expect(mail.body).not_to be_empty 47 | expect(mail.deliver_now).to eq mail 48 | expect(mail.mailgun_recipient_variables.size).to eq 1 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/models/commontator/comment_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | require 'acts_as_votable' 3 | 4 | RSpec.describe Commontator::Comment, type: :model do 5 | before do 6 | setup_model_spec 7 | @comment = described_class.new 8 | @comment.thread = @thread 9 | @comment.creator = @user 10 | @comment.body = 'Something' 11 | end 12 | 13 | it 'is votable if acts_as_votable is installed' do 14 | expect(described_class).to respond_to(:acts_as_votable) 15 | expect(described_class.is_votable?).to eq true 16 | end 17 | 18 | it 'knows if it has been modified' do 19 | @comment.save! 20 | 21 | expect(@comment.is_modified?).to eq false 22 | 23 | @comment.body = 'Something else' 24 | @comment.editor = @user 25 | @comment.save! 26 | 27 | expect(@comment.is_modified?).to eq true 28 | end 29 | 30 | it 'knows if it has been deleted' do 31 | user = DummyUser.new 32 | 33 | expect(@comment.is_deleted?).to eq false 34 | expect(@comment.editor).to be_nil 35 | 36 | @comment.delete_by(user) 37 | 38 | expect(@comment.is_deleted?).to eq true 39 | expect(@comment.editor).to eq user 40 | 41 | @comment.undelete_by(user) 42 | 43 | expect(@comment.is_deleted?).to eq false 44 | end 45 | 46 | it 'makes proper timestamps' do 47 | @comment.save! 48 | 49 | expect(@comment.created_timestamp).to eq( 50 | I18n.t('commontator.comment.status.created_at', 51 | created_at: I18n.l(@comment.created_at, format: :commontator)) 52 | ) 53 | expect(@comment.updated_timestamp).to eq( 54 | I18n.t('commontator.comment.status.updated_at', 55 | editor_name: @user.name, 56 | updated_at: I18n.l(@comment.updated_at, format: :commontator)) 57 | ) 58 | 59 | user2 = DummyUser.create 60 | @comment.body = 'Something else' 61 | @comment.editor = user2 62 | @comment.save! 63 | 64 | expect(@comment.created_timestamp).to eq( 65 | I18n.t('commontator.comment.status.created_at', 66 | created_at: I18n.l(@comment.created_at, format: :commontator)) 67 | ) 68 | expect(@comment.updated_timestamp).to eq( 69 | I18n.t('commontator.comment.status.updated_at', 70 | editor_name: user2.name, 71 | updated_at: I18n.l(@comment.updated_at, format: :commontator)) 72 | ) 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/models/commontator/subscription_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Commontator::Subscription, type: :model do 4 | before do 5 | setup_model_spec 6 | @subscription = described_class.new 7 | @subscription.thread = @thread 8 | @subscription.subscriber = @user 9 | @subscription.save! 10 | end 11 | 12 | it 'counts unread comments' do 13 | expect(@subscription.unread_comments(false).count).to eq 0 14 | 15 | comment = Commontator::Comment.new 16 | comment.thread = @thread 17 | comment.creator = @user 18 | comment.body = 'Something' 19 | comment.save! 20 | 21 | expect(@subscription.reload.unread_comments(false).count).to eq 1 22 | 23 | comment = Commontator::Comment.new 24 | comment.thread = @thread 25 | comment.creator = @user 26 | comment.body = 'Something else' 27 | comment.save! 28 | 29 | expect(@subscription.reload.unread_comments(false).count).to eq 2 30 | 31 | # Wait until 1 second after the comment was created 32 | sleep([1 - (Time.current - comment.created_at), 0].max) 33 | @subscription.touch 34 | 35 | expect(@subscription.reload.unread_comments(false).count).to eq 0 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/models/commontator/thread_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Commontator::Thread, type: :model do 4 | before { setup_model_spec } 5 | 6 | it 'has a config' do 7 | expect(@thread.config).to be_a(Commontator::CommontableConfig) 8 | @thread.update_attribute(:commontable_id, nil) 9 | expect(Commontator::Thread.find(@thread.id).config).to eq Commontator 10 | end 11 | 12 | it 'filters comments' do 13 | expect(@thread.config).to receive(:comment_filter).and_return( 14 | Commontator::Comment.arel_table[:deleted_at].eq(nil) 15 | ) 16 | comment = Commontator::Comment.new 17 | comment.thread = @thread 18 | comment.creator = @user 19 | comment.body = 'Something' 20 | comment.save! 21 | comment.delete_by @user 22 | 23 | comment2 = Commontator::Comment.new 24 | comment2.thread = @thread 25 | comment2.creator = @user 26 | comment2.body = 'Something else' 27 | comment2.save! 28 | 29 | comments = [ comment, comment2 ] 30 | all_comments = @thread.filtered_comments(true) 31 | expect(comments - all_comments).to be_empty 32 | expect(all_comments - comments).to be_empty 33 | 34 | filtered_comments = @thread.filtered_comments(false) 35 | expect(comments - filtered_comments).to eq [ comment ] 36 | expect(filtered_comments - comments).to be_empty 37 | end 38 | 39 | [ :e, :l, :ve, :vl ].each do |comment_order| 40 | context "comment_order #{comment_order}" do 41 | before { expect(@thread.config).to receive(:comment_order).and_return(comment_order) } 42 | 43 | it 'orders comments' do 44 | comment = Commontator::Comment.new 45 | comment.thread = @thread 46 | comment.creator = @user 47 | comment.body = 'Something' 48 | comment.save! 49 | comment.downvote_from @user 50 | 51 | comment2 = Commontator::Comment.new 52 | comment2.thread = @thread 53 | comment2.creator = @user 54 | comment2.body = 'Something else' 55 | comment2.save! 56 | comment2.upvote_from @user 57 | 58 | comment3 = Commontator::Comment.new 59 | comment3.thread = @thread 60 | comment3.creator = @user 61 | comment3.body = 'Another thing' 62 | comment3.save! 63 | comment3.upvote_from @user 64 | 65 | expect(@thread.ordered_comments(true)).to eq case comment_order 66 | when :e 67 | [ comment, comment2, comment3 ] 68 | when :l 69 | [ comment3, comment2, comment ] 70 | when :ve 71 | [ comment2, comment3, comment ] 72 | when :vl 73 | [ comment3, comment2, comment ] 74 | end 75 | end 76 | end 77 | end 78 | 79 | it 'paginates comments' do 80 | expect(@thread.config).to receive(:comments_per_page).twice.and_return([ 2 ]) 81 | 82 | comment = Commontator::Comment.new 83 | comment.thread = @thread 84 | comment.creator = @user 85 | comment.body = 'Something' 86 | comment.save! 87 | 88 | comment2 = Commontator::Comment.new 89 | comment2.thread = @thread 90 | comment2.creator = @user 91 | comment2.body = 'Something else' 92 | comment2.save! 93 | 94 | comment3 = Commontator::Comment.new 95 | comment3.thread = @thread 96 | comment3.creator = @user 97 | comment3.body = 'Another thing' 98 | comment3.save! 99 | 100 | expect(@thread.paginated_comments(1, nil, true)).to eq [ comment, comment2 ] 101 | expect(@thread.paginated_comments(2, nil, true)).to eq [ comment3 ] 102 | end 103 | 104 | [ :n, :q, :i, :b ].each do |comment_reply_style| 105 | context "comment_reply_style #{comment_reply_style}" do 106 | before do 107 | expect(@thread.config).to( 108 | receive(:comment_reply_style).at_least(:once).and_return(comment_reply_style) 109 | ) 110 | end 111 | 112 | it [ :n, :q ].include?(comment_reply_style) ? 113 | 'ignores comment parent_ids' : 'returns comments with a given parent_id' do 114 | comment = Commontator::Comment.new 115 | comment.thread = @thread 116 | comment.creator = @user 117 | comment.body = 'Something' 118 | comment.save! 119 | 120 | comment2 = Commontator::Comment.new 121 | comment2.thread = @thread 122 | comment2.creator = @user 123 | comment2.body = 'Something else' 124 | comment2.parent = comment 125 | comment2.save! 126 | 127 | comment3 = Commontator::Comment.new 128 | comment3.thread = @thread 129 | comment3.creator = @user 130 | comment3.body = 'Another thing' 131 | comment3.save! 132 | 133 | expect(@thread.comments_with_parent_id(nil, true)).to eq( 134 | [ :n, :q ].include?(comment_reply_style) ? [ comment, comment2, comment3 ] : 135 | [ comment, comment3 ] 136 | ) 137 | 138 | expect(@thread.comments_with_parent_id(comment.id, true)).to eq( 139 | [ :n, :q ].include?(comment_reply_style) ? [] : [ comment2 ] 140 | ) 141 | end 142 | 143 | it "#{ [ :n, :q ].include?(comment_reply_style) ? 'does not nest' : 'nests' } comments" do 144 | expect(@thread.config).to receive(:comments_per_page).at_least(:twice).and_return([ 2, 1 ]) 145 | 146 | comment = Commontator::Comment.new 147 | comment.thread = @thread 148 | comment.creator = @user 149 | comment.body = 'Something' 150 | comment.save! 151 | 152 | comment2 = Commontator::Comment.new 153 | comment2.thread = @thread 154 | comment2.creator = @user 155 | comment2.body = 'Something else' 156 | comment2.parent = comment 157 | comment2.save! 158 | 159 | comment3 = Commontator::Comment.new 160 | comment3.thread = @thread 161 | comment3.creator = @user 162 | comment3.body = 'Another thing' 163 | comment3.parent = comment 164 | comment3.save! 165 | 166 | comments = @thread.paginated_comments(1, nil, true) 167 | expect(@thread.nested_comments_for(@user, comments, true)).to eq( 168 | [ :n, :q ].include?(comment_reply_style) ? 169 | [ [ comment, [] ], [ comment2, [] ] ] : [ [ comment, [ [ comment2, [] ] ] ] ] 170 | ) 171 | end 172 | end 173 | end 174 | 175 | it 'allows users to subscribe to the thread' do 176 | @thread.subscribe(@user) 177 | @thread.subscribe(DummyUser.create) 178 | 179 | @thread.subscriptions.each do |sp| 180 | expect(@thread.subscribers).to include(sp.subscriber) 181 | end 182 | end 183 | 184 | it 'finds the subscription for each user' do 185 | @thread.subscribe(@user) 186 | user2 = DummyUser.create 187 | @thread.subscribe(user2) 188 | 189 | subscription = @thread.subscription_for(@user) 190 | expect(subscription.thread).to eq @thread 191 | expect(subscription.subscriber).to eq @user 192 | subscription = @thread.subscription_for(user2) 193 | expect(subscription.thread).to eq @thread 194 | expect(subscription.subscriber).to eq user2 195 | end 196 | 197 | it 'returns nil subscription for nil or false subscriber' do 198 | expect(@thread.subscription_for(nil)).to eq nil 199 | expect(@thread.subscription_for(false)).to eq nil 200 | end 201 | 202 | it 'knows if it is closed' do 203 | expect(@thread.is_closed?).to eq false 204 | 205 | @thread.close(@user) 206 | 207 | expect(@thread.is_closed?).to eq true 208 | expect(@thread.closer).to eq @user 209 | 210 | @thread.reopen 211 | 212 | expect(@thread.is_closed?).to eq false 213 | end 214 | 215 | it 'marks comments as read' do 216 | @thread.subscribe(@user) 217 | 218 | subscription = @thread.subscription_for(@user) 219 | expect(subscription.unread_comments(false).count).to eq 0 220 | 221 | comment = Commontator::Comment.new 222 | comment.thread = @thread 223 | comment.creator = @user 224 | comment.body = 'Something' 225 | comment.save! 226 | 227 | expect(subscription.reload.unread_comments(false).count).to eq 1 228 | 229 | # Wait until 1 second after the comment was created 230 | sleep([1 - (Time.current - comment.created_at), 0].max) 231 | @thread.mark_as_read_for(@user) 232 | 233 | expect(subscription.reload.unread_comments(false).count).to eq 0 234 | end 235 | 236 | it 'can clear comments' do 237 | comment = Commontator::Comment.new 238 | comment.thread = @thread 239 | comment.creator = @user 240 | comment.body = 'Something' 241 | comment.save! 242 | 243 | @thread.close(@user) 244 | 245 | expect(@thread.commontable).to eq @commontable 246 | expect(@thread.comments).to include(comment) 247 | expect(@thread.is_closed?).to eq true 248 | expect(@thread.closer).to eq @user 249 | 250 | @commontable = DummyModel.find(@commontable.id) 251 | expect(@commontable.commontator_thread).to eq @thread 252 | 253 | @thread.clear 254 | 255 | expect(@thread.commontable).to be_nil 256 | expect(@thread.comments).to include(comment) 257 | 258 | @commontable = DummyModel.find(@commontable.id) 259 | expect(@commontable.commontator_thread).not_to be_nil 260 | expect(@commontable.commontator_thread).not_to eq @thread 261 | expect(@commontable.commontator_thread.comments).not_to include(comment) 262 | end 263 | 264 | it 'preserves the thread and comments by default when the commontable is gone' do 265 | comment = Commontator::Comment.new 266 | comment.thread = @thread 267 | comment.creator = @user 268 | comment.body = 'Undead' 269 | comment.save! 270 | comment.reload 271 | 272 | expect(described_class.find(@thread.id)).to eq @thread 273 | expect(Commontator::Comment.find(comment.id)).to eq comment 274 | 275 | @commontable.destroy! 276 | 277 | expect(described_class.find(@thread.id)).to eq @thread 278 | expect(Commontator::Comment.find(comment.id)).to eq comment 279 | end 280 | 281 | it 'deletes the thread and comments when commontable has dependent :destroy' do 282 | commontable = DummyDependentModel.create 283 | thread = commontable.commontator_thread 284 | 285 | comment = Commontator::Comment.new 286 | comment.thread = thread 287 | comment.creator = @user 288 | comment.body = 'Undead' 289 | comment.save! 290 | comment.reload 291 | 292 | expect(described_class.find(thread.id)).to eq thread 293 | expect(Commontator::Comment.find(comment.id)).to eq comment 294 | 295 | commontable.destroy! 296 | 297 | expect { described_class.find(thread.id) }.to raise_exception(ActiveRecord::RecordNotFound) 298 | expect do 299 | Commontator::Comment.find(comment.id) 300 | end.to raise_exception(ActiveRecord::RecordNotFound) 301 | end 302 | end 303 | -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to spec/ when you run 'rails generate rspec:install' 2 | ENV["RAILS_ENV"] ||= 'test' 3 | 4 | require_relative 'spec_helper' 5 | require_relative 'dummy/config/environment' 6 | require 'rspec/rails' 7 | # Add additional requires below this line. Rails is not loaded until this point! 8 | require 'rails-controller-testing' 9 | Rails::Controller::Testing.install 10 | 11 | # Requires supporting ruby files with custom matchers and macros, etc, in 12 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are 13 | # run as spec files by default. This means that files in spec/support that end 14 | # in _spec.rb will both be required and run as specs, causing the specs to be 15 | # run twice. It is recommended that you do not name files matching this glob to 16 | # end with _spec.rb. You can configure this pattern with the --pattern 17 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. 18 | # 19 | # The following line is provided for convenience purposes. It has the downside 20 | # of increasing the boot-up time by auto-requiring all files in the support 21 | # directory. Alternatively, in the individual `*_spec.rb` files, manually 22 | # require only the support files necessary. 23 | # 24 | # Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } 25 | 26 | # Checks for pending migrations before tests are run. 27 | # If you are not using ActiveRecord, you can remove this line. 28 | ActiveRecord::Migration.maintain_test_schema! 29 | 30 | RSpec.configure do |config| 31 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures 32 | # config.fixture_path = "#{::Rails.root}/spec/fixtures" 33 | 34 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 35 | # examples within a transaction, remove the following line or assign false 36 | # instead of true. 37 | # Fix for https://github.com/brianmario/mysql2/issues/947 38 | # Testing for Mysql2: https://stackoverflow.com/a/2536743 39 | config.use_transactional_fixtures = ActiveRecord::Base.connection.adapter_name != 'Mysql2' 40 | 41 | # RSpec Rails can automatically mix in different behaviours to your tests 42 | # based on their file location, for example enabling you to call `get` and 43 | # `post` in specs under `spec/controllers`. 44 | # 45 | # You can disable this behaviour by removing the line below, and instead 46 | # explicitly tag your specs with their type, e.g.: 47 | # 48 | # RSpec.describe UsersController, type: :controller do 49 | # # ... 50 | # end 51 | # 52 | # The different available types are documented in the features, such as in 53 | # https://relishapp.com/rspec/rspec-rails/docs 54 | config.infer_spec_type_from_file_location! 55 | end 56 | 57 | def setup_model_spec 58 | @user = DummyUser.create 59 | @commontable = DummyModel.create 60 | @thread = @commontable.commontator_thread 61 | end 62 | 63 | def setup_controller_spec 64 | setup_model_spec 65 | Thread.current[:user] = nil 66 | @commontable_path = Rails.application.routes.url_helpers.dummy_model_path(@commontable) 67 | end 68 | 69 | def setup_mailer_spec 70 | setup_model_spec 71 | end 72 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | if ENV['CODECLIMATE_REPO_TOKEN'] 2 | require 'codeclimate-test-reporter' 3 | CodeClimate::TestReporter.start 4 | else 5 | require 'simplecov' 6 | SimpleCov.start 'rails' 7 | end 8 | 9 | require 'rspec/core' 10 | 11 | # This file was generated by the `rails generate rspec:install` command. Conventionally, all 12 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 13 | # The generated `.rspec` file contains `--require spec_helper` which will cause this 14 | # file to always be loaded, without a need to explicitly require it in any files. 15 | # 16 | # Given that it is always loaded, you are encouraged to keep this file as 17 | # light-weight as possible. Requiring heavyweight dependencies from this file 18 | # will add to the boot time of your test suite on EVERY test run, even for an 19 | # individual file that may not need all of that loaded. Instead, consider making 20 | # a separate helper file that requires the additional dependencies and performs 21 | # the additional setup, and require it from the spec files that actually need it. 22 | # 23 | # The `.rspec` file also contains a few flags that are not defaults but that 24 | # users commonly want. 25 | # 26 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 27 | RSpec.configure do |config| 28 | # rspec-expectations config goes here. You can use an alternate 29 | # assertion/expectation library such as wrong or the stdlib/minitest 30 | # assertions if you prefer. 31 | config.expect_with :rspec do |expectations| 32 | # This option will default to `true` in RSpec 4. It makes the `description` 33 | # and `failure_message` of custom matchers include text for helper methods 34 | # defined using `chain`, e.g.: 35 | # be_bigger_than(2).and_smaller_than(4).description 36 | # # => "be bigger than 2 and smaller than 4" 37 | # ...rather than: 38 | # # => "be bigger than 2" 39 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 40 | end 41 | 42 | # rspec-mocks config goes here. You can use an alternate test double 43 | # library (such as bogus or mocha) by changing the `mock_with` option here. 44 | config.mock_with :rspec do |mocks| 45 | # Prevents you from mocking or stubbing a method that does not exist on 46 | # a real object. This is generally recommended, and will default to 47 | # `true` in RSpec 4. 48 | mocks.verify_partial_doubles = true 49 | end 50 | 51 | # The settings below are suggested to provide a good initial experience 52 | # with RSpec, but feel free to customize to your heart's content. 53 | # These two settings work together to allow you to limit a spec run 54 | # to individual examples or groups you care about by tagging them with 55 | # `:focus` metadata. When nothing is tagged with `:focus`, all examples 56 | # get run. 57 | config.filter_run :focus 58 | config.run_all_when_everything_filtered = true 59 | 60 | # Limits the available syntax to the non-monkey patched syntax that is recommended. 61 | # For more details, see: 62 | # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax 63 | # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 64 | # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching 65 | config.disable_monkey_patching! 66 | 67 | # Many RSpec users commonly either run the entire suite or an individual 68 | # file, and it's useful to allow more verbose output when running an 69 | # individual spec file. 70 | if config.files_to_run.one? 71 | # Use the documentation formatter for detailed output, 72 | # unless a formatter has already been configured 73 | # (e.g. via a command-line flag). 74 | config.default_formatter = 'doc' 75 | end 76 | 77 | # Print the 10 slowest examples and example groups at the 78 | # end of the spec run, to help surface which specs are running 79 | # particularly slow. 80 | #config.profile_examples = 10 81 | 82 | # Run specs in random order to surface order dependencies. If you find an 83 | # order dependency and want to debug it, you can fix the order by providing 84 | # the seed, which is printed after each run. 85 | # --seed 1234 86 | config.order = :random 87 | 88 | # Seed global randomization in this process using the `--seed` CLI option. 89 | # Setting this allows you to use `--seed` to deterministically reproduce 90 | # test failures related to randomization by passing the same `--seed` value 91 | # as the one that triggered the failure. 92 | Kernel.srand config.seed 93 | end 94 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/mentionsInput/jquery.mentionsInput.css: -------------------------------------------------------------------------------- 1 | .mentions-input-box { 2 | position: relative; 3 | background: #fff; 4 | } 5 | 6 | .mentions-input-box textarea { 7 | overflow: hidden; 8 | background: transparent; 9 | position: relative; 10 | resize: none; 11 | 12 | -webkit-box-sizing: border-box; 13 | -moz-box-sizing: border-box; 14 | box-sizing: border-box; 15 | } 16 | 17 | .mentions-input-box .mentions-autocomplete-list { 18 | display: none; 19 | background: #fff; 20 | border: 1px solid #b2b2b2; 21 | position: absolute; 22 | left: 0; 23 | right: 0; 24 | z-index: 10000; 25 | margin-top: -2px; 26 | 27 | border-radius:5px; 28 | border-top-right-radius:0; 29 | border-top-left-radius:0; 30 | 31 | -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.148438); 32 | -moz-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.148438); 33 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.148438); 34 | } 35 | 36 | .mentions-input-box .mentions-autocomplete-list ul { 37 | margin: 0; 38 | padding: 0; 39 | } 40 | 41 | .mentions-input-box .mentions-autocomplete-list li { 42 | background-color: #fff; 43 | padding: 0 5px; 44 | margin: 0; 45 | width: auto; 46 | border-bottom: 1px solid #eee; 47 | height: 26px; 48 | line-height: 26px; 49 | overflow: hidden; 50 | cursor: pointer; 51 | list-style: none; 52 | white-space: nowrap; 53 | } 54 | 55 | .mentions-input-box .mentions-autocomplete-list li:last-child { 56 | border-radius:5px; 57 | } 58 | 59 | .mentions-input-box .mentions-autocomplete-list li > img, 60 | .mentions-input-box .mentions-autocomplete-list li > div.icon { 61 | width: 16px; 62 | height: 16px; 63 | float: left; 64 | margin-top:5px; 65 | margin-right: 5px; 66 | -moz-background-origin:3px; 67 | 68 | border-radius:3px; 69 | } 70 | 71 | .mentions-input-box .mentions-autocomplete-list li em { 72 | font-weight: bold; 73 | font-style: none; 74 | } 75 | 76 | .mentions-input-box .mentions-autocomplete-list li:hover, 77 | .mentions-input-box .mentions-autocomplete-list li.active { 78 | background-color: #f2f2f2; 79 | } 80 | 81 | .mentions-input-box .mentions-autocomplete-list li b { 82 | background: #ffff99; 83 | font-weight: normal; 84 | } 85 | 86 | .mentions-input-box .mentions { 87 | position: absolute; 88 | left: 1px; 89 | right: 0; 90 | top: 1px; 91 | bottom: 0; 92 | padding: 9px; 93 | color: #fff; 94 | overflow: hidden; 95 | 96 | white-space: pre-wrap; 97 | word-wrap: break-word; 98 | } 99 | 100 | .mentions-input-box .mentions > div { 101 | color: #fff; 102 | white-space: pre-wrap; 103 | width: 100%; 104 | } 105 | 106 | .mentions-input-box .mentions > div > strong { 107 | font-weight:normal; 108 | background: #d8dfea; 109 | } 110 | 111 | .mentions-input-box .mentions > div > strong > span { 112 | filter: progid:DXImageTransform.Microsoft.Alpha(opacity=0); 113 | } 114 | --------------------------------------------------------------------------------