martlet 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 35a2b85f2c458fe8574f563eda6789bafe0944c3
4
+ data.tar.gz: 56f1b0405e9918adc07b75a57523d9fc825c0856
5
+ SHA512:
6
+ metadata.gz: 568ec7e3a3347e0273d48e03158d0906217eacf942bd46dc2cd6e4d6ec318b285f3097d7a65112cb07143a75ed49a471d1330e14e5a1a0fd99aab9f5ee28a59b
7
+ data.tar.gz: c3ac26d611c43935969620f116344c7ed0112fa4c7381bb7a6d5c0a312170b608c900d4d097ab8ed6d9a8d555e5033e8202ae2607cd5f717837fd885affbd309
data/bin/martlet CHANGED
@@ -2,61 +2,6 @@
2
2
  $:.unshift File.join File.dirname(__FILE__), '..', 'lib'
3
3
  require 'rubygems'
4
4
  require 'martlet'
5
- require 'yaml'
6
- require 'io/console'
5
+ require 'martlet/cli'
7
6
 
8
- def usage
9
- puts <<-STR
10
- USAGE:
11
- martlet [command]
12
-
13
- COMMANDS:
14
- grades Lists all your grades. This is the default command.
15
- STR
16
- end
17
-
18
- def get_credentials
19
- config_path = File.join ENV['HOME'], '.martlet'
20
- email = nil
21
- password = nil
22
-
23
- if File.exist?(config_path)
24
- config = YAML.load_file config_path
25
- email = config['email']
26
- password = config['password']
27
- else
28
- print 'Minerva email: '
29
- email = gets.chomp
30
-
31
- print 'Password: '
32
- password = STDIN.noecho(&:gets).chomp
33
- puts
34
- end
35
-
36
- return email, password
37
- end
38
-
39
- def run(args)
40
- case args.shift
41
- when nil, 'grades'
42
- email, password = get_credentials
43
-
44
- puts 'Authenticating...'
45
- client = Martlet.new(email, password)
46
-
47
- puts 'Fetching grades...'
48
- grades = client.grades
49
-
50
- grades.each do |number, grade|
51
- puts "#{number}: #{grade}"
52
- end
53
- else
54
- usage
55
- end
56
- rescue Martlet::AuthenticationError => e
57
- puts "An error has occurred: #{e.message}"
58
- end
59
-
60
- args = ARGV.dup
61
- ARGV.clear
62
- run(args)
7
+ Martlet::CLI.start(ARGV)
@@ -0,0 +1,91 @@
1
+ require 'thor'
2
+ require 'yaml'
3
+ require 'io/console'
4
+
5
+ module Martlet
6
+ class CLI < Thor
7
+ default_task :grades
8
+
9
+ desc "grades", "Lists all your grades"
10
+ method_option :sort, aliases: 's', enum: ['course', 'grade']
11
+ def grades
12
+ puts 'Fetching grades...'
13
+ grades = client.grades
14
+
15
+ grades = case options['sort']
16
+ when 'course' then grades.sort_by { |k,v| k }
17
+ when 'grade' then grades.sort_by { |k,v| Grade.new v }
18
+ else grades
19
+ end
20
+
21
+ grades.each do |number, grade|
22
+ puts "#{number}: #{grade}"
23
+ end
24
+ end
25
+
26
+ desc "courses SEMESTER YEAR", "List current courses or courses for given semester and year"
27
+ def courses(semester = nil, year = nil)
28
+ if semester.nil? || year.nil?
29
+ semester, year = current_semester_and_year
30
+ end
31
+
32
+ puts 'Fetching courses...'
33
+ courses = client.courses(semester, year)
34
+ course_name_size = courses.map { |c| c.name.length }.max
35
+
36
+ puts "#{semester.capitalize} #{year} courses"
37
+
38
+ courses.each do |course|
39
+ course_row_format = "%-11s %-#{course_name_size}s @ %s\n"
40
+ printf course_row_format, "#{course.number}", course.name, course.location
41
+ end
42
+
43
+ puts 'No courses found' if courses.empty?
44
+ end
45
+
46
+ private
47
+
48
+ def credentials
49
+ email = nil
50
+ password = nil
51
+
52
+ if File.exist?(config_path)
53
+ # Get credentials from config file
54
+ config = YAML.load_file config_path
55
+ email = config['email']
56
+ password = config['password']
57
+ else
58
+ # Get credentials from stdin
59
+ print 'Minerva email: '
60
+ email = gets.chomp
61
+
62
+ print 'Password: '
63
+ password = STDIN.noecho(&:gets).chomp
64
+ puts
65
+ end
66
+
67
+ return email, password
68
+ end
69
+
70
+ def client
71
+ @client ||= begin
72
+ puts 'Authenticating...'
73
+ Martlet.new(*credentials)
74
+ end
75
+ end
76
+
77
+ def config_path
78
+ "#{ENV['HOME']}/.martlet"
79
+ end
80
+
81
+ def current_semester_and_year
82
+ semester = case Time.now.month
83
+ when 1..4 then 'winter'
84
+ when 5..8 then 'summer'
85
+ when 9..12 then 'fall'
86
+ end
87
+
88
+ return semester, Time.now.year
89
+ end
90
+ end
91
+ end
@@ -6,7 +6,7 @@ module Martlet
6
6
 
