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

Use postgres DB in production.

Added share link on schedule page.
parent 9a909447
...@@ -11,5 +11,5 @@ ENV RAILS_ENV production ...@@ -11,5 +11,5 @@ ENV RAILS_ENV production
RUN bundle install RUN bundle install
RUN export SECRET_KEY_BASE=$(rails secret) RUN export SECRET_KEY_BASE=$(rails secret)
RUN rake assets:precompile RUN rake assets:precompile
RUN rails db:migrate #RUN rails db:migrate
RUN rails db:seed #RUN rails db:seed
...@@ -9,6 +9,7 @@ end ...@@ -9,6 +9,7 @@ end
gem 'rails', '5.1.6.1' gem 'rails', '5.1.6.1'
# Use sqlite3 as the database for Active Record # Use sqlite3 as the database for Active Record
gem 'sqlite3' gem 'sqlite3'
gem 'pg'
# Use Puma as the app server # Use Puma as the app server
gem 'puma', '~> 3.7' gem 'puma', '~> 3.7'
# Use SCSS for stylesheets # Use SCSS for stylesheets
......
...@@ -101,6 +101,7 @@ GEM ...@@ -101,6 +101,7 @@ GEM
parallel (1.12.1) parallel (1.12.1)
parser (2.5.3.0) parser (2.5.3.0)
ast (~> 2.4.0) ast (~> 2.4.0)
pg (1.1.3)
powerpack (0.1.2) powerpack (0.1.2)
pry (0.12.2) pry (0.12.2)
coderay (~> 1.1.0) coderay (~> 1.1.0)
...@@ -216,6 +217,7 @@ DEPENDENCIES ...@@ -216,6 +217,7 @@ DEPENDENCIES
listen (>= 3.0.5, < 3.2) listen (>= 3.0.5, < 3.2)
maruku maruku
nokogiri nokogiri
pg
pry pry
pry-doc pry-doc
puma (~> 3.7) puma (~> 3.7)
......
...@@ -52,4 +52,6 @@ const initListeners = () => { ...@@ -52,4 +52,6 @@ const initListeners = () => {
document.getElementById('open-modal-btn').onclick = setUrlInModal; document.getElementById('open-modal-btn').onclick = setUrlInModal;
document.getElementById('download-ics').onclick = downloadIcs; document.getElementById('download-ics').onclick = downloadIcs;
document.getElementById('add-to-system').onclick = addToSystemCalendar; document.getElementById('add-to-system').onclick = addToSystemCalendar;
document.getElementById('share-url').innerText = `${window.location.protocol}//${window.location.hostname}/schedule/view?section_ids=${window.cart._courses.join(',')}`;
}; };
document.addEventListener('DOMContentLoaded', () => {
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);
};
/**
* 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?section_ids=${window.cart._courses.join(',')}`;
};
const downloadIcs = async () => {
const response = await fetch(`${window.location.protocol}//${window.location.hostname}/api/schedules?section_ids=${window.cart._courses.join(',')}`);
const text = await response.text();
const blob = new Blob([text], { type: 'text/calendar;charset=utf-8' });
saveAs(blob, 'GMU Schedule.ics');
};
const addToSystemCalendar = () => {
window.open(`webcal://${window.location.hostname}/api/schedules?section_ids=${window.cart._courses.join(',')}`);
};
const initListeners = () => {
document.getElementById('open-modal-btn').onclick = setUrlInModal;
document.getElementById('download-ics').onclick = downloadIcs;
document.getElementById('add-to-system').onclick = addToSystemCalendar;
document.getElementById('share-url').innerText = `${window.location.protocol}//${window.location.hostname}/schedule/view?section_ids=${window.cart._courses.join(',')}`;
};
...@@ -13,3 +13,7 @@ ...@@ -13,3 +13,7 @@
.section-item.selected:hover { .section-item.selected:hover {
background-color: red; background-color: red;
} }
#share-header {
margin-top: 16px;
}
...@@ -11,4 +11,9 @@ class SchedulesController < ApplicationController ...@@ -11,4 +11,9 @@ class SchedulesController < ApplicationController
@all = valid_ids.map { |id| CourseSection.find_by_id id } @all = valid_ids.map { |id| CourseSection.find_by_id id }
@events = generate_fullcalender_events(valid_ids) @events = generate_fullcalender_events(valid_ids)
end end
def view
@all = params[:section_ids].split(',').map { |id| CourseSection.find_by_id id.to_s }
@events = generate_fullcalender_events(params[:section_ids].split(','))
end
end end
...@@ -23,7 +23,9 @@ class SessionsController < ApplicationController ...@@ -23,7 +23,9 @@ class SessionsController < ApplicationController
def add_bulk def add_bulk
crns = params[:crns].split(',') crns = params[:crns].split(',')
crns.each { |crn| crns.each { |crn|
section_id = CourseSection.find_by_crn(crn).id.to_s s = CourseSection.find_by_crn(crn)
next if s.nil?
section_id = s.id.to_s
@cart << section_id unless @cart.include?(section_id) @cart << section_id unless @cart.include?(section_id)
} }
cookies[:cart] = @cart.to_json cookies[:cart] = @cart.to_json
......
...@@ -45,7 +45,7 @@ module SearchHelper ...@@ -45,7 +45,7 @@ module SearchHelper
CourseReplacementHelper.replace!(query_string) CourseReplacementHelper.replace!(query_string)
base_query = Course.select("courses.*, count(course_sections.id) AS section_count") base_query = Course.select("courses.*, count(course_sections.id) AS section_count")
.left_outer_joins(:course_sections) .left_outer_joins(:course_sections)
.having("section_count > 0") .having("count(course_sections.id) > 0")
.where("courses.semester_id = ?", query_data.semester) .where("courses.semester_id = ?", query_data.semester)
.group("courses.id") .group("courses.id")
...@@ -53,6 +53,7 @@ module SearchHelper ...@@ -53,6 +53,7 @@ module SearchHelper
query_string.scan(/(?<= |^)([a-zA-Z]{2,4})(?=$| )/).each do |a| query_string.scan(/(?<= |^)([a-zA-Z]{2,4})(?=$| )/).each do |a|
s = a[0] s = a[0]
next unless get_count(Course.from_subject(base_query, s)).positive? next unless get_count(Course.from_subject(base_query, s)).positive?
# next unless Course.from_subject(base_query, s).count.positive?
subj = s subj = s
base_query = Course.from_subject(base_query, subj) base_query = Course.from_subject(base_query, subj)
query_string.remove!(s) query_string.remove!(s)
...@@ -61,6 +62,7 @@ module SearchHelper ...@@ -61,6 +62,7 @@ module SearchHelper
query_string.scan(/(?<= |^)(\d{3})(?=$| )/).each do |a| query_string.scan(/(?<= |^)(\d{3})(?=$| )/).each do |a|
s = a[0] s = a[0]
next unless !subj.nil? && get_count(Course.from_course_number(base_query, s)).positive? 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) base_query = Course.from_course_number(base_query, s)
return base_query.all return base_query.all
end end
...@@ -90,7 +92,7 @@ module SearchHelper ...@@ -90,7 +92,7 @@ module SearchHelper
def self.get_count(base_query) def self.get_count(base_query)
# I think I finally hit a limit of active record # I think I finally hit a limit of active record
ActiveRecord::Base.connection.execute("SELECT COUNT(*) AS count FROM (#{base_query.to_sql})")[0]["count"] ActiveRecord::Base.connection.execute("SELECT COUNT(*) AS count FROM (#{base_query.to_sql}) as q")[0]["count"]
end end
def to_s def to_s
......
...@@ -19,15 +19,17 @@ class CourseSection < ApplicationRecord ...@@ -19,15 +19,17 @@ class CourseSection < ApplicationRecord
# Select all course sections that have an instructor that matches the given name # Select all course sections that have an instructor that matches the given name
def self.with_instructor(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 end
def self.from_crn(base_query, crn) def self.from_crn(base_query, crn)
base_query.where("course_sections.crn = ?", crn) base_query.where(crn: crn)
end end
def self.from_course_id(base_query, course_id) def self.from_course_id(base_query, course_id)
base_query.where("course_sections.course_id = ?", course_id) base_query.where(course_id: course_id)
end end
# Select all revelevant course sections given the provided filters # Select all revelevant course sections given the provided filters
......
...@@ -2,6 +2,6 @@ class Instructor < ApplicationRecord ...@@ -2,6 +2,6 @@ class Instructor < ApplicationRecord
has_many :course_sections has_many :course_sections
def self.from_name(base_query, name) def self.from_name(base_query, name)
base_query.where("instructors.name LIKE ?", "%#{name}%") base_query.where("upper(instructors.name) LIKE ?", "%#{name.upcase}%")
end end
end end
...@@ -34,6 +34,10 @@ ...@@ -34,6 +34,10 @@
</div> </div>
</form> </form>
<h3 id="share-header">Share</h3>
Want to share your schedule with your friends? Send them this link:<br/>
<blockquote ><code id="share-url"></code></blockquote>
<template id="events" data-events="<%= @events.to_json %>"></template> <template id="events" data-events="<%= @events.to_json %>"></template>
<hr /> <hr />
......
<%= javascript_include_tag 'schedules_view' %>
<%= stylesheet_link_tag 'schedules' %>
<%= javascript_include_tag 'moment.min' %>
<%= javascript_include_tag 'FileSaver' %>
<%= javascript_include_tag 'fullcalendar.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>
<div id="calendar"></div>
<template id="events" data-events="<%= @events.to_json %>"></template>
<hr />
<h2>Selected Courses</h2>
<%= render partial: 'shared/section', collection: @all, locals: { editable: false } %>
<!-- Export Modal -->
<div class="modal fade" id="exportModal" tabindex="-1" role="dialog" aria-labelledby="exportModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exportModalLabel">Your calendar has been generated!</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<h5>Apple Calendar</h5>
To add your schedule to Apple Calendar, click the "Add to calendar" button below. If you are on a device
running macOS or iOS, this will open a dialogue which will walk you through adding the calendar.
<hr />
<h5>Google Calendar</h5>
<strong>On desktop:</strong>
<br />
Open your <a href="https://calendar.google.com/">Google Calendar</a>. Click the "Settings" button in the top
right, and then click the Settings tab. In the menu on the left, click "Add calendar" and "From URL". Now,
paste the following link inside the text box: <br />
<code id="calendar-link"></code>
<br />
<strong>On mobile (Android only):</strong>
<br />
Click the "Download calendar file" button. This will download the calendar file which you may then open and
add to your calendar.
<hr />
<h5>.ics file</h5>
To download a .ics file containing your schedule, click the "Download calendar file" button below.
</div>
<div class="modal-footer flex">
<button id="download-ics" type="button" class="btn btn-secondary">Download calendar file</button>
<button id="add-to-system" type="button" class="btn btn-primary">Add to system calendar</button>
</div>
</div>
</div>
</div>
...@@ -43,9 +43,9 @@ ...@@ -43,9 +43,9 @@
<!-- List of Course Sections --> <!-- List of Course Sections -->
<div class="list-group list-group-flush sections" style="display: <%= expanded ? "flex" : "none" %>"> <div class="list-group list-group-flush sections" style="display: <%= expanded ? "flex" : "none" %>">
<% if defined?(@instructor) %> <% if defined?(@instructor) %>
<%= render partial: 'shared/section', collection: course.course_sections.where(instructor: @instructor), locals: { course: course } %> <%= render partial: 'shared/section', collection: course.course_sections.where(instructor: @instructor).order(:name), locals: { course: course } %>
<% else %> <% else %>
<%= render partial: 'shared/section', collection: course.course_sections, locals: { course: course } %> <%= render partial: 'shared/section', collection: course.course_sections.order(:name), locals: { course: course } %>
<% end %> <% end %>
</div> </div>
</div> </div>
......
<% unless defined?(editable) %>
<% editable = true %>
<% end %>
<li id="section-<%= section.id %>" <li id="section-<%= section.id %>"
class="list-group-item section-item" class="list-group-item section-item"
data-crn="<%= section.crn %>" data-crn="<%= section.crn %>"
...@@ -7,13 +11,20 @@ ...@@ -7,13 +11,20 @@
> >
<p><b class="subj"><%= "#{section.name}" %></b>: <%= section.title %></p> <p><b class="subj"><%= "#{section.name}" %></b>: <%= section.title %></p>
<% if in_cart? section.id %> <% if editable %>
<span class="float-right text-center add-remove-btn"><i id="icon" class="fas fa-minus"></i><br/><span class="text">Remove</span></span> <% if in_cart? section.id %>
<% else %> <span class="float-right text-center add-remove-btn"><i id="icon" class="fas fa-minus"></i><br/><span class="text">Remove</span></span>
<span class="float-right text-center add-remove-btn"><i id="icon" class="fas fa-plus"></i><br/><span class="text">Add</span></span> <% else %>
<span class="float-right text-center add-remove-btn"><i id="icon" class="fas fa-plus"></i><br/><span class="text">Add</span></span>
<% end %>
<% end %> <% end %>
<i class="fas fa-chalkboard-teacher"></i> <%= link_to section.instructor.name, section.instructor %> <br/> <i class="fas fa-chalkboard-teacher"></i>
<% if section.instructor.name == "TBA" %>
TBA
<% else %>
<%= link_to section.instructor.name, section.instructor %><% end %> <br/>
<i class="fas fa-map-marker-alt"></i> <%= section.location %> <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/> <i class="fas fa-clock"></i> <%= "#{section.days}, #{section.start_time}-#{section.end_time}" %> <br/>
</li> </li>
...@@ -21,5 +21,12 @@ test: ...@@ -21,5 +21,12 @@ test:
database: db/test.sqlite3 database: db/test.sqlite3
production: production:
<<: *default adapter: postgresql
database: db/production.sqlite3 encoding: unicode
database: <%= ENV.fetch("DB_DATABASE") { "" } %>
username: <%= ENV.fetch("DB_USERNAME") { "" } %>
password: <%= ENV.fetch("DB_PASSWORD") { "" } %>
host: <%= ENV.fetch("DB_HOST") { "localhost" } %>
port: <%= ENV.fetch("DB_PORT") { 5432 } %>
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000
...@@ -16,6 +16,7 @@ Rails.application.config.assets.precompile += %w( ...@@ -16,6 +16,7 @@ Rails.application.config.assets.precompile += %w(
search.scss search.scss
schedules.js schedules.js
schedules_view.js
schedules.scss schedules.scss
cart.js cart.js
......
...@@ -9,6 +9,7 @@ Rails.application.routes.draw do ...@@ -9,6 +9,7 @@ Rails.application.routes.draw do
resources :courses, only: [:show] resources :courses, only: [:show]
resources :instructors, only: [:index, :show] resources :instructors, only: [:index, :show]
get 'schedule', to: 'schedules#show', as: 'schedule' get 'schedule', to: 'schedules#show', as: 'schedule'
get 'schedule/view', to: 'schedules#view', as: 'view_schedule'
scope :api, module: 'api' do # Register /api routes scope :api, module: 'api' do # Register /api routes
resources :courses, only: [:index, :show], as: 'api_courses' resources :courses, only: [:index, :show], as: 'api_courses'
......
...@@ -12,9 +12,12 @@ ...@@ -12,9 +12,12 @@
ActiveRecord::Schema.define(version: 20180927140017) do ActiveRecord::Schema.define(version: 20180927140017) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
create_table "closures", force: :cascade do |t| create_table "closures", force: :cascade do |t|
t.date "date" t.date "date"
t.integer "semester_id" t.bigint "semester_id"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["semester_id"], name: "index_closures_on_semester_id" t.index ["semester_id"], name: "index_closures_on_semester_id"
...@@ -35,10 +38,10 @@ ActiveRecord::Schema.define(version: 20180927140017) do ...@@ -35,10 +38,10 @@ ActiveRecord::Schema.define(version: 20180927140017) do
t.string "campus" t.string "campus"
t.string "notes" t.string "notes"
t.integer "size_limit" t.integer "size_limit"
t.integer "course_id" t.bigint "course_id"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.integer "instructor_id" t.bigint "instructor_id"
t.index ["course_id"], name: "index_course_sections_on_course_id" t.index ["course_id"], name: "index_course_sections_on_course_id"
t.index ["instructor_id"], name: "index_course_sections_on_instructor_id" t.index ["instructor_id"], name: "index_course_sections_on_instructor_id"
end end
...@@ -46,7 +49,7 @@ ActiveRecord::Schema.define(version: 20180927140017) do ...@@ -46,7 +49,7 @@ ActiveRecord::Schema.define(version: 20180927140017) do
create_table "courses", force: :cascade do |t| create_table "courses", force: :cascade do |t|
t.string "subject" t.string "subject"
t.string "course_number" t.string "course_number"
t.integer "semester_id" t.bigint "semester_id"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.string "description" t.string "description"
...@@ -69,4 +72,8 @@ ActiveRecord::Schema.define(version: 20180927140017) do ...@@ -69,4 +72,8 @@ ActiveRecord::Schema.define(version: 20180927140017) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
end end
add_foreign_key "closures", "semesters"
add_foreign_key "course_sections", "courses"
add_foreign_key "course_sections", "instructors"
add_foreign_key "courses", "semesters"
end end
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