e9_tags 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/README.rdoc +61 -0
- data/Rakefile +2 -0
- data/app/views/e9_tags/_form.html.haml +31 -0
- data/app/views/e9_tags/_tag_list.html.haml +3 -0
- data/app/views/e9_tags/_tag_list_with_context.html.haml +2 -0
- data/app/views/e9_tags/_template.html.haml +5 -0
- data/config/locales/en.yml +10 -0
- data/config/routes.rb +6 -0
- data/e9_tags.gemspec +24 -0
- data/lib/e9_tags/controller.rb +21 -0
- data/lib/e9_tags/helper.rb +63 -0
- data/lib/e9_tags/model.rb +151 -0
- data/lib/e9_tags/rack/tag_auto_completer.rb +39 -0
- data/lib/e9_tags/rack/tag_context_auto_completer.rb +36 -0
- data/lib/e9_tags/rack/tags_js.rb +14 -0
- data/lib/e9_tags/rails_extensions.rb +7 -0
- data/lib/e9_tags/tagging_extension.rb +41 -0
- data/lib/e9_tags/version.rb +3 -0
- data/lib/e9_tags.rb +80 -0
- data/lib/generators/e9_tags/install_generator.rb +13 -0
- data/lib/generators/e9_tags/templates/e9_tags.js +211 -0
- metadata +178 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
== E9Tags
|
2
|
+
|
3
|
+
An extension to ActsAsTaggableOn[http://github.com/mbleigh/acts-as-taggable-on] which "improves" on custom tagging, or at least makes it more dynamic. Additionally it
|
4
|
+
provides some autocomplete rack apps and the corresponding javascript.
|
5
|
+
|
6
|
+
== Installation
|
7
|
+
|
8
|
+
1. E9Tags requires jquery and jquery-ui for the autocompletion and tag-adding form, be sure they're loaded in your
|
9
|
+
pages where the tags form will be rendered.
|
10
|
+
|
11
|
+
2. E9Tags extends ActsAsTaggableOn and requires it. Run it's generator if you have not.
|
12
|
+
|
13
|
+
3. Run the E9Tags install script to copy over the required JS
|
14
|
+
|
15
|
+
rails g e9_tags:install
|
16
|
+
|
17
|
+
4. Then make sure it is loaded, how you do that doesn't matter, e.g.
|
18
|
+
|
19
|
+
<%= javascript_include_tag 'e9_tags' %>
|
20
|
+
|
21
|
+
5. Create an initializer for that sets up the taggable models and their controllers. This gives the models the tag
|
22
|
+
associations and methods and prepares their controller to handle the otherwise unexpected tag params.
|
23
|
+
|
24
|
+
require 'e9_tags'
|
25
|
+
require 'contacts_controller'
|
26
|
+
require 'contact'
|
27
|
+
|
28
|
+
E9Tags.controllers << ContactsController
|
29
|
+
E9Tags.models << Contact
|
30
|
+
|
31
|
+
OR
|
32
|
+
|
33
|
+
You can just include the modules in your classes yourself. The first way really exists for the case where the
|
34
|
+
classes you wish to extend are part of another plugin/gem.
|
35
|
+
|
36
|
+
# in contact.rb
|
37
|
+
include E9Tags:Model
|
38
|
+
|
39
|
+
# in contacts_controller.rb
|
40
|
+
include E9Tags::Controller
|
41
|
+
|
42
|
+
6. Render the tags form partial in whatever model forms require it.
|
43
|
+
|
44
|
+
= render 'e9_tags/form', :f => f
|
45
|
+
|
46
|
+
If you pass a context, it will be locked and no longer possible to change/add the contexts on the form (and as
|
47
|
+
a side effect, the tags autocompletion will be restricted to that context).
|
48
|
+
|
49
|
+
= render 'e9_tags/form', :f => f, :context => :users
|
50
|
+
|
51
|
+
Finally if you pass a 2nd arg to :context you can set a tag context to be "private" (default is false). In this
|
52
|
+
case the tag context will be locked as private (typically suffixed with *), meaning that the tags will not be
|
53
|
+
publicly searchable/visible. This is useful for organizational tags tags, say if you wanted to arbitrarily
|
54
|
+
group records, or create a custom search based on a tag context.
|
55
|
+
|
56
|
+
= render 'e9_tags/form', :f => f, :context => [:users, true]
|
57
|
+
|
58
|
+
NOTE: The form and javascript are intended to work out of the box, but the certainly aren't going to look pretty.
|
59
|
+
If you do intend to use the forms, you'll no doubt need to style them.
|
60
|
+
|
61
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
<!--BEGIN e9_tags_js-->
|
2
|
+
:javascript
|
3
|
+
var tag_template = "#{escape_javascript(tag_template(f.object).html_safe)}";
|
4
|
+
<!--END e9_tags_js-->
|
5
|
+
|
6
|
+
%fieldset.tags
|
7
|
+
%legend= label_tag 'add-tag-fld', t(:tags_name, :scope => :e9_tags)
|
8
|
+
#tag-fields
|
9
|
+
- if local_assigns[:context]
|
10
|
+
= hidden_field_tag 'tag-context', humanize_context(*local_assigns[:context])
|
11
|
+
- else
|
12
|
+
.field
|
13
|
+
= label_tag 'tag-context', t(:tag_context_name, :scope => :e9_tags).singularize
|
14
|
+
= text_field_tag 'tag-context', nil
|
15
|
+
.field
|
16
|
+
= label_tag 'add-tag-fld', t(:tags_name, :scope => :e9_tags).singularize
|
17
|
+
= text_field_tag 'add-tag-fld', nil
|
18
|
+
.actions
|
19
|
+
%button#add-tag-btn= t(:add_tag_link, :scope => :e9_tags)
|
20
|
+
-# NOTE this escaping bit doesn't work without a tooltips plugin (browser does not render HTML in default title tooltips)
|
21
|
+
- t = raw t(:tag_instructions, :scope => :e9_tags).gsub('"', '"')
|
22
|
+
<button title="#{t}" rel="tooltip">?</button>
|
23
|
+
|
24
|
+
#tag-contexts{:class => local_assigns[:context] ? 'locked-context' : nil }
|
25
|
+
- f.object.tagging_contexts(:show_all => true).each do |context|
|
26
|
+
- h_context = humanize_context(context)
|
27
|
+
%div{:id => "#{context}-context-list", :class => 'context-list'}
|
28
|
+
%span.context-header= h_context unless local_assigns[:context]
|
29
|
+
%ul
|
30
|
+
- f.object.tag_list_on(context).each do |tag_word|
|
31
|
+
= tag_template(f.object, tag_word, h_context, context)
|
@@ -0,0 +1,5 @@
|
|
1
|
+
%li
|
2
|
+
%span.admin-tag
|
3
|
+
%span= tag
|
4
|
+
= link_to t(:remove_tag), 'javascript:;', :class => 'delete-tag', :title => t(:delete_tag_link_instructions, :scope => :e9_tags)
|
5
|
+
= hidden_field_tag "#{class_name}[#{u_context}_tag_list][]", tag, :id => "#{class_name}_#{u_context}_tag_list"
|
@@ -0,0 +1,10 @@
|
|
1
|
+
en:
|
2
|
+
e9_tags:
|
3
|
+
tags_name: Tags
|
4
|
+
add_tag_link: Add
|
5
|
+
delete_tag: Delete
|
6
|
+
tag_context_name: Context
|
7
|
+
tag_context_visible: "Tags created with this context will be searchable and visible on their tagged content."
|
8
|
+
tag_context_hidden: "Tags created with this context will be unsearchable and will not be visible on their tagged content."
|
9
|
+
tag_instructions: "<p>Tags are words or phrases that describe or classify the content on this page.</p><p>Context allows you to contextualize each tag you enter. For a tag of \"coca cola\" you might give it a context of \"brand\", or you could give it a context of \"soda\".</p><p>Enter a tag, click Add. Repeat to add more.</p><p>Tags suffixed with a * are not publically visible or searchable, making them suitable for organizational purposes.</p>"
|
10
|
+
delete_tag_link_instructions: Click to remove this tag
|
data/config/routes.rb
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
Rails.application.routes.draw do
|
2
|
+
# TODO fix the fact that we route to apps, then the apps themselves check those paths
|
3
|
+
get '/js/tags' => E9Tags::Rack::TagsJs
|
4
|
+
get '/autocomplete/tags' => E9Tags::Rack::TagAutoCompleter
|
5
|
+
get '/autocomplete/tag-contexts' => E9Tags::Rack::TagContextAutoCompleter
|
6
|
+
end
|
data/e9_tags.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "e9_tags/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "e9_tags"
|
7
|
+
s.version = E9Tags::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Travis Cox"]
|
10
|
+
s.email = ["travis@e9digital.com"]
|
11
|
+
s.homepage = "http://github.com/e9digital/e9_tags"
|
12
|
+
s.summary = %q{Extension to ActsAsTaggableOn used in e9 Rails 3 projects}
|
13
|
+
s.description = File.open('README.rdoc').read rescue nil
|
14
|
+
|
15
|
+
s.rubyforge_project = "e9_tags"
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
|
22
|
+
s.add_dependency("rails", "~> 3.0.0")
|
23
|
+
s.add_dependency("acts-as-taggable-on", "~> 2.0.6")
|
24
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module E9Tags
|
2
|
+
module Controller
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
prepend_before_filter :extract_tag_lists_from_params, :only => [:create, :update]
|
7
|
+
end
|
8
|
+
|
9
|
+
def extract_tag_lists_from_params
|
10
|
+
params[resource_instance_name] ||= {}
|
11
|
+
|
12
|
+
list_keys = params[resource_instance_name].keys.select {|k| k.to_s =~ /_tag_list$/ }
|
13
|
+
tags = params[resource_instance_name].slice(*list_keys)
|
14
|
+
|
15
|
+
params[resource_instance_name].except!(*list_keys)
|
16
|
+
params[resource_instance_name][:tag_lists] = tags
|
17
|
+
end
|
18
|
+
|
19
|
+
protected :extract_tag_lists_from_params
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module E9Tags
|
2
|
+
module Helper
|
3
|
+
def tag_template(object, tag = "__TAG__", context = "__CONTEXT__", u_context = "__UCONTEXT__")
|
4
|
+
render('e9_tags/template', {
|
5
|
+
:tag => tag,
|
6
|
+
:context => context,
|
7
|
+
:u_context => u_context,
|
8
|
+
:class_name => object.class.name.underscore
|
9
|
+
})
|
10
|
+
end
|
11
|
+
|
12
|
+
def humanize_context(*context_and_is_private)
|
13
|
+
context, is_private = context_and_is_private
|
14
|
+
|
15
|
+
context = E9Tags.unescape_context(context)
|
16
|
+
|
17
|
+
if is_private && context !~ /#{E9Tags::PRIVATE_TAG_SUFFIX_REGEX}$/
|
18
|
+
context << E9Tags::PRIVATE_TAG_SUFFIX
|
19
|
+
end
|
20
|
+
|
21
|
+
context
|
22
|
+
end
|
23
|
+
|
24
|
+
def tag_list(resource, highlighted_tag = nil, options = {})
|
25
|
+
_tag_list(resource.tags(options), highlighted_tag) if resource.respond_to?(:tags)
|
26
|
+
end
|
27
|
+
|
28
|
+
def tag_list_with_context(resource, highlighted_tag = nil, options = {})
|
29
|
+
if resource.respond_to?(:tagging_contexts)
|
30
|
+
|
31
|
+
# by default, we don't want to show all tags
|
32
|
+
options[:show_all] = false if options.blank?
|
33
|
+
|
34
|
+
content_tag(:div, :class => 'tag-lists') do
|
35
|
+
''.html_safe.tap do |html|
|
36
|
+
resource.tagging_contexts(options).each do |context|
|
37
|
+
str = ''.html_safe
|
38
|
+
str.safe_concat content_tag(:div, "#{humanize_context(E9Tags.unescape_context(context))}:", :class => 'heading')
|
39
|
+
str.safe_concat _tag_list(resource.tag_list_on(context), highlighted_tag)
|
40
|
+
|
41
|
+
html.safe_concat content_tag(:div, str.html_safe, :class => 'tag-context-list')
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
# NOTE this subroutine is kinda useless, should be rewritten to handle both tag list types
|
51
|
+
def _tag_list(tags, highlighted_tag = nil)
|
52
|
+
content_tag(:div, :class => 'tag-list') do
|
53
|
+
tags.each_with_index.map do |tag, index|
|
54
|
+
css_class = index == tags.length - 1 ? 'last' : nil
|
55
|
+
link_class = 'tag'
|
56
|
+
link_class << ' highlight' if highlighted_tag && highlighted_tag == tag
|
57
|
+
link_class << ' last' if index == tags.length - 1
|
58
|
+
link_to(tag, searches_path(:query => tag), :class => link_class)
|
59
|
+
end.join(', ').html_safe
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
module E9Tags
|
2
|
+
module Model
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
#
|
6
|
+
# It should be noted that our usage of tag contexts is apparently
|
7
|
+
# incongruent with acts_as_taggable_on's intentions. It goes to lengths
|
8
|
+
# to write class methods for each context with the apparent intention that
|
9
|
+
# these be used separately, whereas we needed contexts to be more dynamic
|
10
|
+
# (all contexts but "tags" are user defined).
|
11
|
+
#
|
12
|
+
# The gem provides custom contexts but they're not very well supported
|
13
|
+
# and somewhat broken (see #tagging_contexts below).
|
14
|
+
#
|
15
|
+
# Because of the way AATO expects contexts to be used, it makes some
|
16
|
+
# assumptions, and writes a bunch of helper methods to the class for each.
|
17
|
+
# These are useless to us really and only serve to add some confusion to
|
18
|
+
# the scope. E.g. for acts_as_taggable_on :tags (the default), the
|
19
|
+
# following methods are created:
|
20
|
+
#
|
21
|
+
# tag_taggings (has_many)
|
22
|
+
# tags (has_many)
|
23
|
+
# tags_list
|
24
|
+
# tags_list=
|
25
|
+
# all_tags_list
|
26
|
+
#
|
27
|
+
# It's important to note that these methods all only deal with the "tags"
|
28
|
+
# context and won't return any tag which does not have it.
|
29
|
+
#
|
30
|
+
# The actual associations on the taggable record are "taggings" and
|
31
|
+
# "base_tags", which ignores context, returning all tags.
|
32
|
+
#
|
33
|
+
|
34
|
+
def has_tags?
|
35
|
+
!base_tags.empty?
|
36
|
+
end
|
37
|
+
|
38
|
+
# Override ActsAsTaggableOn's broken handling of tagging_contexts.
|
39
|
+
#
|
40
|
+
# This method by default reads:
|
41
|
+
#
|
42
|
+
# def tagging_contexts
|
43
|
+
# custom_contexts + self.class.tag_types.map(&:to_s)
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# ...where custom_contexts is an non-persistent instance variable
|
47
|
+
# on the record and tag_types are the tagging contexts defined
|
48
|
+
# on the class itself, e.g. acts_as_taggable_on :some_context.
|
49
|
+
#
|
50
|
+
# This means that proper custom contexts aren't reflected in
|
51
|
+
# tagging_contexts on retrieved records.
|
52
|
+
#
|
53
|
+
# For this reason we override/monkeypatch a few methods
|
54
|
+
# after including acts_as_taggable, making them more useful. The concept
|
55
|
+
# of "hidden" tags is also added, with the default "tagging_contexts" association
|
56
|
+
# overridden to return only "visible" tags. "Hidden" tags are denoted
|
57
|
+
# by having a context ending in __H__, a contrivance created in the javascript
|
58
|
+
# that handles adding/reading tags.
|
59
|
+
#
|
60
|
+
# Additionally contexts are further escaped by subsituting dashes and spaces with
|
61
|
+
# __D__ and __S__, respectively. This allows for those character in custom contexts,
|
62
|
+
# which is otherwise impossible due to the way that AATO caches contexts on the
|
63
|
+
# taggable object by dynamically writing context-named instance variables.
|
64
|
+
#
|
65
|
+
|
66
|
+
#
|
67
|
+
# this won't take unless the record is saved successfully
|
68
|
+
#
|
69
|
+
def clear_all_tags
|
70
|
+
tagging_contexts.each {|context| set_tag_list_on(context, '') }
|
71
|
+
end
|
72
|
+
|
73
|
+
def tag_lists=(hash)
|
74
|
+
self.clear_all_tags
|
75
|
+
hash.each do |context, tags|
|
76
|
+
c = context.to_s.sub(/_tag_list$/, '')
|
77
|
+
c = E9Tags.escape_context(c)
|
78
|
+
|
79
|
+
self.set_tag_list_on(c, tags)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
module DelayedClassMethods
|
84
|
+
def tagged_with(tags, options = {})
|
85
|
+
retv = super
|
86
|
+
|
87
|
+
# for whatever reason, tagged_with by default returns an empty Hash (??) if there are no results
|
88
|
+
return where("1=0") if retv.blank?
|
89
|
+
|
90
|
+
# if :show_all is true, just return super with no modification
|
91
|
+
#
|
92
|
+
if !options[:show_all] && retv.joins_values.present? && retv.to_sql =~ /JOIN taggings (\S+)/
|
93
|
+
#
|
94
|
+
# otherwise we'll show hidden OR visible tags based on whether :show_hidden was passed
|
95
|
+
#
|
96
|
+
retv = retv.where(
|
97
|
+
%Q[#{$1}.context #{options[:show_hidden] ? '' : ' NOT '} like '%__H__']
|
98
|
+
)
|
99
|
+
end
|
100
|
+
|
101
|
+
retv
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
included do
|
106
|
+
acts_as_taggable
|
107
|
+
|
108
|
+
extend DelayedClassMethods
|
109
|
+
|
110
|
+
# TODO Tagged_with unfortunately does not handle context-only searches. Rewriting it to do so would save this unnecessary subquery
|
111
|
+
scope :tagged_with_context, lambda {|context|
|
112
|
+
where(:id => select(Arel::Distinct.new(arel_table[:id])).joins(:taggings).where(Tagging.arel_table[:context].eq(E9Tags.escape_context(context))).map(&:id))
|
113
|
+
}
|
114
|
+
|
115
|
+
def filtered_taggings(*args)
|
116
|
+
options = args.extract_options!
|
117
|
+
|
118
|
+
retv = taggings(*args << options.slice!(:show_all, :show_hidden))
|
119
|
+
|
120
|
+
if !options[:show_all]
|
121
|
+
retv = retv.where(
|
122
|
+
%Q[taggings.context #{options[:show_hidden] ? '' : ' NOT '} like '%__H__']
|
123
|
+
)
|
124
|
+
end
|
125
|
+
|
126
|
+
retv
|
127
|
+
end
|
128
|
+
|
129
|
+
def tags(options = {})
|
130
|
+
filtered_taggings(options).map {|tagging| tagging.tag.name if tagging }.compact.uniq
|
131
|
+
end
|
132
|
+
|
133
|
+
#
|
134
|
+
# rewrite tagging_contexts as mentioned above
|
135
|
+
#
|
136
|
+
# NOTE unlike tags & taggings, tagging_contexts returns all contexts
|
137
|
+
# This is crucial because AATO uses tagging_contexts internally
|
138
|
+
# and it needs to behave like the default method
|
139
|
+
#
|
140
|
+
def tagging_contexts(options = {})
|
141
|
+
|
142
|
+
# NOTE this takes options but it only affects taggings.
|
143
|
+
# if there are custom_contexts that are hidden they will
|
144
|
+
# be returned
|
145
|
+
options[:show_all] = true if options.blank?
|
146
|
+
|
147
|
+
(custom_contexts + filtered_taggings(options).map(&:context) + self.class.tag_types.map(&:to_s)).uniq
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module E9Tags::Rack
|
2
|
+
class TagAutoCompleter
|
3
|
+
DEFAULT_LIMIT = 10
|
4
|
+
|
5
|
+
def self.call(env)
|
6
|
+
if env["PATH_INFO"] =~ /^\/autocomplete\/tags/
|
7
|
+
terms = []
|
8
|
+
@params = Rack::Request.new(env).params
|
9
|
+
|
10
|
+
if @term = @params['term']
|
11
|
+
tags, taggings = ::Tag.arel_table, ::Tagging.arel_table
|
12
|
+
|
13
|
+
relation = tags.join(taggings).on(tags[:id].eq(taggings[:tag_id])).
|
14
|
+
where(tags[:name].matches("#{@term}%")).
|
15
|
+
group(tags[:id]).
|
16
|
+
take(@params['limit'] ? @params['limit'].to_i : DEFAULT_LIMIT).
|
17
|
+
project(tags[:name], tags[:name].count.as('count')).
|
18
|
+
#having('count > 0'). # having count > 0 is unnecessary because of the join type
|
19
|
+
order('name ASC')
|
20
|
+
|
21
|
+
if @context = @params['context']
|
22
|
+
relation = relation.where(taggings[:context].matches(E9Tags.escape_context(@context)))
|
23
|
+
end
|
24
|
+
|
25
|
+
# NOTE this select is stolen from Arel::SelectManager's deprecated to_a method, but since Arel has been re-written
|
26
|
+
# (and even before that) it'd probably be smarter here to avoid arel tables and just use AR and to_json
|
27
|
+
#
|
28
|
+
terms = ::ActiveRecord::Base.connection.send(:select, relation.to_sql, 'Tag Autocomplete').map do |row|
|
29
|
+
{ :label => "#{row['name']} - #{row['count']}", :value => row['name'], :count => row['count'] }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
[200, {"Content-Type" => "application/json", "Cache-Control" => "max-age=3600, must-revalidate"}, [terms.to_json]]
|
34
|
+
else
|
35
|
+
[404, {"Content-Type" => "text/html", "X-Cascade" => "pass"}, ["Not Found"]]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module E9Tags::Rack
|
2
|
+
class TagContextAutoCompleter
|
3
|
+
DEFAULT_LIMIT = 10
|
4
|
+
|
5
|
+
def self.call(env)
|
6
|
+
if env["PATH_INFO"] =~ /^\/autocomplete\/tag\-contexts/
|
7
|
+
terms = []
|
8
|
+
|
9
|
+
if @term = Rack::Request.new(env).params['term']
|
10
|
+
|
11
|
+
taggings = ::Tagging.arel_table
|
12
|
+
|
13
|
+
relation = taggings.
|
14
|
+
group(taggings[:context]).
|
15
|
+
where(taggings[:context].matches("#{@term}%")).
|
16
|
+
project(taggings[:context], taggings[:context].count.as('count')).
|
17
|
+
take(DEFAULT_LIMIT).
|
18
|
+
order('context ASC')
|
19
|
+
|
20
|
+
# NOTE this select is stolen from Arel::SelectManager's deprecated to_a method, but since Arel has been re-written
|
21
|
+
# (and even before that) it'd probably be smarter here to avoid arel tables and just use AR and to_json
|
22
|
+
#
|
23
|
+
terms = ::ActiveRecord::Base.send(:select, relation.to_sql, 'Tag Context Autocomplete').map do |row|
|
24
|
+
unescaped_context = E9Tags.unescape_context(row['context'])
|
25
|
+
|
26
|
+
{ :label => "#{unescaped_context} - #{row['count']}", :value => unescaped_context, :count => row['count'] }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
[200, {"Content-Type" => "application/json", "Cache-Control" => "max-age=3600, must-revalidate"}, [terms.to_json]]
|
31
|
+
else
|
32
|
+
[404, {"Content-Type" => "text/html", "X-Cascade" => "pass"}, ["Not Found"]]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module E9Tags::Rack
|
2
|
+
class TagsJs
|
3
|
+
def self.call(env)
|
4
|
+
if env["PATH_INFO"] =~ /^\/js\/tags/
|
5
|
+
tags = ::Tagging.joins(:tag).order('tags.name').group_by(&:context).to_json
|
6
|
+
js = "window.e9=window.e9||{};window.e9.tags=#{tags};"
|
7
|
+
|
8
|
+
[200, {"Content-Type" => "text/javascript", "Cache-Control" => "max-age=3600, must-revalidate"}, [js]]
|
9
|
+
else
|
10
|
+
[404, {"Content-Type" => "text/html", "X-Cascade" => "pass"}, ["Not Found"]]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module E9Tags
|
2
|
+
module TaggingExtension
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
alias :content :taggable
|
7
|
+
|
8
|
+
delegate :name, :to => :tag
|
9
|
+
|
10
|
+
scope :hidden, lambda {
|
11
|
+
where(arel_table[:contexts].matches('%__H__').not)
|
12
|
+
}
|
13
|
+
|
14
|
+
scope :excluding_tags, lambda {|*tags|
|
15
|
+
joins(:tag).where(Tag.arel_table[:name].in(tags.flatten).not)
|
16
|
+
}
|
17
|
+
|
18
|
+
scope :including_tags, lambda {|*tags|
|
19
|
+
joins(:tag).where(Tag.arel_table[:name].in(tags.flatten))
|
20
|
+
}
|
21
|
+
|
22
|
+
def as_json(options = {})
|
23
|
+
tag.try(:name)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
module ClassMethods
|
28
|
+
def contexts(options = {})
|
29
|
+
scope = arel_table.project(:context)
|
30
|
+
|
31
|
+
if !options[:show_all]
|
32
|
+
condition = arel_table[:context].matches('%__H__')
|
33
|
+
condition = condition.not unless options[:show_hidden]
|
34
|
+
scope = scope.where(condition)
|
35
|
+
end
|
36
|
+
|
37
|
+
scope.map {|row| row.tuple.first }.uniq
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/e9_tags.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'rails'
|
2
|
+
require 'acts-as-taggable-on'
|
3
|
+
require 'e9_tags/tagging_extension'
|
4
|
+
require 'e9_tags/rails_extensions'
|
5
|
+
|
6
|
+
module E9Tags
|
7
|
+
autoload :Controller, 'e9_tags/controller'
|
8
|
+
autoload :Model, 'e9_tags/model'
|
9
|
+
autoload :Helper, 'e9_tags/helper'
|
10
|
+
|
11
|
+
module Rack
|
12
|
+
autoload :TagAutoCompleter, 'e9_tags/rack/tag_auto_completer'
|
13
|
+
autoload :TagContextAutoCompleter, 'e9_tags/rack/tag_context_auto_completer'
|
14
|
+
autoload :TagsJs, 'e9_tags/rack/tags_js'
|
15
|
+
end
|
16
|
+
|
17
|
+
#
|
18
|
+
# Controllers that are prepared to handle taggable models
|
19
|
+
#
|
20
|
+
mattr_accessor :controllers
|
21
|
+
@@controllers = []
|
22
|
+
|
23
|
+
#
|
24
|
+
# Models that are taggable
|
25
|
+
#
|
26
|
+
mattr_accessor :models
|
27
|
+
@@models = []
|
28
|
+
|
29
|
+
ESCAPED_DASH = '__d__'
|
30
|
+
ESCAPED_DASH_REGEX = Regexp.new(Regexp.escape(ESCAPED_DASH), true)
|
31
|
+
|
32
|
+
ESCAPED_SPACE = '__s__'
|
33
|
+
ESCAPED_SPACE_REGEX = Regexp.new(Regexp.escape(ESCAPED_SPACE), true)
|
34
|
+
|
35
|
+
ESCAPED_PRIVATE = '__h__'
|
36
|
+
ESCAPED_PRIVATE_REGEX = Regexp.new(Regexp.escape(ESCAPED_PRIVATE), true)
|
37
|
+
|
38
|
+
PRIVATE_TAG_SUFFIX = '*'
|
39
|
+
PRIVATE_TAG_SUFFIX_REGEX = Regexp.new(Regexp.escape(PRIVATE_TAG_SUFFIX), true)
|
40
|
+
|
41
|
+
def E9Tags.escape_context(context)
|
42
|
+
context.to_s.strip.
|
43
|
+
downcase.
|
44
|
+
gsub(/\s+/, ESCAPED_SPACE).
|
45
|
+
gsub(/-/, ESCAPED_DASH).
|
46
|
+
sub(PRIVATE_TAG_SUFFIX_REGEX, ESCAPED_PRIVATE)
|
47
|
+
end
|
48
|
+
|
49
|
+
def E9Tags.unescape_context(context)
|
50
|
+
context.to_s.strip.
|
51
|
+
gsub(ESCAPED_SPACE_REGEX, ' ').
|
52
|
+
gsub(ESCAPED_DASH_REGEX, '-').
|
53
|
+
sub(ESCAPED_PRIVATE_REGEX, PRIVATE_TAG_SUFFIX).
|
54
|
+
downcase.
|
55
|
+
titleize
|
56
|
+
end
|
57
|
+
|
58
|
+
def E9Tags.setup!
|
59
|
+
ActsAsTaggableOn::Tagging.send(:include, E9Tags::TaggingExtension)
|
60
|
+
|
61
|
+
E9Tags.models.each {|m| m.send(:include, E9Tags::Model) }
|
62
|
+
E9Tags.controllers.each {|m| m.send(:include, E9Tags::Controller) }
|
63
|
+
end
|
64
|
+
|
65
|
+
class Engine < ::Rails::Engine
|
66
|
+
config.e9_tags = E9Tags
|
67
|
+
|
68
|
+
config.to_prepare { E9Tags.setup! }
|
69
|
+
|
70
|
+
initializer 'e9_tags.include_helper' do
|
71
|
+
ActiveSupport.on_load(:action_view) do
|
72
|
+
include E9Tags::Helper
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Alias ActsAsTaggableOn classes
|
79
|
+
Tag = ActsAsTaggableOn::Tag
|
80
|
+
Tagging = ActsAsTaggableOn::Tagging
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'rails/generators/base'
|
2
|
+
|
3
|
+
module E9Tags
|
4
|
+
module Generators
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
6
|
+
source_root File.expand_path('../templates', __FILE__)
|
7
|
+
|
8
|
+
def install_javascript
|
9
|
+
copy_file 'e9_tags.js', 'public/javascripts/e9_tags.js'
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,211 @@
|
|
1
|
+
jQuery(function($) {
|
2
|
+
var
|
3
|
+
$tag_context = $('#tag-context'),
|
4
|
+
tcv = $.trim($tag_context.val()),
|
5
|
+
|
6
|
+
/*
|
7
|
+
* If tag-context is blank, default it to Tags. This is important also in that
|
8
|
+
* if the tag-context is pre-set, it will considered hidden and not used as
|
9
|
+
* a label in the form.
|
10
|
+
*/
|
11
|
+
default_context_value = tcv.length ? tcv : "Tags",
|
12
|
+
|
13
|
+
/*
|
14
|
+
* tag contexts in acts-as-taggable-on must be legal instance variable names.
|
15
|
+
* Replace spaces with __S__ and dashes with __D__ to solve the issue. As a side effect
|
16
|
+
* it safe to always use the escaped context names as dom ids
|
17
|
+
*/
|
18
|
+
escape_context = function(context) {
|
19
|
+
return $.trim(context)
|
20
|
+
.toLowerCase()
|
21
|
+
.replace(/\s+/g, '__s__')
|
22
|
+
.replace(/-/, '__d__')
|
23
|
+
.replace(/\*/, '__h__');
|
24
|
+
},
|
25
|
+
|
26
|
+
unescape_context = function(context) {
|
27
|
+
return $.trim(context)
|
28
|
+
.replace(/__s__/g, ' ')
|
29
|
+
.replace(/__d__/g, '-')
|
30
|
+
.replace(/__h__/g, '*')
|
31
|
+
.toLowerCase()
|
32
|
+
.replace(/(^|\s)([a-z])/g, function(match, m1, m2) { return m1 + m2.toUpperCase(); });
|
33
|
+
};
|
34
|
+
|
35
|
+
// hide empty context-lists
|
36
|
+
$('.context-list ul').each(function(ele) {
|
37
|
+
var ct = $(this).find('li').length;
|
38
|
+
if(ct == 0) $(this).parent().hide();
|
39
|
+
});
|
40
|
+
|
41
|
+
var context_cache = {};
|
42
|
+
var has_clicked_context = false;
|
43
|
+
|
44
|
+
$tag_context
|
45
|
+
/*
|
46
|
+
* initial setting of context and context blur use this:
|
47
|
+
* set context value to default value if context is blank
|
48
|
+
*/
|
49
|
+
.blur(function() {
|
50
|
+
$(this).val(function(i, cVal) {
|
51
|
+
return $.trim(cVal).length ? cVal : default_context_value
|
52
|
+
});
|
53
|
+
})
|
54
|
+
|
55
|
+
/*
|
56
|
+
* focus does the reverse and set's the value to blank if
|
57
|
+
* the context is the default context
|
58
|
+
*/
|
59
|
+
.focus(function() {
|
60
|
+
$(this).val(function(i, cVal) {
|
61
|
+
return cVal == default_context_value ? "" : cVal;
|
62
|
+
});
|
63
|
+
})
|
64
|
+
|
65
|
+
.autocomplete({
|
66
|
+
delay: 350,
|
67
|
+
source: function(request, response) {
|
68
|
+
var request_str = "term=" + request.term;
|
69
|
+
//if (context_cache.term == request.term && context_cache.content) {
|
70
|
+
//response(context_cache.content);
|
71
|
+
//return;
|
72
|
+
//}
|
73
|
+
//if (new RegExp(context_cache.term).test(request.term) && context_cache.content && context_cache.content.length < 13) {
|
74
|
+
//response($.ui.autocomplete.filter(context_cache.content, request.term));
|
75
|
+
//return;
|
76
|
+
//}
|
77
|
+
$.ajax({
|
78
|
+
url: "/autocomplete/tag-contexts",
|
79
|
+
dataType: "json",
|
80
|
+
data: request_str,
|
81
|
+
success: function(data) {
|
82
|
+
context_cache.term = request.term;
|
83
|
+
context_cache.content = data;
|
84
|
+
response(data);
|
85
|
+
}
|
86
|
+
});
|
87
|
+
},
|
88
|
+
focus: function(evt, ui) {
|
89
|
+
$("#tag-context").val(ui.item.label);
|
90
|
+
return false;
|
91
|
+
}
|
92
|
+
})
|
93
|
+
|
94
|
+
// blur on load (set to default if blank)
|
95
|
+
.blur()
|
96
|
+
;
|
97
|
+
|
98
|
+
var $addtf = $("#add-tag-fld"), cache = {};
|
99
|
+
|
100
|
+
$addtf.autocomplete({
|
101
|
+
delay: 350,
|
102
|
+
source: function(request, response) {
|
103
|
+
var request_str = "term=" + request.term;
|
104
|
+
var context = $("#tag-context-select").val() || $("#tag-context").val();
|
105
|
+
|
106
|
+
if (context != undefined && context != '') {
|
107
|
+
request_str += "&context=" + context;
|
108
|
+
}
|
109
|
+
|
110
|
+
// TODO Caching tag autocomplete with context
|
111
|
+
//if (cache.term == request.term && cache.content) {
|
112
|
+
//response(cache.content);
|
113
|
+
//return;
|
114
|
+
//}
|
115
|
+
//if (new RegExp(cache.term).test(request.term) && cache.content && cache.content.length < 13) {
|
116
|
+
//response($.ui.autocomplete.filter(cache.content, request.term));
|
117
|
+
//return;
|
118
|
+
//}
|
119
|
+
|
120
|
+
$.ajax({
|
121
|
+
url: "/autocomplete/tags",
|
122
|
+
dataType: "json",
|
123
|
+
data: request_str,
|
124
|
+
success: function(data) {
|
125
|
+
//cache.term = request.term;
|
126
|
+
//cache.content = data;
|
127
|
+
response(data);
|
128
|
+
}
|
129
|
+
});
|
130
|
+
},
|
131
|
+
focus: function(evt, ui) {
|
132
|
+
$addtf.val(ui.item.label);
|
133
|
+
return false;
|
134
|
+
}
|
135
|
+
});
|
136
|
+
|
137
|
+
// some variation of this one is probably better as it leaves a blank field in the form?
|
138
|
+
$(".admin-tag").live("click", function(e) {
|
139
|
+
e.preventDefault();
|
140
|
+
var $li = $(this).closest('li');
|
141
|
+
if (!$li.siblings().length) $li.closest('.context-list').hide();
|
142
|
+
$li.remove();
|
143
|
+
});
|
144
|
+
|
145
|
+
$("#tag-context-select").change(function(e) {
|
146
|
+
$addtf.autocomplete('search');
|
147
|
+
});
|
148
|
+
|
149
|
+
$("#add-tag-btn").click(function(evt) {
|
150
|
+
evt.preventDefault();
|
151
|
+
|
152
|
+
if (!$.trim($addtf.val()).length) return;
|
153
|
+
|
154
|
+
var
|
155
|
+
$tcf = $("#tag-context"),
|
156
|
+
regex = /^[a-zA-Z][a-zA-Z0-9- ]*\*?$/,
|
157
|
+
message = " must begin with a letter and contain only letters, numbers, spaces, and hyphens",
|
158
|
+
tag,
|
159
|
+
context,
|
160
|
+
u_context;
|
161
|
+
|
162
|
+
tag = $addtf.val();
|
163
|
+
if (!tag.match(regex)) {
|
164
|
+
alert("Tags" + message);
|
165
|
+
return;
|
166
|
+
}
|
167
|
+
|
168
|
+
context = $.trim($tcf.val());
|
169
|
+
if (!context.match(regex)) {
|
170
|
+
alert("Tag contexts" + message);
|
171
|
+
return;
|
172
|
+
}
|
173
|
+
|
174
|
+
context = unescape_context(context);
|
175
|
+
u_context = escape_context(context);
|
176
|
+
|
177
|
+
var list = $("#"+u_context+"-context-list ul");
|
178
|
+
if(list.length == 0) {
|
179
|
+
var
|
180
|
+
html = '<div id="'+ u_context +'-context-list" class="context-list">';
|
181
|
+
|
182
|
+
// if the tag context value was not preset then we use the humanized
|
183
|
+
// context value as a label for the separate context lists
|
184
|
+
if (!tcv.length) {
|
185
|
+
html += '<span>'+context+'</span>';
|
186
|
+
}
|
187
|
+
|
188
|
+
html += '<ul></ul></div>';
|
189
|
+
|
190
|
+
$('#tag-contexts').append(html);
|
191
|
+
}
|
192
|
+
|
193
|
+
list = $("#"+u_context+"-context-list ul");
|
194
|
+
|
195
|
+
if ($.trim(list.find("input[value='"+tag+"']").val()).length) {
|
196
|
+
alert("That label exists for this content. You can't create a duplicate record.");
|
197
|
+
return;
|
198
|
+
}
|
199
|
+
|
200
|
+
var ele = tag_template;
|
201
|
+
ele = ele.replace(/__TAG__/g, tag);
|
202
|
+
ele = ele.replace(/__CONTEXT__/g, context);
|
203
|
+
ele = ele.replace(/__UCONTEXT__/g, u_context);
|
204
|
+
|
205
|
+
list.append(ele).parent().show();
|
206
|
+
|
207
|
+
$addtf.val('');
|
208
|
+
$tcf.val('').blur();
|
209
|
+
});
|
210
|
+
|
211
|
+
});
|
metadata
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: e9_tags
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 8
|
9
|
+
version: 0.0.8
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Travis Cox
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2011-04-15 00:00:00 -04:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: rails
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ~>
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 3
|
30
|
+
- 0
|
31
|
+
- 0
|
32
|
+
version: 3.0.0
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: acts-as-taggable-on
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
segments:
|
44
|
+
- 2
|
45
|
+
- 0
|
46
|
+
- 6
|
47
|
+
version: 2.0.6
|
48
|
+
type: :runtime
|
49
|
+
version_requirements: *id002
|
50
|
+
description: |
|
51
|
+
== E9Tags
|
52
|
+
|
53
|
+
An extension to ActsAsTaggableOn[http://github.com/mbleigh/acts-as-taggable-on] which "improves" on custom tagging, or at least makes it more dynamic. Additionally it
|
54
|
+
provides some autocomplete rack apps and the corresponding javascript.
|
55
|
+
|
56
|
+
== Installation
|
57
|
+
|
58
|
+
1. E9Tags requires jquery and jquery-ui for the autocompletion and tag-adding form, be sure they're loaded in your
|
59
|
+
pages where the tags form will be rendered.
|
60
|
+
|
61
|
+
2. E9Tags extends ActsAsTaggableOn and requires it. Run it's generator if you have not.
|
62
|
+
|
63
|
+
3. Run the E9Tags install script to copy over the required JS
|
64
|
+
|
65
|
+
rails g e9_tags:install
|
66
|
+
|
67
|
+
4. Then make sure it is loaded, how you do that doesn't matter, e.g.
|
68
|
+
|
69
|
+
<%= javascript_include_tag 'e9_tags' %>
|
70
|
+
|
71
|
+
5. Create an initializer for that sets up the taggable models and their controllers. This gives the models the tag
|
72
|
+
associations and methods and prepares their controller to handle the otherwise unexpected tag params.
|
73
|
+
|
74
|
+
require 'e9_tags'
|
75
|
+
require 'contacts_controller'
|
76
|
+
require 'contact'
|
77
|
+
|
78
|
+
E9Tags.controllers << ContactsController
|
79
|
+
E9Tags.models << Contact
|
80
|
+
|
81
|
+
OR
|
82
|
+
|
83
|
+
You can just include the modules in your classes yourself. The first way really exists for the case where the
|
84
|
+
classes you wish to extend are part of another plugin/gem.
|
85
|
+
|
86
|
+
# in contact.rb
|
87
|
+
include E9Tags:Model
|
88
|
+
|
89
|
+
# in contacts_controller.rb
|
90
|
+
include E9Tags::Controller
|
91
|
+
|
92
|
+
6. Render the tags form partial in whatever model forms require it.
|
93
|
+
|
94
|
+
= render 'e9_tags/form', :f => f
|
95
|
+
|
96
|
+
If you pass a context, it will be locked and no longer possible to change/add the contexts on the form (and as
|
97
|
+
a side effect, the tags autocompletion will be restricted to that context).
|
98
|
+
|
99
|
+
= render 'e9_tags/form', :f => f, :context => :users
|
100
|
+
|
101
|
+
Finally if you pass a 2nd arg to :context you can set a tag context to be "private" (default is false). In this
|
102
|
+
case the tag context will be locked as private (typically suffixed with *), meaning that the tags will not be
|
103
|
+
publicly searchable/visible. This is useful for organizational tags tags, say if you wanted to arbitrarily
|
104
|
+
group records, or create a custom search based on a tag context.
|
105
|
+
|
106
|
+
= render 'e9_tags/form', :f => f, :context => [:users, true]
|
107
|
+
|
108
|
+
NOTE: The form and javascript are intended to work out of the box, but the certainly aren't going to look pretty.
|
109
|
+
If you do intend to use the forms, you'll no doubt need to style them.
|
110
|
+
|
111
|
+
|
112
|
+
|
113
|
+
email:
|
114
|
+
- travis@e9digital.com
|
115
|
+
executables: []
|
116
|
+
|
117
|
+
extensions: []
|
118
|
+
|
119
|
+
extra_rdoc_files: []
|
120
|
+
|
121
|
+
files:
|
122
|
+
- .gitignore
|
123
|
+
- Gemfile
|
124
|
+
- README.rdoc
|
125
|
+
- Rakefile
|
126
|
+
- app/views/e9_tags/_form.html.haml
|
127
|
+
- app/views/e9_tags/_tag_list.html.haml
|
128
|
+
- app/views/e9_tags/_tag_list_with_context.html.haml
|
129
|
+
- app/views/e9_tags/_template.html.haml
|
130
|
+
- config/locales/en.yml
|
131
|
+
- config/routes.rb
|
132
|
+
- e9_tags.gemspec
|
133
|
+
- lib/e9_tags.rb
|
134
|
+
- lib/e9_tags/controller.rb
|
135
|
+
- lib/e9_tags/helper.rb
|
136
|
+
- lib/e9_tags/model.rb
|
137
|
+
- lib/e9_tags/rack/tag_auto_completer.rb
|
138
|
+
- lib/e9_tags/rack/tag_context_auto_completer.rb
|
139
|
+
- lib/e9_tags/rack/tags_js.rb
|
140
|
+
- lib/e9_tags/rails_extensions.rb
|
141
|
+
- lib/e9_tags/tagging_extension.rb
|
142
|
+
- lib/e9_tags/version.rb
|
143
|
+
- lib/generators/e9_tags/install_generator.rb
|
144
|
+
- lib/generators/e9_tags/templates/e9_tags.js
|
145
|
+
has_rdoc: true
|
146
|
+
homepage: http://github.com/e9digital/e9_tags
|
147
|
+
licenses: []
|
148
|
+
|
149
|
+
post_install_message:
|
150
|
+
rdoc_options: []
|
151
|
+
|
152
|
+
require_paths:
|
153
|
+
- lib
|
154
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
155
|
+
none: false
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
segments:
|
160
|
+
- 0
|
161
|
+
version: "0"
|
162
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
163
|
+
none: false
|
164
|
+
requirements:
|
165
|
+
- - ">="
|
166
|
+
- !ruby/object:Gem::Version
|
167
|
+
segments:
|
168
|
+
- 0
|
169
|
+
version: "0"
|
170
|
+
requirements: []
|
171
|
+
|
172
|
+
rubyforge_project: e9_tags
|
173
|
+
rubygems_version: 1.3.7
|
174
|
+
signing_key:
|
175
|
+
specification_version: 3
|
176
|
+
summary: Extension to ActsAsTaggableOn used in e9 Rails 3 projects
|
177
|
+
test_files: []
|
178
|
+
|