spraypaint 1.0.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,4 @@
1
+ .DS_Store
2
+ test/log/*
3
+ test/db/*.sqlite3
4
+ pkg/*
@@ -0,0 +1,4 @@
1
+ 0.9.1
2
+
3
+ * Changed Rakefile to use secret new penknife plugin tasks
4
+ * Improved documentation and TODO list
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Tom Ward (tom@popdog.net)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,17 @@
1
+ Features:
2
+
3
+ f = Film.find_by_title("The Umbrella's of Cherbourg")
4
+ f.tags
5
+ => ["french", "umbrella", "catherine-deneuve"]
6
+
7
+ Tags can be added using standard array methods
8
+
9
+ f.tags << "jacques-demy"
10
+ f.save!
11
+ f.reload
12
+ f.tags
13
+ => ["french", "umbrella", "catherine-deneuve", "jacques-demy"]
14
+
15
+ Tags maintain their order
16
+
17
+
@@ -0,0 +1,21 @@
1
+ $: << File.expand_path(File.join(File.dirname(__FILE__), "lib"))
2
+ $: << File.expand_path(File.join(File.dirname(__FILE__), "vendor/penknife/lib"))
3
+
4
+ require 'penknife/rake/plugin_tasks'
5
+
6
+ namespace :spraypaint do
7
+ Penknife::Rake::PluginTasks.new do |plugin|
8
+ plugin.plugin_root = File.dirname(File.expand_path(__FILE__))
9
+ plugin.name = 'spraypaint'
10
+ plugin.summary = 'Simple tagging in a can'
11
+ plugin.authors = ['Tom Ward (tomafro)']
12
+ plugin.email = 'tom@popdog.net'
13
+ plugin.homepage = 'http://github.com/tomafro/spraypaint'
14
+ plugin.code = 'http://github.com/tomafro/spraypaint.git'
15
+ plugin.license = 'MIT'
16
+ plugin.rails_version = '2.3+'
17
+ plugin.files = `git ls-files`.split("\n")
18
+ end
19
+ end
20
+
21
+ task 'default' => ['spraypaint:spec', 'spraypaint:features']
data/TODO ADDED
@@ -0,0 +1,7 @@
1
+ 1.1
2
+
3
+ * Add namespace, predicate and value columns - very simple machine tagging
4
+
5
+ 1.2
6
+
7
+ * Add ownership of tags
@@ -0,0 +1,4 @@
1
+ ---
2
+ :patch: 0
3
+ :major: 1
4
+ :minor: 0
@@ -0,0 +1,10 @@
1
+ ---
2
+ :version: 1.0.0
3
+ :license: MIT
4
+ :rails_version: 2.3+
5
+ :author: Tom Ward (tomafro)
6
+ :summary: Simple tagging in a can
7
+ :plugin: http://github.com/tomafro/spraypaint.git
8
+ :email: tom@popdog.net
9
+ :name: spraypaint
10
+ :homepage: http://github.com/tomafro/spraypaint
@@ -0,0 +1,21 @@
1
+ require 'spraypaint'
2
+
3
+ class SpraypaintMigrationGenerator < Rails::Generator::NamedBase
4
+ def initialize(runtime_args, runtime_options)
5
+ super(["create_spraypnt_tagging_tables"], runtime_options)
6
+ end
7
+
8
+ def manifest
9
+ record do |m|
10
+ m.migration_template 'spraypaint_migration.rb', 'db/migrate'
11
+ end
12
+ end
13
+
14
+ def tags_table
15
+ Spraypaint::Model::Tag.table_name
16
+ end
17
+
18
+ def taggings_table
19
+ Spraypaint::Model::Tagging.table_name
20
+ end
21
+ end
@@ -0,0 +1,25 @@
1
+ class <%= class_name.underscore.camelize %> < ActiveRecord::Migration
2
+ def self.up
3
+ create_table '<%= tags_table %>', :force => true do |table|
4
+ table.string 'name', :null => false
5
+ end
6
+
7
+ add_index '<%= tags_table %>', 'name', :unique => true
8
+
9
+ create_table '<%= taggings_table %>', :force => true do |table|
10
+ table.integer 'spraypaint_tag_id', :null => false
11
+ table.integer 'target_id', :null => false
12
+ table.string 'target_type', :null => false
13
+ end
14
+
15
+ add_index '<%= taggings_table %>', 'target_id'
16
+ add_index '<%= taggings_table %>', 'target_type'
17
+ add_index '<%= taggings_table %>', 'spraypaint_tag_id'
18
+ add_index '<%= taggings_table %>', ['target_type', 'target_id', 'spraypaint_tag_id'], :unique => true, :name => 'spraypaint_unique_tagging_index'
19
+ end
20
+
21
+ def self.down
22
+ drop_table '<%= taggings_table %>'
23
+ drop_table '<%= tags_table %>'
24
+ end
25
+ end
@@ -0,0 +1,59 @@
1
+ # = Spraypaint - simple tagging in a can
2
+ #
3
+ # == Getting started
4
+ #
5
+ # Generate and run the spraypaint migration
6
+ # script/generate spraypaint_migration
7
+ # rake db:migrate
8
+ #
9
+ # Enable spraypaint in a model
10
+ # class Film < ActiveRecord::Base
11
+ # tag_with_spraypaint
12
+ #
13
+ # def inspect
14
+ # "<#{self.title}>"
15
+ # end
16
+ # end
17
+ #
18
+ # Create some tagged films:
19
+ # Film.create! :title => 'The Umbrellas of Cherbourg', :director => 'Demy', :tags => ['umbrellas', 'french', 'musical']
20
+ # Film.create! :title => 'Jules Et Jim', :director => 'Truffaut', :tags => ['threesome', 'bicycle', 'french']
21
+ # Film.create! :title => 'Shoot The Pianist', :director => 'Truffaut', :tags => ['piano', 'gangster', 'french']
22
+ # Film.create! :title => 'The Sound of Music', :tags => ['musical', 'ww2']
23
+ #
24
+ # Use the tagged_with method to find records matching tags
25
+ # Film.tagged_with('musical')
26
+ # => [<The Umbrellas Of Cherbourg>, <The Sound Of Music>]
27
+ # Film.tagged_with('french', 'musical')
28
+ # => [<The Umbrellas Of Cherbourg>]
29
+ #
30
+ # As tagged_with is just a scope, you can chain it with standard finders or calculations
31
+ # Film.tagged_with('french').all(:order => 'title')
32
+ # => [<Jules Et Jim>, <Shoot The Pianist>, <The Umbrellas Of Cherbourg>]
33
+ # Film.tagged_with('musicl').all(:conditions => "name like '%Music%'")
34
+ # => [<The Sound Of Music>]
35
+ # Film.tagged_with('french').count
36
+ # => 3
37
+ #
38
+ # To find all tags on a model use #tags
39
+ # Film.tags
40
+ # => ['umbrellas', 'french', 'musical', 'threesome', 'bicycle', 'piano', 'gangster', 'ww2']
41
+ #
42
+ # The #tags method also plays nicely with scopes
43
+ # Film.all(:conditions => {'director' => 'Truffaut'}).tags
44
+ # => ['french', 'threesome', 'bicycle', 'piano', 'gangster']
45
+ # Film.tagged_with('french').tags
46
+ # => ['musical', 'french', 'threesome', 'bicycle', 'piano', 'gangster']
47
+ #
48
+
49
+ module Spraypaint
50
+ def self.activate_plugin
51
+ ActiveRecord::Base.extend(ClassMethods)
52
+ end
53
+
54
+ module ClassMethods
55
+ def tag_with_spraypaint(options = {})
56
+ include Spraypaint::Behaviour
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,11 @@
1
+ require 'spraypaint'
2
+
3
+ module Spraypaint::Behaviour
4
+ include Manipulation, Persistence, Discovery
5
+
6
+ def self.included(base)
7
+ self.included_modules.each do |m|
8
+ m.__send__ :included, base
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,57 @@
1
+ module Spraypaint::Behaviour::Discovery
2
+ def self.included(base)
3
+ unless base == parent
4
+ base.named_scope :tagged_with, lambda {|*tags|
5
+ base.spraypaint_condition_hash_for(tags)
6
+ }
7
+
8
+ base.extend(ClassMethods)
9
+ end
10
+ end
11
+
12
+ module ClassMethods
13
+ class TagString < String
14
+ def initialize(string, count)
15
+ super(string)
16
+ @tag_count = count.to_i
17
+ end
18
+
19
+ def frequency
20
+ @tag_count
21
+ end
22
+ end
23
+
24
+ def spraypaint_condition_hash_for(tags)
25
+ tags = self.tag_sanitizer.sanitize_array([*tags])
26
+ {:conditions => %{
27
+ EXISTS (
28
+ SELECT 1 FROM #{Spraypaint::Model::Tag.table_name}, #{Spraypaint::Model::Tagging.table_name}
29
+ WHERE #{Spraypaint::Model::Tagging.table_name}.target_id = #{self.table_name}.id
30
+ AND #{Spraypaint::Model::Tagging.table_name}.target_type = '#{self.base_class.name}'
31
+ AND #{Spraypaint::Model::Tag.table_name}.id = #{Spraypaint::Model::Tagging.table_name}.tag_id
32
+ AND (#{Spraypaint::Model::Tag.tag_condition(tags)})
33
+ GROUP BY #{Spraypaint::Model::Tagging.table_name}.target_id
34
+ HAVING count(distinct #{Spraypaint::Model::Tagging.table_name}.tag_id) = #{tags.size}
35
+ )
36
+ }}
37
+ end
38
+
39
+ def tags(options = {})
40
+
41
+ self.all({
42
+ :select => ['spraypaint_tags.id, spraypaint_tags.name tag_name, count(*) tag_count'],
43
+ :joins => %{
44
+ INNER JOIN spraypaint_taggings ON (spraypaint_taggings.target_id = #{self.table_name}.#{self.primary_key} AND spraypaint_taggings.target_type = '#{self.base_class.name}')
45
+ INNER JOIN spraypaint_tags ON (spraypaint_tags.id = spraypaint_taggings.tag_id)
46
+ },
47
+ :group => 'spraypaint_tags.id',
48
+ :order => 'count(*) desc, spraypaint_tags.name',
49
+ }.merge(options.slice(:conditions, :limit, :offset, :order))).collect {|tag| TagString.new(tag.tag_name, tag.tag_count) }
50
+ end
51
+
52
+ def related_tags(*tags)
53
+ options = tags.extract_options!
54
+ tags(options)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,71 @@
1
+ module Spraypaint::Behaviour::Manipulation
2
+ def self.included(base)
3
+ unless base == parent
4
+ base.alias_method_chain :read_attribute, :spraypaint
5
+ base.alias_method_chain :create_or_update, :spraypaint
6
+ base.extend ClassMethods
7
+ end
8
+ end
9
+
10
+ def tags
11
+ read_attribute('tags')
12
+ end
13
+
14
+ def tags=(tags)
15
+ write_attribute('tags', self.class.tag_sanitizer.sanitize_array([*tags]))
16
+ end
17
+
18
+ def tag_string
19
+ read_attribute('tag_string') || tags.join(", ")
20
+ end
21
+
22
+ def tag_string=(string)
23
+ write_attribute('tag_string', string)
24
+ self.tags = string && string.split(",").collect(&:strip)
25
+ end
26
+
27
+ def tags_changed?
28
+ attribute_changed?('tags')
29
+ end
30
+
31
+ def tags_change
32
+ attribute_change('tags')
33
+ end
34
+
35
+ def tags_was
36
+ attribute_was('tags')
37
+ end
38
+
39
+ def tags_will_change!
40
+ attribute_will_change('tags')
41
+ end
42
+
43
+ private
44
+
45
+ def create_or_update_with_spraypaint
46
+ change = tags_change
47
+ returning create_or_update_without_spraypaint do |result|
48
+ if result && change
49
+ save_tag_names(change.last)
50
+ end
51
+ end
52
+ end
53
+
54
+ def read_attribute_with_spraypaint(attribute)
55
+ if attribute.to_s == "tags"
56
+ read_attribute_without_spraypaint(attribute) || load_tag_names
57
+ else
58
+ read_attribute_without_spraypaint(attribute)
59
+ end
60
+ end
61
+
62
+ module ClassMethods
63
+ def tag_sanitizer
64
+ @tag_sanitizer ||= Spraypaint::DefaultSanitizer.new
65
+ end
66
+
67
+ def tag_sanitizer=(sanitizer)
68
+ @tag_sanitizer = sanitizer
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,24 @@
1
+ module Spraypaint::Behaviour::Persistence
2
+ def self.included(base)
3
+ unless base == parent
4
+ base.has_many 'spraypaint_taggings', :as => 'target', :dependent => :destroy, :class_name => 'Spraypaint::Model::Tagging'
5
+ base.has_many 'spraypaint_tags', :through => 'spraypaint_taggings', :source => 'tag', :class_name => '::Spraypaint::Model::Tag', :order => 'spraypaint_taggings.id'
6
+ end
7
+ end
8
+
9
+ private
10
+
11
+ def load_tag_names
12
+ self.spraypaint_tags.collect(&:name)
13
+ end
14
+
15
+ def save_tag_names(names)
16
+ transaction do
17
+ spraypaint_taggings.destroy_all
18
+ names.each do |tag_name|
19
+ tag = Spraypaint::Model::Tag.find_or_create_by_name(tag_name)
20
+ self.spraypaint_tags << tag
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,17 @@
1
+ class Spraypaint::DefaultSanitizer
2
+ include Spraypaint::Sanitizer
3
+
4
+ attr_accessor :allowed_characters
5
+
6
+ def initialize(allowed_characters = /[\w -]/)
7
+ self.allowed_characters = allowed_characters
8
+ end
9
+
10
+ def sanitize_tag(tag)
11
+ return nil if tag.nil?
12
+ string = tag.strip
13
+ string = string.mb_chars.normalize(:d).gsub(/[^\0-\x80]/, '')
14
+ string = string.scan(self.allowed_characters).join
15
+ string.empty? ? nil : string.to_s
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ class Spraypaint::Model::Tag < ActiveRecord::Base
2
+ # By default spraypaint uses the spraypaint namespace for all its tables, so
3
+ # the tags themselves are stored in spraypaint_tags. To change this, simply
4
+ # set the table name to something else
5
+ # Spraypaint::Model::Tag.set_table_name 'special_tags'
6
+ set_table_name :spraypaint_tags
7
+
8
+ # All tags must have a name. In practise this is enforced by the way tags are
9
+ # set and in the database, so this validation is redundant in normal usage
10
+ validates_presence_of :name
11
+
12
+ def self.tag_condition(tag)
13
+ sanitize_sql(:name => tag)
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ class Spraypaint::Model::Tagging < ActiveRecord::Base
2
+ set_table_name :spraypaint_taggings
3
+
4
+ # Main association between an individual tagging and the actual tag
5
+ belongs_to :tag, :class_name => 'Spraypaint::Model::Tag'
6
+
7
+ # Association between the tagging and the object being tagged
8
+ belongs_to :target, :polymorphic => true
9
+
10
+ validates_presence_of :tag, :target
11
+ end
@@ -0,0 +1,11 @@
1
+ module Spraypaint::Sanitizer
2
+
3
+ # Sanitizes an array of tags, passing each one through #sanitize_tag and removing
4
+ # all nils and duplicates.
5
+
6
+ def sanitize_array(array)
7
+ array.collect do |tag|
8
+ sanitize_tag(tag)
9
+ end.compact.uniq
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ require 'spraypaint'
2
+
3
+ Spraypaint.activate_plugin
@@ -0,0 +1,78 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{spraypaint}
8
+ s.version = "1.0.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Tom Ward (tomafro)"]
12
+ s.date = %q{2009-10-18}
13
+ s.email = %q{tom@popdog.net}
14
+ s.extra_rdoc_files = [
15
+ "README"
16
+ ]
17
+ s.files = [
18
+ ".gitignore",
19
+ "CHANGELOG",
20
+ "MIT-LICENSE",
21
+ "Rakefile",
22
+ "TODO",
23
+ "VERSION.yml",
24
+ "about.yml",
25
+ "generators/spraypaint_migration/spraypaint_migration_generator.rb",
26
+ "generators/spraypaint_migration/templates/spraypaint_migration.rb",
27
+ "lib/spraypaint.rb",
28
+ "lib/spraypaint/behaviour.rb",
29
+ "lib/spraypaint/behaviour/discovery.rb",
30
+ "lib/spraypaint/behaviour/manipulation.rb",
31
+ "lib/spraypaint/behaviour/persistence.rb",
32
+ "lib/spraypaint/default_sanitizer.rb",
33
+ "lib/spraypaint/model/tag.rb",
34
+ "lib/spraypaint/model/tagging.rb",
35
+ "lib/spraypaint/sanitizer.rb",
36
+ "rails/init.rb",
37
+ "spraypaint.gemspec",
38
+ "test/config/boot.rb",
39
+ "test/config/database.yml",
40
+ "test/config/environment.rb",
41
+ "test/config/environments/test.rb",
42
+ "test/db/schema.rb",
43
+ "test/spec/default_sanitizer_spec.rb",
44
+ "test/spec/models/tag_spec.rb",
45
+ "test/spec/models/tagging_spec.rb",
46
+ "test/spec/spec_helper.rb",
47
+ "test/spec/spraypaint_spec.rb",
48
+ "vendor/penknife/lib/penknife.rb",
49
+ "vendor/penknife/lib/penknife/rake.rb",
50
+ "vendor/penknife/lib/penknife/rake/plugin_tasks.rb"
51
+ ]
52
+ s.homepage = %q{http://github.com/tomafro/spraypaint}
53
+ s.rdoc_options = ["--charset=UTF-8"]
54
+ s.require_paths = ["lib"]
55
+ s.rubygems_version = %q{1.3.5}
56
+ s.summary = %q{Simple tagging in a can}
57
+ s.test_files = [
58
+ "test/config/boot.rb",
59
+ "test/config/environment.rb",
60
+ "test/config/environments/test.rb",
61
+ "test/db/schema.rb",
62
+ "test/spec/default_sanitizer_spec.rb",
63
+ "test/spec/models/tag_spec.rb",
64
+ "test/spec/models/tagging_spec.rb",
65
+ "test/spec/spec_helper.rb",
66
+ "test/spec/spraypaint_spec.rb"
67
+ ]
68
+
69
+ if s.respond_to? :specification_version then
70
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
71
+ s.specification_version = 3
72
+
73
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
74
+ else
75
+ end
76
+ else
77
+ end
78
+ end