tableau 0.0.1
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/Rakefile +18 -0
- data/lib/tableau.rb +21 -0
- data/lib/tableau/baseparser.rb +136 -0
- data/lib/tableau/class.rb +34 -0
- data/lib/tableau/classarray.rb +30 -0
- data/lib/tableau/module.rb +40 -0
- data/lib/tableau/moduleparser.rb +51 -0
- data/lib/tableau/tablebuilder.rb +134 -0
- data/lib/tableau/timetable.rb +114 -0
- data/lib/tableau/timetableparser.rb +54 -0
- data/lib/tableau/uribuilder.rb +43 -0
- data/lib/tableau/version.rb +3 -0
- data/lib/tasks/tableau_tasks.rake +4 -0
- metadata +157 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 78a2bd4d06b44a5c5fac93cc6eb728f0b405e9e7
|
4
|
+
data.tar.gz: 44763957d728478811a5f91c9509f334f3966752
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 28b0d5f1082b31434cf4debb8b463a2c4bd823c9ab93b6e9ac6a51a70bcd9ff1672639f0d6c375cfa2c892f3a27cecf54df162702f60b95c44244dbc3b7c890d
|
7
|
+
data.tar.gz: f0e570d93e64733397d197f6d64d356312796b317534abe3bd88f44e92152c5f039835864e08f79f95ebebd980d077f6bca606b32df09ab7a5e3f9777020092b
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2013 Matt Ryder
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'Tableau'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.rdoc')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
Bundler::GemHelper.install_tasks
|
18
|
+
|
data/lib/tableau.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'tableau/timetable'
|
3
|
+
require 'tableau/moduleparser'
|
4
|
+
require 'tableau/tablebuilder'
|
5
|
+
|
6
|
+
module Tableau
|
7
|
+
class << self
|
8
|
+
|
9
|
+
def generate(table_id, module_codes)
|
10
|
+
timetable = Tableau::Timetable.new(table_id, module_codes)
|
11
|
+
builder = Tableau::TableBuilder.new(timetable)
|
12
|
+
builder.to_html
|
13
|
+
end
|
14
|
+
|
15
|
+
# Return the Name, Code and Types (2Prac / PracA / PracB etc) from the timetable
|
16
|
+
def module_info(module_code)
|
17
|
+
Tableau::ModuleParser.new(module_code).module_info
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'tableau/timetable'
|
2
|
+
require 'tableau/module'
|
3
|
+
require 'tableau/class'
|
4
|
+
require 'tableau/uribuilder'
|
5
|
+
require 'tableau/classarray'
|
6
|
+
|
7
|
+
module Tableau
|
8
|
+
class BaseParser
|
9
|
+
|
10
|
+
@@COURSE_DESCRIPTION_XPATH = '/html/body/table[1]//td//td/text()'
|
11
|
+
@@WEEKS_REGEX = /^([\d]{2}-[\d]{2})|^([\d]{2})/
|
12
|
+
|
13
|
+
# Create a new instance of the BaseParser, with an ID to lookup
|
14
|
+
def initalize(lookup_id)
|
15
|
+
@time, @day = Time.new(2013, 1, 1, 9, 0, 0), 0
|
16
|
+
@lookup_id = lookup_id
|
17
|
+
end
|
18
|
+
|
19
|
+
# Parse the module table for any classes
|
20
|
+
def parse_table(table_rows)
|
21
|
+
classes = Tableau::ClassArray.new
|
22
|
+
@day = 0
|
23
|
+
|
24
|
+
# delete the time header row
|
25
|
+
table_rows.delete(table_rows.first)
|
26
|
+
|
27
|
+
table_rows.each do |row|
|
28
|
+
@time = Time.new(2013, 1, 1, 9, 0, 0)
|
29
|
+
|
30
|
+
# drop the 'Day' cell from the row
|
31
|
+
row_items = row.xpath('td')
|
32
|
+
row_items.delete(row_items.first)
|
33
|
+
|
34
|
+
row_items.each do |cell|
|
35
|
+
if cell.attribute('colspan')
|
36
|
+
intervals = cell.attribute('colspan').value
|
37
|
+
classes << create_class(cell)
|
38
|
+
else intervals = 1
|
39
|
+
end
|
40
|
+
|
41
|
+
inc_time(intervals)
|
42
|
+
end
|
43
|
+
|
44
|
+
@day += 1
|
45
|
+
end
|
46
|
+
|
47
|
+
classes
|
48
|
+
end
|
49
|
+
|
50
|
+
# Create a Class from the given data element
|
51
|
+
def create_class(class_element)
|
52
|
+
begin
|
53
|
+
tt_class = Tableau::Class.new(@day, @time)
|
54
|
+
data = class_element.xpath('table/tr/td//text()')
|
55
|
+
raise "Misformed cell for #{module_id}" if data.count < 4
|
56
|
+
rescue Exception => e
|
57
|
+
p "EXCEPTION: #{e.message}", "Data Parsed:", data
|
58
|
+
return nil
|
59
|
+
end
|
60
|
+
|
61
|
+
# If the weeks are in the 2nd index, it's a core timetable
|
62
|
+
if @@WEEKS_REGEX.match(data[1].text())
|
63
|
+
tt_class.code = data[0].text().match(/^[A-Za-z0-9\-]+/).to_s
|
64
|
+
tt_class.type = data[0].text().gsub(tt_class.code, '').gsub('/', '')
|
65
|
+
|
66
|
+
tt_class.weeks = create_class_weeks(data[1].text())
|
67
|
+
tt_class.location = data[2].text()
|
68
|
+
tt_class.name = data[3].text()
|
69
|
+
else # this is a module timetable, laid out differently
|
70
|
+
if data[0].to_s != ""
|
71
|
+
tt_class.code = data[0].text().match(/^[A-Za-z0-9\-]+/).to_s
|
72
|
+
tt_class.type = data[0].text().gsub(tt_class.code, '').gsub('/', '')
|
73
|
+
end
|
74
|
+
|
75
|
+
tt_class.location = data[1].text()
|
76
|
+
tt_class.name = data[2].text()
|
77
|
+
tt_class.weeks = create_class_weeks(data[3].text())
|
78
|
+
end
|
79
|
+
|
80
|
+
# Same attribute on both timetables, DRY'd here
|
81
|
+
tt_class.tutor = data[4] ? data[4].text() : nil
|
82
|
+
|
83
|
+
if intervals = class_element.attribute('colspan').value
|
84
|
+
tt_class.intervals = intervals.to_i
|
85
|
+
end
|
86
|
+
|
87
|
+
tt_class
|
88
|
+
end
|
89
|
+
|
90
|
+
# Create the week range array for the given week string
|
91
|
+
def create_class_weeks(week_data)
|
92
|
+
week_span_regex = /([\d]{2}-[\d]{2})/
|
93
|
+
week_start_regex = /^[0-9]{2}/
|
94
|
+
week_end_regex = /[0-9]{2}$/
|
95
|
+
week_single_regex = /[\d]{2}/
|
96
|
+
|
97
|
+
class_weeks = Array.new
|
98
|
+
|
99
|
+
week_data.scan(@@WEEKS_REGEX).each do |weekspan|
|
100
|
+
|
101
|
+
# if it's a 28-39 week span
|
102
|
+
if weekspan =~ week_span_regex
|
103
|
+
start = week_start_regex.match(weekspan)[0].to_i
|
104
|
+
finish = week_end_regex.match(weekspan)[0].to_i
|
105
|
+
|
106
|
+
while start <= finish
|
107
|
+
class_weeks << start
|
108
|
+
start += 1
|
109
|
+
end
|
110
|
+
|
111
|
+
# some single week (30, 31, 32 etc) support
|
112
|
+
elsif weekspan =~ week_single_regex
|
113
|
+
class_weeks << week_single_regex.match(weekspan)[0].to_i
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
class_weeks
|
118
|
+
end
|
119
|
+
|
120
|
+
# Increments the @time by 15 minute intervals
|
121
|
+
def inc_time(intervals)
|
122
|
+
intervals.to_i.times { @time += 900 }
|
123
|
+
end
|
124
|
+
|
125
|
+
# Get the ID string in the first <table> element (usually <:code> - <:name>)
|
126
|
+
def get_info(timetable_data)
|
127
|
+
timetable_data.xpath(@@COURSE_DESCRIPTION_XPATH).to_html
|
128
|
+
end
|
129
|
+
|
130
|
+
# Returns the XPath for the nth table
|
131
|
+
def xpath_for_table(table_count)
|
132
|
+
"/html/body/table[#{3 * table_count - 1}]/tr"
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Tableau
|
2
|
+
class Class
|
3
|
+
|
4
|
+
attr_accessor :type, :location, :name, :tutor,
|
5
|
+
:day, :time, :intervals, :weeks,
|
6
|
+
:code
|
7
|
+
|
8
|
+
def defaults
|
9
|
+
def_opts = {
|
10
|
+
type: '',
|
11
|
+
location: '',
|
12
|
+
name: '',
|
13
|
+
tutor: '',
|
14
|
+
intervals: 4,
|
15
|
+
day: 0,
|
16
|
+
code: 0,
|
17
|
+
time: Time.new
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(day, time, options = {})
|
22
|
+
@day = day
|
23
|
+
@time = time
|
24
|
+
@weeks = Array.new
|
25
|
+
defaults.merge!(options)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Duration of the class in hours
|
29
|
+
def duration
|
30
|
+
@intervals / 4
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Tableau
|
2
|
+
class ClassArray < Array
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
super
|
6
|
+
end
|
7
|
+
|
8
|
+
# Returns an array of all the classes for the day
|
9
|
+
def classes_for_day(day)
|
10
|
+
days_classes = ClassArray.new
|
11
|
+
self.each { |c| days_classes << c if c.day == day }
|
12
|
+
days_classes.count > 0 ? days_classes : nil
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns the earliest class in the module
|
16
|
+
def earliest_class
|
17
|
+
earliest = self.first
|
18
|
+
self.each { |c| earliest = c if c.time < earliest.time }
|
19
|
+
earliest
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns the latest class in the module
|
23
|
+
def latest_class
|
24
|
+
latest = self.first
|
25
|
+
self.each { |c| latest = c if c.time > latest.time }
|
26
|
+
latest
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Tableau
|
2
|
+
class Module
|
3
|
+
|
4
|
+
attr_reader :module_id, :name, :classes
|
5
|
+
|
6
|
+
def initialize(id, options = {})
|
7
|
+
@module_id = id
|
8
|
+
|
9
|
+
@name = options[:name] || ''
|
10
|
+
@classes = options[:classes] || Tableau::ClassArray.new
|
11
|
+
end
|
12
|
+
|
13
|
+
# Add a class to the module
|
14
|
+
def add_class(new_class)
|
15
|
+
@classes << new_class
|
16
|
+
end
|
17
|
+
|
18
|
+
# Returns an array of all the classes for the day
|
19
|
+
def classes_for_day(day)
|
20
|
+
days_classes = Tableau::ClassArray.new
|
21
|
+
@classes.each { |c| days_classes << c if c.day == day }
|
22
|
+
days_classes.count > 0 ? days_classes : nil
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns the earliest class in the module
|
26
|
+
def earliest_class
|
27
|
+
earliest = @classes.first
|
28
|
+
@classes.each { |c| earliest = c if c.time < earliest.time }
|
29
|
+
earliest
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns the latest class in the module
|
33
|
+
def latest_class
|
34
|
+
latest = @classes.first
|
35
|
+
@classes.each { |c| latest = c if c.time > latest.time }
|
36
|
+
latest
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
require 'nokogiri'
|
3
|
+
require 'tableau/baseparser'
|
4
|
+
|
5
|
+
module Tableau
|
6
|
+
class ModuleParser < Tableau::BaseParser
|
7
|
+
|
8
|
+
@@MODULE_ID_REGEX = /^CE[\d]{5}-[1-8]/
|
9
|
+
attr_reader :raw_timetable
|
10
|
+
|
11
|
+
# Create a new ModuleParser, with an optional module code
|
12
|
+
def initialize(module_code = nil)
|
13
|
+
begin
|
14
|
+
timetable_response = Tableau::UriBuilder.new(module_code, module_lookup: true).read
|
15
|
+
@raw_timetable = Nokogiri::HTML(timetable_response) if timetable_response
|
16
|
+
rescue OpenURI::HTTPError
|
17
|
+
return nil
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def module_info
|
22
|
+
mod, types = parse, Set.new
|
23
|
+
mod.classes.each { |c| types.add?(c.type) }
|
24
|
+
|
25
|
+
return { name: mod.name, code: mod.module_id, types: types }
|
26
|
+
end
|
27
|
+
|
28
|
+
def parse
|
29
|
+
raise "No module timetable loaded!" unless @raw_timetable
|
30
|
+
|
31
|
+
#Get the ID and Name from the first <table>
|
32
|
+
raw_info = @raw_timetable.xpath(@@COURSE_DESCRIPTION_XPATH).to_html
|
33
|
+
module_id = @@MODULE_ID_REGEX.match(raw_info).to_s
|
34
|
+
module_name = raw_info.gsub(module_id, '')
|
35
|
+
|
36
|
+
mod = Tableau::Module.new(module_id, name: module_name)
|
37
|
+
table_count = 1
|
38
|
+
table_data = @raw_timetable.xpath(xpath_for_table(table_count))
|
39
|
+
|
40
|
+
# Iterate through each timetable until xpath returns no more timetable tables
|
41
|
+
while !table_data.empty?
|
42
|
+
tables_classes = parse_table(table_data)
|
43
|
+
tables_classes.each { |c| mod.classes << c }
|
44
|
+
table_data = @raw_timetable.xpath(xpath_for_table(table_count += 1))
|
45
|
+
end
|
46
|
+
|
47
|
+
mod #return the module to the caller
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
module Tableau
|
2
|
+
class TableBuilder
|
3
|
+
|
4
|
+
def css_defaults
|
5
|
+
css_defaults = {
|
6
|
+
body_color: '#F4F5F6',
|
7
|
+
border_color: 'rgba(0, 0, 0, 0.25)',
|
8
|
+
class_color: '#a9b1b9',
|
9
|
+
header_bg_color: '#CFD3D7',
|
10
|
+
header_fg_color: '#25292D',
|
11
|
+
empty_color: '#EBEDEE'
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(timetable, css_options = {})
|
16
|
+
@timetable = timetable
|
17
|
+
@css = css_defaults.merge(css_options)
|
18
|
+
end
|
19
|
+
|
20
|
+
def build_css
|
21
|
+
%Q{
|
22
|
+
body {
|
23
|
+
background-color: #{@css[:body_color]};
|
24
|
+
}
|
25
|
+
|
26
|
+
table {
|
27
|
+
border-collapse: collapse;
|
28
|
+
border: 1px solid #{@css[:border_color]};
|
29
|
+
}
|
30
|
+
|
31
|
+
#time {
|
32
|
+
background-color: #{@css[:header_bg_color]};
|
33
|
+
color: #{@css[:header_fg_color]};
|
34
|
+
}
|
35
|
+
|
36
|
+
#time > th {
|
37
|
+
font-weight: lighter;
|
38
|
+
border-left: 1px solid #{@css[:border_color]};
|
39
|
+
}
|
40
|
+
|
41
|
+
.dh {
|
42
|
+
background-color: #{@css[:header_bg_color]};
|
43
|
+
color: #{@css[:header_fg_color]};
|
44
|
+
}
|
45
|
+
|
46
|
+
td {
|
47
|
+
background-color: #{@css[:empty_color]};
|
48
|
+
border: 1px solid #{@css[:border_color]};
|
49
|
+
padding: 5px;
|
50
|
+
}
|
51
|
+
|
52
|
+
.class_item {
|
53
|
+
background-color: #{@css[:class_color]};
|
54
|
+
}
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
def day_row(day, classes, end_time = nil)
|
59
|
+
days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri']
|
60
|
+
time = Time.new(2013, 1, 1, 9, 0 , 0)
|
61
|
+
end_time ||= Time.new(2013, 1, 1, 21, 0, 0)
|
62
|
+
|
63
|
+
day_row = %Q{<td class="dh">#{days[day]}</td>}
|
64
|
+
|
65
|
+
if !classes || classes.count == 0
|
66
|
+
while time < end_time
|
67
|
+
day_row += "<td></td>"
|
68
|
+
time += 900 # 15 mins in seconds
|
69
|
+
end
|
70
|
+
else
|
71
|
+
while time < end_time
|
72
|
+
class_at_time = @timetable.class_for_time(day, time)
|
73
|
+
if class_at_time
|
74
|
+
day_row += make_class(class_at_time)
|
75
|
+
time += (900 * class_at_time.intervals)
|
76
|
+
else
|
77
|
+
day_row += "<td></td>"
|
78
|
+
time += 900 # 15 mins in seconds
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
day_row
|
83
|
+
end
|
84
|
+
|
85
|
+
# Create the HTML for a class item on the table
|
86
|
+
def make_class(klass)
|
87
|
+
%Q{
|
88
|
+
<td class="class_item" colspan="#{klass.intervals}">
|
89
|
+
<p>#{klass.name} - #{klass.location}</p>
|
90
|
+
<p>#{klass.type}</p>
|
91
|
+
</td>}
|
92
|
+
end
|
93
|
+
|
94
|
+
# HTML5 representation of the timetable
|
95
|
+
def to_html
|
96
|
+
time_header, rows = '<th></th>', Array.new
|
97
|
+
end_time = Time.new(2013, 1, 1, 21, 0, 0)
|
98
|
+
|
99
|
+
# make the time row
|
100
|
+
@time = Time.new(2013, 1, 1, 9, 0, 0)
|
101
|
+
while @time < end_time
|
102
|
+
time_header += "<th>#{@time.strftime("%-k:%M")}</th>"
|
103
|
+
@time += 900
|
104
|
+
end
|
105
|
+
|
106
|
+
#make each day row
|
107
|
+
(0..4).each do |day|
|
108
|
+
classes = @timetable.classes_for_day(day)
|
109
|
+
rows << day_row(day, classes)
|
110
|
+
end
|
111
|
+
|
112
|
+
rows_str, id_str = '', "id=\"#{@timetable.name}\""
|
113
|
+
rows.each{ |r| rows_str += "<tr class=\"day\">\n#{r}\n</tr>\n" }
|
114
|
+
|
115
|
+
%Q{
|
116
|
+
<!DOCTYPE html>
|
117
|
+
<html>
|
118
|
+
<head>
|
119
|
+
<title>#{@timetable.name || 'Timetable' } - Timetablr.co</title>
|
120
|
+
<style>#{build_css}</style>
|
121
|
+
</head>
|
122
|
+
<body>
|
123
|
+
<h3>#{@timetable.name}</h3>
|
124
|
+
<table #{id_str if @timetable.name}>
|
125
|
+
<tr id="time">#{time_header}</tr>
|
126
|
+
#{rows_str}
|
127
|
+
</table>
|
128
|
+
</body>
|
129
|
+
</html>
|
130
|
+
}
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module Tableau
|
2
|
+
class Timetable
|
3
|
+
|
4
|
+
attr_accessor :name, :modules
|
5
|
+
|
6
|
+
# Create a new Timetable, with a Timetable Name and Student Set ID
|
7
|
+
def initialize(timetable_name)
|
8
|
+
@name = name || "Timetable"
|
9
|
+
@modules = Array.new
|
10
|
+
end
|
11
|
+
|
12
|
+
# Pushes an existing module into the timetable
|
13
|
+
def push_module(mod)
|
14
|
+
@modules << mod
|
15
|
+
end
|
16
|
+
|
17
|
+
# Adds a Module to the Timetable via the Parser
|
18
|
+
def add_module(module_code)
|
19
|
+
@module = Tableau::ModuleParser.new(module_code).parse
|
20
|
+
@modules << @module if @module
|
21
|
+
end
|
22
|
+
|
23
|
+
# Mass-adds an array of modules objects to the timetable
|
24
|
+
def add_modules(modules)
|
25
|
+
modules.each { |mod_code| add_module(mod_code) }
|
26
|
+
end
|
27
|
+
|
28
|
+
# Removes a class from the timetable
|
29
|
+
def remove_class(rem_class)
|
30
|
+
@modules.each do |m|
|
31
|
+
if m.name == rem_class.name
|
32
|
+
m.classes.delete(rem_class)
|
33
|
+
break
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns an array of the given day's classes
|
39
|
+
def classes_for_day(day)
|
40
|
+
classes = Tableau::ClassArray.new
|
41
|
+
|
42
|
+
@modules.each do |mod|
|
43
|
+
cfd = mod.classes_for_day(day)
|
44
|
+
cfd.each { |cl| classes << cl } if cfd
|
45
|
+
end
|
46
|
+
|
47
|
+
classes.count > 0 ? classes : nil
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns the class at the given day & time
|
51
|
+
def class_for_time(day, time)
|
52
|
+
cfd = self.classes_for_day(day)
|
53
|
+
cfd.each { |c| return c if c.time == time }
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns the module with given Module Code
|
58
|
+
def module_for_name(module_name)
|
59
|
+
modules.each { |m| return m if m.name }
|
60
|
+
nil
|
61
|
+
end
|
62
|
+
|
63
|
+
# Return the Tableau::Module that matches a given code
|
64
|
+
def module_for_code(mod_code)
|
65
|
+
@modules.each { |m| return m if m.module_id == mod_code } if @modules
|
66
|
+
nil
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns the earliest class on the timetable
|
70
|
+
def earliest_class
|
71
|
+
earliest_classes = Tableau::ClassArray.new
|
72
|
+
@modules.each { |m| earliest_classes << m.earliest_class }
|
73
|
+
|
74
|
+
earliest = earliest_classes.first
|
75
|
+
earliest_classes.each { |c| earliest = c if c.time < earliest.time }
|
76
|
+
earliest
|
77
|
+
end
|
78
|
+
|
79
|
+
# Returns the latest class on the timetable
|
80
|
+
def latest_class
|
81
|
+
latest_classes = Tableau::ClassArray.new
|
82
|
+
@modules.each { |m| latest_classes << m.latest_class }
|
83
|
+
|
84
|
+
latest = latest_classes.first
|
85
|
+
latest_classes.each { |c| latest = c if c.time > latest.time }
|
86
|
+
latest
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns an array of time conflicts found in the timetable
|
90
|
+
def conflicts
|
91
|
+
conflicts = Tableau::ClassArray.new
|
92
|
+
|
93
|
+
(0..4).each do |day|
|
94
|
+
days_classes = self.classes_for_day(day)
|
95
|
+
next if !days_classes || days_classes.count == 0
|
96
|
+
|
97
|
+
# get the last element index
|
98
|
+
last = days_classes.count - 1
|
99
|
+
|
100
|
+
for i in 0..last
|
101
|
+
i_c = days_classes[i]
|
102
|
+
time_range = i_c.time..(i_c.time + 3600 * i_c.duration)
|
103
|
+
|
104
|
+
for j in (i+1)..last
|
105
|
+
if time_range.cover?(days_classes[j].time)
|
106
|
+
conflicts << [days_classes[i], days_classes[j]]
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
conflicts # return the conflicts
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'tableau/baseparser'
|
2
|
+
|
3
|
+
module Tableau
|
4
|
+
class TimetableParser < Tableau::BaseParser
|
5
|
+
|
6
|
+
@@TIMETABLE_CODE_REGEX = /^[A-Za-z0-9\(\)]+/
|
7
|
+
|
8
|
+
# Create a new TimetableParser, with a Student Set ID (aka Core Timetable)
|
9
|
+
def initialize(student_set_id = nil)
|
10
|
+
begin
|
11
|
+
timetable_response = Tableau::UriBuilder.new(student_set_id).read
|
12
|
+
@raw_timetable = Nokogiri::HTML(timetable_response) if timetable_response
|
13
|
+
rescue OpenURI::HTTPError
|
14
|
+
return nil
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Get a summary of information about this timetable
|
19
|
+
def timetable_info
|
20
|
+
end
|
21
|
+
|
22
|
+
# Parse the Timetable for all Modules within
|
23
|
+
# Returns: A Tableau::Timetable
|
24
|
+
def parse
|
25
|
+
raise "No Timetable loaded!" unless @raw_timetable
|
26
|
+
|
27
|
+
table_info = @@TIMETABLE_CODE_REGEX.match(get_info(@raw_timetable))
|
28
|
+
timetable = Tableau::Timetable.new(table_info)
|
29
|
+
|
30
|
+
table_count = 1
|
31
|
+
table_data = @raw_timetable.xpath(xpath_for_table(table_count))
|
32
|
+
|
33
|
+
while !table_data.empty?
|
34
|
+
table_classes = parse_table(table_data)
|
35
|
+
sort_classes(timetable, table_classes)
|
36
|
+
table_data = @raw_timetable.xpath(xpath_for_table(table_count += 1))
|
37
|
+
end
|
38
|
+
timetable
|
39
|
+
end
|
40
|
+
|
41
|
+
# Sort all the parsed classes into modules
|
42
|
+
def sort_classes(timetable, classes)
|
43
|
+
classes.each do |c|
|
44
|
+
if !(cmodule = timetable.module_for_code(c.code))
|
45
|
+
cmodule = Tableau::Module.new(c.code)
|
46
|
+
timetable.push_module(cmodule)
|
47
|
+
end
|
48
|
+
|
49
|
+
cmodule.add_class(c)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Tableau
|
2
|
+
class UriBuilder
|
3
|
+
|
4
|
+
def defaults
|
5
|
+
@options = {
|
6
|
+
root: 'crwnmis3.staffs.ac.uk/Reporting/',
|
7
|
+
timetable_type: 'Individual',
|
8
|
+
lookup_type: 'Student+Sets',
|
9
|
+
timetable_template: 'Design+Template',
|
10
|
+
weeks: '10-25', # 10-25 for Semester 1, 26-42 for Semester 2
|
11
|
+
days: '1-5', # Mon - Fri
|
12
|
+
period_from: '5', # 15 min intervals since 8AM
|
13
|
+
period_to: '52',
|
14
|
+
optional_params: '&width=0&height=0' #optional params
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(lookup_id, options = {})
|
19
|
+
@lookup_id = lookup_id
|
20
|
+
defaults.merge!(options)
|
21
|
+
|
22
|
+
if options[:module_lookup]
|
23
|
+
@options[:lookup_type] = "Modules"
|
24
|
+
@options[:timetable_template] = "Module%20Individual%20SOC"
|
25
|
+
end
|
26
|
+
|
27
|
+
@options[:weeks] = ENV["TABLEAU_SEMESTER"] == 1 ? '10-25' : '26-42'
|
28
|
+
end
|
29
|
+
|
30
|
+
def read
|
31
|
+
open(self.to_s){ |io| io.read }
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_s
|
35
|
+
"http://#{@options[:root]}#{@options[:timetable_type]};#{@options[:lookup_type]};name;" +
|
36
|
+
"#{@lookup_id}?&template=#{@options[:timetable_template].gsub(' ', '%20')}" +
|
37
|
+
"&weeks=#{@options[:weeks]}&days=#{@options[:days]}" +
|
38
|
+
"&periods=#{@options[:period_from]}-#{@options[:period_to]}" +
|
39
|
+
"#{@options[:optional_params]}"
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
metadata
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tableau
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Matt Ryder
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-09-26 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 4.0.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 4.0.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: nokogiri
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
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: sqlite3
|
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
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec-rails
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
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: guard-spork
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: guard-rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: debugger
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
description: Parses, analyses and creates Timetables for Staffordshire University
|
112
|
+
students.Handles both Course and individual Module timetables.
|
113
|
+
email:
|
114
|
+
- matt@mattryder.co.uk
|
115
|
+
executables: []
|
116
|
+
extensions: []
|
117
|
+
extra_rdoc_files: []
|
118
|
+
files:
|
119
|
+
- lib/tableau/timetable.rb
|
120
|
+
- lib/tableau/class.rb
|
121
|
+
- lib/tableau/uribuilder.rb
|
122
|
+
- lib/tableau/timetableparser.rb
|
123
|
+
- lib/tableau/moduleparser.rb
|
124
|
+
- lib/tableau/baseparser.rb
|
125
|
+
- lib/tableau/classarray.rb
|
126
|
+
- lib/tableau/module.rb
|
127
|
+
- lib/tableau/tablebuilder.rb
|
128
|
+
- lib/tableau/version.rb
|
129
|
+
- lib/tasks/tableau_tasks.rake
|
130
|
+
- lib/tableau.rb
|
131
|
+
- MIT-LICENSE
|
132
|
+
- Rakefile
|
133
|
+
homepage: http://www.github.com/MattRyder/tableau
|
134
|
+
licenses: []
|
135
|
+
metadata: {}
|
136
|
+
post_install_message:
|
137
|
+
rdoc_options: []
|
138
|
+
require_paths:
|
139
|
+
- lib
|
140
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - '>='
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '0'
|
145
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
146
|
+
requirements:
|
147
|
+
- - '>='
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: '0'
|
150
|
+
requirements: []
|
151
|
+
rubyforge_project:
|
152
|
+
rubygems_version: 2.0.3
|
153
|
+
signing_key:
|
154
|
+
specification_version: 4
|
155
|
+
summary: Parses, analyses and creates Timetables for Staffordshire University students.Handles
|
156
|
+
both Course and individual Module timetables.
|
157
|
+
test_files: []
|