martlet 0.0.6 → 0.0.7

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 35a2b85f2c458fe8574f563eda6789bafe0944c3
4
- data.tar.gz: 56f1b0405e9918adc07b75a57523d9fc825c0856
3
+ metadata.gz: a7a4692527f2677c94a47163661dc644e22bd2b0
4
+ data.tar.gz: 82efea83f3312f4910bafebebdbd0c6f99889b94
5
5
  SHA512:
6
- metadata.gz: 568ec7e3a3347e0273d48e03158d0906217eacf942bd46dc2cd6e4d6ec318b285f3097d7a65112cb07143a75ed49a471d1330e14e5a1a0fd99aab9f5ee28a59b
7
- data.tar.gz: c3ac26d611c43935969620f116344c7ed0112fa4c7381bb7a6d5c0a312170b608c900d4d097ab8ed6d9a8d555e5033e8202ae2607cd5f717837fd885affbd309
6
+ metadata.gz: 0e3c2707c80dc889363960d4669e26fbd8dafe72328061198795f76bbec8b34f98a898607b3b84538410df24fbc5d04af824315442409d7419bb16babe041156
7
+ data.tar.gz: af8da5fcb0c00dcb4da686fbe373a101b7ae899ae49a25684dc53b4646d34f7d7bdc16003a2d1802989fc9108739a94d11db9ad28b192f75884564402692ef5b
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ .DS_Store
data/bin/martlet CHANGED
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
- $:.unshift File.join File.dirname(__FILE__), '..', 'lib'
3
- require 'rubygems'
2
+
4
3
  require 'martlet'
5
4
  require 'martlet/cli'
6
5
 
@@ -0,0 +1,66 @@
1
+ module Martlet
2
+ class CalendarExporter
3
+ include CalendarHelpers
4
+
5
+ def initialize(filename, courses)
6
+ @filename = filename
7
+ @courses = courses
8
+ end
9
+
10
+ def export
11
+ f = File.new(@filename, 'w')
12
+ f.write("BEGIN:VCALENDAR\r\n")
13
+
14
+ @courses.each do |course|
15
+ course.meetings.each do |meeting|
16
+ if meeting.start_time
17
+ f.write(calendar_vevent(course, meeting))
18
+ else
19
+ puts "Warning: schedule information unavailable for #{course.number}"
20
+ end
21
+ end
22
+ end
23
+
24
+ f.write("END:VCALENDAR\r\n")
25
+ ensure
26
+ f.close
27
+ end
28
+
29
+ private
30
+
31
+ def calendar_dtstart(meeting)
32
+ event_date = calendar_date(meeting.start_date)
33
+ start_time = calendar_time(meeting.start_time)
34
+ "#{event_date}T#{start_time}"
35
+ end
36
+
37
+ def calendar_dtend(meeting)
38
+ event_date = calendar_date(meeting.start_date)
39
+ end_time = calendar_time(meeting.end_time)
40
+ "#{event_date}T#{end_time}"
41
+ end
42
+
43
+ def calendar_rrule(meeting)
44
+ end_date = calendar_date(meeting.end_date)
45
+ repeat_days = calendar_days(meeting.days)
46
+ "FREQ=WEEKLY;INTERVAL=1;UNTIL=#{end_date}T235959;BYDAY=#{repeat_days}"
47
+ end
48
+
49
+ def calendar_vevent(course, meeting)
50
+ begin_vevent = "BEGIN:VEVENT\r\n"
51
+ summary = "SUMMARY:#{course.number} - #{course.name}\r\n"
52
+ location = "LOCATION:#{meeting.location}\r\n"
53
+ dtstart = "DTSTART:#{calendar_dtstart(meeting)}\r\n"
54
+ dtend = "DTEND:#{calendar_dtend(meeting)}\r\n"
55
+ rrule = "RRULE:#{calendar_rrule(meeting)}\r\n"
56
+ end_vevent = "END:VEVENT\r\n"
57
+
58
+ if exclude_first_day?(meeting)
59
+ exdate = "EXDATE:#{calendar_dtstart(meeting)}\r\n"
60
+ [begin_vevent, summary, location, dtstart, dtend, rrule, exdate, end_vevent].join('')
61
+ else
62
+ [begin_vevent, summary, location, dtstart, dtend, rrule, end_vevent].join('')
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,40 @@
1
+ module Martlet
2
+ module CalendarHelpers
3
+ include DayConversions
4
+
5
+ def calendar_time(time)
6
+ sprintf "%02d%02d00", time.hour, time.min
7
+ end
8
+
9
+ def calendar_date(date)
10
+ sprintf "%04d%02d%02d", date.year, date.month, date.day
11
+ end
12
+
13
+ def calendar_day(day)
14
+ case day.capitalize
15
+ when 'Sunday' then 'SU'
16
+ when 'Monday' then 'MO'
17
+ when 'Tuesday' then 'TU'
18
+ when 'Wednesday' then 'WE'
19
+ when 'Thursday' then 'TH'
20
+ when 'Friday' then 'FR'
21
+ when 'Saturday' then 'SA'
22
+ end
23
+ end
24
+
25
+ def calendar_days(days)
26
+ repeat_days = days.map { |day| calendar_day(day) }
27
+ repeat_days.join(',')
28
+ end
29
+
30
+ def in_days?(date, days)
31
+ wday = date.wday
32
+ wdays = days.map { |day| number_from_day(day) }
33
+ wdays.include?(wday)
34
+ end
35
+
36
+ def exclude_first_day?(meeting)
37
+ !in_days?(meeting.start_date, meeting.days)
38
+ end
39
+ end
40
+ end
data/lib/martlet/cli.rb CHANGED
@@ -7,7 +7,7 @@ module Martlet
7
7
  default_task :grades
