acts_as_mentionable 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a9e8c57fc330401901ba129fabddc2b3383f0c93b018a7bedf86f7a1c98a4835
4
+ data.tar.gz: da143817eb31788245c13d3ae16e978320db6f8d78de4bdfd1c66405e61df5e8
5
+ SHA512:
6
+ metadata.gz: ab5f7ababfd7e3dc833d1a63fde4c1fb1516d636843d80853cb448b274f134bd4a088ec6be6b183278b08746799ff508a38ef76de021e611c783ba8e37c9f76e
7
+ data.tar.gz: 215a73fc8de11cb20c7c3caf465cdf31cbc1a50dc8a9c7a36158010c1d88fc51378b46796b0a02d58f8768c82c4b7cf4f7348a6979f2d363f38b1aef4bbf535d
@@ -0,0 +1,66 @@
1
+ # Ruby CircleCI 2.0 configuration file
2
+ #
3
+ # Check https://circleci.com/docs/2.0/language-ruby/ for more details
4
+ #
5
+ version: 2
6
+ jobs:
7
+ build:
8
+ docker:
9
+ - image: circleci/ruby:2.3.8-jessie
10
+
11
+ # Specify service dependencies here if necessary
12
+ # CircleCI maintains a library of pre-built images
13
+ # documented at https://circleci.com/docs/2.0/circleci-images/
14
+ # - image: circleci/postgres:9.4
15
+
16
+ working_directory: ~/acts_as_mentionable
17
+
18
+ steps:
19
+ - checkout
20
+
21
+ # Download and cache dependencies
22
+ - restore_cache:
23
+ keys:
24
+ - v1-dependencies-
25
+
26
+ - run:
27
+ name: install dependencies
28
+ command: |
29
+ bundle install --jobs=4 --retry=3 --path vendor/bundle
30
+
31
+ - save_cache:
32
+ paths:
33
+ - ./vendor/bundle
34
+ key: v1-dependencies-{{ checksum "Gemfile.lock" }}
35
+
36
+ # run tests
37
+ - run:
38
+ name: Run Commit Message Checker
39
+ command: |
40
+ bundle exec fuse-dev-tools git validate_commit_message
41
+ - run:
42
+ name: Run Pull Request Validator
43
+ command: |
44
+ bundle exec fuse-dev-tools git validate_pull_request
45
+ - run:
46
+ name: Run Rubocop
47
+ command: |
48
+ bundle exec rubocop
49
+ - run:
50
+ name: Run Rspec
51
+ command: |
52
+ mkdir /tmp/test-results
53
+ TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)"
54
+
55
+ bundle exec rspec --format progress \
56
+ --format RspecJunitFormatter \
57
+ --out /tmp/test-results/rspec.xml \
58
+ --format progress \
59
+ $TEST_FILES
60
+
61
+ # collect reports
62
+ - store_test_results:
63
+ path: /tmp/test-results
64
+ - store_artifacts:
65
+ path: /tmp/test-results
66
+ destination: test-results
data/.gitignore ADDED
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /spec/debug.log
10
+ /Gemfile.lock
11
+
12
+ # rspec failure tracking
13
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,5 @@
1
+ inherit_gem:
2
+ fuse-dev-tools: lib/fuse_dev_tools/templates/rubocop.yml
3
+
4
+ AllCops:
5
+ TargetRubyVersion: 2.3
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.4
5
+ before_install: gem install bundler -v 1.17.1
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source 'https://rubygems.org'
2
+
3
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in acts_as_mentionable.gemspec
6
+ gemspec
7
+
8
+ group :development do
9
+ gem 'fuse-dev-tools', github: 'Fuseit/fuse-dev-tools'
10
+ gem 'pry'
11
+ gem 'pry-byebug'
12
+ gem 'rubocop'
13
+ gem 'rubocop-rspec'
14
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Baron Bloomer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,212 @@
1
+ # ActsAsMentionable
2
+
3
+ ActsAsMentionable is an ActiveRecord gem, which is called to help you build your own mentioning system on top of your Ruby On Rails project. With its help you can mention ActiveRecord objects from ActiveRecord objects, such as users from within the comment's text. For instance, when John Doe mentioned Richard Roe in a comment, then Richard Roe usually will receive the notification about being mentioned and mention will be stored in database.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'acts_as_mentionable'
11
+ ```
12
+
13
+ And then execute:
14
+ ```shell
15
+ $ bundle
16
+ ```
17
+
18
+ Or install it yourself as:
19
+ ```shell
20
+ $ gem install acts_as_mentionable
21
+ ```
22
+
23
+ #### Post Installation
24
+
25
+ Install migrations
26
+ ```shell
27
+ rake acts_as_mentionable_engine:install:migrations
28
+ ```
29
+
30
+ Review the generated migrations then migrate:
31
+ ```shell
32
+ rake db:migrate
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ Let's continue with mentioning users from the comment. In a such case comment is a mentioner, because it can mention users, and user is a mentionable, as user can be mentioned.
38
+
39
+ ### Mentioner model
40
+
41
+ ```ruby
42
+ # == Schema Information
43
+ #
44
+ # id :integer not null, primary key
45
+ # body :text(65535)
46
+ # parsed_body :text(65535)
47
+ #
48
+
49
+ class Comment < ActiveRecord::Base
50
+ # ...
51
+ acts_as_mentioner :body
52
+ # ...
53
+ def retrieve_mentions_callback
54
+ mentionables = ActsAsMentionable::MentionerParser.new(self).parse!
55
+ replace_mentionables(mentionables, save: true)
56
+ end
57
+ end
58
+ ```
59
+
60
+ Mentioner (`Comment`) model is expected to have two fields:
61
+ 1. The field, which contains, let's say, plain version of the comment - in the example above it's `body`. This field, by default, is rewritten by the parser and contains all the mentions as the plain text, e.g. `Hello, @john!`.
62
+ 2. The field, which contains comment with the mentions. It's named as the `parsed_*`, where `*` is meant to be the field, which contains plain version of the comment. In the example above it's named `parsed_body`. Mentions are recognized by the following template - `{#|RECORD_ID|RECORD_MODEL_NAME}`, where `RECORD_ID` is the ID of record being mentioned, for instance, user ID, and, `RECORD_MODEL_NAME` is the name of the model being mentioned, for instance, for `User` model it is `user`. According to this, `parsed_body` field may have the following value - `Hello, {#|42|user}!`.
63
+
64
+ So, as you may have already noticed, the argument provided to the `#acts_as_mentioner` method is the name of the field, which is a plain version of the comment - `body`, and the name of the field, which contains mentions is generated automatically - `parsed_body`.
65
+
66
+ ### Mentionable model
67
+
68
+ ```ruby
69
+ # == Schema Information
70
+ #
71
+ # id :integer not null, primary key
72
+ # username :string(255)
73
+ #
74
+
75
+ class User < ActiveRecord::Base
76
+ # ...
77
+ acts_as_mentionable :username
78
+ # ...
79
+ end
80
+ ```
81
+ The argument `username` provided to `#acts_as_mentionable` method, is a field on user's model, which is used as a replacement for mention template in order to build a plain version of the comment. For example, we have the following code:
82
+ ```ruby
83
+ user = User.create! id: 42, username: 'john'
84
+ comment = Comment.create! parsed_body: 'Hello, {#|42|user}!'
85
+ puts comment.body
86
+ # => Hello, @john!
87
+ ```
88
+ As you can see, it this case `{#|42|user}` in `parsed_body` is replaced by `@` + `username` field of the user in comment's `body`.
89
+
90
+ ### Retrieving mentions
91
+
92
+ Mentioner's `parsed_*` field is not parsed by default. You have to invoke `ActsAsMentionable::MentionerParser#parse!` method in order to parse it and retrieve mentionables (users in our case).
93
+
94
+ ```ruby
95
+ mentionables = ActsAsMentionable::MentionerParser.new(comment).parse!
96
+ ```
97
+
98
+ The line above parses comment's `parsed_body` field and does the following:
99
+ 1. Generates and saves the plain version of the comment - `body` field.
100
+ 2. Returns mentionables - in our case only users are expected, but it's possible to mix different types of mentionables.
101
+
102
+ We can mix together several types of mentionables. Let's check the following code:
103
+
104
+ ```ruby
105
+ comment = Comment.create! \
106
+ parsed_body: 'Hello, {#|42|user}! Can you please join the {#|123|community}?'
107
+
108
+ mentionables = ActsAsMentionable::MentionerParser.new(comment).parse!
109
+
110
+ comment.body
111
+ # => Hello, @john! Can you please join the @developers?
112
+
113
+ mentionables
114
+ # => [#<User:...>, #<Community:...>]
115
+ ```
116
+
117
+ In this case `User` with ID `42` and `Community` with ID `123` are mentioned, but only if `Community` model has `acts_as_mentionable` defined on it. Otherwise, `{#|123|community}` will be left as is:
118
+ ```ruby
119
+ comment.body
120
+ # => Hello, @john! Can you please join the {#|123|community}?
121
+
122
+ mentionables
123
+ # => [#<User:...>]
124
+ ```
125
+
126
+ Just in case, `ActsAsMentionable::MentionerParser` class does not fit your needs, you can either extend or define your own class, which parses `parsed_body` field, writes `body` and returns mentioned records. Here you can see implementation details of the existing one [mentioner_parser.rb](https://github.com/Fuseit/acts_as_mentionable/blob/master/lib/acts_as_mentionable/mentioner_parser.rb).
127
+
128
+ ### Saving mentions
129
+
130
+ In order to save mentioned records you can use of the following methods defined on mentioner:
131
+
132
+ Mention records, `save` option is set to `false` by default:
133
+ ```ruby
134
+ comment.mention mentionables, save: false
135
+ ```
136
+
137
+ Unmention records, `save` option is set to `false` by default:
138
+ ```ruby
139
+ comment.unmention mentionables, save: false
140
+ ```
141
+
142
+ Replace mentioned records, `save` option is set to `false` by default:
143
+ ```ruby
144
+ comment.replace_mentionables mentionables, save: false
145
+ ```
146
+
147
+ Save changes to mentions:
148
+ ```ruby
149
+ comment.save_mentions
150
+ ```
151
+
152
+ ### Callbacks
153
+
154
+ When there are changes to mentions and mentions being saved a callback with mentioner record and mention changes can be invoked. In order to define a callback, add the Rails initializer like the following:
155
+
156
+ ```ruby
157
+ # config/initializers/acts_as_mentionable.rb
158
+
159
+ ActsAsMentionable.setup do |configuration|
160
+ configuration.mentions_updated_callback = lambda do |mentioner, changes|
161
+ p mentioner
162
+ # => #<Comment:...>
163
+
164
+ p changes
165
+ # =>
166
+ # {
167
+ # changed: true,
168
+ # added: [#<User:...>, #<Community:...>],
169
+ # removed: [#<User:...>],
170
+ # previous: [#<User:...>],
171
+ # current: [#<User:...>, #<Community:...>]
172
+ # }
173
+ end
174
+ end
175
+ ```
176
+
177
+ Where, `mentioner` in our case is a comment record, which mentioned users, and changes is a hash of changes, which has the following keys:
178
+ * `:changed` - detects if changed is being made to mentions
179
+ * `:added` - defines, which mentions were added
180
+ * `:removed` - defines, which mentions were removed
181
+ * `:previous` - defines, which mentions were before
182
+ * `:current` - defines, which mentions are currently saved
183
+
184
+ ### Mentioner methods
185
+
186
+ * `#mentioner?` - is always set to `true` for mentioner records (e.g. comment), otherwise is set to `false`
187
+ * `#mentionables` - returns mentioned records
188
+ * `#mention *mentionables, save: false` - mention records
189
+ * `#unmention *mentionables, save: false` - unmention records
190
+ * `#replace_mentionables *mentionables, save: false` - replace mentioned records
191
+ * `#save_mentions` - save changes to mentions
192
+ * `#retrieve_mentions_callback` - is a callback method, which should retrieve and save mentions. It does nothing by default, so you have to redefine it.
193
+ * `#need_retrieve_mentions?` - returns `true` if `#retrieve_mentions_callback` should be invoked. By default it's an alias to mentioner's `parsed_*_changed?` attribute, e.g. `parsed_body_changed?`. You can redefine this method if you want to have custom logic for invoking `#retrieve_mentions_callback`.
194
+
195
+ ### Mentionable methods
196
+
197
+ * `#mentionable?` - is always set to `true` for mentionable records (e.g. user), otherwise is set to `false`
198
+ * `#mentionables` - returns mentioner records, e. g. an array of comments, where user is mentioned
199
+
200
+ ## Development
201
+
202
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
203
+
204
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
205
+
206
+ ## Contributing
207
+
208
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Fuseit/acts_as_mentionable.
209
+
210
+ ## License
211
+
212
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,37 @@
1
+ lib = File.expand_path('lib', __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'acts_as_mentionable/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'acts_as_mentionable'
7
+ spec.version = ActsAsMentionable::VERSION
8
+ spec.authors = ['Baron Bloomer', 'Dmitry Radionov', 'Nazar Vinnychuk']
9
+ spec.email = ['baronbloomer@gmail.com']
10
+
11
+ spec.summary = 'Add the ability to mention ActiveRecord objects such as users within text!'
12
+ spec.description = 'With ActsAsMentionable you can mention a different models in different contents.'
13
+ spec.homepage = 'https://github.com/Fuseit/acts_as_mentionable'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+
20
+ spec.bindir = 'exe'
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+
23
+ spec.require_paths = %w[lib]
24
+ spec.required_ruby_version = '>= 2.3.4'
25
+
26
+ # Need to support different ActiveRecord versions i.e. 4.2, 5+
27
+ # Differentiate through versioned code?
28
+ spec.add_runtime_dependency 'activerecord', '~> 4.2'
29
+
30
+ spec.add_development_dependency 'bundler', '~> 1.17'
31
+ spec.add_development_dependency 'database_cleaner', '~> 1.7'
32
+ spec.add_development_dependency 'rake', '~> 10.5'
33
+ spec.add_development_dependency 'rspec', '~> 3.8'
34
+ spec.add_development_dependency 'rspec-its', '~> 1.2'
35
+ spec.add_development_dependency 'rspec_junit_formatter'
36
+ spec.add_development_dependency 'sqlite3', '~> 1.3'
37
+ end
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'acts_as_mentionable'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require 'irb'
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,20 @@
1
+ if ActiveRecord.gem_version >= Gem::Version.new('5.0')
2
+ class ActsAsMentionableMigration < ActiveRecord::Migration[4.2]; end
3
+ else
4
+ class ActsAsMentionableMigration < ActiveRecord::Migration; end
5
+ end
6
+
7
+ ActsAsMentionableMigration.class_eval do
8
+ def change
9
+ create_table ActsAsMentionable.mentions_table do |t|
10
+ t.references :mentionable, polymorphic: true, index: { name: 'mentions_mentionable_idx' }
11
+ t.references :mentioner, polymorphic: true, index: { name: 'mentions_mentioner_idx' }
12
+ t.timestamps
13
+ end
14
+
15
+ add_index ActsAsMentionable.mentions_table,
16
+ %i[mentionable_id mentionable_type mentioner_id mentioner_type],
17
+ name: 'mentions_mentionable_mentioner_idx',
18
+ unique: true
19
+ end
20
+ end
@@ -0,0 +1,40 @@
1
+ module ActsAsMentionable
2
+ module ActiveRecordMethods
3
+ extend ActiveSupport::Concern
4
+
5
+ class_methods do
6
+ def acts_as_mentionable mentionable_field
7
+ class_eval do
8
+ cattr_reader :mentionable_field
9
+
10
+ class_variable_set('@@mentionable_field', mentionable_field)
11
+ end
12
+ include ActsAsMentionable::Mentionable
13
+ end
14
+
15
+ def acts_as_mentioner mention_field
16
+ class_eval do
17
+ cattr_reader :mention_field, :mention_parsed_field
18
+
19
+ after_save :retrieve_mentions_callback, if: :need_retrieve_mentions? if self <= ActiveRecord::Base
20
+
21
+ define_method(:need_retrieve_mentions?) { send "parsed_#{self.class.mention_field}_changed?" }
22
+
23
+ define_method(:retrieve_mentions_callback) { nil }
24
+
25
+ class_variable_set('@@mention_field', mention_field)
26
+ class_variable_set('@@mention_parsed_field', "parsed_#{mention_field}".to_sym)
27
+ end
28
+ include ActsAsMentionable::Mentioner
29
+ end
30
+ end
31
+
32
+ def mentionable?
33
+ false
34
+ end
35
+
36
+ def mentioner?
37
+ false
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,4 @@
1
+ module ActsAsMentionable
2
+ class Engine < ::Rails::Engine
3
+ end
4
+ end
@@ -0,0 +1,46 @@
1
+ # == Schema Information
2
+ #
3
+ # Table name: mentions
4
+ #
5
+ # id :integer not null, primary key
6
+ # mentionable_id :integer not null
7
+ # mentionable_type :string(255) not null
8
+ # mentioner_id :integer not null
9
+ # mentioner_type :string(255) not null
10
+ # created_at :datetime not null
11
+ # updated_at :datetime not null
12
+ #
13
+
14
+ module ActsAsMentionable
15
+ class Mention < ::ActiveRecord::Base
16
+ self.table_name = ActsAsMentionable.mentions_table
17
+
18
+ belongs_to :mentioner, polymorphic: true
19
+ belongs_to :mentionable, polymorphic: true
20
+
21
+ scope :by_mentioners, ->(mentioners) { where mentioner: mentioners }
22
+ scope :by_mentionables, ->(mentionables) { where mentionable: mentionables }
23
+
24
+ validate :validate_mentioner
25
+ validate :validate_mentionable
26
+
27
+ def self.remove_mentionables_for_mentioner mentioner, mentionables
28
+ by_mentioners(mentioner).by_mentionables(mentionables).delete_all
29
+ end
30
+
31
+ def self.add_mentionables_for_mentioner mentioner, mentionables
32
+ attributes_list = Array(mentionables).map { |mentionable| { mentionable: mentionable } }
33
+ by_mentioners(mentioner).create! attributes_list
34
+ end
35
+
36
+ private
37
+
38
+ def validate_mentioner
39
+ errors.add :mentioner, :invalid unless mentioner.respond_to?(:mentioner?) && mentioner.mentioner?
40
+ end
41
+
42
+ def validate_mentionable
43
+ errors.add :mentionable, :invalid unless mentionable.respond_to?(:mentionable?) && mentionable.mentionable?
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,20 @@
1
+ module ActsAsMentionable
2
+ module Mentionable
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ has_many :mentions,
7
+ as: :mentionable,
8
+ dependent: :delete_all,
9
+ class_name: '::ActsAsMentionable::Mention'
10
+ end
11
+
12
+ def mentionable?
13
+ true
14
+ end
15
+
16
+ def mentioners
17
+ RetrievePolymorphic.new(mentions, :mentioner).call
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,66 @@
1
+ module ActsAsMentionable
2
+ class MentionablesManipulator
3
+ attr_reader :previous_set, :current_set
4
+
5
+ def initialize mentionables
6
+ @previous_set = mentionables.to_set
7
+ @current_set = mentionables.to_set
8
+ end
9
+
10
+ def add *mentionables
11
+ current_set.merge prepare_mentionables(mentionables)
12
+ self
13
+ end
14
+
15
+ def remove *mentionables
16
+ current_set.subtract prepare_mentionables(mentionables)
17
+ self
18
+ end
19
+
20
+ def replace *mentionables
21
+ current_set.replace prepare_mentionables(mentionables)
22
+ self
23
+ end
24
+
25
+ def fix_changes!
26
+ previous_set.replace current_set
27
+ self
28
+ end
29
+
30
+ def changes
31
+ {
32
+ changed: changed?,
33
+ added: added,
34
+ removed: removed,
35
+ previous: previous,
36
+ current: current
37
+ }
38
+ end
39
+
40
+ def changed?
41
+ current_set != previous_set
42
+ end
43
+
44
+ def added
45
+ (current_set - previous_set).to_a
46
+ end
47
+
48
+ def removed
49
+ (previous_set - current_set).to_a
50
+ end
51
+
52
+ def previous
53
+ previous_set.to_a
54
+ end
55
+
56
+ def current
57
+ current_set.to_a
58
+ end
59
+
60
+ private
61
+
62
+ def prepare_mentionables mentionables
63
+ mentionables.flatten
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,69 @@
1
+ module ActsAsMentionable
2
+ module Mentioner
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ has_many :mentions,
7
+ as: :mentioner,
8
+ dependent: :delete_all,
9
+ class_name: '::ActsAsMentionable::Mention'
10
+ end
11
+
12
+ def mentioner?
13
+ true
14
+ end
15
+
16
+ def mentionables
17
+ RetrievePolymorphic.new(mentions, :mentionable).call
18
+ end
19
+
20
+ def mention *mentionables, save: false
21
+ mentionables_manipulator.add(*mentionables)
22
+ return_changes { save_mentions if save }
23
+ end
24
+
25
+ def unmention *mentionables, save: false
26
+ mentionables_manipulator.remove(*mentionables)
27
+ return_changes { save_mentions if save }
28
+ end
29
+
30
+ def replace_mentionables *mentionables, save: false
31
+ mentionables_manipulator.replace(*mentionables)
32
+ return_changes { save_mentions if save }
33
+ end
34
+
35
+ def save_mentions
36
+ return unless mentionables_manipulator.changed?
37
+
38
+ return_changes do
39
+ MentionsUpdater.new(self, mentionables_manipulator.changes).call do
40
+ fix_mentionables_changes!
41
+ end
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def return_changes
48
+ mentionables_manipulator.changes.tap { yield }
49
+ end
50
+
51
+ def mentionables_manipulator
52
+ @mentionables_manipulator ||= MentionablesManipulator.new mentionables
53
+ end
54
+
55
+ def refresh_mentionables_manipulator!
56
+ remove_instance_variable :@mentionables_manipulator
57
+ end
58
+
59
+ def fix_mentionables_changes!
60
+ mentionables_manipulator.fix_changes!
61
+
62
+ TransactionCallbacks.on_rolled_back do
63
+ current = mentionables_manipulator.current
64
+ refresh_mentionables_manipulator!
65
+ mentionables_manipulator.replace current
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,41 @@
1
+ module ActsAsMentionable
2
+ class MentionerParser
3
+ attr_reader :mentioner
4
+
5
+ def initialize mentioner
6
+ @mentioner = mentioner
7
+ end
8
+
9
+ def parse!
10
+ mentionables = []
11
+ new_mention_field = mentioner.send(mentioner.class.mention_parsed_field).gsub(template_for_parsing) do |template|
12
+ _delimiter, mentionable_id, mentionable_klass = template.gsub(/[\{,\}]/, '').split('|')
13
+ mentionable = retrieve_mentionable mentionable_klass, mentionable_id
14
+ mentionables << mentionable if mentionable.respond_to?(:mentionable?) && mentionable.mentionable?
15
+ metionable_template mentionable, template
16
+ end
17
+ mentioner.update_column(mentioner.class.mention_field, new_mention_field)
18
+ mentionables
19
+ end
20
+
21
+ def retrieve_mentionable mentionable_klass, mentionable_id
22
+ mentionable_klass.classify.constantize.find mentionable_id
23
+ rescue NameError
24
+ nil
25
+ end
26
+
27
+ private
28
+
29
+ def template_for_parsing
30
+ /\{@\|\d+\|\w+\}/
31
+ end
32
+
33
+ def metionable_template mentionable, template
34
+ if mentionable.respond_to?(:mentionable?) && mentionable.mentionable?
35
+ "<U+2063>@#{mentionable.send(mentionable.class.mentionable_field)}<U+2063>"
36
+ else
37
+ template
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,35 @@
1
+ module ActsAsMentionable
2
+ class MentionsUpdater
3
+ attr_reader :mentioner, :changes
4
+
5
+ def initialize mentioner, changes
6
+ @mentioner = mentioner
7
+ @changes = changes
8
+ end
9
+
10
+ def call
11
+ Mention.transaction do
12
+ remove_old_mentionables
13
+ add_new_mentionables
14
+ invoke_mentions_updated_callback
15
+ yield if block_given?
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def remove_old_mentionables
22
+ Mention.remove_mentionables_for_mentioner mentioner, changes[:removed] unless changes[:removed].empty?
23
+ end
24
+
25
+ def add_new_mentionables
26
+ Mention.add_mentionables_for_mentioner mentioner, changes[:added] unless changes[:added].empty?
27
+ end
28
+
29
+ def invoke_mentions_updated_callback
30
+ TransactionCallbacks.on_committed do
31
+ ActsAsMentionable.mentions_updated_callback.call mentioner, changes
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,26 @@
1
+ module ActsAsMentionable
2
+ class RetrievePolymorphic
3
+ attr_reader :relation, :polymorphic_association_name
4
+
5
+ def initialize relation, polymorphic_association_name
6
+ @relation = relation
7
+ @polymorphic_association_name = polymorphic_association_name
8
+ end
9
+
10
+ def call
11
+ type_to_ids.map { |type, ids| type.constantize.unscoped.find ids }.flatten
12
+ end
13
+
14
+ private
15
+
16
+ def type_and_id_list
17
+ relation.pluck "#{polymorphic_association_name}_type", "#{polymorphic_association_name}_id"
18
+ end
19
+
20
+ def type_to_ids
21
+ type_and_id_list.group_by(&:first).each_with_object({}) do |(type, type_and_id), hash|
22
+ hash[type] = type_and_id.map(&:last)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,42 @@
1
+ module ActsAsMentionable
2
+ class TransactionCallbacks
3
+ class << self
4
+ def on_committed &block
5
+ if current_transaction.open?
6
+ add_transaction_record new(block, nil)
7
+ else
8
+ yield
9
+ end
10
+ end
11
+
12
+ def on_rolled_back &block
13
+ add_transaction_record new(nil, block)
14
+ end
15
+
16
+ private
17
+
18
+ def add_transaction_record record
19
+ current_transaction.add_record record
20
+ end
21
+
22
+ def current_transaction
23
+ Mention.connection.current_transaction
24
+ end
25
+ end
26
+
27
+ attr_reader :on_committed_block, :on_rolled_back_block
28
+
29
+ def initialize on_committed_block, on_rolled_back_block
30
+ @on_committed_block = on_committed_block
31
+ @on_rolled_back_block = on_rolled_back_block
32
+ end
33
+
34
+ def committed! _should_run_callbacks = true
35
+ on_committed_block&.call
36
+ end
37
+
38
+ def rolledback! _force_restore_state = false, _should_run_callbacks = true
39
+ on_rolled_back_block&.call
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,3 @@
1
+ module ActsAsMentionable
2
+ VERSION = '0.2.0'.freeze
3
+ end
@@ -0,0 +1,57 @@
1
+ require 'active_record'
2
+ require 'acts_as_mentionable/version'
3
+
4
+ begin
5
+ require 'rails/engine'
6
+ require 'acts_as_mentionable/engine'
7
+ rescue LoadError
8
+ puts 'Rails enviroment is not detected - database migrations are not appended.'
9
+ end
10
+
11
+ module ActsAsMentionable
12
+ extend ActiveSupport::Autoload
13
+
14
+ autoload :Mention
15
+ autoload :MentionablesManipulator
16
+ autoload :MentionsUpdater
17
+ autoload :Mentionable
18
+ autoload :Mentioner
19
+ autoload :ActiveRecordMethods
20
+ autoload :RetrievePolymorphic
21
+ autoload :TransactionCallbacks
22
+ autoload :MentionerParser
23
+
24
+ def self.setup
25
+ @configuration ||= Configuration.new
26
+ yield @configuration if block_given?
27
+ end
28
+
29
+ def self.respond_to_missing? method_name, include_private = false
30
+ @configuration.respond_to?(method_name) || super
31
+ end
32
+
33
+ def self.method_missing method_name, *args, &block
34
+ if @configuration.respond_to?(method_name)
35
+ @configuration.send(method_name, *args, &block)
36
+ else
37
+ super
38
+ end
39
+ end
40
+
41
+ class Configuration
42
+ attr_accessor \
43
+ :mentions_table,
44
+ :mentions_updated_callback
45
+
46
+ def initialize
47
+ @mentions_table = :acts_as_mentionable_mentions
48
+ @mentions_updated_callback = ->(_mentioner, _changes) { nil }
49
+ end
50
+ end
51
+
52
+ setup
53
+ end
54
+
55
+ ActiveSupport.on_load :active_record do
56
+ include ActsAsMentionable::ActiveRecordMethods
57
+ end
metadata ADDED
@@ -0,0 +1,184 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: acts_as_mentionable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Baron Bloomer
8
+ - Dmitry Radionov
9
+ - Nazar Vinnychuk
10
+ autorequire:
11
+ bindir: exe
12
+ cert_chain: []
13
+ date: 2019-06-18 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activerecord
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - "~>"
20
+ - !ruby/object:Gem::Version
21
+ version: '4.2'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - "~>"
27
+ - !ruby/object:Gem::Version
28
+ version: '4.2'
29
+ - !ruby/object:Gem::Dependency
30
+ name: bundler
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - "~>"
34
+ - !ruby/object:Gem::Version
35
+ version: '1.17'
36
+ type: :development
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - "~>"
41
+ - !ruby/object:Gem::Version
42
+ version: '1.17'
43
+ - !ruby/object:Gem::Dependency
44
+ name: database_cleaner
45
+ requirement: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '1.7'
50
+ type: :development
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - "~>"
55
+ - !ruby/object:Gem::Version
56
+ version: '1.7'
57
+ - !ruby/object:Gem::Dependency
58
+ name: rake
59
+ requirement: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - "~>"
62
+ - !ruby/object:Gem::Version
63
+ version: '10.5'
64
+ type: :development
65
+ prerelease: false
66
+ version_requirements: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - "~>"
69
+ - !ruby/object:Gem::Version
70
+ version: '10.5'
71
+ - !ruby/object:Gem::Dependency
72
+ name: rspec
73
+ requirement: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - "~>"
76
+ - !ruby/object:Gem::Version
77
+ version: '3.8'
78
+ type: :development
79
+ prerelease: false
80
+ version_requirements: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - "~>"
83
+ - !ruby/object:Gem::Version
84
+ version: '3.8'
85
+ - !ruby/object:Gem::Dependency
86
+ name: rspec-its
87
+ requirement: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - "~>"
90
+ - !ruby/object:Gem::Version
91
+ version: '1.2'
92
+ type: :development
93
+ prerelease: false
94
+ version_requirements: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - "~>"
97
+ - !ruby/object:Gem::Version
98
+ version: '1.2'
99
+ - !ruby/object:Gem::Dependency
100
+ name: rspec_junit_formatter
101
+ requirement: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ type: :development
107
+ prerelease: false
108
+ version_requirements: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ - !ruby/object:Gem::Dependency
114
+ name: sqlite3
115
+ requirement: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - "~>"
118
+ - !ruby/object:Gem::Version
119
+ version: '1.3'
120
+ type: :development
121
+ prerelease: false
122
+ version_requirements: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - "~>"
125
+ - !ruby/object:Gem::Version
126
+ version: '1.3'
127
+ description: With ActsAsMentionable you can mention a different models in different
128
+ contents.
129
+ email:
130
+ - baronbloomer@gmail.com
131
+ executables: []
132
+ extensions: []
133
+ extra_rdoc_files: []
134
+ files:
135
+ - ".circleci/config.yml"
136
+ - ".gitignore"
137
+ - ".rspec"
138
+ - ".rubocop.yml"
139
+ - ".travis.yml"
140
+ - Gemfile
141
+ - LICENSE.txt
142
+ - README.md
143
+ - Rakefile
144
+ - acts_as_mentionable.gemspec
145
+ - bin/console
146
+ - bin/setup
147
+ - db/migrate/1_acts_as_mentionable_migration.rb
148
+ - lib/acts_as_mentionable.rb
149
+ - lib/acts_as_mentionable/active_record_methods.rb
150
+ - lib/acts_as_mentionable/engine.rb
151
+ - lib/acts_as_mentionable/mention.rb
152
+ - lib/acts_as_mentionable/mentionable.rb
153
+ - lib/acts_as_mentionable/mentionables_manipulator.rb
154
+ - lib/acts_as_mentionable/mentioner.rb
155
+ - lib/acts_as_mentionable/mentioner_parser.rb
156
+ - lib/acts_as_mentionable/mentions_updater.rb
157
+ - lib/acts_as_mentionable/retrieve_polymorphic.rb
158
+ - lib/acts_as_mentionable/transaction_callbacks.rb
159
+ - lib/acts_as_mentionable/version.rb
160
+ homepage: https://github.com/Fuseit/acts_as_mentionable
161
+ licenses:
162
+ - MIT
163
+ metadata: {}
164
+ post_install_message:
165
+ rdoc_options: []
166
+ require_paths:
167
+ - lib
168
+ required_ruby_version: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: 2.3.4
173
+ required_rubygems_version: !ruby/object:Gem::Requirement
174
+ requirements:
175
+ - - ">="
176
+ - !ruby/object:Gem::Version
177
+ version: '0'
178
+ requirements: []
179
+ rubyforge_project:
180
+ rubygems_version: 2.7.8
181
+ signing_key:
182
+ specification_version: 4
183
+ summary: Add the ability to mention ActiveRecord objects such as users within text!
184
+ test_files: []