7
7
  def initialize(email, password)
8
8
  @agent = Mechanize.new
9
- @email = format_email(email)
9
+ @email = email.include?('@') ? email : "#{email}@mail.mcgill.ca"
10
10
 
11
11
  authenticator = Authenticator.new(@agent)
12
12
  authenticator.authenticate(@email, password)
@@ -16,15 +16,10 @@ module Martlet
16
16
  transcript = Transcript.new(@agent)
17
17
  transcript.fetch_grades
18
18
  end
19
-
20
- private
21
-
22
- def format_email(email)
23
- if email.include?('@')
24
- email
25
- else
26
- "#{email}@mail.mcgill.ca"
27
- end
19
+
20
+ def courses(semester, year)
21
+ schedule = Schedule.new(@agent, semester, year)
22
+ schedule.fetch_courses
28
23
  end
29
24
  end
30
- end
25
+ end
@@ -0,0 +1,28 @@
1
+ module Martlet
2
+ class Course
3
+ include DayConversions
4
+
5
+ attr_accessor :name, :number, :term, :crn, :instructor, :credits, :level, :campus,
6
+ :time, :days, :location, :date_range, :type, :instructors
7
+
8
+ def initialize(args)
9
+ @name = args[:name] || ''
10
+ @number = args[:number]
11
+ @term = args[:term]
12
+ @crn = args[:crn]
13
+ @instructor = args[:instructor]
14
+ @credits = args[:credits]
15
+ @level = args[:level]
16
+ @campus = args[:campus]
17
+ @time = args[:time]
18
+ @days = args[:days] || ''
19
+ @location = args[:location]
20
+ @date_range = args[:date_range]
21
+ @type = args[:type]
22
+ @instructors = args[:instructors]
23
+
24
+ @name.gsub! /\.$/, ''
25
+ @days = @days.split('').map { |letter| day_from_letter(letter) }
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,35 @@
1
+ module Martlet
2
+ module DayConversions
3
+ def day_from_letter(letter)
4
+ case letter.upcase
5
+ when 'M' then 'Monday'
6
+ when 'T' then 'Tuesday'
7
+ when 'W' then 'Wednesday'
8
+ when 'R' then 'Thursday'
9
+ when 'F' then 'Friday'
10
+ else letter
11
+ end
12
+ end
13
+
14
+ def short_day_name(day)
15
+ case day.capitalize
16
+ when 'M', 'Monday' then 'Mon'
17
+ when 'T', 'Tuesday' then 'Tue'
18
+ when 'W', 'Wednesday' then 'Wed'
19
+ when 'R', 'Thursday' then 'Thu'
20
+ when 'F', 'Friday' then 'Fri'
21
+ else day
22
+ end
23
+ end
24
+
25
+ def number_from_day(day)
26
+ case day.capitalize
27
+ when 'M', 'Monday' then 1
28
+ when 'T', 'Tuesday' then 2
29
+ when 'W', 'Wednesday' then 3
30
+ when 'R', 'Thursday' then 4
31
+ when 'F', 'Friday' then 5
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,21 @@
1
+ module Martlet
2
+ class Grade
3
+ include Comparable
4
+
5
+ attr_reader :letter
6
+
7
+ def initialize(letter)
8
+ @letter = letter.upcase
9
+ end
10
+
11
+ def <=>(other)
12
+ letters.index(letter) <=> letters.index(other.letter)
13
+ end
14
+
15
+ private
16
+
17
+ def letters
18
+ %w{P S R CO A A- B+ B B- C+ C D F U HH J K KE K* KF IC KK L LE L* NA && NE NR W WF WL W-- -- IP Q}
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,38 @@
1
+ module Martlet
2
+ class Schedule
3
+ def initialize(agent, semester, year)
4
+ @agent = agent
5
+ @semester = semester
6
+ @year = year
7
+ @html = fetch_schedule_html
8
+ end
9
+
10
+ def fetch_courses
11
+ parser = ScheduleParser.new(@html)
12
+ parser.parse_courses
13
+ end
14
+
15
+ private
16
+
17
+ def schedule_url
18
+ 'https://horizon.mcgill.ca/pban1/bwskfshd.P_CrseSchdDetl'
19
+ end
20
+
21
+ def number_from_semester(semester)
22
+ case semester.to_s.downcase
23
+ when 'winter' then '01'
24
+ when 'summer' then '05'
25
+ when 'fall' then '09'
26
+ end
27
+ end
28
+
29
+ def schedule_params
30
+ { 'term_in' => "#{@year}#{number_from_semester(@semester)}" }
31
+ end
32
+
33
+ def fetch_schedule_html
34
+ page = @agent.post(schedule_url, schedule_params)
35
+ page.body
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,45 @@
1
+ module Martlet
2
+ class ScheduleParser
3
+ def initialize(html)
4
+ @html = html
5
+ end
6
+
7
+ def parse_courses
8
+ courses = []
9
+ document = Nokogiri::HTML(@html)
10
+ course_tables = document.search("br+table[@class='datadisplaytable']")
11
+ course_times = document.search("br+table[@class='datadisplaytable']+table tr+tr")
12
+
13
+ course_tables.each_with_index do |table, index|
14
+ course_name_info = table.search('caption').first
15
+ course_name_info = course_name_info.text.split(' - ')
16
+ course_name = course_name_info[0]
17
+ course_number = course_name_info[1]
18
+
19
+ course_time_info = course_times[index].search('td').map { |d| d.text.strip }
20
+
21
+ course_data = table.search('tr td').map { |d| d.text.strip }
22
+ args = {
23
+ name: course_name,
24
+ number: course_number,
25
+ term: course_data[0],
26
+ crn: course_data[1],
27
+ instructor: course_data[3],
28
+ credits: course_data[5],
29
+ level: course_data[6],
30
+ campus: course_data[7],
31
+ time: course_time_info[0],
32
+ days: course_time_info[1],
33
+ location: course_time_info[2],
34
+ date_range: course_time_info[3],
35
+ type: course_time_info[4],
36
+ instructors: course_time_info[5]
37
+ }
38
+
39
+ courses << Course.new(args)
40
+ end
41
+
42
+ courses
43
+ end
44
+ end
45
+ end
@@ -1,3 +1,3 @@
1
1
  module Martlet
