my_banner 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,58 @@
1
+ module MyBanner
2
+ class Section
3
+
4
+ attr_accessor :metadata, :course, :section, :title, :instructor, :weekdays, :location, :time_zone, :term_start, :term_end, :start_time, :end_time
5
+
6
+ def initialize(metadata={})
7
+ @metadata = metadata
8
+
9
+ @course = metadata.try(:[], :course)
10
+ @section = metadata.try(:[], :section)
11
+ @title = metadata.try(:[], :title)
12
+
13
+ schedule_info = metadata.try(:[], :scheduled_meeting_times)
14
+ @instructor = schedule_info.try(:[], :instructors).try(:first)
15
+ @weekdays = schedule_info.try(:[], :days).try(:each_char).try(:to_a)
16
+ @location = schedule_info.try(:[], :where)
17
+ @time_zone = "America/New_York" #todo: lookup or customize
18
+
19
+ term_info = schedule_info.try(:[], :date_range) #todo: validate string splits into two-member array
20
+ @term_start = Date.parse( term_info.try(:split, " - ").try(:first) ) rescue nil
21
+ @term_end = Date.parse( term_info.try(:split, " - ").try(:last) ) rescue nil
22
+
23
+ time_info = schedule_info.try(:[], :time) #todo: validate string splits into two-member array
24
+ @start_time = time_info.try(:split, " - ").try(:first)
25
+ @end_time = time_info.try(:split, " - ").try(:last)
26
+ end
27
+
28
+ def abbreviation
29
+ "#{course}-#{section}" if course && section
30
+ end
31
+ alias_method :abbrev, :abbreviation
32
+
33
+ # @note does not exclude meetings cancelled due to holidays
34
+ # @todo cross-reference the "Holidays in the United States" calendar events to make a best guess at which classes to exclude
35
+ def meetings
36
+ meeting_dates.map do |date|
37
+ start_at = DateTime.parse("#{date} #{start_time}")
38
+ end_at = DateTime.parse("#{date} #{end_time}")
39
+ Meeting.new(start_at: start_at, end_at: end_at)
40
+ end
41
+ end
42
+
43
+ def meeting_dates
44
+ term_date_range.select { |date| weekday_numbers.include?(date.wday) }
45
+ end
46
+
47
+ def term_date_range
48
+ term_start..term_end
49
+ end
50
+
51
+ def weekday_numbers
52
+ weekdays.map { |char| WEEKDAYS_MAP[char.to_sym] }
53
+ end
54
+
55
+ WEEKDAYS_MAP = { M: 1, T: 2, W: 3, R: 4, F: 5, S: 6, N: 0 } # S and N assumed but not yet verified
56
+
57
+ end
58
+ end
@@ -0,0 +1,27 @@
1
+ module MyBanner
2
+ class Section::Meeting
3
+
4
+ attr_reader :start_at, :end_at
5
+
6
+ def initialize(options={})
7
+ @start_at = options[:start_at]
8
+ @end_at = options[:end_at]
9
+ validate_datetimes
10
+ end
11
+
12
+ def to_s
13
+ "#{start_at.try(:strftime, '%Y-%m-%d %H:%M')} ... #{end_at.try(:strftime, '%Y-%m-%d %H:%M')}"
14
+ end
15
+
16
+ def to_h
17
+ { start_at: start_at, end_at: end_at }
18
+ end
19
+
20
+ private
21
+
22
+ def validate_datetimes
23
+ raise "expecting datetimes" unless start_at && end_at && start_at.is_a?(DateTime) && end_at.is_a?(DateTime)
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,16 @@
1
+ require "google/apis/sheets_v4"
2
+
3
+ module MyBanner
4
+ class SpreadsheetAuthorization < GoogleAuthorization
5
+
6
+ AUTH_SCOPE = Google::Apis::SheetsV4::AUTH_SPREADSHEETS #> "https://www.googleapis.com/auth/spreadsheets"
7
+
8
+ def initialize(options={})
9
+ options[:scope] ||= AUTH_SCOPE
10
+ options[:credentials_filepath] ||= "auth/spreadsheet_credentials.json"
11
+ options[:token_filepath] ||= "auth/spreadsheet_token.yaml"
12
+ super(options)
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ require "google/apis/sheets_v4"
2
+
3
+ module MyBanner
4
+ class SpreadsheetClient < Google::Apis::SheetsV4::SheetsService
5
+
6
+ # @param authorization [SpreadsheetAuthorization]
7
+ def initialize(authorization=nil)
8
+ super()
9
+ self.client_options.application_name = "MyBanner Spreadsheet Client"
10
+ self.client_options.application_version = VERSION
11
+ authorization ||= SpreadsheetAuthorization.new
12
+ self.authorization = authorization.stored_credentials || authorization.user_provided_credentials
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,75 @@
1
+
2
+
3
+ require "google/apis/sheets_v4"
4
+
5
+ module MyBanner
6
+ class SpreadsheetService
7
+ attr_reader :spreadsheet_title #, :sheet_name #, :sheet_values
8
+
9
+ def initialize(spreadsheet_title)
10
+ @spreadsheet_title = spreadsheet_title
11
+ #@sheet_name = "roster-todo" # todo: "roster-#{Date.today.to_s}"
12
+ #@sheet_values = [["email", "registration_number", "net_id"]] + 27.times.map { |i| ["student#{i+1}@todo.edu", i+1, "student#{i+1}"] } # todo: get from roster
13
+ end
14
+
15
+ #def execute
16
+ # spreadsheet
17
+ # #update_values
18
+ #end
19
+
20
+ # # @return Google::Apis::SheetsV4::UpdateValuesResponse
21
+ # def update_values
22
+ # range = "#{sheet_name}!A1"
23
+ # vals = Google::Apis::SheetsV4::ValueRange.new(values: sheet_values)
24
+ # client.update_spreadsheet_value(spreadsheet.spreadsheet_id, range, vals, value_input_option: "RAW")
25
+ # end
26
+
27
+ # @return Google::Apis::SheetsV4::Spreadsheet
28
+ def spreadsheet
29
+ @spreadsheet ||= if spreadsheet_file
30
+ client.get_spreadsheet(spreadsheet_file.id)
31
+ else
32
+ client.create_spreadsheet(new_spreadsheet)
33
+ end
34
+ end
35
+
36
+ def client
37
+ @client ||= SpreadsheetClient.new
38
+ end
39
+
40
+ def new_spreadsheet
41
+ #roster_sheet = Google::Apis::SheetsV4::Sheet.new(properties: {title: sheet_name, sheet_type: "GRID"})
42
+ #new_spreadsheet_attrs = { properties: {title: spreadsheet_title}, sheets: [roster_sheet] }
43
+ new_spreadsheet_attrs = { properties: { title: spreadsheet_title } }
44
+ Google::Apis::SheetsV4::Spreadsheet.new(new_spreadsheet_attrs)
45
+ end
46
+
47
+ #
48
+ # DRIVE FILES
49
+ #
50
+
51
+ # @return Google::Apis::DriveV3::File
52
+ def spreadsheet_file
53
+ file_list.files.find { |f| f.name == spreadsheet_title }
54
+ end
55
+
56
+ # @return Google::Apis::DriveV3::FileList
57
+ def file_list
58
+ @file_list ||= begin
59
+ request_options = {q: "mimeType='application/vnd.google-apps.spreadsheet'", order_by: "createdTime desc", page_size: 25}
60
+ drive_client.list_files(request_options)
61
+ end
62
+ end
63
+
64
+ def drive_client
65
+ @drive_client ||= DriveClient.new
66
+ end
67
+
68
+ private
69
+
70
+ def delete_spreadsheet # DANGER!
71
+ drive_client.delete_file(spreadsheet.spreadsheet_id)
72
+ end
73
+
74
+ end
75
+ end
@@ -0,0 +1,3 @@
1
+ module MyBanner
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,120 @@
1
+ require_relative "../my_banner"
2
+
3
+ # namespace :my_banner do
4
+
5
+ task :parse_schedule do
6
+ page = MyBanner::Schedule.new
7
+ sections = page.sections
8
+ puts "-----------------------"
9
+ puts "SECTIONS: #{sections.count}"
10
+ puts "-----------------------"
11
+ sections.each do |section|
12
+ puts ""
13
+ puts "#{section.title.upcase}:"
14
+ puts ""
15
+ pp section.metadata
16
+ puts ""
17
+ end
18
+ end
19
+
20
+ # @example bundle exec rake create_calendars
21
+ task :create_calendars do
22
+ page = MyBanner::Schedule.new
23
+ sections = page.sections
24
+ puts "-----------------------"
25
+ puts "SECTIONS: #{sections.count}"
26
+ puts "-----------------------"
27
+ sections.each do |section|
28
+ puts "\nSECTION: #{section.abbreviation} (#{section.title})"
29
+ service = MyBanner::CalendarService.new(section)
30
+
31
+ calendar = service.calendar
32
+ puts "\nCALENDAR: #{calendar.summary} (#{calendar.id})"
33
+
34
+ meetings = section.meetings
35
+ puts "\nMEETINGS: #{meetings.count}"
36
+ meetings.each do |meeting|
37
+ puts " + #{meeting.to_s}"
38
+ end
39
+
40
+ events = service.events
41
+ puts "\nFUTURE EVENTS: #{events.count}"
42
+ events.each do |event|
43
+ event_start = event.start.date_time.try(:strftime, "%Y-%m-%d %H:%M") || event.start.date.try(:strftime, "%Y-%m-%d")
44
+ event_end = event.end.date_time.try(:strftime, "%Y-%m-%d %H:%M") || event.end.date.try(:strftime, "%Y-%m-%d")
45
+ puts " + #{event_start} ... #{event_end} (#{event.class})"
46
+ end
47
+
48
+ service.execute
49
+ puts "\n-----------------------"
50
+ end
51
+ end
52
+
53
+ # @example bundle exec rake clear_calendars
54
+ task :clear_calendars do
55
+ page = MyBanner::Schedule.new
56
+ sections = page.sections
57
+ sections.each do |section|
58
+ service = MyBanner::CalendarService.new(section)
59
+ service.send(:delete_events)
60
+ end
61
+ end
62
+
63
+ # @example bundle exec rake create_spreadsheets
64
+ task :create_spreadsheets do
65
+ page = MyBanner::Schedule.new
66
+ sections = page.sections
67
+
68
+ puts "-----------------------"
69
+ puts "SECTIONS: #{sections.count}"
70
+ puts "-----------------------"
71
+
72
+ sections.each do |section|
73
+
74
+ puts "\nSECTION: #{section.abbreviation} (#{section.title.upcase})\n"
75
+ course = section.course
76
+ start_month = section.term_start.strftime("%Y%m")
77
+ spreadsheet_title = "Gradebook - #{course} (#{start_month})"
78
+
79
+ service = MyBanner::SpreadsheetService.new(spreadsheet_title)
80
+ spreadsheet = service.spreadsheet
81
+ puts "SPREADSHEET: #{spreadsheet.properties.title.upcase} (#{spreadsheet.spreadsheet_id})"
82
+ puts "SHEETS:"
83
+ spreadsheet.sheets.each do |sheet|
84
+ grid_props = sheet.properties.grid_properties
85
+ puts " + #{sheet.properties.title.upcase} (#{grid_props.row_count} rows x #{grid_props.column_count} cols)"
86
+ end
87
+
88
+ #puts "DATA:"
89
+ #setter_response = service.update_values
90
+ #getter_response = service.client.get_spreadsheet_values(spreadsheet.spreadsheet_id, setter_response.updated_range)
91
+ #getter_response.values.each do |row|
92
+ # puts " #{row.join(" | ")}"
93
+ #end
94
+ end
95
+ end
96
+
97
+ # @example bundle exec rake delete_spreadsheets
98
+ task :delete_spreadsheets do
99
+ page = MyBanner::Schedule.new
100
+ sections = page.sections
101
+
102
+ puts "-----------------------"
103
+ puts "SECTIONS: #{sections.count}"
104
+ puts "-----------------------"
105
+
106
+ sections.each do |section|
107
+ puts "\nSECTION: #{section.abbreviation} (#{section.title.upcase})\n"
108
+ course = section.course
109
+ start_month = section.term_start.strftime("%Y%m")
110
+ spreadsheet_title = "Gradebook - #{course} (#{start_month})"
111
+ service = MyBanner::SpreadsheetService.new(spreadsheet_title)
112
+
113
+ spreadsheet = service.spreadsheet
114
+ puts "SPREADSHEET: #{spreadsheet.properties.title.upcase} (#{spreadsheet.spreadsheet_id})"
115
+ puts "DELETING..."
116
+ service.send(:delete_spreadsheet) # temporary, helps to test file creation
117
+ end
118
+ end
119
+
120
+ #end
@@ -0,0 +1,36 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "my_banner/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "my_banner"
8
+ spec.version = MyBanner::VERSION
9
+ spec.authors = ["MJ Rossetti"]
10
+ spec.email = ["prof.mj.rossetti@gmail.com", "mjr300@georgetown.edu"]
11
+
12
+ spec.summary = "This program processes detailed schedule information from your school's Ellucian Banner information system to generate Google Calendar events and/or Google Sheets gradebook files for each of your scheduled classes."
13
+ #spec.description = %q{TODO: Write a longer description or delete this line.}
14
+ spec.homepage = "https://github.com/prof-rossetti/my-banner-rb"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_dependency "activesupport", "~> 5.2"
25
+ spec.add_dependency "google-api-client", "~> 0.27"
26
+ spec.add_dependency "nokogiri", '~> 1.9'
27
+ spec.add_dependency "pry" # really just for development purposes
28
+
29
+ spec.add_development_dependency "bundler", "~> 1.16"
30
+ spec.add_development_dependency "factory_bot", "~> 4.11"
31
+ spec.add_development_dependency "rake", "~> 10.0"
32
+ spec.add_development_dependency "rspec", "~> 3.0"
33
+ spec.add_development_dependency "webmock", "~> 3.4"
34
+ spec.add_development_dependency "simplecov", "~> 0.16"
35
+ spec.add_development_dependency "simplecov-console", "~> 0.4"
36
+ end
@@ -0,0 +1,2 @@
1
+ *
2
+ !.gitignore
metadata ADDED
@@ -0,0 +1,234 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: my_banner
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - MJ Rossetti
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-12-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: google-api-client
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.27'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.27'
41
+ - !ruby/object:Gem::Dependency
42
+ name: nokogiri
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.9'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.9'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.16'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.16'
83
+ - !ruby/object:Gem::Dependency
84
+ name: factory_bot
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '4.11'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '4.11'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '10.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '10.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '3.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '3.0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: webmock
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '3.4'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '3.4'
139
+ - !ruby/object:Gem::Dependency
140
+ name: simplecov
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '0.16'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '0.16'
153
+ - !ruby/object:Gem::Dependency
154
+ name: simplecov-console
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '0.4'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '0.4'
167
+ description:
168
+ email:
169
+ - prof.mj.rossetti@gmail.com
170
+ - mjr300@georgetown.edu
171
+ executables: []
172
+ extensions: []
173
+ extra_rdoc_files: []
174
+ files:
175
+ - ".codeclimate.yml"
176
+ - ".gitignore"
177
+ - ".rspec"
178
+ - ".ruby_version"
179
+ - ".travis.yml"
180
+ - CONTRIBUTING.md
181
+ - CREDITS.md
182
+ - Gemfile
183
+ - Gemfile.lock
184
+ - LICENSE.md
185
+ - README.md
186
+ - Rakefile
187
+ - auth/.gitignore
188
+ - bin/console
189
+ - bin/setup
190
+ - lib/my_banner.rb
191
+ - lib/my_banner/calendar_authorization.rb
192
+ - lib/my_banner/calendar_client.rb
193
+ - lib/my_banner/calendar_service.rb
194
+ - lib/my_banner/drive_authorization.rb
195
+ - lib/my_banner/drive_client.rb
196
+ - lib/my_banner/google_authorization.rb
197
+ - lib/my_banner/schedule.rb
198
+ - lib/my_banner/schedule/tableset.rb
199
+ - lib/my_banner/section.rb
200
+ - lib/my_banner/section/meeting.rb
201
+ - lib/my_banner/spreadsheet_authorization.rb
202
+ - lib/my_banner/spreadsheet_client.rb
203
+ - lib/my_banner/spreadsheet_service.rb
204
+ - lib/my_banner/version.rb
205
+ - lib/tasks/my_banner.rake
206
+ - my_banner.gemspec
207
+ - pages/.gitignore
208
+ homepage: https://github.com/prof-rossetti/my-banner-rb
209
+ licenses:
210
+ - MIT
211
+ metadata: {}
212
+ post_install_message:
213
+ rdoc_options: []
214
+ require_paths:
215
+ - lib
216
+ required_ruby_version: !ruby/object:Gem::Requirement
217
+ requirements:
218
+ - - ">="
219
+ - !ruby/object:Gem::Version
220
+ version: '0'
221
+ required_rubygems_version: !ruby/object:Gem::Requirement
222
+ requirements:
223
+ - - ">="
224
+ - !ruby/object:Gem::Version
225
+ version: '0'
226
+ requirements: []
227
+ rubyforge_project:
228
+ rubygems_version: 2.7.3
229
+ signing_key:
230
+ specification_version: 4
231
+ summary: This program processes detailed schedule information from your school's Ellucian
232
+ Banner information system to generate Google Calendar events and/or Google Sheets
233
+ gradebook files for each of your scheduled classes.
234
+ test_files: []