Commit 41b03a55 authored by Zac Wood's avatar Zac Wood

Turbolinks

parent 0bc0ea22
{
"systemParams": "darwin-x64-72",
"modulesFolders": [
"node_modules"
],
"flags": [],
"linkedModules": [],
"topLevelPatterns": [
"turbolinks@^5.2.0"
],
"lockfileEntries": {
"turbolinks@^5.2.0": "https://registry.yarnpkg.com/turbolinks/-/turbolinks-5.2.0.tgz#e6877a55ea5c1cb3bb225f0a4ae303d6d32ff77c"
},
"files": [],
"artifacts": {}
}
\ No newline at end of file
{
"dependencies": {
"turbolinks": "^5.2.0"
}
}
......@@ -3,6 +3,7 @@
"tabWidth": 4,
"singleQuote": true,
"useTabs": false,
"semi": false,
"jsxBracketSameLine": true,
"trailingComma": "es5"
}
// Place all the styles related to the about controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
......@@ -17,6 +17,7 @@
$gray: #4a4a4a;
$gold: #febf10;
$green: #01693f;
$blue: #297dc5;
* {
font-family: 'Roboto', sans-serif;
......@@ -63,7 +64,7 @@ h6 {
& i {
width: 2em;
margin: auto;
color: #297dc5;
color: $blue;
}
& button {
......@@ -196,3 +197,50 @@ a {
margin-bottom: 0.5em;
}
}
.underline {
text-decoration: underline;
}
.cart {
position: fixed;
right: 5%;
bottom: 3%;
background-color: $green;
width: 3.5em;
height: 3.5em;
border-radius: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
i {
font-size: 1.5em;
margin: auto;
color: white;
}
}
.stats {
margin-bottom: 0.5em;
}
.ratings {
margin-bottom: 0.5em;
}
footer {
margin: 4em auto 1em;
text-align: center;
}
#count {
position: absolute;
top: 0;
left: -0.5em;
color: white;
background-color: $blue;
width: 1.5em;
text-align: center;
border-radius: 33%;
}
#cart {
display: none;
}
#cart-button {
color: black;
}
#cart-button:hover {
transition: 0.15s;
color: green;
}
// Place all the styles related to the course_sections controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
// Place all the styles related to the Courses controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
.section-item.selected {
background-color: lightgreen;
}
.section-item:hover {
transition: 0.15s;
background-color: lightgray;
}
// Place all the styles related to the home controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
// Place all the styles related to the instructors controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
.center-vert {
display: flex;
justify-content: center;
align-items: center;
}
#navbar {
margin-top: 8px;
margin-bottom: 48px;
}
#logo {
font-size: 24pt;
color: black;
white-space: nowrap;
margin-right: 8px;
}
#semester-select {
min-width: 100px;
margin-right: 8px;
}
#cart-button {
margin-top: 24px;
}
#calendar {
background-color: white;
padding: 16px;
margin-bottom: 8px;
margin-top: 8px;
min-width: 1000px;
min-height: 800px;
}
.section-item.selected {
background-color: white;
}
.section-item.selected:hover {
background-color: red;
}
#share-header {
margin-top: 16px;
}
.btn-variant{
background-color: transparent;
text-align: left;
border: none;
padding: 0px;
}
// Place all the styles related to the search controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
// .section-item.selected {
// transition: 0.15s;
// background-color: lightgreen;
// }
// .section-item.selected:hover {
// transition: 0.15s;
// background-color: rgba(255, 0, 0, 0.6);
// }
// Place all the styles related to the Sections controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
......@@ -15,7 +15,7 @@ module BySemester
@semester = if params.key?(:semester_id)
Semester.find_by_id(params[:semester_id])
else
Semester.first
Semester.sorted_by_date.first
end
end
end
......@@ -8,11 +8,22 @@ class CoursesController < ApplicationController
.joins(:semester)
.select("semesters.id")
puts semester_ids.map(&:id)
@semesters = Semester.where(id: semester_ids.map(&:id))
@semesters = Semester.sorted_by_date(@semesters)
@taught_in = Set.new(@semesters.map(&:season))
@taught_in = sort_seasons(@taught_in.to_a).join(", ")
@taught_in = if @taught_in.empty?
"Has not been offered since #{Semester.sorted_by_date.last.to_s}"
else
"Has been offered in #{(@taught_in.to_a).join(", ")}"
end
if @semesters.first != Semester.sorted_by_date.first
@semesters = [Semester.sorted_by_date.first, *@semesters]
end
@sections = @course.course_sections.where(semester: @semester).group_by { |s| s.section_type }
end
......
......@@ -11,31 +11,18 @@ class SearchController < ApplicationController
@instructors = nil
@courses = nil
/[[:alpha:]]{2,4} \d{3}/.match(params[:query]) do |m|
subj, num = m[0].split(' ')
/(?<subj>[[:alpha:]]{2,4}) ?(?<num>\d{3})/.match(params[:query]) do |m|
subj, num = m[:subj], m[:num]
course = Course.find_by(subject: subj.upcase, course_number: num)
redirect_to(course_url(course)) unless course.nil?
end
/[[:alpha:]]{2,4}/i.match(params[:query]) do |m|
@courses = Course.where(subject: m[0].upcase)
.joins(:course_sections)
.merge(CourseSection.in_semester(@semester))
.uniq
@courses = Course.where(subject: m[0].upcase).uniq
if @courses.empty?
@courses = Course.where("(courses.title LIKE ?)", "%#{params[:query]}%")
.joins(:course_sections)
.merge(CourseSection.in_semester(@semester))
.uniq
other = Course.where("(courses.description LIKE ?)", "%#{params[:query]}%")
.joins(:course_sections)
.merge(CourseSection.in_semester(@semester))
.uniq
@courses = [*@courses, *other].uniq
query = "%#{params[:query]}%"
@courses = Course.where("(courses.title LIKE ?) OR (courses.description LIKE ?)", query, query).uniq
@instructors = Instructor.named(params[:query])
end
......@@ -49,9 +36,9 @@ class SearchController < ApplicationController
end
if @courses&.count == 1 && @instructors&.count&.zero?
redirect_to course_url(@courses.first["id"])
redirect_to(course_url(@courses.first["id"]))
elsif @courses&.count&.zero? && @instructors&.count == 1
redirect_to instructor_url(@instructors.first)
redirect_to(instructor_url(@instructors.first))
end
end
end
module SearchHelper
class GenericQueryData
attr_reader :semester
attr_reader :sort_mode
attr_reader :search_string
def initialize(search_string, sort_mode, semester)
@semester = semester
@sort_mode = sort_mode
@search_string = search_string
end
end
class GenericItem
attr_reader :data
attr_reader :type
def initialize(type, data)
@type = type
@data = data
end
def self.fetchall(search_string, sort_mode: :auto, semester: :fall2018)
query_data = GenericQueryData.new(search_string, sort_mode, semester)
models = []
models += fetch_instructors(query_data)
models += fetch_courses(query_data)
build_list(models)
end
def self.fetch_instructors(query_data)
Instructor.from_name(Instructor.select("instructors.*, COUNT(courses.id) AS section_count").from("course_sections"), query_data.search_string)
.joins("LEFT OUTER JOIN instructors ON instructors.id = course_sections.instructor_id")
.joins("LEFT OUTER JOIN courses ON courses.id = course_sections.course_id AND course_sections.semester_id = #{query_data.semester.id}")
.group("instructors.id").all
end
def self.fetch_courses(query_data)
query_string = query_data.search_string
query_string.upcase!
CourseReplacementHelper.replace!(query_string)
base_query = Course.select("courses.*, count(course_sections.id) AS section_count")
.left_outer_joins(:course_sections)
.having("count(course_sections.id) > 0")
.where("course_sections.semester_id = ?", query_data.semester)
.group("courses.id")
subj = nil
query_string.scan(/(?<= |^)([a-zA-Z]{2,4})(?=$| )/).each do |a|
s = a[0]
next unless get_count(Course.from_subject(base_query, s)).positive?
# next unless Course.from_subject(base_query, s).count.positive?
subj = s
base_query = Course.from_subject(base_query, subj)
query_string.remove!(s)
end
query_string.scan(/(?<= |^)(\d{3})(?=$| )/).each do |a|
s = a[0]
next unless !subj.nil? && get_count(Course.from_course_number(base_query, s)).positive?
# next unless !subj.nil? && Course.from_course_number(base_query, s).count.positive?
base_query = Course.from_course_number(base_query, s)
return base_query.all
end
stripped_query_string = query_string.gsub(/ +/, " ").strip
# There's more to parse
base_query = if stripped_query_string.length.positive?
Course.from_title(base_query, stripped_query_string)
.order("section_count DESC")
else
base_query.order("courses.course_number ASC")
end
base_query.all
end
# Given a set of models, create a list of GenericItems for each model's data
def self.build_list(models)
list = []
models.each do |model|
list.push(GenericItem.new(model.class.name.underscore.to_sym, model))
end
list
end
def self.get_count(base_query)
# I think I finally hit a limit of active record
ActiveRecord::Base.connection.execute("SELECT COUNT(*) AS count FROM (#{base_query.to_sql}) as q")[0]["count"]
end
def to_s
@type
end
end
end
......@@ -7,28 +7,74 @@
// To reference this file, add <%= javascript_pack_tag 'application' %> to the appropriate
// layout file, like app/views/layouts/application.html.erb
import 'url-polyfill';
import 'url-polyfill'
import Turbolinks from 'turbolinks'
window.addEventListener('DOMContentLoaded', () => {
Turbolinks.start()
setLinks()
addListeners()
})
function setLinks(crns) {}
function addListeners() {
const links = Array.from(document.querySelectorAll('.add-section'))
for (const link of links) {
link.addEventListener('click', e => {
e.preventDefault()
const crn = link.dataset.crn
toggleSection(crn)
console.log(getCart())
writeLinks()
})
}
}
function getCart() {
return JSON.parse(localStorage.getItem('cart') || '[]')
}
function toggleSection(crn) {
if (getCart().includes(crn)) {
removeSection(crn)
} else {
addSection(crn)
}
}
function addSection(crn) {
const newCart = [...getCart(), crn]
localStorage.setItem('cart', JSON.stringify(newCart))
}
function removeSection(crn) {
const newCart = getCart().filter(c => c !== crn)
localStorage.setItem('cart', JSON.stringify(newCart))
}
function writeLinks() {}
const elementFromString = string => {
const html = new DOMParser().parseFromString(string, 'text/html');
return html.body.firstChild;
};
const html = new DOMParser().parseFromString(string, 'text/html')
return html.body.firstChild
}
if (!HTMLCanvasElement.prototype.toBlob) {
Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
value: function(callback, type, quality) {
var canvas = this;
var canvas = this
setTimeout(function() {
var binStr = atob(canvas.toDataURL(type, quality).split(',')[1]),
len = binStr.length,
arr = new Uint8Array(len);
arr = new Uint8Array(len)
for (var i = 0; i < len; i++) {
arr[i] = binStr.charCodeAt(i);
arr[i] = binStr.charCodeAt(i)
}
callback(new Blob([arr], { type: type || 'image/png' }));
});
callback(new Blob([arr], { type: type || 'image/png' }))
})
},
});
})
}
......@@ -42,7 +42,8 @@ class CourseSection < ApplicationRecord
end
def self.latest_by_crn(crn)
where(crn: crn).min_by { |s| s.semester.id }
sems = Semester.sorted_by_date
where(crn: crn).min_by { |s| sems.find_index(s) }
end
# Select all course sections that have an instructor that matches the given name
......@@ -51,34 +52,4 @@ class CourseSection < ApplicationRecord
.where("instructors.name LIKE ?", "%#{name}%")
.select('course_sections.*, instructors.name as instructor_name')
end
def self.from_crn(base_query, crn)
base_query.where(crn: crn)
end
def self.from_course_id(base_query, course_id)
base_query.where(course_id: course_id)
end
# Select all revelevant course sections given the provided filters
def self.fetch(filters)
query = CourseSection.joins(:course).select("course_sections.*")
filters.each do |filter, value|
case filter
when "crn"
query = from_crn(query, value)
when "course_id"
query = from_course_id(query, value)
when "course_number"
query = Course.from_course_number(query, value)
when "subject"
query = Course.from_subject(query, value)
when "title"
query = Course.from_title(query, value)
end
end
query
end
end
......@@ -16,7 +16,7 @@
</p>
<p>
<i class="fas fa-clock"></i>
<span>Has been offered in <%= @taught_in %></span>
<span><%= @taught_in %></span>
</p>
</div>
......@@ -48,5 +48,3 @@
<% end %>
</div>
</div>
<%= stylesheet_link_tag 'search' %>
......@@ -35,5 +35,3 @@
<% end %>
</div>
</div>
<%= stylesheet_link_tag 'search' %>
......@@ -5,17 +5,13 @@
<%= csrf_meta_tags %>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<%# <%= Gon::Base.render_data %>
<!-- Bootstrap -->
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<%= stylesheet_link_tag 'application' %>
<%= javascript_pack_tag 'application' %>
<!-- FontAwesome icons -->
<script src="https://kit.fontawesome.com/ea45f7e94d.js" crossorigin="anonymous"></script>
<!-- Fonts -->
......@@ -42,29 +38,29 @@
<%= favicon_link_tag %>
<link href="<%= asset_path 'favicon-32x32.png' %>" sizes="32x32" rel="shortcut icon" type="image/png" />
<!-- Custom styles and code -->
<%= stylesheet_link_tag('application') %>
<%= javascript_pack_tag('application') %>
</head>
<body>
<%# <%= render partial: 'shared/navbar' %>
<%= render 'shared/page' do %>
<%= yield %>
<% end %>
<%= render 'shared/page' do %>
<%= yield %>
<% end %>
<!-- Matomo -->
<script type="text/javascript">
var _paq = _paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="//matomo.srct.gmu.edu/";
_paq.push(['setTrackerUrl', u+'piwik.php']);
_paq.push(['setSiteId', '3']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<!-- End Matomo Code -->
<!-- Matomo -->
<script type="text/javascript">
var _paq = _paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="//matomo.srct.gmu.edu/";
_paq.push(['setTrackerUrl', u+'piwik.php']);
_paq.push(['setSiteId', '3']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s);
})();
</script>
</body>
</html>
<nav>
<span><a href="/"><i class="fas fa-calendar-alt"></i> Schedules</a></span>
<form id="search-container" action="/search" method="GET">
<input name="query" placeholder="PSYC, CS 112, 71926, Jonathan Bell, ..." value="<%= params[:query] %>"/>
<button type="submit">
<i class="fas fa-search"></i>
</button>
</form>
</nav>
<%= render(partial: 'shared/navbar') %>
<div class="search-results">
<% @courses.each do |course| %>
......@@ -19,5 +11,3 @@
</section>
<% end %>
</div>
<%= stylesheet_link_tag 'search' %>
<nav>
<span><a href="/"><i class="fas fa-calendar-alt"></i> Schedules</a></span>
<form id="search-container" action="/search" method="GET">
<input aria-label="Search" name="query" placeholder="PSYC, CS 112, 71926, Jonathan Bell, ..." value="<%= params[:query] %>"/>
<input id="search-input" aria-label="Search" name="query" placeholder="PSYC, CS 112, 71926, Jonathan Bell, ..." value="<%= params[:query] %>"/>
<button aria-label="Submit" type="submit">
<i class="fas fa-search"></i>
</button>
......
<main class="container">
<!-- The main screen consists of a row with two columns: the search results, and the cart -->
<div id="page" class="row">
<!-- Search result, List of Courses -->
<div class="col-lg-12 col-12 col-sm-12 mx-auto order-2 order-lg-0" id="search-list">
<%= yield %>
</div>
<%# <div id="page" class="row"> %>
<!-- Search result, List of Courses -->
<div class="col-lg-12 col-12 col-sm-12 mx-auto order-2 order-lg-0" id="search-list">
<%= yield %>
</div>
<%# </div> %>
<!-- List of sections in the cart -->
</div>
<div class="cart">
<span id="count">1</span>
<i class="fas fa-shopping-cart"></i>
</div>
<%# <footer class="footer">
Schedules was built by <a href="https://srct.gmu.edu">Mason SRCT</a> and is completely open source. <br/>
Want to contribute? View the code on the <a href="https://git.gmu.edu/srct/schedules">SRCT GitLab</a>.<br/>
</footer> %>
<footer class="footer">
Schedules was built by <a href="https://srct.gmu.edu">Mason SRCT</a> and is completely open source. <br/>
Want to contribute? View the code on the <a href="https://git.gmu.edu/srct/schedules">SRCT GitLab</a>.<br/>
</footer>
</main>
\ No newline at end of file
<li id="section-<%= section.id %>" class="list-group-item card section-item">
<li id="section-<%= section.id %>" data-crn="<%= "#{section.crn}" %>" class="list-group-item card section-item">
<p><strong class="subj"><%= "#{section.name}" %></strong>: <%= section.title %> <em><%= "(##{section.crn})"%></em></p>
<i class="fas fa-chalkboard-teacher"></i>
<% if section.instructor.name == "TBA" %>
TBA
<% else %>
<%= link_to section.instructor.name, section.instructor %>
<% unless section.instructor.rating.nil? %>
<%= render partial: 'shared/stars', locals: { percent: (section.instructor.rating[0] / 5 * 100).to_i }%>
<% end %>
<% end %>
<br/>
<i class="fas fa-map-marker-alt"></i> <%= section.location %> <br/>
<i class="fas fa-clock"></i> <%= "#{section.days}, #{section.start_time}-#{section.end_time}" %> <br/>
<div class="stats">
<i class="fas fa-chalkboard-teacher"></i>