spraypaint 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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