e9_tags 0.0.8
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.
- 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
|
+
|