cheap_imports 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. data/.bzr/README +3 -0
  2. data/.bzr/branch/branch.conf +2 -0
  3. data/.bzr/branch/format +1 -0
  4. data/.bzr/branch/last-revision +1 -0
  5. data/.bzr/branch/tags +0 -0
  6. data/.bzr/branch-format +1 -0
  7. data/.bzr/checkout/conflicts +1 -0
  8. data/.bzr/checkout/dirstate +0 -0
  9. data/.bzr/checkout/format +1 -0
  10. data/.bzr/checkout/merge-hashes +6 -0
  11. data/.bzr/repository/format +1 -0
  12. data/.bzr/repository/indices/3b0e60c570a8cfc21b7d22db489579e7.iix +0 -0
  13. data/.bzr/repository/indices/3b0e60c570a8cfc21b7d22db489579e7.rix +0 -0
  14. data/.bzr/repository/indices/3b0e60c570a8cfc21b7d22db489579e7.six +5 -0
  15. data/.bzr/repository/indices/3b0e60c570a8cfc21b7d22db489579e7.tix +0 -0
  16. data/.bzr/repository/indices/d139719d6bdd037de4ec3718dcf1c0c4.iix +0 -0
  17. data/.bzr/repository/indices/d139719d6bdd037de4ec3718dcf1c0c4.rix +0 -0
  18. data/.bzr/repository/indices/d139719d6bdd037de4ec3718dcf1c0c4.six +5 -0
  19. data/.bzr/repository/indices/d139719d6bdd037de4ec3718dcf1c0c4.tix +0 -0
  20. data/.bzr/repository/indices/d5dffeca0774033e54854302570cb330.iix +0 -0
  21. data/.bzr/repository/indices/d5dffeca0774033e54854302570cb330.rix +0 -0
  22. data/.bzr/repository/indices/d5dffeca0774033e54854302570cb330.six +5 -0
  23. data/.bzr/repository/indices/d5dffeca0774033e54854302570cb330.tix +0 -0
  24. data/.bzr/repository/indices/e5571628132ac072d270e0a249095c14.iix +0 -0
  25. data/.bzr/repository/indices/e5571628132ac072d270e0a249095c14.rix +0 -0
  26. data/.bzr/repository/indices/e5571628132ac072d270e0a249095c14.six +5 -0
  27. data/.bzr/repository/indices/e5571628132ac072d270e0a249095c14.tix +0 -0
  28. data/.bzr/repository/pack-names +0 -0
  29. data/.bzr/repository/packs/3b0e60c570a8cfc21b7d22db489579e7.pack +0 -0
  30. data/.bzr/repository/packs/d139719d6bdd037de4ec3718dcf1c0c4.pack +0 -0
  31. data/.bzr/repository/packs/d5dffeca0774033e54854302570cb330.pack +0 -0
  32. data/.bzr/repository/packs/e5571628132ac072d270e0a249095c14.pack +0 -0
  33. data/.bzrignore +2 -0
  34. data/History.txt +4 -0
  35. data/README.txt +47 -0
  36. data/Rakefile +38 -0
  37. data/bin/cheap_imports +8 -0
  38. data/lib/cheap_imports/cheap_imports.rb +154 -0
  39. data/lib/cheap_imports/import.rb +64 -0
  40. data/lib/cheap_imports.rb +52 -0
  41. data/spec/cheap_imports_spec.rb +7 -0
  42. data/spec/orms/active_record_spec.rb +20 -0
  43. data/spec/orms/datamapper_spec.rb +53 -0
  44. data/spec/spec_helper.rb +16 -0
  45. data/tasks/ann.rake +80 -0
  46. data/tasks/bones.rake +20 -0
  47. data/tasks/gem.rake +201 -0
  48. data/tasks/git.rake +40 -0
  49. data/tasks/notes.rake +27 -0
  50. data/tasks/post_load.rake +34 -0
  51. data/tasks/rdoc.rake +51 -0
  52. data/tasks/rubyforge.rake +55 -0
  53. data/tasks/setup.rb +292 -0
  54. data/tasks/spec.rake +54 -0
  55. data/tasks/svn.rake +47 -0
  56. data/tasks/test.rake +40 -0
  57. data/tasks/zentest.rake +36 -0
  58. metadata +123 -0