8
8
 
9
9
  desc "grades", "Lists all your grades"
10
- method_option :sort, aliases: 's', enum: ['course', 'grade']
10
+ option :sort, aliases: 's', enum: ['course', 'grade']
11
11
  def grades
12
12
  puts 'Fetching grades...'
13
13
  grades = client.grades
@@ -24,13 +24,16 @@ module Martlet
24
24
  end
25
25
 
26
26
  desc "courses SEMESTER YEAR", "List current courses or courses for given semester and year"
27
+ option :export, type: :boolean, default: false,
28
+ desc: "Export course schedule as an iCalendar (.ics) file in the current directory"
27
29
  def courses(semester = nil, year = nil)
28
30
  if semester.nil? || year.nil?
29
31
  semester, year = current_semester_and_year
30
32
  end
31
33
 
32
34
  puts 'Fetching courses...'
33
- courses = client.courses(semester, year)
35
+ schedule = client.schedule(semester, year)
36
+ courses = schedule.fetch_courses
34
37
  course_name_size = courses.map { |c| c.name.length }.max
35
38
 
36
39
  puts "#{semester.capitalize} #{year} courses"
@@ -41,6 +44,12 @@ module Martlet
41
44
  end
42
45
 
43
46
  puts 'No courses found' if courses.empty?
47
+
48
+ if options[:export] && !courses.empty?
49
+ filename = "#{semester}_#{year}.ics"
50
+ puts "Exporting courses to #{filename}..."
51
+ client.export_calendar(filename, courses)
52
+ end
44
53
  end
45
54
 
46
55
  private
@@ -17,9 +17,17 @@ module Martlet
17
17
  transcript.fetch_grades
18
18
  end
19
19
 
20
+ def schedule(semester, year)
21
+ Schedule.new(@agent, semester, year)
22
+ end
23
+
20
24
  def courses(semester, year)
21
- schedule = Schedule.new(@agent, semester, year)
22
- schedule.fetch_courses
25
+ schedule(semester, year).fetch_courses
26
+ end
27
+
28
+ def export_calendar(filename, courses)
29
+ exporter = CalendarExporter.new(filename, courses)
30
+ exporter.export
23
31
  end
24
32
  end
25
33
  end
