Commit c9161a32 authored by Zac Wood's avatar Zac Wood
Browse files

Merge branch 'dev-v2' into 'master'

Dev v2

See merge request !36
parents 8970cc67 160d7746
Pipeline #3490 passed with stage
in 2 minutes and 22 seconds
module SearchHelper
def in_cart?(id)
@cart.include? id.to_s
end
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 courses.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("courses.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
# Contains logic regarding the +Course+ model.
class Course < ApplicationRecord
# Each course belongs to a +Semester+
belongs_to :semester
has_many :course_sections
# Ensure all necessary are fields present.
validates :course_number, presence: true
validates :subject, presence: true
validates :semester_id, presence: true
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
end
# Contains logic belonging to the +CourseSection+ model.
#
# TODO: Add more docs
class CourseSection < ApplicationRecord
# Each +CourseSection+ belongs to a +Course+ and an +Instructor+.
belongs_to :course
......@@ -12,8 +10,47 @@ class CourseSection < ApplicationRecord
validates :title, presence: true
validates :course_id, presence: true
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)
(t1_start <= t2_end && t2_start <= t1_end) && Set.new(days.split).intersect?(Set.new(other.days.split))
end
# Select all course sections that have an instructor that matches the given name
def self.with_instructor(name: "")
joins(:instructor).where("instructors.name LIKE ?", "%#{name}%").select('course_sections.*, instructors.name as instructor_name')
joins(:instructor)
.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
class Instructor < ApplicationRecord
has_many :course_sections
def self.named(name)
where("name LIKE ?", "%#{name}%")
def self.from_name(base_query, name)
base_query.where("upper(instructors.name) LIKE ?", "%#{name.upcase}%")
end
end
require 'icalendar'
require 'time'
# Creates a iCal object given a list of section ids
class Schedule
def initialize(crns)
def initialize(ids)
@cal = Icalendar::Calendar.new
@cal.x_wr_calname = 'GMU Fall 2018'
@cal.x_wr_calname = 'GMU Schedule'
@course_sections = crns.map do |crn|
CourseSection.find_by crn: crn
end
@course_sections = ids.map { |id| CourseSection.find_by_id id }
@course_sections.compact!
load_events
......@@ -27,10 +26,10 @@ class Schedule
@cal.add_event(event)
end
if section.days.start_with? "M"
col_day_makeup = generate_event_after_columbus_day(section)
@cal.add_event(col_day_makeup)
end
# if section.days.start_with? "M"
# col_day_makeup = generate_event_after_columbus_day(section)
# @cal.add_event(col_day_makeup)
# end
end
end
......@@ -95,7 +94,7 @@ class Schedule
# Every section's start_date is the first Monday of the semester.
# So we need to add an exclusion for that day unless the class is held on Mondays
unless section.days.start_with? "M"
unless section.days.start_with? "T"
exdates << generate_exdate(
section.start_date.to_formatted_s(:number),
section.start_time
......@@ -103,12 +102,12 @@ class Schedule
end
# If the section meets on Tuesdays, add an exdate for the day after columbus day
if section.days.start_with? "T"
exdates << generate_exdate(
Date.new(2018, 10, 9).to_formatted_s(:number),
section.start_time
)
end
# if section.days.start_with? "T"
# exdates << generate_exdate(
# Date.new(2018, 10, 9).to_formatted_s(:number),
# section.start_time
# )
# end
exdates
end
......
......@@ -3,12 +3,13 @@
# A +Semester+ is a simple model that consists of a +year+ and a +season+, e.g. "Fall 2018".
class Semester < ApplicationRecord
has_many :courses
has_many :closures
# Ensure necessary fields are present.
validates :year, presence: true
validates :season, presence: true
def courses
Course.where semester_id: id
def to_s
"#{season} #{year}"
end
end
<div class="row">
<div class="col-12 col-lg">
<h1><%= @course.full_name %></h1>
<h4><%= @course.title %></h4>
<div class="d-flex">
<div class="attr-list justify-content-start">
<div class="attr">
<div class="icon">
<i class="fa fa-book"></i>
</div>
<%= @course.credits %> credits
</div>
&nbsp;&nbsp;&nbsp;
<div class="attr">
<div class="icon">
<i class="fa fa-bars"></i>
</div>
<%= @course.course_sections.count %> sections
</div>
</div>
</div>
<p><%= @course.description %></p>
</div>
<div class="col-12 col-lg">
<%= render partial: 'shared/section', collection: @course.course_sections %>
</div>
</div>
<%= javascript_include_tag 'search' %>
<%= stylesheet_link_tag 'search' %>
<div class="jumbotron text-center">
<h1>SRCT Schedules</h1>
<p class="lead">Build, share, and export your schedule. Search for classes and professors.</p>
<hr>
<p>
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>.
</p>
</div>
<div class="row">
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">Powerful search</h5>
<p class="card-text">Want to see what courses your favorite professor is teaching this semester? Or what they've taught in the past? Search for any professor that's taught at Mason easily.</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">Search classes</h5>
<p class="card-text">Looking for cool electives? Want to see who's teaching a class you're looking to take? Search for classes using keywords, subject, or the subject and course number.</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">Export your schedule</h5>
<p class="card-text">Select the courses you're taking this semester and see them in a beautiful calendar view. Easily export your schedule to your favorite calendar apps. Schedules even knows about breaks!</p>
</div>
</div>
</div>
</div>
<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>
<h1>Instructors</h1>
<ul>
<% @instructors.each do |i| %>
<li><%= link_to i.name, instructor_path(i) %></li>
<% end %>
</ul>
<div class="row">
<div class="col-lg-4 col-12">
<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>
<% 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>
<% end %>
</div>
</div>
<%= javascript_include_tag 'search' %>
<%= stylesheet_link_tag 'search' %>
<!DOCTYPE html>
<html lang="en">
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>SRCT Schedules • Welcome</title>
<meta name="theme-color" content="#006633" />
<title>Schedules</title>
<%= csrf_meta_tags %>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<%= javascript_include_tag 'masonstrap.min' %>
<%= javascript_include_tag 'application' %>
<%= stylesheet_link_tag 'masonstrap.min' %>
<%= stylesheet_link_tag 'application' %>
<!-- FB/Opengraph tags -->
<meta property="og:url" content="https://schedules.gmu.edu/">
......@@ -26,24 +30,25 @@
</head>
<body>
<div id="root"></div>
<!-- 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 -->
<%= render partial: 'shared/navbar' %>
<%= 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 -->
</body>
</html>
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment