taglish 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.
@@ -0,0 +1,115 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+
3
+ ##
4
+ # Contains a list of strings.
5
+ # Works like an array.
6
+ class Taglish::TagList < Array
7
+ attr_accessor :tag_type
8
+ attr_accessor :taggable
9
+
10
+ def initialize(tag_type, *args)
11
+ self.tag_type = tag_type or raise "tag_type is required"
12
+ add(*args)
13
+ end
14
+
15
+ ##
16
+ # Add tags to the tag_list. Duplicate or blank tags will be ignored.
17
+ #
18
+ # Example:
19
+ # tag_list.add("Fun", "Happy")
20
+ # tag_list.add("Fun, Happy", :parse => true)
21
+ def add(*names)
22
+ extract_and_apply_options!(names)
23
+ concat(names)
24
+ clean!
25
+ self
26
+ end
27
+
28
+ ##
29
+ # Returns a new TagList using the given tag string.
30
+ #
31
+ # Example:
32
+ # tag_list = TagList.from("One , Two, Three")
33
+ # tag_list # ["One", "Two", "Three"]
34
+ def self.from(tag_type, string)
35
+ string = string.join(tag_type.glue) if string.respond_to?(:join)
36
+
37
+ new(tag_type).tap do |tag_list|
38
+ string = string.to_s.dup
39
+
40
+ # Parse the quoted tags
41
+ d = tag_type.delimiter
42
+ d = d.join("|") if d.kind_of?(Array)
43
+ string.gsub!(/(\A|#{d})\s*"(.*?)"\s*(#{d}\s*|\z)/) { tag_list << $2; $3 }
44
+ string.gsub!(/(\A|#{d})\s*'(.*?)'\s*(#{d}\s*|\z)/) { tag_list << $2; $3 }
45
+
46
+ tag_list.add(string.split(Regexp.new d))
47
+ end
48
+ end
49
+
50
+ ##
51
+ # Remove specific tags from the tag_list.
52
+ # Use the <tt>:parse</tt> option to add an unparsed tag string.
53
+ #
54
+ # Example:
55
+ # tag_list.remove("Sad", "Lonely")
56
+ # tag_list.remove("Sad, Lonely", :parse => true)
57
+ def remove(*names)
58
+ extract_and_apply_options!(names)
59
+ if tag_type.scored
60
+ delete_if { |name| names.include?(name.sub(Taglish::Taggable::SCORED_TAG_REGEX, '\1')) }
61
+ else
62
+ delete_if { |name| names.include?(name) }
63
+ end
64
+ self
65
+ end
66
+
67
+ ##
68
+ # Transform the tag_list into a tag string suitable for edting in a form.
69
+ # The tags are joined with <tt>TagList.delimiter</tt> and quoted if necessary.
70
+ #
71
+ # Example:
72
+ # tag_list = TagList.new("Round", "Square,Cube")
73
+ # tag_list.to_s # 'Round, "Square,Cube"'
74
+ def to_s
75
+ tags = frozen? ? self.dup : self
76
+ tags.send(:clean!)
77
+
78
+ tags.map do |name|
79
+ d = tag_type.delimiter
80
+ d = Regexp.new d.join("|") if d.kind_of? Array
81
+ name.index(d) ? "\"#{name}\"" : name
82
+ end.join(tag_type.glue)
83
+ end
84
+
85
+ def to_tagging_array
86
+ map { |name|
87
+ ar = tag_type.name_and_score(name)
88
+ Taglish::Tagging.new(:name => ar[0], :score => ar[1])
89
+ }
90
+ end
91
+
92
+ private
93
+
94
+ # Remove whitespace, duplicates, and blanks.
95
+ def clean!
96
+ # Do this in self.from instead, or wherever we parse from strings:
97
+ reject!(&:blank?)
98
+ map!(&:strip)
99
+ map!(&:downcase) if tag_type.force_lowercase
100
+ map!(&:parameterize) if tag_type.force_parameterize
101
+
102
+ uniq!
103
+ end
104
+
105
+ def extract_and_apply_options!(args)
106
+ options = args.last.is_a?(Hash) ? args.pop : {}
107
+ options.assert_valid_keys :parse
108
+
109
+ if options[:parse]
110
+ args.map! { |a| self.class.from(a) }
111
+ end
112
+
113
+ args.flatten!
114
+ end
115
+ end
@@ -0,0 +1,65 @@
1
+ class Taglish::TagType
2
+ attr_accessor :name, :scored, :ordered, :delimiter, :score_delimiter,
3
+ :force_parameterize, :force_lowercase
4
+
5
+ def initialize(name, opts={})
6
+ opts = {
7
+ :scored => false,
8
+ :ordered => false,
9
+ :delimiter => Taglish::DEFAULT_DELIMITER,
10
+ :score_delimiter => Taglish::DEFAULT_SCORE_DELIMITER,
11
+ :force_parameterize => false,
12
+ :force_lowercase => false
13
+ }.merge(opts)
14
+
15
+ self.name = name
16
+ self.scored = opts[:scored]
17
+ self.ordered = opts[:ordered]
18
+ self.delimiter = opts[:delimiter]
19
+ self.score_delimiter = opts[:score_delimiter]
20
+ self.force_parameterize = opts[:force_parameterize]
21
+ self.force_lowercase = opts[:force_lowercase]
22
+ end
23
+
24
+ def scored?
25
+ scored
26
+ end
27
+
28
+ def ordered?
29
+ ordered
30
+ end
31
+
32
+ def name_and_score(tag_str)
33
+ if scored
34
+ tag_str =~ Taglish::Taggable::SCORED_TAG_REGEX or raise "Scored tag has no score: #{tag_str}"
35
+ [$1, $2.to_i]
36
+ else
37
+ [tag_str, nil]
38
+ end
39
+ end
40
+
41
+ def find_or_create_tags(*tag_list)
42
+ return [] if tag_list.empty?
43
+
44
+ list = scored ? tag_list.map{|t| name_and_score(t)[0]} : tag_list
45
+
46
+ existing_tags = Tag.named_any(list).all
47
+ new_tag_names = list.reject do |name|
48
+ name = comparable_name(name)
49
+ existing_tags.any? {|tag| comparable_name(tag.name) == name}
50
+ end
51
+ created_tags = new_tag_names.map {|name| Tag.create(:name => name) }
52
+
53
+ existing_tags + created_tags
54
+ end
55
+
56
+ ##
57
+ # Returns the proper string used to join tags:
58
+ # basically the first choice of delimiters,
59
+ # with a space after each delimiter.
60
+ def glue
61
+ d = delimiter.kind_of?(Array) ? delimiter[0] : delimiter
62
+ d.ends_with?(" ") ? d : "#{d} "
63
+ end
64
+
65
+ end
@@ -0,0 +1,144 @@
1
+ module Taglish::Taggable
2
+
3
+ SCORED_TAG_REGEX = /^(.+):(-?\d+)$/
4
+
5
+ def taggable?
6
+ false
7
+ end
8
+
9
+ def taglish
10
+ taglish_on(:tags)
11
+ end
12
+
13
+ def ordered_taglish
14
+ ordered_taglish_on(:tags)
15
+ end
16
+
17
+ def scored_taglish
18
+ scored_taglish_on(:tags)
19
+ end
20
+
21
+ ##
22
+ # Make a model taggable on specified contexts.
23
+ #
24
+ # @param [Array] tag_types An array of taggable contexts
25
+ #
26
+ # Example:
27
+ # class User < ActiveRecord::Base
28
+ # scored_taglish_on :languages, :skills
29
+ # end
30
+ def scored_taglish_on(*tag_types)
31
+ taggable_on(false, true, tag_types)
32
+ end
33
+
34
+ ##
35
+ # Make a model taggable on specified contexts.
36
+ #
37
+ # @param [Array] tag_types An array of taggable contexts
38
+ #
39
+ # Example:
40
+ # class User < ActiveRecord::Base
41
+ # taglish_on :languages, :skills
42
+ # end
43
+ def taglish_on(*tag_types)
44
+ taggable_on(false, false, tag_types)
45
+ end
46
+
47
+ ##
48
+ # Make a model taggable on specified contexts.
49
+ #
50
+ # @param [Array] tag_types An array of taggable contexts
51
+ #
52
+ # Example:
53
+ # class User < ActiveRecord::Base
54
+ # scored_taglish_on :languages, :skills
55
+ # end
56
+ def ordered_taglish_on(*tag_types)
57
+ taggable_on(true, false, tag_types)
58
+ end
59
+
60
+ private
61
+
62
+ def taggable_on(ordered, scored, *new_tag_types)
63
+ # Assume new_tag_types has plural forms, like `skills`:
64
+ new_tag_types = new_tag_types.to_a.flatten.compact.map(&:to_sym)
65
+ unless taggable?
66
+ class_eval do
67
+ class_attribute :tag_types
68
+ self.tag_types = HashWithIndifferentAccess.new
69
+
70
+ has_many :taggings, :as => :taggable, :dependent => :destroy,
71
+ :include => :tag, :class_name => "Taglish::Tagging"
72
+ has_many :all_tags, :through => :taggings, :source => :tag,
73
+ :class_name => "Taglish::Tag"
74
+
75
+ def self.taggable?
76
+ true
77
+ end
78
+
79
+ include Taglish::Util
80
+ include Taglish::Core
81
+ end
82
+ end
83
+ # THEN: Copy/paste the save method, and think about how to implement
84
+ # set_tag_list_on.
85
+ # THEN: Implement tag_list_on.
86
+ # LATER: Implement TagList (extending Array) to save back tags
87
+ # if the user removes from/adds to the array.
88
+ new_tag_types.each do |ptt| # ptt is the plural form
89
+ stt = ptt.to_s.singularize
90
+
91
+ tag_type = Taglish::TagType.new(ptt, :scored => scored, :ordered => ordered)
92
+ self.tag_types[ptt] = tag_type
93
+
94
+ taggings_scope_name = ptt.to_sym
95
+ taggings_order = tag_type.ordered ? "#{Taglish::Tagging.table_name}.id" : nil
96
+
97
+ class_eval do
98
+ has_many taggings_scope_name, :as => :taggable,
99
+ :dependent => :destroy,
100
+ :include => :tag,
101
+ :class_name => 'Taglish::Tagging',
102
+ :conditions => ["#{Taglish::Tagging.table_name}.context = ?", ptt],
103
+ :order => taggings_order
104
+
105
+ has_many "#{stt}_tags".to_sym, :through => taggings_scope_name,
106
+ :source => :tag,
107
+ :class_name => "Taglish:Tag",
108
+ :order => taggings_order
109
+
110
+ end
111
+
112
+ class_eval %(
113
+ def #{stt}_list
114
+ tag_list_on(tag_types['#{ptt}'])
115
+ end
116
+
117
+ def #{stt}_list=(new_tags)
118
+ set_tag_list_on(tag_types['#{ptt}'], new_tags)
119
+ end
120
+
121
+ def all_#{ptt}
122
+ all_tags_list_on('#{ptt}')
123
+ end
124
+
125
+ def add_#{stt}(tag)
126
+ add_tag_on('#{ptt}', tag)
127
+ end
128
+ )
129
+
130
+ if scored
131
+ class_eval %(
132
+ def score_for_#{stt}(tag)
133
+ raise "TODO"
134
+ end
135
+
136
+ def set_score_for_#{stt}(tag, score)
137
+ set_score_for_tag_on('#{ptt}', tag, score)
138
+ end
139
+ )
140
+ end
141
+ end
142
+ end
143
+
144
+ end
@@ -0,0 +1,50 @@
1
+ class Taglish::Tagging < ActiveRecord::Base
2
+ include Taglish::Util
3
+
4
+ attr_accessible :tag, :tag_id, :context, :taggable, :taggable_type, :taggable_id,
5
+ :tagger, :tagger_type, :tagger_id, :score, :name
6
+
7
+ belongs_to :tag, :class_name => 'Taglish::Tag'
8
+ belongs_to :taggable, :polymorphic => true
9
+ belongs_to :tagger, :polymorphic => true
10
+
11
+ validates_presence_of :context
12
+ validates_presence_of :tag_id
13
+ validates_uniqueness_of :tag_id, :scope => [ :taggable_type, :taggable_id, :context, :tagger_id, :tagger_type ]
14
+
15
+ # after_destroy :remove_unused_tags
16
+
17
+ def tag_type
18
+ @tag_type ||= taggable.tag_types[context]
19
+ end
20
+
21
+ def name
22
+ @name ||= tag.name
23
+ end
24
+
25
+ def name=(v)
26
+ raise "Can't change the name of a tag/tagging" if @name or (tag and tag.name)
27
+ @name = v
28
+ end
29
+
30
+ def ==(object)
31
+ super || (
32
+ object.is_a?(Tagging) &&
33
+ context == object.context &&
34
+ name == object.name &&
35
+ score == object.score)
36
+ end
37
+
38
+ def to_s
39
+ score ? "#{name}:#{score}" : name
40
+ end
41
+
42
+ private
43
+
44
+ def remove_unused_tags
45
+ if taggable.remove_unused_tags_for?(context)
46
+ tag.destroy if tag.taggings.count.zero?
47
+ end
48
+ end
49
+
50
+ end
@@ -0,0 +1,4 @@
1
+ module Taglish::Util
2
+
3
+
4
+ end
@@ -0,0 +1,3 @@
1
+ module Taglish
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1 @@
1
+ require 'taglish'
@@ -0,0 +1,19 @@
1
+ sqlite3:
2
+ adapter: sqlite3
3
+ database: acts_as_taggable_on.sqlite3
4
+
5
+ mysql:
6
+ adapter: mysql2
7
+ hostname: localhost
8
+ username: root
9
+ password:
10
+ database: acts_as_taggable_on
11
+ charset: utf8
12
+
13
+ postgresql:
14
+ adapter: postgresql
15
+ hostname: localhost
16
+ username: postgres
17
+ password:
18
+ database: acts_as_taggable_on
19
+ encoding: utf8
@@ -0,0 +1,64 @@
1
+ class TaggableModel < ActiveRecord::Base
2
+ taglish
3
+ taglish_on :languages
4
+ taglish_on :skills
5
+ taglish_on :needs, :offerings
6
+ has_many :untaggable_models
7
+ end
8
+
9
+ class CachedModel < ActiveRecord::Base
10
+ taglish
11
+ end
12
+
13
+ class OtherCachedModel < ActiveRecord::Base
14
+ taglish_on :languages, :statuses, :glasses
15
+ end
16
+
17
+ class OtherTaggableModel < ActiveRecord::Base
18
+ taglish_on :tags, :languages
19
+ taglish_on :needs, :offerings
20
+ end
21
+
22
+ class InheritingTaggableModel < TaggableModel
23
+ end
24
+
25
+ class AlteredInheritingTaggableModel < TaggableModel
26
+ taglish_on :parts
27
+ end
28
+
29
+ class TaggableUser < ActiveRecord::Base
30
+ # TODO LATER
31
+ # acts_as_tagger
32
+ end
33
+
34
+ class InheritingTaggableUser < TaggableUser
35
+ end
36
+
37
+ class UntaggableModel < ActiveRecord::Base
38
+ belongs_to :taggable_model
39
+ end
40
+
41
+ class NonStandardIdTaggableModel < ActiveRecord::Base
42
+ primary_key = "an_id"
43
+ taglish
44
+ taglish_on :languages
45
+ taglish_on :skills
46
+ taglish_on :needs, :offerings
47
+ has_many :untaggable_models
48
+ end
49
+
50
+ class OrderedTaggableModel < ActiveRecord::Base
51
+ ordered_taglish
52
+ ordered_taglish_on :colors
53
+ end
54
+
55
+ class MixedTaggableModel < ActiveRecord::Base
56
+ taglish_on :skills
57
+ ordered_taglish_on :colors
58
+ scored_taglish_on :question_counts
59
+ taglish_on :needs, :offerings
60
+ end
61
+
62
+ class ScoredTaggableModel < ActiveRecord::Base
63
+ scored_taglish_on :question_counts
64
+ end