martlet 0.0.6 → 0.0.7

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