@@ -1,9 +1,6 @@
1
1
  module Martlet
2
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
3
+ attr_accessor :name, :number, :term, :crn, :instructor, :credits, :level, :campus, :meetings
7
4
 
8
5
  def initialize(args)
9
6
  @name = args[:name] || ''
@@ -14,15 +11,13 @@ module Martlet
14
11
  @credits = args[:credits]
15
12
  @level = args[:level]
16
13
  @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]
14
+ @meetings = args[:meetings]
23
15
 
24
16
  @name.gsub! /\.$/, ''
25
- @days = @days.split('').map { |letter| day_from_letter(letter) }
17
+ end
18
+
19
+ def location
20
+ meetings.first.location
26
21
  end
27
22
  end
28
- end
23
+ end
@@ -0,0 +1,14 @@
1
+ module Martlet
2
+ class CourseMeeting
3
+ attr_reader :start_time, :end_time, :start_date, :end_date, :days, :location
4
+
5
+ def initialize(args)
6
+ @start_time = args[:start_time]
7
+ @end_time = args[:end_time]
8
+ @start_date = args[:start_date]
9
+ @end_date = args[:end_date]
10
+ @days = args[:days]
11
+ @location = args[:location]
12
+ end
13
+ end
14
+ end
@@ -1,5 +1,7 @@
1
1
  module Martlet
2
2
  class Schedule
3
+ attr_reader :semester, :year
4
+
3
5
  def initialize(agent, semester, year)
4
6
  @agent = agent
5
7
  @semester = semester
@@ -1,25 +1,44 @@
1
1
  module Martlet
2
2
  class ScheduleParser
3
+ include DayConversions
4
+
3
5
  def initialize(html)
4
6
  @html = html
5
7
  end
6
8
 
7
9
  def parse_courses
8
- courses = []
9
10
  document = Nokogiri::HTML(@html)
10
11
  course_tables = document.search("br+table[@class='datadisplaytable']")
11
- course_times = document.search("br+table[@class='datadisplaytable']+table tr+tr")
12
+ course_times = document.search("br+table[@class='datadisplaytable']+table tr")
13
+ course_times = split_course_times(course_times)
12
14
 
13
- course_tables.each_with_index do |table, index|
15
+ courses = course_tables.map.with_index do |table, index|
14
16
  course_name_info = table.search('caption').first
15
17
  course_name_info = course_name_info.text.split(' - ')
16
18
  course_name = course_name_info[0]
17
19
  course_number = course_name_info[1]
18
20
 
19
- course_time_info = course_times[index].search('td').map { |d| d.text.strip }
20
-
21
21
  course_data = table.search('tr td').map { |d| d.text.strip }
22
- args = {
22
+ course_time_info = course_times[index].map do |course_time|
23
+ course_time.search('td').map { |d| d.text.strip }
24
+ end
25
+
26
+ meetings = course_time_info.map do |course_time|
27
+ start_time, end_time = parse_time_range(course_time[0])
28
+ start_date, end_date = parse_date_range(course_time[3])
29
+ days = parse_days(course_time[1])
30
+
31
+ CourseMeeting.new({
32
+ start_time: start_time,
33
+ end_time: end_time,
34
+ start_date: start_date,
35
+ end_date: end_date,
36
+ days: days,
37
+ location: course_time[2]
38
+ })
39
+ end
40
+
41
+ Course.new({
23
42
  name: course_name,
24
43
  number: course_number,
25
44
  term: course_data[0],
@@ -28,18 +47,41 @@ module Martlet
28
47
  credits: course_data[5],
29
48
  level: course_data[6],
30
49
  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)
50
+ meetings: meetings
51
+ })
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def split_course_times(course_times)
58
+ split = []
59
+ course_times.each do |info|
60
+ if headers?(info)
61
+ split << []
62
+ else
63
+ split.last << info
64
+ end
40
65
  end
