my_banner 1.0.0

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