2
- VERSION = "0.0.5"
2
+ VERSION = "0.0.6"
3
3
  end
data/lib/martlet.rb CHANGED
@@ -1,8 +1,13 @@
1
1
  require 'martlet/client'
2
2
  require 'martlet/authenticator'
3
+ require 'martlet/day_conversions'
3
4
  require 'martlet/transcript'
4
5
  require 'martlet/transcript_parser'
6
+ require 'martlet/schedule'
7
+ require 'martlet/schedule_parser'
5
8
  require 'martlet/record'
9
+ require 'martlet/grade'
10
+ require 'martlet/course'
6
11
  require 'martlet/version'
7
12
 
8
13
  module Martlet
data/martlet.gemspec CHANGED
@@ -20,6 +20,8 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.add_development_dependency "bundler", "~> 1.3"
22
22
  spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "pry"
23
24
 
24
25
  spec.add_dependency "mechanize", "~> 2.7"
26
+ spec.add_dependency "thor", "~> 0.19.1"
25
27
  end
metadata CHANGED
@@ -1,64 +1,85 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: martlet
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
5
- prerelease:
4
+ version: 0.0.6
6
5
  platform: ruby
7
6
  authors:
8
7
  - Alex Coco
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2013-12-27 00:00:00.000000000 Z
11
+ date: 2014-04-18 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: bundler
16
15
  requirement: !ruby/object:Gem::Requirement