66
+ split
67
+ end
68
+
69
+ def headers?(row)
70
+ !row.search('th').empty?
71
+ end
72
+
73
+ def parse_time_range(time_range)
74
+ times = time_range.scan(/\d+:\d+\s[AP]M/)
75
+ times.map { |time| Time.parse(time) }
76
+ end
77
+
78
+ def parse_date_range(date_range)
79
+ dates = date_range.scan(/\w+\s\d+,\s\d+/)
80
+ dates.map { |date| Date.parse(date) }
81
+ end
41
82
 
42
- courses
83
+ def parse_days(days)
84
+ days.split('').map { |letter| day_from_letter(letter) }
43
85
  end
44
86
  end
45
- end
87
+ end
@@ -1,3 +1,3 @@
1
1
  module Martlet
2
- VERSION = "0.0.6"
2
+ VERSION = "0.0.7"
3
3
  end
data/lib/martlet.rb CHANGED
@@ -5,9 +5,12 @@ require 'martlet/transcript'
5
5
  require 'martlet/transcript_parser'
6
6
  require 'martlet/schedule'
7
7
  require 'martlet/schedule_parser'
8
+ require 'martlet/calendar_helpers'
9
+ require 'martlet/calendar_exporter'
8
10
  require 'martlet/record'
9
11
  require 'martlet/grade'
10
12
  require 'martlet/course'
13
+ require 'martlet/course_meeting'
11
14
  require 'martlet/version'
12
15
 
13
16
  module Martlet
data/martlet.gemspec CHANGED
@@ -19,9 +19,8 @@ Gem::Specification.new do |spec|
19
19
  spec.require_paths = ["lib"]
20
20
 
21
21
  spec.add_development_dependency "bundler", "~> 1.3"
22
- spec.add_development_dependency "rake"
23
- spec.add_development_dependency "pry"
22
+ spec.add_development_dependency "rake", "~> 10.3"
24
23
 
25
24
  spec.add_dependency "mechanize", "~> 2.7"
26
- spec.add_dependency "thor", "~> 0.19.1"
25
+ spec.add_dependency "thor", "~> 0.19"
27
26
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: martlet
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Coco
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-04-18 00:00:00.000000000 Z
11
+ date: 2014-08-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -28,30 +28,16 @@ dependencies:
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
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
- - - ">="
31
+ - - "~>"
46
32
  - !ruby/object:Gem::Version
47
- version: '0'
33
+ version: '10.3'
48
34
  type: :development
49
35
  prerelease: false
50
36
  version_requirements: !ruby/object:Gem::Requirement
51
37
  requirements:
52
- - - ">="
38
+ - - "~>"
53
39
  - !ruby/object:Gem::Version
54
- version: '0'
40
+ version: '10.3'
55
41
  - !ruby/object:Gem::Dependency
56
42
  name: mechanize
57
43
  requirement: !ruby/object:Gem::Requirement
@@ -72,14 +58,14 @@ dependencies:
72
58
  requirements:
73
59
  - - "~>"
74
60
  - !ruby/object:Gem::Version
75
- version: 0.19.1
61
+ version: '0.19'
76
62
  type: :runtime
77
63
  prerelease: false
78
64
  version_requirements: !ruby/object:Gem::Requirement
79
65
  requirements:
80
66
  - - "~>"
81
67
  - !ruby/object:Gem::Version
82
- version: 0.19.1
68
+ version: '0.19'
83
69
  description: Ruby client for McGill's student portal, Minerva.
84
70
  email:
85
71
  - hello@alexcoco.com
@@ -96,9 +82,12 @@ files:
96
82
  - bin/martlet
97
83
  - lib/martlet.rb
98
84
  - lib/martlet/authenticator.rb
85
+ - lib/martlet/calendar_exporter.rb
86
+ - lib/martlet/calendar_helpers.rb
99
87
  - lib/martlet/cli.rb
100
88
  - lib/martlet/client.rb
101
89
  - lib/martlet/course.rb
90
+ - lib/martlet/course_meeting.rb
102
91
  - lib/martlet/day_conversions.rb
103
92
  - lib/martlet/grade.rb
104
93
  - lib/martlet/record.rb