simple_hashtag 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.rspec +2 -0
- data/.travis.yml +6 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +174 -0
- data/Rakefile +6 -0
- data/lib/generators/simple_hashtag/migration_generator.rb +20 -0
- data/lib/generators/simple_hashtag/templates/migrations/create_hashtaggings_migration.rb +11 -0
- data/lib/generators/simple_hashtag/templates/migrations/create_hashtags_migration.rb +10 -0
- data/lib/generators/simple_hashtag/templates/views/hashtags_controller.rb +8 -0
- data/lib/generators/simple_hashtag/templates/views/hashtags_helper.rb +16 -0
- data/lib/generators/simple_hashtag/templates/views/hashtags_index.html.erb +8 -0
- data/lib/generators/simple_hashtag/views_generator.rb +14 -0
- data/lib/simple_hashtag/hashtag.rb +39 -0
- data/lib/simple_hashtag/hashtaggable.rb +45 -0
- data/lib/simple_hashtag/hashtagging.rb +8 -0
- data/lib/simple_hashtag/version.rb +3 -0
- data/lib/simple_hashtag.rb +6 -0
- data/simple_hashtag.gemspec +28 -0
- data/spec/dummy/README.rdoc +261 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/assets/javascripts/application.js +15 -0
- data/spec/dummy/app/assets/stylesheets/application.css +13 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/mailers/.gitkeep +0 -0
- data/spec/dummy/app/models/.gitkeep +0 -0
- data/spec/dummy/app/models/picture.rb +4 -0
- data/spec/dummy/app/models/post.rb +3 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config/application.rb +56 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +37 -0
- data/spec/dummy/config/environments/production.rb +67 -0
- data/spec/dummy/config/environments/test.rb +37 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +15 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +2 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/db/migrate/20130919041824_create_posts.rb +9 -0
- data/spec/dummy/db/migrate/20130919041825_create_pictures.rb +9 -0
- data/spec/dummy/db/migrate/20131004085836_create_simple_hashtag_hashtags.rb +9 -0
- data/spec/dummy/db/migrate/20131004085907_create_simple_hashtag_hashtaggings.rb +10 -0
- data/spec/dummy/db/schema.rb +43 -0
- data/spec/dummy/lib/assets/.gitkeep +0 -0
- data/spec/dummy/log/.gitkeep +0 -0
- data/spec/dummy/log/development.log +92 -0
- data/spec/dummy/log/test.log +3423 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +25 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/simple_hashtag_spec.rb +103 -0
- data/spec/spec_helper.rb +13 -0
- metadata +221 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e8124940426b09197f994c911e96bc38f9e9e5c6
|
4
|
+
data.tar.gz: caea8d6daeab342ad2f59293811dfd31d4f83a28
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 437c0a695584060eb1d62d244f8dab530eb08275ee578ef098140e4d472df5ee8aa258139453755a46afa8965ba1064277c4074548b439154180b32bdcc35529
|
7
|
+
data.tar.gz: 911e3593a7869ff82bb6198f1b06a78bea959e5f2691b5b22544665f679734503fe082b28a8fadc0164d02c98a59f6a770d5843fc022d63c950aa0e8eaa7f4c8
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Raphael Campardou
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
# SimpleHashtag
|
2
|
+
|
3
|
+
Ruby gem for Rails that parses, stores, retreives and formats hashtags in your model.
|
4
|
+
|
5
|
+
_Simple Hashtag_ is a mix between–well–hashtags, as we know them, and categories.
|
6
|
+
It will scan your Active Record attribute for a hashtag, store it in an index, and display a page with each object containing the tag.
|
7
|
+
|
8
|
+
It's simple, and like all things simple, it can create a nice effect, quickly.
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Add this line to your application's Gemfile:
|
13
|
+
```ruby
|
14
|
+
gem 'simple_hashtag'
|
15
|
+
```
|
16
|
+
|
17
|
+
And execute:
|
18
|
+
```shell
|
19
|
+
$ bundle
|
20
|
+
```
|
21
|
+
|
22
|
+
Then you have to generate the migration files:
|
23
|
+
```shell
|
24
|
+
$ rails g simple_hashtag:migration
|
25
|
+
```
|
26
|
+
|
27
|
+
This will create two migration files, one for the `hashtags` table and one for the `hashtagging` table.
|
28
|
+
You will need to run `rake db:migrate` to actually create the tables.
|
29
|
+
|
30
|
+
__Optionnally__, you can create views,
|
31
|
+
if only to guide you through your own implementation:
|
32
|
+
```shell
|
33
|
+
$ rails g simple_hashtag:views
|
34
|
+
```
|
35
|
+
|
36
|
+
This will create a __basic controller__, a __short index view__ and a __small helper__.
|
37
|
+
It assume your views follow the convention of having a directory named after your model's plural, and a partial named after your model's name.
|
38
|
+
```
|
39
|
+
app
|
40
|
+
|-- views
|
41
|
+
| |-- posts
|
42
|
+
| | |-- _post.html.erb
|
43
|
+
```
|
44
|
+
|
45
|
+
|
46
|
+
## Usage
|
47
|
+
|
48
|
+
Just add `include SimpleHashtag::Hashtaggable` in your model.
|
49
|
+
|
50
|
+
_Simple Hasthag_ will parse the `body` attribute by default:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
class Post < ActiveRecord::Base
|
54
|
+
include SimpleHashtag::Hashtaggable
|
55
|
+
end
|
56
|
+
```
|
57
|
+
|
58
|
+
|
59
|
+
If you need to parse another attribute instead,
|
60
|
+
add `hashtaggable_attribute` followed by the name of your attribute, i.e.:
|
61
|
+
```ruby
|
62
|
+
class Picture < ActiveRecord::Base
|
63
|
+
include SimpleHashtag::Hashtaggable
|
64
|
+
hashtaggable_attribute :caption
|
65
|
+
end
|
66
|
+
```
|
67
|
+
|
68
|
+
From here on, if your text contains a hashtag, say _#RubyRocks_,
|
69
|
+
_Simple Hasthag_ will find it, store it in a table and retreive it and its associated object if asked.
|
70
|
+
Helpers are also available to create a link when displaying the text.
|
71
|
+
|
72
|
+
### Controller and Views
|
73
|
+
If you don't want to bother looking at the genrerated controller and views, here is a quick peek.
|
74
|
+
In a Controller, search for a Hashtag, and its associated records:
|
75
|
+
```ruby
|
76
|
+
class HashtagsController < ApplicationController
|
77
|
+
def index
|
78
|
+
@hashtag = SimpleHashtag::Hashtag.find_by_name(params[:hashtag])
|
79
|
+
@hashtagged = @hashtag.hashtaggables if @hashtag
|
80
|
+
end
|
81
|
+
end
|
82
|
+
```
|
83
|
+
|
84
|
+
|
85
|
+
The index view could resemble something like this:
|
86
|
+
```erb
|
87
|
+
<h1><%= params[:hashtag] %></h1>
|
88
|
+
<% if @hashtagged %>
|
89
|
+
<% @hashtagged.each do |hashtagged| %>
|
90
|
+
<% view = hashtagged.class.to_s.underscore.pluralize %>
|
91
|
+
<% partial = hashtagged.class.to_s.underscore %>
|
92
|
+
<%= render "#{view}/#{partial}", {partial.to_sym => hashtagged} %>
|
93
|
+
<% end -%>
|
94
|
+
<% else -%>
|
95
|
+
<p>There is no match for the <em><%= params[:hashtag] %></em> hashtag.</p>
|
96
|
+
<% end -%>
|
97
|
+
```
|
98
|
+
In the gem it is actually extracted in a helper.
|
99
|
+
|
100
|
+
|
101
|
+
### Routes
|
102
|
+
|
103
|
+
If you use the provided controller and views, add this line to your `config/routes.rb` file:
|
104
|
+
```ruby
|
105
|
+
get 'hashtags/:hashtag', to: 'hashtags#index', as: :hashtag
|
106
|
+
```
|
107
|
+
|
108
|
+
The helper generating the link relies on it.
|
109
|
+
|
110
|
+
|
111
|
+
|
112
|
+
### Spring Cleaning
|
113
|
+
There is a class method `SimpleHashtag::Hashtag#clean_orphans` to remove unused hashtags from the DB.
|
114
|
+
It is currently not hooked, for two reasons:
|
115
|
+
- It is not optimised at all, DB-wise.
|
116
|
+
- Destructive method should be called explicitly.
|
117
|
+
|
118
|
+
Knowing all this, you can hook it after each change, or automate it with a Cron job, or even spring-clean manually once in a while.
|
119
|
+
|
120
|
+
Improvements for this method are listed in the Todo section below.
|
121
|
+
|
122
|
+
|
123
|
+
## Gotchas
|
124
|
+
### Association Query
|
125
|
+
The association between a Hashtag and your models is a polymorphic many-to-many.
|
126
|
+
When querying the polymorphic association from the other side (tag.hashtaggables),
|
127
|
+
we perform a DB query for each hashtaggable, resulting in an n+1 query.
|
128
|
+
|
129
|
+
The object returned by the query is an array, not an Arel query, so you can't chain (i.e.: to specify the order), and should do it by hand:
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
hashtag = SimpleHashtag.find_by_name("RubyRocks")
|
133
|
+
posts_and_picts = hashtag.hattaggables
|
134
|
+
posts_and_picts.sort_by! { |p| p.created_at }
|
135
|
+
```
|
136
|
+
|
137
|
+
### find_by
|
138
|
+
|
139
|
+
To preserve coherence, Hashtags are stored downcased.
|
140
|
+
To ensure coherence, they are also searched downcased.
|
141
|
+
Internally, the model overrides `find_by_name` to perform the downcase query.
|
142
|
+
Should you search Hashtags manually you should use the `SimpleHashtag::Hashtag#find_by_name` method, instead of `SimpleHashtag::Hashtag.find_by(name: 'RubyRocks')`
|
143
|
+
|
144
|
+
|
145
|
+
## ToDo
|
146
|
+
|
147
|
+
_Simple Hashtag_ is in its very early stage and would need a lot of love to reach 1.0.0.
|
148
|
+
Among the many improvement areas:
|
149
|
+
|
150
|
+
- Make the Regex that parses the text for the hashtag much more robust.
|
151
|
+
This is how Twitter does it:
|
152
|
+
[https://github.com/twitter/twitter-text-rb/blob/master/lib/twitter-text/regex.rb](https://github.com/twitter/twitter-text-rb/blob/master/lib/twitter-text/regex.rb)
|
153
|
+
(Yes, that's 362 lines of regex. Neat.)
|
154
|
+
- Allow for multiple hashtagable attributes on the same model
|
155
|
+
- Allow a change in the name of the classes and tables to avoid conflicts
|
156
|
+
- Make it ORM agnostic
|
157
|
+
- Add an option so the helper displays the `#` or not, global or per model
|
158
|
+
- Add an option to clean orphans after each edit or not
|
159
|
+
- Improve the `SimpleHashtag::Hashtag#clean_orphans` method to do only one SQL query
|
160
|
+
|
161
|
+
## Contributing
|
162
|
+
|
163
|
+
All contributions are welcome.
|
164
|
+
I might not develop new features (unless a project does require it),
|
165
|
+
but I will definitely merge any interesting feature or bug fix quickly.
|
166
|
+
|
167
|
+
You know the drill:
|
168
|
+
|
169
|
+
1. Fork it
|
170
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
171
|
+
3. Add passing tests
|
172
|
+
4. Commit your changes (`git commit -am 'Add some feature'`)
|
173
|
+
5. Push to the branch (`git push origin my-new-feature`)
|
174
|
+
6. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rails/generators/active_record'
|
2
|
+
|
3
|
+
module SimpleHashtag
|
4
|
+
module Generators
|
5
|
+
class MigrationGenerator < Rails::Generators::Base
|
6
|
+
include Rails::Generators::Migration
|
7
|
+
|
8
|
+
source_root File.expand_path('../templates', __FILE__)
|
9
|
+
|
10
|
+
def self.next_migration_number(path)
|
11
|
+
ActiveRecord::Generators::Base.next_migration_number(path)
|
12
|
+
end
|
13
|
+
|
14
|
+
def generate_migration
|
15
|
+
migration_template "migrations/create_hashtags_migration.rb", "db/migrate/create_simple_hashtag_hashtags.rb"
|
16
|
+
migration_template "migrations/create_hashtaggings_migration.rb", "db/migrate/create_simple_hashtag_hashtaggings.rb"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# This migration comes from simple_hashtag
|
2
|
+
class CreateSimpleHashtagHashtaggings < ActiveRecord::Migration
|
3
|
+
def change
|
4
|
+
create_table :simple_hashtag_hashtaggings do |t|
|
5
|
+
t.references :hashtag, :index => true
|
6
|
+
t.references :hashtaggable, :polymorphic => true
|
7
|
+
end
|
8
|
+
add_index :simple_hashtag_hashtaggings, ["hashtaggable_id", "hashtaggable_type"],
|
9
|
+
:name => 'index_hashtaggings_hashtaggable_id_hashtaggable_type'
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module HashtagsHelper
|
2
|
+
def linkify_hashtags(hashtaggable_content)
|
3
|
+
regex = SimpleHashtag::Hashtag::HASHTAG_REGEX
|
4
|
+
hashtagged_content = hashtaggable_content.to_s.gsub(regex) do
|
5
|
+
link_to($1, hashtag_path($2), {class: :hashtag})
|
6
|
+
end
|
7
|
+
hashtagged_content.html_safe
|
8
|
+
end
|
9
|
+
|
10
|
+
def render_hashtaggable(hashtaggable)
|
11
|
+
klass = hashtaggable.class.to_s.underscore
|
12
|
+
view_dirname = klass.pluralize
|
13
|
+
partial = klass
|
14
|
+
render "#{view_dirname}/#{partial}", {klass.to_sym => hashtaggable}
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module SimpleHashtag
|
2
|
+
module Generators
|
3
|
+
class ViewsGenerator < Rails::Generators::Base
|
4
|
+
|
5
|
+
source_root File.expand_path('../templates', __FILE__)
|
6
|
+
|
7
|
+
def generate_views
|
8
|
+
copy_file "views/hashtags_controller.rb", "app/controllers/hashtags_controller.rb"
|
9
|
+
copy_file "views/hashtags_helper.rb", "app/helpers/hashtags_helper.rb"
|
10
|
+
copy_file "views/hashtags_index.html.erb", "app/views/hashtags/index.html.erb"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module SimpleHashtag
|
2
|
+
class Hashtag < ActiveRecord::Base
|
3
|
+
self.table_name = "simple_hashtag_hashtags"
|
4
|
+
|
5
|
+
has_many :hashtaggings
|
6
|
+
|
7
|
+
# TODO Beef up the regex (ie.:what if content is HTML)
|
8
|
+
# this is how Twitter does it:
|
9
|
+
# https://github.com/twitter/twitter-text-rb/blob/master/lib/twitter-text/regex.rb
|
10
|
+
HASHTAG_REGEX = /\s(#([a-z0-9\-_]+))/i
|
11
|
+
|
12
|
+
def self.find_by_name(name)
|
13
|
+
Hashtag.where("lower(name) =?", name.downcase).first
|
14
|
+
end
|
15
|
+
|
16
|
+
def name=(val)
|
17
|
+
write_attribute(:name, val.downcase)
|
18
|
+
end
|
19
|
+
|
20
|
+
def name
|
21
|
+
read_attribute(:name).downcase
|
22
|
+
end
|
23
|
+
|
24
|
+
def hashtaggables
|
25
|
+
self.hashtaggings.collect { |h| h.hashtaggable }
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_s
|
29
|
+
name
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.clean_orphans # From DB
|
33
|
+
# TODO Make this method call a single SQL query
|
34
|
+
orphans = self.all.select { |h| h.hashtaggables.size == 0 }
|
35
|
+
orphans.map(&:destroy)
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module SimpleHashtag
|
2
|
+
module Hashtaggable
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
has_many :hashtaggings, as: :hashtaggable, class_name: "SimpleHashtag::Hashtagging"
|
7
|
+
has_many :hashtags, through: :hashtaggings, class_name: "SimpleHashtag::Hashtag"
|
8
|
+
|
9
|
+
before_save :update_hashtags
|
10
|
+
|
11
|
+
def hashtaggable_content
|
12
|
+
self.class.hashtaggable_attribute # to ensure it has been called at least once
|
13
|
+
content = self.send(self.class.hashtaggable_attribute_name)
|
14
|
+
content.to_s
|
15
|
+
end
|
16
|
+
|
17
|
+
def update_hashtags
|
18
|
+
self.hashtags = parsed_hashtags
|
19
|
+
end
|
20
|
+
|
21
|
+
def parsed_hashtags
|
22
|
+
parsed_hashtags = []
|
23
|
+
array_of_hashtags_as_string = scan_for_hashtags(hashtaggable_content)
|
24
|
+
array_of_hashtags_as_string.each do |s|
|
25
|
+
parsed_hashtags << Hashtag.find_or_create_by(name: s[1])
|
26
|
+
end
|
27
|
+
parsed_hashtags
|
28
|
+
end
|
29
|
+
|
30
|
+
def scan_for_hashtags(content)
|
31
|
+
match = content.scan(Hashtag::HASHTAG_REGEX)
|
32
|
+
match.uniq!
|
33
|
+
match
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
module ClassMethods
|
38
|
+
attr_accessor :hashtaggable_attribute_name
|
39
|
+
|
40
|
+
def hashtaggable_attribute(name=nil)
|
41
|
+
self.hashtaggable_attribute_name ||= name || :body
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'simple_hashtag/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "simple_hashtag"
|
8
|
+
spec.version = SimpleHashtag::VERSION
|
9
|
+
spec.authors = ["Raphael Campardou"]
|
10
|
+
spec.email = ["ralovely@gmail.com"]
|
11
|
+
spec.description = %q{Parse, store retreive and format hashtags in your text.}
|
12
|
+
spec.summary = %q{Simple Hashtag is a mix between–well–hashtags as we know them and categories. It will scan your Active Record attribute for a tag and store it in an index.}
|
13
|
+
spec.homepage = "https://github.com/ralovely/simple_hashtag"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.required_ruby_version = ">= 1.9.3"
|
22
|
+
spec.add_dependency "rails", "> 3.2.0"
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
25
|
+
spec.add_development_dependency "rake"
|
26
|
+
spec.add_development_dependency "rspec-rails"
|
27
|
+
spec.add_development_dependency "sqlite3"
|
28
|
+
end
|