17
- none: false
18
16
  requirements:
19
- - - ~>
17
+ - - "~>"
20
18
  - !ruby/object:Gem::Version
21
19
  version: '1.3'
22
20
  type: :development
23
21
  prerelease: false
24
22
  version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
23
  requirements:
27
- - - ~>
24
+ - - "~>"
28
25
  - !ruby/object:Gem::Version
29
26
  version: '1.3'
30
27
  - !ruby/object:Gem::Dependency
31
28
  name: rake
32
29
  requirement: !ruby/object:Gem::Requirement
33
- none: false
34
30
  requirements:
35
- - - ! '>='
31
+ - - ">="
36
32
  - !ruby/object:Gem::Version
37
33
  version: '0'
38
34
  type: :development
39
35
  prerelease: false
40
36
  version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
37
  requirements:
43
- - - ! '>='
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pry
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
44
53
  - !ruby/object:Gem::Version
45
54
  version: '0'
46
55
  - !ruby/object:Gem::Dependency
47
56
  name: mechanize
48
57
  requirement: !ruby/object:Gem::Requirement
49
- none: false
50
58
  requirements:
51
- - - ~>
59
+ - - "~>"
52
60
  - !ruby/object:Gem::Version
53
61
  version: '2.7'
54
62
  type: :runtime
55
63
  prerelease: false
56
64
  version_requirements: !ruby/object:Gem::Requirement
57
- none: false
58
65
  requirements:
59
- - - ~>
66
+ - - "~>"
60
67
  - !ruby/object:Gem::Version
61
68
  version: '2.7'
69
+ - !ruby/object:Gem::Dependency
70
+ name: thor
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.19.1
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.19.1
62
83
  description: Ruby client for McGill's student portal, Minerva.
63
84
  email:
64
85
  - hello@alexcoco.com
@@ -67,7 +88,7 @@ executables:
67
88
  extensions: []
68
89
  extra_rdoc_files: []
69
90
  files:
70
- - .gitignore
91
+ - ".gitignore"
71
92
  - Gemfile
72
93
  - LICENSE.txt
73
94
  - README.md
@@ -75,8 +96,14 @@ files:
75
96
  - bin/martlet
76
97
  - lib/martlet.rb
77
98
  - lib/martlet/authenticator.rb
99
+ - lib/martlet/cli.rb
78
100
  - lib/martlet/client.rb
101
+ - lib/martlet/course.rb
102
+ - lib/martlet/day_conversions.rb
103
+ - lib/martlet/grade.rb
79
104
  - lib/martlet/record.rb
105
+ - lib/martlet/schedule.rb
106
+ - lib/martlet/schedule_parser.rb
80
107
  - lib/martlet/transcript.rb
81
108
  - lib/martlet/transcript_parser.rb
82
109
  - lib/martlet/version.rb
@@ -84,26 +111,25 @@ files:
84
111
  homepage: https://github.com/alexcoco/martlet
85
112
  licenses:
86
113
  - MIT
114
+ metadata: {}
87
115
  post_install_message:
88
116
  rdoc_options: []
89
117
  require_paths:
90
118
  - lib
91
119
  required_ruby_version: !ruby/object:Gem::Requirement
92
- none: false
93
120
  requirements:
94
- - - ! '>='
121
+ - - ">="
95
122
  - !ruby/object:Gem::Version
96
123
  version: '0'
97
124
  required_rubygems_version: !ruby/object:Gem::Requirement
98
- none: false
99
125
  requirements:
100
- - - ! '>='
126
+ - - ">="
101
127
  - !ruby/object:Gem::Version
102
128
  version: '0'
103
129
  requirements: []
104
130
  rubyforge_project:
105
- rubygems_version: 1.8.23
131
+ rubygems_version: 2.2.2
106
132
  signing_key:
107
- specification_version: 3
133
+ specification_version: 4
108
134
  summary: Client for McGill's student portal
109
135
  test_files: []