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

Merge branch 'revert-ea27d283' into 'master'

Revert "Merge branch '44-instructor-search' into 'master'"

See merge request !49
parents ea27d283 5ca634f2
Pipeline #4315 passed with stage
in 3 minutes and 51 seconds
......@@ -2,57 +2,8 @@ class SearchController < ApplicationController
def index
redirect_to(home_url) unless params[:query].length > 1
if params[:query].casecmp('god').zero?
bell = Instructor.find_by_name('Jonathan Bell')
redirect_to(instructor_url(bell))
end
@instructors = nil
@courses = nil
/[[:alpha:]]{2,4} \d{3}/.match(params[:query]) do |m|
subj, num = m[0].split(' ')
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
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
@instructors = Instructor.named(params[:query])
end
@courses.map! do |c|
c.serializable_hash.merge(url: course_url(c))
end
gon.courses = @courses
gon.instructors = @instructors
end
/[0-9]{5}/.match(params[:query]) do |m|
redirect_to(course_url(CourseSection.latest_by_crn(m[0]).course))
end
if @courses&.count == 1 && @instructors&.count&.zero?
redirect_to course_url(@courses.first["id"])
elsif @courses&.count&.zero? && @instructors&.count == 1
redirect_to instructor_url(@instructors.first)
end
results = SearchHelper::GenericItem.fetchall(String.new(params[:query]), semester: @semester).group_by(&:type)
@instructors = results[:instructor]&.map(&:data)
@courses = results[:course]&.map(&:data)
end
end
......@@ -19,9 +19,7 @@ module SchedulesHelper
{
title: s.name,
start: "#{formatted_date}T#{time}",
end: "#{formatted_date}T#{endtime}",
crn: s.crn,
active: true
end: "#{formatted_date}T#{endtime}"
}
end
end.flatten
......
module SearchHelper
def in_cart?(crn)
@cart.include? crn.to_s
end
class GenericQueryData
attr_reader :semester
attr_reader :sort_mode
......
......@@ -10,9 +10,6 @@
import '@babel/polyfill';
import 'url-polyfill';
import React from 'react';
import Cart from 'src/Cart';
const elementFromString = string => {
const html = new DOMParser().parseFromString(string, 'text/html');
return html.body.firstChild;
......
import React from 'react';
import ReactDOM from 'react-dom';
import Cart from 'src/Cart';
import QuickAdd from 'src/QuickAdd';
document.addEventListener('DOMContentLoaded', () => {
//const calendarUrl = `${window.location.protocol}//${window.location.hostname}${window.location.port == 3000 ? ':3000' : ''}/schedule`;
const calendarUrl = '/schedule';
ReactDOM.render(
<QuickAdd
loadCalendar={() => {
window.location.href = calendarUrl;
}}
/>,
document.getElementById('quick-add')
);
});
// /**
// * Either adds or removes a section from the cart depending on
// * if it is currently in the cart.
// */
import $ from 'jquery';
import Cart from 'src/Cart';
const addOrRemoveFromCart = async (event, sectionNode) => {
event && event.stopPropagation();
if (event.target.tagName === 'A') return;
const section = { ...sectionNode.dataset };
Cart.toggleCrn(section.crn);
const icon = $(sectionNode.querySelector('.add-remove-btn #icon'));
const text = sectionNode.querySelector('.add-remove-btn .text');
if (Cart.includesCrn(section.crn)) {
icon.addClass('fa-minus').removeClass('fa-plus');
text.innerText = 'Remove';
} else {
icon.addClass('fa-plus').removeClass('fa-minus');
text.innerText = 'Add';
}
};
const initSearchListeners = () => {
const sectionItems = Array.from(document.querySelectorAll('.section-item'));
sectionItems.forEach(item => {
item.onclick = event => addOrRemoveFromCart(event, item);
});
setTimeout(() => {
sectionItems.forEach(item => {
const icon = $(item.querySelector('.add-remove-btn #icon'));
const text = item.querySelector('.add-remove-btn .text');
if (Cart.includesCrn(item.dataset.crn)) {
icon.addClass('fa-minus').removeClass('fa-plus');
text.innerText = 'Remove';
} else {
icon.addClass('fa-plus').removeClass('fa-minus');
text.innerText = 'Add';
}
});
}, 100);
};
document.addEventListener('DOMContentLoaded', initSearchListeners);
import React from 'react';
import ReactDOM from 'react-dom';
import Cart from 'src/Cart';
import Cart from 'src/cart';
import { saveAs } from 'file-saver';
import html2canvas from 'html2canvas';
import $ from 'jquery';
import CalendarPage from 'src/CalendarPage';
import 'fullcalendar';
import 'moment';
document.addEventListener('DOMContentLoaded', () => {
ReactDOM.render(<CalendarPage />, document.getElementById('root'));
const eventsTemplate = document.querySelector('#events');
if (eventsTemplate) {
const eventsJSON = eventsTemplate.dataset.events;
const events = JSON.parse(eventsJSON);
window.events = events;
$('#calendar').fullCalendar({
defaultDate: new Date(2019, 0, 14),
defaultView: 'agendaWeek',
header: false,
events: renderEvents,
columnHeaderFormat: 'dddd',
allDaySlot: false,
});
}
initListeners();
});
const renderEvents = (start, end, timezone, callback) => {
callback(window.events);
};
const remove = async item => {
await Cart.toggleSection({ ...item.dataset });
location.reload(true);
};
/**
* Generates a URL for the current sections in the schedule
* and sets the link in the modal to it.
*/
const setUrlInModal = () => {
document.getElementById('calendar-link').innerText = `${window.location.protocol}//${window.location.hostname}/api/schedules?crns=${Cart._courses.join(',')}`;
};
const downloadIcs = async () => {
const response = await fetch(`${window.location.protocol}//${window.location.hostname}/api/schedules?crns=${Cart._courses.join(',')}`);
const text = await response.text();
......@@ -39,7 +69,7 @@ const initListeners = () => {
document.getElementById('save-image').onclick = saveImage;
document.getElementById('share-url').innerText = `${window.location.protocol}//${window.location.hostname}/schedule/view?crns=${Cart._courses.join(',')}`;
document.getElementById('share-url').href = `${window.location.protocol}//${window.location.hostname}/schedule/view?crns=${Cart._courses.join(',')}`;
document.getElementById('share-url').href= `${window.location.protocol}//${window.location.hostname}/schedule/view?crns=${Cart._courses.join(',')}`;
};
if (!HTMLCanvasElement.prototype.toBlob) {
......
import Cart from 'src/Cart';
import Cart from 'src/cart';
import { saveAs } from 'file-saver';
import html2canvas from 'html2canvas';
import $ from 'jquery';
......
// Place all the behaviors and hooks related to the matching controller here.
// All this logic will automatically be available in application.js.
// import Cart from 'src/cart';
import Cart from 'src/cart';
// /**
// * Toggles the display of the schedule
// */
// const toggleSections = course => {
// const sections = course.querySelector('.sections');
// const chev = $(course.querySelector('#course-chevron'));
// const label = course.querySelector('#chevron-label');
/**
* Either adds or removes a section from the cart depending on
* if it is currently in the cart.
*/
const addOrRemoveFromCart = async (event, sectionNode) => {
event && event.stopPropagation();
const section = { ...sectionNode.dataset };
// if (sections.style.display === 'flex') {
// sections.style.display = 'none';
// chev.addClass('fa-chevron-down').removeClass('fa-chevron-up');
// label.innerText = 'Expand';
// } else {
// sections.style.display = 'flex';
// chev.addClass('fa-chevron-up').removeClass('fa-chevron-down');
// label.innerText = 'Minimize';
// }
// };
await Cart.toggleSection(section);
const icon = $(sectionNode.querySelector('.add-remove-btn #icon'));
const text = sectionNode.querySelector('.add-remove-btn .text');
if (Cart.includesSection(section)) {
icon.addClass('fa-minus').removeClass('fa-plus');
text.innerText = 'Remove';
} else {
icon.addClass('fa-plus').removeClass('fa-minus');
text.innerText = 'Add';
}
};
import React from 'react';
import ReactDOM from 'react-dom';
import SearchList from 'src/SearchList';
/**
* Toggles the display of the schedule
*/
const toggleSections = course => {
const sections = course.querySelector('.sections');
const chev = $(course.querySelector('#course-chevron'));
const label = course.querySelector('#chevron-label');
document.addEventListener('DOMContentLoaded', () => {
ReactDOM.render(<SearchList courses={gon.courses} instructors={gon.instructors} />, document.getElementById('root'));
});
if (sections.style.display === 'flex') {
sections.style.display = 'none';
chev.addClass('fa-chevron-down').removeClass('fa-chevron-up');
label.innerText = 'Expand';
} else {
sections.style.display = 'flex';
chev.addClass('fa-chevron-up').removeClass('fa-chevron-down');
label.innerText = 'Minimize';
}
};
const initSearchListeners = () => {
const courseCards = Array.from(document.querySelectorAll('.course-card'));
courseCards.forEach(card => {
card.onclick = () => toggleSections(card);
});
const sectionItems = Array.from(document.querySelectorAll('.section-item'));
sectionItems.forEach(item => (item.onclick = event => addOrRemoveFromCart(event, item)));
};
document.addEventListener('DOMContentLoaded', initSearchListeners);
import React from 'react';
import BigCalendar from 'react-big-calendar';
import Toolbar from 'src/Toolbar';
import moment from 'moment';
import '!style-loader!css-loader!react-big-calendar/lib/css/react-big-calendar.css';
import withSizes from 'react-sizes';
const localizer = BigCalendar.momentLocalizer(moment);
const Calendar = props => (
<div className="full-width" style={{ backgroundColor: 'white', padding: '24px' }}>
<BigCalendar
localizer={localizer}
events={props.events}
title=""
components={{ toolbar: Toolbar }}
defaultView="week"
views={['week', 'day']}
startAccessor="start"
endAccessor="end"
defaultDate={moment('2019-01-14').toDate()}
formats={{
dayFormat: (date, culture, localizer) => localizer.format(date, 'ddd', culture),
dayHeaderFormat: (date, culture, localizer) => localizer.format(date, 'ddd', culture),
dayRangeHeaderFormat: () => '',
}}
style={{ height: '75vh' }}
/>
</div>
);
export default Calendar;
import React from 'react';
import Calendar from 'src/Calendar';
import Cart from 'src/Cart';
import SectionList from 'src/SectionList';
import QuickAdd from 'src/QuickAdd';
import moment from 'moment';
export default class CalendarPage extends React.Component {
state = { events: [], sections: [] };
constructor(props) {
super(props);
this.loadEvents();
}
loadEvents = async () => {
const response = await fetch(`/schedule/events?crns=${Cart.crns.join(',')}`);
const json = await response.json();
this.setState({ ...json });
Cart.crns = json.sections.map(s => s.crn);
};
events = () => {
return this.state.events.filter(e => e.active).map(e => ({ ...e, start: moment(e.start).toDate(), end: moment(e.end).toDate() }));
};
toggleSection = crn => {
const events = this.state.events.map(e => ({ ...e, active: e.crn == crn ? !e.active : e.active }));
this.setState({ events });
};
removeAll = () => {
Cart.crns = [];
location.reload();
};
render() {
return (
<div>
<Calendar events={this.events()} />
{this.state.sections.length > 0 ? (
<div className="d-flex justify-content-between align-items-end">
<h2 className="mt-4">Your Schedule</h2>{' '}
<button type="button" onClick={this.removeAll} className="btn btn-danger mb-8">
Remove all sections
</button>
</div>
) : null}
<SectionList onClick={this.toggleSection} sections={this.state.sections} expanded={true} />
<QuickAdd loadCalendar={this.loadEvents} />
</div>
);
}
}
import React from 'react';
export default class Chevron extends React.Component {
constructor(props) {
super(props);
}
render() {
const base = { display: 'block', textAlign: 'center' };
return (
<div>
<div style={this.props.open ? { ...base } : { display: 'none' }}>
<p id="chevron-label" style={{ marginBottom: '4px', fontSize: '10px' }}>
Minimize
</p>
<i id="course-chevron" className="fas fa-chevron-up" />
</div>
<div style={this.props.open ? { display: 'none' } : { ...base }}>
<p id="chevron-label" style={{ marginBottom: '4px', fontSize: '10px' }}>
Expand
</p>
<i id="course-chevron" className="fas fa-chevron-down" />
</div>
</div>
);
}
}
import React from 'react';
import SectionList from 'src/SectionList';
export default class Course extends React.Component {
constructor(props) {
super(props);
this.state = { expanded: false, sections: [] };
}
async onClick() {
if (this.state.sections.length === 0) {
const resp = await fetch(`/api/course_sections?course_id=${this.props.id}`);
const json = await resp.json();
this.setState({ sections: json });
}
this.setState({ expanded: !this.state.expanded });
}
prereqs = () => {
if (this.props.prereqs) {
const [first, rest] = this.props.prereqs.split(':');
const [reqs, note] = rest.split('.');
return (
<p>
<strong>{first}</strong>
{reqs}
<sub>{note}</sub>
</p>
);
}
return <div />;
};
// <% first, rest = course.prereqs.split(':') %>
// <% prereqs, note = rest.split('.') %>
// <p><strong><%= first %>:</strong> <%= prereqs %> <sub><%= note %></sub></p>
render() {
const { id, subject, course_number, title, credits, description, url } = this.props;
return (
<div className="card course-card" onClick={() => this.onClick()}>
<div className="card-header">
<div className="row">
<div className="col">
<a href={url}>
<h4 className="title">{`${subject} ${course_number}`}</h4>
</a>
</div>
</div>
<div className="d-md-flex justify-content-between">
<h5>
<em>{title}</em>
</h5>
<div className="attr-list justify-content-start">
<div className="attr">
<div className="icon">
<i className="fa fa-book" />
</div>
{credits} credits
</div>
</div>
</div>
</div>
<div className="card-body">
<p>{description}</p>
{this.prereqs()}
<div className="list-group list-group-flush sections" style={{ display: 'none' }} />
<SectionList {...this.state} expandable={true} />
</div>
</div>
);
}
}
import React from 'react';
import Course from 'src/Course';
export default class CourseList extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
{this.props.courses.map(course => (
<Course key={course.id} {...course} />
))}
</div>
);
}
}
import React from 'react';
export default class InstructorCard extends React.Component {
render() {
const inst = this.props.instructor;
return (
<div className="card p-3">
<span>
<i className="fas fa-chalkboard-teacher mr-2" />
<a href={`/instructors/${inst.id}`}>{this.props.instructor.name}</a>
</span>
</div>
);
}
}
import React from 'react';
import InstructorCard from 'src/InstructorCard';
export default class InstructorList extends React.Component {
render() {
return <div>{this.props.instructors && this.props.instructors.map(i => <InstructorCard instructor={i} />)}</div>;
}
}
import React from 'react';
import Cart from 'src/Cart';
export default class QuickAdd extends React.Component {
constructor(props) {
super(props);
this.state = { crnString: '' };
}
add = e => {
e.preventDefault();
const crns = this.state.crnString.split(',');
crns.forEach(c => c.length === 5 && Cart.addCrn(c));
this.props.loadCalendar();
};
render() {
return (
<div>
<h3 className="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 onSubmit={this.add} className="form">