torque 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,166 @@
1
+ require_relative 'file_system'
2
+
3
+ class Torque
4
+
5
+ ##
6
+ # Determines the date range for a release notes generation
7
+ class DateSettings
8
+
9
+ ##
10
+ # @param options A hash of the command line options used to run Torque
11
+ # @param last_run_path The path to the lastRun file
12
+ # @param fs An instance of the FileSystem class
13
+ def initialize(options, last_run_path, fs)
14
+ @options = options
15
+ @last_run_path = last_run_path
16
+ @fs = fs
17
+
18
+ @current_date = Date.today
19
+ @beginning_of_time = Date.new(1900, 1, 1)
20
+ @date_format = "%Y-%m-%d"
21
+ end
22
+
23
+ ##
24
+ # Generates the accept_from and accept_to dates if they don't already exist
25
+ #
26
+ # Returns [accept_from, accept_to]
27
+ def get_dates
28
+ generate_dates if !@accept_from || !@accept_to
29
+ return @accept_from, @accept_to
30
+ end
31
+
32
+ ##
33
+ # Returns true if the date range was set manually via command line, else false
34
+ def custom_date_range?
35
+ generate_dates if !@custom_date_range
36
+ return @custom_date_range
37
+ end
38
+
39
+
40
+ private
41
+
42
+ # Generates values for @accept_from and @accept_to
43
+ # Returns [@accept_from, @accept_to]
44
+ def generate_dates
45
+
46
+ # If custom date settings exist, takes dates from there
47
+ if @options[:accept_from] || @options[:accept_to]
48
+ get_dates_from_custom_range
49
+ @custom_date_range = true
50
+
51
+ # If not:
52
+ # @accept_to = today
53
+ # @accept_from is calculated from the dates of previous generations
54
+ else
55
+ get_dates_from_default
56
+ @custom_date_range = false
57
+ end
58
+
59
+ return @accept_from, @accept_to
60
+
61
+ end
62
+
63
+ # Handles date values that were set manually in the environment
64
+ # Returns [@accept_from, @accept_to]
65
+ def get_dates_from_custom_range
66
+ begin
67
+ @accept_from = Date.parse(@options[:accept_from].nil? ? "1900-01-01" : @options[:accept_from])
68
+ rescue ArgumentError
69
+ raise ArgumentError.new("Date #{@options[:accept_from]} could not be parsed")
70
+ end
71
+
72
+ begin
73
+ @accept_to = (@options[:accept_to].nil? ? @current_date : Date.parse(@options[:accept_to]))
74
+ rescue ArgumentError
75
+ raise ArgumentError.new("Date #{@options[:accept_to]} could not be parsed")
76
+ end
77
+
78
+ if @accept_from > @accept_to
79
+ raise ArgumentError.new("Date ACCEPT_FROM (#{@accept_from}) occurs after ACCEPT_TO (#{@accept_to})")
80
+ end
81
+
82
+ return @accept_from, @accept_to
83
+ end
84
+
85
+ # dates: An array of the dates contained by .lastRun
86
+ #
87
+ # Reads from the .last-run file for the project to figure out when notes were last generated
88
+ # Returns [@accept_from, @accept_to]
89
+ def get_dates_from_default
90
+
91
+ dates = update_last_run_file
92
+
93
+ @accept_to = @current_date
94
+ @accept_from = dates[1] || @beginning_of_time
95
+
96
+ return @accept_from, @accept_to
97
+ end
98
+
99
+ # Updates .last-run on the filesystem, returning the dates it contains after the update
100
+ def update_last_run_file
101
+
102
+ # Structure of last-run file:
103
+ #
104
+ # <Most recent generation date>
105
+ # <Second most recent generation date>
106
+
107
+ # Creates the .last-run if none exists
108
+ if !@fs.path_exist? @last_run_path
109
+ @fs.file_create(@last_run_path)
110
+ end
111
+
112
+ # Generates and writes the new file contents
113
+ new_file_str, dates = update_last_run_file_string(@fs.file_read(@last_run_path))
114
+ @fs.file_write(@last_run_path, new_file_str)
115
+
116
+ # Returns the new dates
117
+ dates
118
+
119
+ end
120
+
121
+ # Takes in a string with the contents of the last-run file, outputs a string with its new contents
122
+ def update_last_run_file_string(file_str)
123
+
124
+ # Parses the dates in .last-run
125
+ date_strings = []
126
+ file_str.each_line do |date_str|
127
+ date_strings << date_str
128
+ end
129
+ dates = date_strings.map do |date_str|
130
+ Date.strptime(date_str, @date_format)
131
+ end
132
+
133
+ #Generates new dates
134
+ new_dates = []
135
+
136
+ if dates.length == 0
137
+ new_dates = [@current_date]
138
+
139
+ elsif dates.length == 1
140
+ if dates[0] == @current_date
141
+ new_dates = [dates[0]]
142
+ else
143
+ new_dates = [@current_date, dates[0]]
144
+ end
145
+
146
+ else
147
+ if dates[0] == @current_date
148
+ new_dates = [dates[0], dates[1]]
149
+ else
150
+ new_dates = [@current_date, dates[0]]
151
+ end
152
+ end
153
+
154
+ # Generates a new file string
155
+ new_file_str = ""
156
+ new_dates.each do
157
+ |date|
158
+ new_file_str += "#{date.strftime(@date_format)}\n"
159
+ end
160
+
161
+ # Returns [new_file_str, new_dates]
162
+ return new_file_str, new_dates
163
+ end
164
+
165
+ end
166
+ end
@@ -0,0 +1,10 @@
1
+ require_relative 'pivotal_api_error'
2
+
3
+ class Torque
4
+
5
+ ##
6
+ # Error to be thrown if a Pivotal Tracker project ID is invalid
7
+ class InvalidProjectError < PivotalAPIError
8
+ end
9
+
10
+ end
@@ -0,0 +1,10 @@
1
+ require_relative 'pivotal_api_error'
2
+
3
+ class Torque
4
+
5
+ ##
6
+ # Error to be thrown if a Pivotal Tracker API token is invalid
7
+ class InvalidTokenError < PivotalAPIError
8
+ end
9
+
10
+ end
@@ -0,0 +1,7 @@
1
+ class Torque
2
+
3
+ ##
4
+ # Error to throw if the output directory is missing
5
+ class MissingOutputDirectoryError < StandardError
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ class Torque
2
+
3
+ ##
4
+ # Error to be thrown if a Pivotal Tracker project ID cannot be found
5
+ class MissingProjectError < StandardError
6
+ end
7
+
8
+ end
@@ -0,0 +1,8 @@
1
+ class Torque
2
+
3
+ ##
4
+ # Error to be thrown if a Pivotal Tracker API token cannot be found
5
+ class MissingTokenError < StandardError
6
+ end
7
+
8
+ end
@@ -0,0 +1,8 @@
1
+ class Torque
2
+
3
+ ##
4
+ # Error to be thrown if no .torqueinfo.yaml file can be found
5
+ class MissingTorqueInfoFileError < StandardError
6
+ end
7
+
8
+ end
@@ -0,0 +1,8 @@
1
+ class Torque
2
+
3
+ ##
4
+ # Catch-all class for errors when making requests to the Pivotal Tracker API
5
+ class PivotalAPIError < StandardError
6
+ end
7
+
8
+ end
@@ -0,0 +1,70 @@
1
+ require 'fileutils'
2
+ require 'pathname'
3
+
4
+ class Torque
5
+
6
+ ##
7
+ # Creates a (limited) interface through which Torque can interact with the local file system
8
+ #
9
+ # Supports:
10
+ #
11
+ # * File creation, reading, line-by-line iteration, and overwriting
12
+ #
13
+ # * Directory creation
14
+ #
15
+ # * Pathname checking
16
+ #
17
+ class FileSystem
18
+
19
+ def initialize
20
+ # Do nothing. The file system's properties are automatically global
21
+ end
22
+
23
+ ##
24
+ # Creates a file, optionally overwriting an existing file to do so
25
+ def file_create(filename, over=false)
26
+ if !path_exist? filename || over
27
+ File.open(filename, "w")
28
+ else
29
+ raise IOError.new "File #{filename} already exists"
30
+ end
31
+ end
32
+
33
+ ##
34
+ # Returns an iterator over each line of a file
35
+ def file_each_line(filename)
36
+ File.open(filename, "r").each_line
37
+ end
38
+
39
+ ##
40
+ # Returns the contents of a file
41
+ def file_read(filename)
42
+ File.read(filename)
43
+ end
44
+
45
+ ##
46
+ # Writes a string to file
47
+ def file_write(filename, string)
48
+ File.write(filename, string)
49
+ end
50
+
51
+ ##
52
+ # Creates a directory
53
+ def mkdir(dirname)
54
+ FileUtils.mkdir(dirname)
55
+ end
56
+
57
+ ##
58
+ # Creates a directory, creating intermediate directories as needed
59
+ def mkdir_p(dirname)
60
+ FileUtils.mkdir_p(dirname)
61
+ end
62
+
63
+ ##
64
+ # Returns true if a pathname exists, else false
65
+ def path_exist?(pathname)
66
+ Pathname.new(pathname).exist?
67
+ end
68
+
69
+ end
70
+ end
@@ -0,0 +1,62 @@
1
+ require 'mail'
2
+
3
+ class Torque
4
+
5
+ ##
6
+ # Handles sending the finished release notes document to a list of emails after Torque generates it
7
+ class Mailer
8
+
9
+ ##
10
+ # @param email An email address
11
+ # @param password A password that matches the email address given
12
+ #
13
+ # Creates a Mailer that will send emails from the email address/password combo given
14
+ def initialize(email, password)
15
+ @email = email
16
+ @password = password
17
+
18
+ options = { :address => "smtp.gmail.com",
19
+ :port => 587,
20
+ :domain => 'gmail.com',
21
+ :user_name => @email,
22
+ :password => @password,
23
+ :authentication => 'plain',
24
+ :enable_starttls_auto => true }
25
+
26
+ Mail.defaults do
27
+ delivery_method :smtp, options
28
+ end
29
+ end
30
+
31
+ # ##
32
+ # # Returns true if the email and password combination is valid, else false
33
+ # def verify
34
+ # end
35
+
36
+ # @param notes_string The release notes file in string form
37
+ # @param subject_line The subject line to use in the email
38
+ # @param address_list A comma-separated list of email addresses to which to send the notes
39
+ #
40
+ # Sends notes_string as an email with subject_line from an arbitrary email address to everyone on address_list
41
+ def send_notes(notes_string, subject_line, address_list)
42
+
43
+ mail = Mail::Message.new
44
+
45
+ mail.charset = "UTF-8"
46
+ mail.to address_list
47
+ mail.from @email
48
+ mail.subject subject_line
49
+ mail.body notes_string
50
+
51
+ begin
52
+ mail.deliver
53
+ rescue Net::SMTPAuthenticationError
54
+ # TODO Remove this output. Should fail silently and be replaced by an independent call to :verify
55
+ puts "Username and password not accepted by Gmail. Check your username and password or try using a Gmail " \
56
+ "account"
57
+ end
58
+
59
+ end
60
+
61
+ end
62
+ end
@@ -0,0 +1,98 @@
1
+ require 'net/http'
2
+ require_relative 'error/invalid_project_error'
3
+ require_relative 'error/invalid_token_error'
4
+ require_relative 'error/pivotal_api_error'
5
+
6
+ class Torque
7
+ class Pivotal
8
+
9
+ ##
10
+ # @param token A Pivotal Tracker api token
11
+ def initialize(token)
12
+ @token = token
13
+ end
14
+
15
+ ##
16
+ # Returns true if a connection to www.pivotaltracker.com exists, else false
17
+ def self.connection?
18
+ begin
19
+ TCPSocket.new "www.pivotaltracker.com", 80
20
+ true
21
+ rescue SocketError
22
+ false
23
+ end
24
+ end
25
+
26
+ ##
27
+ # Returns true if the token supplied is a valid token, else false
28
+ def check_token
29
+ begin
30
+ get_project_data
31
+ true
32
+ rescue InvalidTokenError
33
+ false
34
+ end
35
+ end
36
+
37
+ ##
38
+ # Sends a request through the Pivotal Tracker API
39
+ #
40
+ # Returns a string of html data from Pivotal Tracker with data on the stories for the given project
41
+ def get_project_stories(project)
42
+
43
+ # Polls story data from pivotal tracker
44
+ host="pivotaltracker.com"
45
+ port=80
46
+ url="http://www.pivotaltracker.com/services/v3/projects/#{project}/stories"
47
+ headers={'X-TrackerToken'=>@token}
48
+
49
+ response=Net::HTTP.new(host, port).get(url, headers)
50
+
51
+ # Handles network errors
52
+ if response.code == "401"
53
+ raise InvalidTokenError.new "The Pivotal Tracker API token supplied is not valid for project #{project}"
54
+
55
+ elsif response.code == "500"
56
+ raise InvalidProjectError.new "The Pivotal Tracker project ID supplied, #{project}, is invalid"
57
+
58
+ elsif response.code != "200"
59
+ raise PivotalAPIError.new(
60
+ "The Pivotal Tracker API responded with an unexpected error code: #{response.code}. Check your " \
61
+ + "project ID, API token, and/or internet connection"
62
+ )
63
+
64
+ end
65
+
66
+ response.body
67
+ end
68
+
69
+ ##
70
+ # Sends a request through the Pivotal Tracker API
71
+ #
72
+ # Returns a string of html data from Pivotal Tracker with data on each of a user's projects
73
+ def get_project_data
74
+
75
+ host="pivotaltracker.com"
76
+ port=80
77
+ url="http://www.pivotaltracker.com/services/v3/projects"
78
+ headers={'X-TrackerToken'=>@token}
79
+
80
+ response=Net::HTTP.new(host, port).get(url, headers)
81
+
82
+ # Handles network errors
83
+ if response.code == "401"
84
+ raise InvalidTokenError.new "The Pivotal Tracker API token supplied is not valid"
85
+
86
+ elsif response.code != "200"
87
+ raise PivotalAPIError.new(
88
+ "The Pivotal Tracker API responded with an unexpected error code: #{response.code}. Check your " \
89
+ "API token, and/or internet connection"
90
+ )
91
+ end
92
+
93
+ response.body
94
+ end
95
+
96
+
97
+ end
98
+ end