cheap_imports 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.bzr/README +3 -0
- data/.bzr/branch/branch.conf +2 -0
- data/.bzr/branch/format +1 -0
- data/.bzr/branch/last-revision +1 -0
- data/.bzr/branch/tags +0 -0
- data/.bzr/branch-format +1 -0
- data/.bzr/checkout/conflicts +1 -0
- data/.bzr/checkout/dirstate +0 -0
- data/.bzr/checkout/format +1 -0
- data/.bzr/checkout/merge-hashes +6 -0
- data/.bzr/repository/format +1 -0
- data/.bzr/repository/indices/3b0e60c570a8cfc21b7d22db489579e7.iix +0 -0
- data/.bzr/repository/indices/3b0e60c570a8cfc21b7d22db489579e7.rix +0 -0
- data/.bzr/repository/indices/3b0e60c570a8cfc21b7d22db489579e7.six +5 -0
- data/.bzr/repository/indices/3b0e60c570a8cfc21b7d22db489579e7.tix +0 -0
- data/.bzr/repository/indices/d139719d6bdd037de4ec3718dcf1c0c4.iix +0 -0
- data/.bzr/repository/indices/d139719d6bdd037de4ec3718dcf1c0c4.rix +0 -0
- data/.bzr/repository/indices/d139719d6bdd037de4ec3718dcf1c0c4.six +5 -0
- data/.bzr/repository/indices/d139719d6bdd037de4ec3718dcf1c0c4.tix +0 -0
- data/.bzr/repository/indices/d5dffeca0774033e54854302570cb330.iix +0 -0
- data/.bzr/repository/indices/d5dffeca0774033e54854302570cb330.rix +0 -0
- data/.bzr/repository/indices/d5dffeca0774033e54854302570cb330.six +5 -0
- data/.bzr/repository/indices/d5dffeca0774033e54854302570cb330.tix +0 -0
- data/.bzr/repository/indices/e5571628132ac072d270e0a249095c14.iix +0 -0
- data/.bzr/repository/indices/e5571628132ac072d270e0a249095c14.rix +0 -0
- data/.bzr/repository/indices/e5571628132ac072d270e0a249095c14.six +5 -0
- data/.bzr/repository/indices/e5571628132ac072d270e0a249095c14.tix +0 -0
- data/.bzr/repository/pack-names +0 -0
- data/.bzr/repository/packs/3b0e60c570a8cfc21b7d22db489579e7.pack +0 -0
- data/.bzr/repository/packs/d139719d6bdd037de4ec3718dcf1c0c4.pack +0 -0
- data/.bzr/repository/packs/d5dffeca0774033e54854302570cb330.pack +0 -0
- data/.bzr/repository/packs/e5571628132ac072d270e0a249095c14.pack +0 -0
- data/.bzrignore +2 -0
- data/History.txt +4 -0
- data/README.txt +47 -0
- data/Rakefile +38 -0
- data/bin/cheap_imports +8 -0
- data/lib/cheap_imports/cheap_imports.rb +154 -0
- data/lib/cheap_imports/import.rb +64 -0
- data/lib/cheap_imports.rb +52 -0
- data/spec/cheap_imports_spec.rb +7 -0
- data/spec/orms/active_record_spec.rb +20 -0
- data/spec/orms/datamapper_spec.rb +53 -0
- data/spec/spec_helper.rb +16 -0
- data/tasks/ann.rake +80 -0
- data/tasks/bones.rake +20 -0
- data/tasks/gem.rake +201 -0
- data/tasks/git.rake +40 -0
- data/tasks/notes.rake +27 -0
- data/tasks/post_load.rake +34 -0
- data/tasks/rdoc.rake +51 -0
- data/tasks/rubyforge.rake +55 -0
- data/tasks/setup.rb +292 -0
- data/tasks/spec.rake +54 -0
- data/tasks/svn.rake +47 -0
- data/tasks/test.rake +40 -0
- data/tasks/zentest.rake +36 -0
- metadata +123 -0
data/.bzr/README
ADDED
data/.bzr/branch/format
ADDED
@@ -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
|
data/.bzr/branch-format
ADDED
@@ -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 @@
|
|
1
|
+
Bazaar pack repository format 1 (needs bzr 0.92)
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
data/.bzrignore
ADDED
data/History.txt
ADDED
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,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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|