Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
SRCT
schedules
Commits
cc42e1da
Commit
cc42e1da
authored
May 11, 2019
by
Zac Wood
Browse files
Added basic favicons.
Should still add more for mobile devices + PWA support
parent
4d7d8564
Changes
84
Hide whitespace changes
Inline
Side-by-side
schedules/app/javascript/src/SearchList.jsx
0 → 100644
View file @
cc42e1da
import
React
from
'
react
'
;
import
CourseList
from
'
src/CourseList
'
;
import
InstructorList
from
'
src/InstructorList
'
;
export
default
class
SearchList
extends
React
.
Component
{
render
()
{
return
(
<
div
>
<
InstructorList
instructors
=
{
this
.
props
.
instructors
}
/>
<
CourseList
courses
=
{
this
.
props
.
courses
}
/>
</
div
>
);
}
}
schedules/app/javascript/src/Section.jsx
0 → 100644
View file @
cc42e1da
import
React
from
'
react
'
;
import
Cart
from
'
src/Cart
'
;
import
Stars
from
'
src/Stars
'
;
export
default
class
Section
extends
React
.
Component
{
constructor
(
props
)
{
super
(
props
);
this
.
state
=
{
inCart
:
Cart
.
includesCrn
(
this
.
props
.
crn
)
};
}
onClick
=
e
=>
{
e
.
stopPropagation
();
console
.
log
(
e
.
target
.
tagName
);
if
(
e
.
target
.
tagName
===
'
A
'
)
return
;
Cart
.
toggleCrn
(
this
.
props
.
crn
);
this
.
setState
({
inCart
:
Cart
.
includesCrn
(
this
.
props
.
crn
)
});
this
.
props
.
onClick
&&
this
.
props
.
onClick
(
this
.
props
.
crn
);
};
render
()
{
const
{
name
,
title
,
crn
,
instructor_name
,
instructor_url
,
teaching_rating
,
course_rating
,
location
,
days
,
start_time
,
end_time
}
=
this
.
props
;
const
{
inCart
}
=
this
.
state
;
const
percent
=
teaching_rating
?
<
Stars
percent
=
{
(
teaching_rating
[
0
]
/
5
)
*
100
}
/>
:
null
;
const
remove
=
(
<
span
className
=
"float-right text-center add-remove-btn"
style
=
{
inCart
?
{}
:
{
display
:
'
none
'
}
}
>
<
i
id
=
"icon"
className
=
"fas fa-minus"
/>
<
br
/>
<
span
className
=
"text"
>
Remove
</
span
>
</
span
>
);
const
add
=
(
<
span
className
=
"float-right text-center add-remove-btn"
style
=
{
inCart
?
{
display
:
'
none
'
}
:
{}
}
>
<
i
id
=
"icon"
className
=
"fas fa-plus"
/>
<
br
/>
<
span
className
=
"text"
>
Add
</
span
>
</
span
>
);
return
(
<
li
className
=
"list-group-item section-item"
onClick
=
{
this
.
onClick
}
>
<
p
>
<
b
>
{
name
}
</
b
>
:
{
title
}{
'
'
}
<
em
>
(#
{
crn
}
)
</
em
>
</
p
>
{
remove
}
{
add
}
<
i
className
=
"fas fa-chalkboard-teacher"
/>
<
a
href
=
{
instructor_url
}
>
{
instructor_name
}
</
a
>
{
percent
}
<
br
/>
<
i
className
=
"fas fa-map-marker-alt"
/>
{
location
}
<
br
/>
<
i
className
=
"fas fa-clock"
/>
{
days
}
,
{
start_time
}
-
{
end_time
}
<
br
/>
</
li
>
);
}
}
schedules/app/javascript/src/SectionList.jsx
0 → 100644
View file @
cc42e1da
import
React
from
'
react
'
;
import
Chevron
from
'
src/Chevron
'
;
import
Section
from
'
src/Section
'
;
export
default
class
SectionList
extends
React
.
Component
{
constructor
(
props
)
{
super
(
props
);
}
render
()
{
return
(
<
div
>
{
this
.
props
.
expandable
?
<
Chevron
open
=
{
this
.
props
.
expanded
}
/>
:
null
}
{
this
.
props
.
expanded
?
(
<
div
className
=
"d-flex list-group list-group-flush sections"
>
{
this
.
props
.
sections
.
map
(
section
=>
(
<
Section
key
=
{
section
.
id
}
onClick
=
{
this
.
props
.
onClick
}
{
...
section
}
/>
))
}
</
div
>
)
:
(
<
div
/>
)
}
</
div
>
);
}
}
schedules/app/javascript/src/Stars.jsx
0 → 100644
View file @
cc42e1da
import
React
from
'
react
'
;
export
default
class
Stars
extends
React
.
Component
{
render
()
{
return
(
<
div
className
=
"star-rating"
>
<
div
className
=
"back-stars"
>
<
i
className
=
"fas fa-star"
aria
-
hidden
=
"true"
/>
<
i
className
=
"fas fa-star"
aria
-
hidden
=
"true"
/>
<
i
className
=
"fas fa-star"
aria
-
hidden
=
"true"
/>
<
i
className
=
"fas fa-star"
aria
-
hidden
=
"true"
/>
<
i
className
=
"fas fa-star"
aria
-
hidden
=
"true"
/>
<
div
className
=
"front-stars"
style
=
{
{
width
:
`
${
this
.
props
.
percent
}
%`
}
}
>
<
i
className
=
"fa fa-star"
aria
-
hidden
=
"true"
/>
<
i
className
=
"fa fa-star"
aria
-
hidden
=
"true"
/>
<
i
className
=
"fa fa-star"
aria
-
hidden
=
"true"
/>
<
i
className
=
"fa fa-star"
aria
-
hidden
=
"true"
/>
<
i
className
=
"fa fa-star"
aria
-
hidden
=
"true"
/>
</
div
>
</
div
>
</
div
>
);
}
}
schedules/app/javascript/src/Toolbar.jsx
0 → 100644
View file @
cc42e1da
import
React
from
'
react
'
;
import
BigCalendar
from
'
react-big-calendar
'
;
import
Toolbar
from
'
react-big-calendar/lib/Toolbar
'
;
import
'
!style-loader!css-loader!react-big-calendar/lib/css/react-big-calendar.css
'
;
import
withSizes
from
'
react-sizes
'
;
class
CustomToolbar
extends
Toolbar
{
render
()
{
const
{
label
,
isMobile
}
=
this
.
props
;
if
(
isMobile
&&
label
===
''
)
{
this
.
view
(
'
day
'
);
}
return
(
<
div
className
=
"rbc-toolbar d-flex justify-content-between"
>
{
!
isMobile
&&
(
<
span
className
=
"rbc-btn-group"
>
<
button
type
=
"button"
onClick
=
{
()
=>
this
.
view
(
'
day
'
)
}
>
Day
</
button
>
<
button
type
=
"button"
onClick
=
{
()
=>
this
.
view
(
'
week
'
)
}
>
Week
</
button
>
</
span
>
)
}
<
span
className
=
"rbc-toolbar-label"
>
{
this
.
props
.
label
}
</
span
>
{
this
.
props
.
view
===
'
day
'
&&
(
<
span
className
=
"rbc-btn-group"
>
{
this
.
props
.
label
!==
'
Sun
'
&&
(
<
button
type
=
"button"
onClick
=
{
()
=>
this
.
navigate
(
'
PREV
'
)
}
>
Back
</
button
>
)
}
{
this
.
props
.
label
!==
'
Sat
'
&&
(
<
button
type
=
"button"
onClick
=
{
()
=>
this
.
navigate
(
'
NEXT
'
)
}
>
Next
</
button
>
)
}
</
span
>
)
}
</
div
>
);
}
navigate
=
action
=>
{
console
.
log
(
action
);
this
.
props
.
onNavigate
(
action
);
};
view
=
action
=>
{
this
.
props
.
onView
(
action
);
};
}
const
mapSizesToProps
=
({
width
})
=>
({
isMobile
:
width
<
1000
,
});
export
default
withSizes
(
mapSizesToProps
)(
CustomToolbar
);
schedules/app/javascript/src/cart.js
View file @
cc42e1da
//import '@babel/polyfill';
class
Cart
{
constructor
()
{
this
.
isOpen
=
false
;
this
.
_courses
=
[];
const
cartData
=
document
.
getElementById
(
'
cart-data
'
);
if
(
cartData
)
{
this
.
_courses
=
JSON
.
parse
(
cartData
.
dataset
.
cart
);
}
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
(
document
.
getElementById
(
'
cart-counter
'
).
innerText
=
this
.
crns
.
length
));
}
_parseData
()
{
const
cartData
=
document
.
getElementById
(
'
cart-data
'
);
if
(
cartData
)
{
this
.
_courses
=
JSON
.
parse
(
cartData
.
dataset
.
cart
);
}
get
crns
()
{
const
crnString
=
localStorage
.
getItem
(
'
crns
'
);
if
(
!
crnString
)
return
[];
return
JSON
.
parse
(
crnString
);
}
toggle
()
{
const
list
=
document
.
getElementById
(
'
cart
'
);
const
icon
=
document
.
getElementById
(
'
schedule-icon
'
);
if
(
this
.
isOpen
)
{
list
.
style
.
display
=
'
none
'
;
icon
.
style
.
color
=
'
black
'
;
}
else
{
list
.
style
.
display
=
'
block
'
;
icon
.
style
.
color
=
'
green
'
;
}
this
.
isOpen
=
!
this
.
isOpen
;
set
crns
(
crnList
)
{
localStorage
.
setItem
(
'
crns
'
,
JSON
.
stringify
(
crnList
));
document
.
getElementById
(
'
cart-counter
'
).
innerText
=
crnList
.
length
;
}
set
courses
(
courses
)
{
this
.
_courses
=
courses
;
for
(
const
courseId
in
this
.
_courses
)
{
if
(
this
.
_courses
[
courseId
].
length
===
0
)
delete
this
.
_courses
[
courseId
];
addCrn
(
crn
)
{
if
(
!
this
.
includesCrn
(
crn
))
{
this
.
crns
=
[...
this
.
crns
,
crn
];
}
document
.
getElementById
(
'
course-counter
'
).
innerText
=
Object
.
keys
(
this
.
_courses
).
length
;
}
async
toggleSection
(
section
)
{
const
resp
=
await
fetch
(
`/sessions/cart?&crn=
${
section
.
crn
}
`
,
{
cache
:
'
no-store
'
,
credentials
:
'
same-origin
'
});
const
json
=
await
resp
.
json
();
this
.
courses
=
json
;
}
includesSection
(
obj
)
{
for
(
const
key
in
this
.
_courses
)
{
const
list
=
this
.
_courses
[
key
];
if
(
list
.
includes
(
obj
.
crn
))
return
true
;
toggleCrn
(
crn
)
{
if
(
!
this
.
includesCrn
(
crn
))
{
this
.
crns
=
[...
this
.
crns
,
crn
];
}
else
{
this
.
crns
=
this
.
crns
.
filter
(
c
=>
c
!=
crn
);
}
}
return
false
;
includesCrn
(
crn
)
{
return
this
.
crns
.
filter
(
c
=>
c
==
crn
).
length
>
0
;
}
}
const
cart
=
new
Cart
();
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
cart
.
_parseData
());
export
default
cart
;
export
default
new
Cart
();
schedules/app/models/course.rb
View file @
cc42e1da
...
...
@@ -9,53 +9,4 @@ class Course < ApplicationRecord
def
full_name
"
#{
subject
}
#{
course_number
}
"
end
def
self
.
from_subject
(
base_query
,
subject
)
base_query
.
where
(
"courses.subject = ?"
,
subject
.
upcase
)
end
def
self
.
from_course_number
(
base_query
,
course_number
)
base_query
.
where
(
"courses.course_number = ?"
,
course_number
)
end
def
self
.
from_title
(
base_query
,
title
)
puts
title
# Temporary really disgusting regex that I hate with all my heart
title
=
(
title
+
" "
).
upcase
.
gsub
(
/(I+) +/
,
'\1$'
).
gsub
(
/ +/
,
"% "
).
tr
(
'$'
,
' '
)
base_query
.
where
(
"UPPER(courses.title) LIKE UPPER(?) or UPPER(courses.title) LIKE UPPER(?)"
,
"%
#{
title
.
strip
}
"
,
"%
#{
title
}
%"
)
end
# Given a list of filters, collect a list of matching elements. This makes it
# so you can just pass the arguments straight thru
def
self
.
fetch
(
filters
)
# join with course_sections so that we can get a section count for each course and then sort by that
query
=
Course
.
left_outer_joins
(
:course_sections
)
.
select
(
"courses.*, COUNT(course_sections.id) AS section_count"
)
.
group
(
"courses.id"
)
.
order
(
"section_count DESC"
)
filters
.
each
do
|
filter
,
value
|
case
filter
when
"subject"
query
=
from_subject
(
query
,
value
)
when
"course_number"
query
=
from_course_number
(
query
,
value
)
when
"title"
query
=
from_title
(
query
,
value
)
when
"instructor"
query
=
Instructor
.
from_name
(
query
.
joins
(
"INNER JOIN instructors ON course_sections.instructor_id = instructors.id"
),
value
)
end
end
query
end
# build_set builds
def
self
.
build_set
(
sections
)
courses
=
[].
to_set
sections
.
each
do
|
s
|
courses
.
add
s
.
course
end
courses
end
end
schedules/app/models/course_section.rb
View file @
cc42e1da
...
...
@@ -14,6 +14,26 @@ class CourseSection < ApplicationRecord
validates
:course_id
,
presence:
true
validates
:semester_id
,
presence:
true
serialize
:rating_questions
,
Array
scope
:in_semester
,
->
(
semester
)
{
where
(
semester:
semester
)
}
def
teaching_rating
if
rating_questions
.
empty?
nil
else
"
#{
rating_questions
[
0
][
'instr_mean'
]
}
/
#{
rating_questions
[
0
][
'resp'
]
}
responses"
end
end
def
course_rating
if
rating_questions
.
empty?
nil
else
"
#{
rating_questions
[
1
][
'instr_mean'
]
}
/
#{
rating_questions
[
1
][
'resp'
]
}
responses"
end
end
def
overlaps?
(
other
)
t1_start
,
t1_end
=
Time
.
parse
(
start_time
),
Time
.
parse
(
end_time
)
t2_start
,
t2_end
=
Time
.
parse
(
other
.
start_time
),
Time
.
parse
(
other
.
end_time
)
...
...
schedules/app/models/instructor.rb
View file @
cc42e1da
class
Instructor
<
ApplicationRecord
has_many
:course_sections
scope
:named
,
->
(
name
)
{
name
.
split
(
' '
).
reduce
(
all
)
do
|
query
,
comp
|
query
.
where
(
"upper(instructors.name) LIKE ?"
,
"%
#{
comp
.
upcase
}
%"
)
end
}
def
self
.
from_name
(
base_query
,
name
)
base_query
.
where
(
"upper(instructors.name) LIKE ?"
,
"%
#{
name
.
upcase
}
%"
)
end
def
rating
(
question
=
0
,
sections
=
CourseSection
.
where
(
instructor_id:
id
))
total
=
0
resp
=
0
sections
.
each
do
|
s
|
next
if
s
.
rating_questions
.
empty?
resp
+=
s
.
rating_questions
[
question
][
"resp"
].
to_i
total
+=
s
.
rating_questions
[
question
][
"instr_mean"
].
to_f
*
s
.
rating_questions
[
0
][
"resp"
].
to_i
end
[(
total
/
resp
).
round
(
2
),
resp
]
unless
resp
.
zero?
end
end
schedules/app/views/about/index.html.erb
0 → 100644
View file @
cc42e1da
<div
class=
"jumbotron text-center"
>
<h1><i
class=
"fas fa-calendar-alt"
></i>
SRCT Schedules
</h1>
<p
class=
"lead"
>
Version 3.0
</p>
<hr
/>
Last updated: 2:00am, 4/14/19
</div>
<h3>
Thank you to our contributors who make Schedules possible!
</h3>
Zac Wood, David Haynes, Zach Perkins, Gilberto Barrientos, Michael Bailey, Nic Anderson
<br
/><br/>
<h3>
Questions?
</h3>
All data in Schedules is sourced from data made publicly avaiable by GMU.
<ul>
<li>
Course and section data can be found on
<a
href=
"https://patriotweb.gmu.edu/pls/prod/bwckschd.p_disp_dyn_sched"
>
Patriot Web
</a></li>
<li>
Course review data can be found
<a
href=
"https://crserating.gmu.edu/ReportOnline/"
>
here
</a></li>
</ul>
Please contact SRCT at
<a
href=
"mailto:srct@gmu.edu"
>
srct@gmu.edu
</a>
with any other questions.
schedules/app/views/course_sections/show.html.erb
0 → 100644
View file @
cc42e1da
<h1>
<%=
@section
.
name
%>
-
<%=
@section
.
semester
.
to_s
%>
-
<%=
@section
.
instructor
.
name
%>
</h1>
<ol>
<%
@section
.
rating_questions
.
each
do
|
q
|
%>
<b>
<li>
<%=
q
[
"q"
]
%>
</li>
</b>
Instructor mean:
<%=
q
[
"instr_mean"
]
%>
, Responses:
<%=
q
[
"resp"
]
%>
<%
end
%>
</ol>
schedules/app/views/courses/show.html.erb
View file @
cc42e1da
...
...
@@ -29,5 +29,5 @@
</div>
<%=
javascript_pack_tag
'
search
'
%>
<%=
javascript_pack_tag
'
instructor
'
%>
<%=
stylesheet_link_tag
'search'
%>
schedules/app/views/home/index.html.erb
View file @
cc42e1da
<div
class=
"jumbotron text-center"
>
<h1>
SRCT Schedules
</h1>
<h1>
<i
class=
"fas fa-calendar-alt"
></i>
SRCT Schedules
</h1>
<p
class=
"lead"
>
Build, share, and export your schedule. Search for classes and professors.
</p>
<hr>
<p>
...
...
@@ -36,24 +36,6 @@
</div>
</div>
<div
id=
"quick-add"
/>
<h3
class=
"quick-add-header"
>
Quick add
</h3>
<p>
Want to quickly generate a calendar populated with your semester's classes? Enter the CRNs in a comma separated list below.
</p>
<form
action=
"/sessions/add_bulk"
class=
"form"
>
<div
class=
"input-group"
>
<input
id=
"crns"
name=
"crns"
type=
"text"
class=
"form-control"
placeholder=
"12345,54321,..."
aria-describedby=
"basic-addon2"
autocomplete=
"off"
>
<div
class=
"input-group-append"
>
<button
type=
"submit"
class=
"btn btn-primary"
type=
"button"
>
Populate Calendar
</button>
</div>
</div>
</form>
<%=
javascript_pack_tag
'home'
%>
schedules/app/views/instructors/show.html.erb
View file @
cc42e1da
<div
class=
"row"
>
<div
class=
"col-lg-4 col-12"
>
<div
class=
"col-lg-4 col-12
mb-4
"
>
<h1>
<%=
@instructor
.
name
%>
</h1>
<%
if
@past
.
count
.
positive?
%>
<strong>
Previously taught:
</strong>
<ul>
<%
@past
.
each
do
|
c
|
%>
<li>
<%=
link_to
(
c
.
full_name
,
course_path
(
c
))
%>
</li>
<%
end
%>
</ul>
<%
unless
@rating
[
:teaching
].
nil?
%>
Average teaching rating:
<%=
@rating
[
:teaching
][
0
]
%>
/
<%=
@rating
[
:teaching
][
1
]
%>
responses
<%
end
%>
</div>
<div
class =
"col-lg-8 col-12"
>
<h3>
<%=
@semester
.
to_s
%>
</h3>
<%
if
@courses
.
any?
%>
<%=
render
(
partial:
'shared/course'
,
collection:
@courses
,
locals:
{
expanded:
true
})
%>
<%
else
%>
<p>
<%=
@instructor
.
name
%>
is not teaching any courses this semester...
</p>
<div
class=
"col-lg-8 col-12"
>
<%
@semesters
.
each
do
|
semester
,
sections
|
%>
<h2>
<%=
semester
%>
</h2>
<%=
render
(
partial:
'shared/section'
,
collection:
sections
)
%>
<br/>
<%
end
%>
</div>
</div>
<%=
javascript_pack_tag
'
search
'
%>
<%=
javascript_pack_tag
'
instructor
'
%>
<%=
stylesheet_link_tag
'search'
%>
schedules/app/views/layouts/application.html.erb
View file @
cc42e1da
...
...
@@ -5,6 +5,8 @@
<%=
csrf_meta_tags
%>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1.0"
>
<%=
Gon
::
Base
.
render_data
%>
<%=
javascript_include_tag
'masonstrap.min'
%>
<%=
stylesheet_link_tag
'masonstrap.min'
%>
...
...
@@ -16,7 +18,7 @@
<meta
property=
"og:url"
content=
"https://schedules.gmu.edu/"
>
<meta
property=
"og:type"
content=
"website"
>
<meta
property=
"og:title"
content=
"SRCT Schedules"
>
<meta
property=
"og:description"
content=
"
Easily generate a calendar with your class
schedule."
>
<meta
property=
"og:description"
content=
"
Browse the GMU catalog, see course reviews, build and share your
schedule."
>
<meta
property=
"og:site_name"
content=
"SRCT Schedules"
>
<meta
property=
"og:locale"
content=
"en_US"
>
<meta
property=
"article:author"
content=
"SRCT"
>
...
...
@@ -27,7 +29,11 @@
<meta
name=
"twitter:creator"
content=
"@MasonSRCT"
>
<meta
name=
"twitter:url"
content=
"https://schedules.gmu.edu/"
>
<meta
name=
"twitter:title"
content=
"SRCT Schedules"
>
<meta
name=
"twitter:description"
content=
"Easily generate a calendar with your class schedule."
>
<meta
name=
"twitter:description"
content=
"Browse the GMU catalog, see course reviews, build and share your schedule."
>
<!-- favicons -->
<%=
favicon_link_tag
%>
<link
href=
"
<%=
asset_path
'favicon-32x32.png'
%>
"
sizes=
"32x32"
rel=
"shortcut icon"
type=
"image/png"
/>
</head>
<body>
...
...
schedules/app/views/schedules/show.html.erb
View file @
cc42e1da
<%=
javascript_pack_tag
'schedules'
%>
<%=
stylesheet_link_tag
'schedules'
%>
<%=
javascript_include_tag
'moment.min'
%>
<%=
stylesheet_link_tag
'fullcalendar.min'
%>
<button
id=
"open-modal-btn"
type=
"button"
class=
"btn btn-primary"
data-toggle=
"modal"
data-target=
"#exportModal"
>
Export Schedule
</button>
<button
id=
"save-image"
class=
"btn btn-secondary"
>
Save Image
</button>
<div
id=
"
calendar
"
></div>
<div
id=
"
root
"
></div>
<h3>
Quick add
</h3>
<p>
Populate your calendar quickly by entering a comma separated list of CRNs.
</p>
<form
action=
"/sessions/add_bulk"
class=
"form"
>
<div
class=
"input-group"
>
<input
id=
"crns"
name=
"crns"
type=
"text"
class=
"form-control"
placeholder=
"12345,54321,..."
aria-describedby=
"basic-addon2"
autocomplete=
"off"
>
<div
class=
"input-group-append"
>
<button
type=
"submit"
class=
"btn btn-primary"
type=
"button"
>
Populate Calendar
</button>
</div>
</div>
</form>
<h3
id=
"share-header"
>
Share
</h3>
Want to share your schedule with your friends? Send them this link:
<br/>
<a
id=
"share-url"
></a>
<template
id=
"events"
data-events=
"
<%=
@events
.
to_json
%>
"
></template>
<hr
/>
<h2>
Selected Courses
</h2>
<%=
render
partial:
'shared/section'
,
collection:
@all
%>
<!-- Export Modal -->
...
...
schedules/app/views/search/index.html.erb
View file @
cc42e1da
<%
unless
@instructors
.
nil?
%>
<h2>
Instructors
</h2>
<div
class=
"row"
>
<%
@instructors
.
each
do
|
instructor
|
%>
<div
class=
"col"
>
<%=
render
partial:
'shared/instructor'
,
object:
instructor
%>
</div>
<%
end
%>
</div>
<hr
/>
<