data/.bzr/README ADDED
@@ -0,0 +1,3 @@
1
+ This is a Bazaar control directory.
2
+ Do not change any files in this directory.
3
+ See http://bazaar-vcs.org/ for more information about Bazaar.
@@ -0,0 +1,2 @@
1
+ parent_location = http://ananelson.com/code/cheap_imports/
2
+ push_location = sftp://anaslist@ananelson.com/~/sites/ananelson.com/code/cheap_imports/
@@ -0,0 +1 @@
1
+ Bazaar Branch Format 6 (bzr 0.15)
@@ -0,0 +1 @@
1
+ 4 ana@ananelson.com-20091103102943-5xrd13o5d0grzb4j
data/.bzr/branch/tags ADDED
File without changes
@@ -0,0 +1 @@
1
+ Bazaar-NG meta directory, format 1
@@ -0,0 +1 @@
1
+ BZR conflict list format 1
Binary file
@@ -0,0 +1 @@
1
+ Bazaar Working Tree Format 4 (bzr 0.15)
@@ -0,0 +1,6 @@
1
+ BZR merge-modified list format 1
2
+ file_id: cheap_imports.rb-20090721140243-7zd6ayu1gjqkmv5d-22
3
+ hash: 8e31b226cd9163bcc12089d289659a8938b11212
4
+
5
+ file_id: cheap_imports.rb-20090721140243-7zd6ayu1gjqkmv5d-16
6
+ hash: 6d4bcc8c2461ffbf20b3535190cf8995002959da
@@ -0,0 +1 @@
1
+ Bazaar pack repository format 1 (needs bzr 0.92)
@@ -0,0 +1,5 @@
1
+ Bazaar Graph Index 1
2
+ node_ref_lists=0
3
+ key_elements=1
4
+ len=0
5
+
@@ -0,0 +1,5 @@
1
+ Bazaar Graph Index 1
2
+ node_ref_lists=0
3
+ key_elements=1
4
+ len=0
5
+
@@ -0,0 +1,5 @@
1
+ Bazaar Graph Index 1
2
+ node_ref_lists=0
3
+ key_elements=1
4
+ len=0
5
+
@@ -0,0 +1,5 @@
1
+ Bazaar Graph Index 1
2
+ node_ref_lists=0
3
+ key_elements=1
4
+ len=0
5
+
Binary file
data/.bzrignore ADDED
@@ -0,0 +1,2 @@
1
+ pkg
2
+ coverage
data/History.txt ADDED
@@ -0,0 +1,4 @@
1
+ == 1.0.0 / 2009-07-20
2
+
3
+ * 1 major enhancement
4
+ * Birthday!
data/README.txt ADDED
@@ -0,0 +1,47 @@
1
+ cheap_imports
2
+ by Ana Nelson
3
+
4
+ == DESCRIPTION:
5
+
6
+ Imports delimited text files to Active Record or Datamapper, with validations.
7
+
8
+ == FEATURES/PROBLEMS:
9
+
10
+ * FIXME (list of features or problems)
11
+
12
+ == SYNOPSIS:
13
+
14
+ FIXME (code sample of usage)
15
+
16
+ == REQUIREMENTS:
17
+
18
+ * FIXME (list of requirements)
19
+
20
+ == INSTALL:
21
+
22
+ * FIXME (sudo gem install, anything else)
23
+
24
+ == LICENSE:
25
+
26
+ (The MIT License)
27
+
28
+ Copyright (c) 2008 FIXME (different license?)
29
+
30
+ Permission is hereby granted, free of charge, to any person obtaining
31
+ a copy of this software and associated documentation files (the
32
+ 'Software'), to deal in the Software without restriction, including
33
+ without limitation the rights to use, copy, modify, merge, publish,
34
+ distribute, sublicense, and/or sell copies of the Software, and to
35
+ permit persons to whom the Software is furnished to do so, subject to
36
+ the following conditions:
37
+
38
+ The above copyright notice and this permission notice shall be
39
+ included in all copies or substantial portions of the Software.
40
+
41
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
42
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
43
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
44
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
45
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
46
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
47
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,38 @@
1
+ # Look in the tasks/setup.rb file for the various options that can be
2
+ # configured in this Rakefile. The .rake files in the tasks directory
3
+ # are where the options are used.
4
+
5
+ begin
6
+ require 'bones'
7
+ Bones.setup
8
+ rescue LoadError
9
+ begin
10
+ load 'tasks/setup.rb'
11
+ rescue LoadError
12
+ raise RuntimeError, '### please install the "bones" gem ###'
13
+ end
14
+ end
15
+
16
+ ensure_in_path 'lib'
17
+ require 'cheap_imports'
18
+
19
+ PROJ.name = 'cheap_imports'
20
+ PROJ.authors = 'Ana Nelson'
21
+ PROJ.email = 'ana@ananelson.com'
22
+ PROJ.url = 'http://ananelson.com'
23
+ PROJ.version = CheapImports::VERSION
24
+ PROJ.rubyforge.name = 'cheap_imports'
25
+
26
+ PROJ.spec.opts << '--color'
27
+
28
+ require 'rake'
29
+ require 'spec/rake/spectask'
30
+ require 'spec/rake/verify_rcov'
31
+
32
+ task :default => :verify_rcov
33
+
34
+ desc "Verify RCOV coverage above threshold"
35
+ RCov::VerifyTask.new(:verify_rcov => 'spec:rcov') do |t|
36
+ t.threshold = 87.7
37
+ t.index_html = 'coverage/index.html'
38
+ end
data/bin/cheap_imports ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path(
4
+ File.join(File.dirname(__FILE__), %w[.. lib cheap_imports]))
5
+
6
+ # Put your code here
7
+
8
+ # EOF
@@ -0,0 +1,154 @@
1
+ module CheapImports
2
+ def self.included(base)
3
+ base.extend CheapImportsClassMethods
4
+
5
+ # Detect what ORM we are using.
6
+ if base.superclass.name == "ActiveRecord::Base"
7
+ @cheap_imports_orm = "ActiveRecord"
8
+ elsif base.included_modules.collect {|m| m.name }.include?("DataMapper::Resource")
9
+ @cheap_imports_orm = "DataMapper"
10
+ base.extend DataMapperMethods
11
+ else
12
+ raise "unknown ORM!"
13
+ end
14
+ end
15
+
16
+ module DataMapperMethods
17
+ def column_names
18
+ properties.collect {|p| p.name.to_s }
19
+ end
20
+
21
+ def columns
22
+ properties
23
+ end
24
+ end
25
+
26
+ module CheapImportsClassMethods
27
+ def init_recognizable_hashes
28
+ default_hash = {}
29
+ column_names.each do |n|
30
+ next if n === 'id'
31
+ default_hash[n.to_sym] = n
32
+ end
33
+ raise "default hash is empty" if default_hash.empty?
34
+ @recognizable_hashes ||= {:default => default_hash}
35
+ end
36
+
37
+ # Add a new import definition.
38
+ #
39
+ # imports :my_descriptive_hash_name => {
40
+ # :database_column_name => "CORRESPONDING_TABLE_HEADER_TEXT"
41
+ # }
42
+ def imports(hash)
43
+ init_recognizable_hashes
44
+ @recognizable_hashes.merge!(hash)
45
+ end
46
+
47
+ # Don't override this. You probably want to override import_rash instead.
48
+ def import(raw, args)
49
+ style = recognize_hash_style(raw)
50
+
51
+ # Want to be able to access this data in the raw hash using either the native
52
+ # key or our nicer symbolic key.
53
+ @recognizable_hashes[style].each do |k, v|
54
+ raw[k] = raw[v]
55
+ end
56
+
57
+ hash = fetch_default_hash(raw, style)
58
+ hash[:imported_at] = args[:imported_at] if column_names.include?("imported_at")
59
+ hash[:history] = "Imported from #{style.to_s} at #{args[:imported_at].to_s}." if column_names.include?("history")
60
+ hash[:source] = style.to_s if column_names.include?("source")
61
+ import_rash(raw, args, style, hash)
62
+ end
63
+
64
+ def import_rash(raw, args, style, hash)
65
+ delete_prior hash[:imported_at]
66
+ create(hash)
67
+ end
68
+
69
+ def delete_prior(imported_at)
70
+ delete_all "imported_at < '#{imported_at}'" if column_names.include?("imported_at")
71
+ end
72
+
73
+ def fetch_default_hash(raw, style)
74
+ hash = {}
75
+ columns.each do |c|
76
+ column_name = c.name.to_sym
77
+ next if column_name == :id
78
+ keys = @recognizable_hashes[style].keys
79
+
80
+ raw_value = nil
81
+ if keys.include?(column_name)
82
+ raw_value = value_of_from(column_name, raw)
83
+ elsif keys.include?("#{name.downcase}_#{column_name}".to_sym)
84
+ raw_value = value_of_from("#{name.downcase}_#{column_name}".to_sym, raw)
85
+ end
86
+ next if raw_value.nil?
87
+
88
+ value = nil
89
+
90
+ type = c.type.to_s.downcase
91
+ case type
92
+ when "string", "text", "integer", "boolean"
93
+ if raw_value === ""
94
+ value = nil
95
+ else
96
+ value = raw_value
97
+ end
98
+ when "date", "datetime", "time"
99
+ date_format_string = @recognizable_hashes[style]["#{column_name}_format".to_sym]
100
+ date_format_string ||= @recognizable_hashes[style]["#{name.downcase}_#{column_name}_format".to_sym]
101
+ raise "nil date_format_string for #{style} #{column_name}" if date_format_string.empty?
102
+ begin
103
+ value = Date.strptime(raw_value, date_format_string) if type === "date"
104
+ value = DateTime.strptime(raw_value, date_format_string) if type === "datetime"
105
+ value = Time.strptime(raw_value, date_format_string) if type === "time"
106
+ rescue ArgumentError
107
+ value = nil
108
+ end
109
+ when "float", "decimal"
110
+ value = BigDecimal.new(raw_value.gsub(/[^0-9\.\-]/, ''))
111
+ else
112
+ raise "unhandled type: #{c.type.to_s.downcase}"
113
+ end
114
+ hash[column_name] = value
115
+ end
116
+ hash
117
+ end
118
+
119
+ # Fetch a row from the raw value hash.
120
+ def value_of_from(field_name, value_hash)
121
+ specification = @recognizable_hashes[recognize_hash_style(value_hash)]
122
+ raise "specification hash should not be nil!" if specification.nil?
123
+ value = value_hash[specification[field_name]].to_s.strip
124
+ block_given? ? yield(value) : value
125
+ end
126
+
127
+ # Can we import this type of hash?
128
+ # All classes have a default hash so we can always import data which was previously exported from the database.
129
+ # TODO Get rid of nasty workaround for dated_on_format keys.
130
+ # Return the name of style, or false if no style is matched.
131
+ def recognize_hash_style(raw_hash, debug = false)
132
+ @recognizable_hashes.each do |s, h|
133
+ puts "trying style: #{s} in class #{self.name}" if debug
134
+
135
+ match = true
136
+ h.each do |k, v|
137
+ this_key_matches = (raw_hash.has_key?(v.to_s) || v.to_s =~ /^\%/)
138
+ puts "key: #{k} matches: #{this_key_matches}" if debug
139
+ match = match && this_key_matches
140
+ end
141
+
142
+ puts "overall match: #{match}" if debug
143
+ return s if match
144
+ end
145
+ false
146
+ end
147
+ end
148
+ end
149
+
150
+ class NilClass
151
+ def id_or_nil
152
+ nil
153
+ end
154
+ end
@@ -0,0 +1,64 @@
1
+ module Import
2
+ # Feel free to add extra delimiters to this array.
3
+ DELIMITERS = ["\t", '|', ","]
4
+
5
+ def Import.import(klasses, params_data, args = {}, debug = false)
6
+ args.merge!({:imported_at => Time.now.strftime("%Y-%m-%d %H:%M:%S")}) unless args.has_key?(:imported_at)
7
+ klasses = [klasses] unless klasses.is_a?(Array)
8
+ delim = auto_detect_delimiter(params_data.to_s)
9
+ header_row = nil
10
+
11
+ objects = []
12
+
13
+ FasterCSV.parse(params_data, {:col_sep => delim, :row_sep => :auto}) do |row|
14
+ next if row.join("") !~ /\w/ # Skip blank rows.
15
+
16
+ header_row ||= row
17
+ next if header_row.join("") === row.join("") # Skip over duplicate header rows.
18
+
19
+ # Clean up the data, get rid of leading/trailing spaces.
20
+ row.collect {|str| str.to_s.strip!}
21
+
22
+ h = Hash[*header_row.zip(row).flatten]
23
+ klasses.each do |c|
24
+ style = c.recognize_hash_style(h, debug)
25
+ objects << c.import(h, args) if style
26
+ end
27
+ end
28
+
29
+ # Return an array of everything that was imported in this batch.
30
+ objects
31
+ end
32
+
33
+ def Import.import_from_file(klasses, filename, debug = false)
34
+ args = {}
35
+ args[:date] = Date.strptime($1) if filename =~ /([0-9]{4}-[0-9]{2}-[0-9]{2})/
36
+ import(klasses, File.read(filename), args, debug)
37
+ end
38
+
39
+ private
40
+
41
+ def Import.import_if_valid_else_ignore
42
+ begin
43
+ yield
44
+ rescue Exception => e
45
+ puts e.inspect
46
+ return nil
47
+ end
48
+ end
49
+
50
+ # Guess the correct text file column delimiter. We look at the first row
51
+ # of data and the most common character from the DELIMITERS array is
52
+ # assumed to be the delimiter. You can add additional characters to the
53
+ # DELIMITERS array but to avoid the risk of false positives it's probably
54
+ # best not to add too many.
55
+ def Import.auto_detect_delimiter(sample_text)
56
+ sample_text = sample_text.split(/$/)[0] || ""
57
+ delim = DELIMITERS[0]
58
+ len = DELIMITERS.length
59
+ DELIMITERS[1-len, len-1].each do |char|
60
+ delim = char if sample_text.count(char) > sample_text.count(delim)
61
+ end
62
+ delim
63
+ end
64
+ end
@@ -0,0 +1,52 @@
1
+ module CheapImports
2
+
3
+ # :stopdoc:
4
+ VERSION = '0.0.2'
5
+ LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
6
+ PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
7
+ # :startdoc:
8
+
9
+ # Returns the version string for the library.
10
+ #
11
+ def self.version
12
+ VERSION
13
+ end
14
+
15
+ # Returns the library path for the module. If any arguments are given,
16
+ # they will be joined to the end of the libray path using
17
+ # <tt>File.join</tt>.
18
+ #
19
+ def self.libpath( *args )
20
+ args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
21
+ end
22
+
23
+ # Returns the lpath for the module. If any arguments are given,
24
+ # they will be joined to the end of the path using
25
+ # <tt>File.join</tt>.
26
+ #
27
+ def self.path( *args )
28
+ args.empty? ? PATH : ::File.join(PATH, args.flatten)
29
+ end
30
+
31
+ # Utility method used to require all files ending in .rb that lie in the
32
+ # directory below this file that has the same name as the filename passed
33
+ # in. Optionally, a specific _directory_ name can be passed in such that
34
+ # the _filename_ does not have to be equivalent to the directory.
35
+ #
36
+ def self.require_all_libs_relative_to( fname, dir = nil )
37
+ dir ||= ::File.basename(fname, '.*')
38
+ search_me = ::File.expand_path(
39
+ ::File.join(::File.dirname(fname), dir, '**', '*.rb'))
40
+
41
+ Dir.glob(search_me).sort.each {|rb| require rb}
42
+ end
43
+
44
+ end # module CheapImports
45
+
46
+ CheapImports.require_all_libs_relative_to(__FILE__)
47
+
48
+ begin
49
+ require "fastercsv"
50
+ rescue LoadError
51
+ raise "FasterCSV is required!" unless RUBY_VERSION >= "1.9.0"
52
+ end
@@ -0,0 +1,7 @@
1
+
2
+ require File.join(File.dirname(__FILE__), %w[spec_helper])
3
+
4
+ describe CheapImports do
5
+ end
6
+
7
+ # EOF
@@ -0,0 +1,20 @@
1
+ # class NewClass < ActiveRecord::Base
2
+ # validates_presence_of :code
3
+ # validates_uniqueness_of :code
4
+ # end
5
+ #
6
+ # context "is_recognized_hash_style" do
7
+ # setup do
8
+ # NewClass.connection.execute("CREATE TABLE new_classes (id INT AUTO_INCREMENT, code TEXT, name TEXT, PRIMARY KEY(id));")
9
+ # @obj = NewClass.create(:code => 'ABC', :name => 'Full Object Name')
10
+ # @hsh = {:newclass_code => "DEF", :newclass_name => "Another Object Name"}
11
+ # end
12
+ #
13
+ # # specify "should raise an error when called on a class with no recognize_hash_style method" do
14
+ # # lambda { NewClass.is_recognized_hash_style?(@hsh) }.should raise_error(RuntimeError, "NewClass does not have a recognize_hash_style method")
15
+ # # end
16
+ #
17
+ # teardown do
18
+ # NewClass.connection.execute("DROP TABLE new_classes;")
19
+ # end
20
+ # end
@@ -0,0 +1,53 @@
1
+ require File.join(File.dirname(__FILE__), '..', %w[spec_helper])
2
+
3
+ require "dm-core"
4
+ DataMapper.setup(:default, 'sqlite3::memory:')
5
+
6
+ class Cat
7
+ include DataMapper::Resource
8
+ property :id, Serial
9
+ property :name, String
10
+ property :age, Integer
11
+
12
+ include CheapImports
13
+ end
14
+
15
+ class Appointment
16
+ include DataMapper::Resource
17
+ property :id, Serial
18
+ property :cat_id, Integer
19
+ property :starts_at, DateTime
20
+
21
+ include CheapImports
22
+ imports :legacy_data => {
23
+ :starts_at => "APPOINTMENT START TIME",
24
+ :starts_at_format => "%d %b %Y %H:%M",
25
+ :cat_name => "ANIMAL IDENTIFIER"
26
+ }
27
+ end
28
+
29
+ DataMapper.auto_migrate!
30
+
31
+ describe Cat, "importing default" do
32
+ it "should import without any config" do
33
+ data = %{name\tage\nFluffy\t7}
34
+ imported_cats = Import.import(Cat, data)
35
+
36
+ cats_named_fluffy = Cat.all(:name => "Fluffy")
37
+
38
+ cats_named_fluffy.should have(1).items
39
+ cats_named_fluffy.first.age.should eql(7)
40
+ end
41
+ end
42
+
43
+ describe Appointment, "importing a specified config" do
44
+ it "should description" do
45
+ data = %{ANIMAL IDENTIFIER,APPOINTMENT START TIME\n}
46
+ data << %{Fluffy,5 Jan 2009 10:30\n}
47
+
48
+ appointments = Import.import(Appointment, data)
49
+ a = appointments.first
50
+
51
+ a.starts_at.should eql(DateTime.new(2009, 1, 5, 10, 30))
52
+ end
53
+ end
@@ -0,0 +1,16 @@
1
+
2
+ require File.expand_path(
3
+ File.join(File.dirname(__FILE__), %w[.. lib cheap_imports]))
4
+
5
+ Spec::Runner.configure do |config|
6
+ # == Mock Framework
7
+ #
8
+ # RSpec uses it's own mocking framework by default. If you prefer to
9
+ # use mocha, flexmock or RR, uncomment the appropriate line:
10
+ #
11
+ # config.mock_with :mocha
12
+ # config.mock_with :flexmock
13
+ # config.mock_with :rr
14
+ end
15
+
16
+ # EOF
data/tasks/ann.rake ADDED
@@ -0,0 +1,80 @@
1
+
2
+ begin
3
+ require 'bones/smtp_tls'
4
+ rescue LoadError
5
+ require 'net/smtp'
6
+ end
7
+ require 'time'
8
+
9
+ namespace :ann do
10
+
11
+ # A prerequisites task that all other tasks depend upon
12
+ task :prereqs
13
+
14
+ file PROJ.ann.file do
15
+ ann = PROJ.ann
16
+ puts "Generating #{ann.file}"
17
+ File.open(ann.file,'w') do |fd|
18
+ fd.puts("#{PROJ.name} version #{PROJ.version}")
19
+ fd.puts(" by #{Array(PROJ.authors).first}") if PROJ.authors
20
+ fd.puts(" #{PROJ.url}") if PROJ.url.valid?
21
+ fd.puts(" (the \"#{PROJ.release_name}\" release)") if PROJ.release_name
22
+ fd.puts
23
+ fd.puts("== DESCRIPTION")
24
+ fd.puts
25
+ fd.puts(PROJ.description)
26
+ fd.puts
27
+ fd.puts(PROJ.changes.sub(%r/^.*$/, '== CHANGES'))
28
+ fd.puts
29
+ ann.paragraphs.each do |p|
30
+ fd.puts "== #{p.upcase}"
31
+ fd.puts
32
+ fd.puts paragraphs_of(PROJ.readme_file, p).join("\n\n")
33
+ fd.puts
34
+ end
35
+ fd.puts ann.text if ann.text
36
+ end
37
+ end
38
+
39
+ desc "Create an announcement file"
40
+ task :announcement => ['ann:prereqs', PROJ.ann.file]
41
+
42
+ desc "Send an email announcement"
43
+ task :email => ['ann:prereqs', PROJ.ann.file] do
44
+ ann = PROJ.ann
45
+ from = ann.email[:from] || Array(PROJ.authors).first || PROJ.email
46
+ to = Array(ann.email[:to])
47
+
48
+ ### build a mail header for RFC 822
49
+ rfc822msg = "From: #{from}\n"
50
+ rfc822msg << "To: #{to.join(',')}\n"
51
+ rfc822msg << "Subject: [ANN] #{PROJ.name} #{PROJ.version}"
52
+ rfc822msg << " (#{PROJ.release_name})" if PROJ.release_name
53
+ rfc822msg << "\n"
54
+ rfc822msg << "Date: #{Time.new.rfc822}\n"
55
+ rfc822msg << "Message-Id: "
56
+ rfc822msg << "<#{"%.8f" % Time.now.to_f}@#{ann.email[:domain]}>\n\n"
57
+ rfc822msg << File.read(ann.file)
58
+
59
+ params = [:server, :port, :domain, :acct, :passwd, :authtype].map do |key|
60
+ ann.email[key]
61
+ end
62
+
63
+ params[3] = PROJ.email if params[3].nil?
64
+
65
+ if params[4].nil?
66
+ STDOUT.write "Please enter your e-mail password (#{params[3]}): "
67
+ params[4] = STDIN.gets.chomp
68
+ end
69
+
70
+ ### send email
71
+ Net::SMTP.start(*params) {|smtp| smtp.sendmail(rfc822msg, from, to)}
72
+ end
73
+ end # namespace :ann
74
+
75
+ desc 'Alias to ann:announcement'
76
+ task :ann => 'ann:announcement'
77
+
78
+ CLOBBER << PROJ.ann.file
79
+
80
+ # EOF