Commit 69e7ffaa authored by Zac Wood's avatar Zac Wood

Merge branch 'calendar-view' into 'dev-v2'

Calendar view

See merge request !31
parents a09028af a4018412
Pipeline #3210 passed with stage
in 2 minutes and 16 seconds
......@@ -27,12 +27,12 @@ document.addEventListener('DOMContentLoaded', () => {
this.schedule = new Schedule();
});
/** Loads FontAwesome icons on load; fixes weird flickering */
document.addEventListener('turbolinks:load', () => {
FontAwesome.dom.i2svg();
});
const setSemester = async select => {
const resp = await fetch(`/sessions/update?semester_id=${select.value}`);
location.reload(true);
};
/** Loads FontAwesome icons on load; fixes weird flickering */
document.addEventListener('turbolinks:load', () => {
FontAwesome.dom.i2svg();
});
class Schedule {
constructor() {
this.isOpen = false;
this._courses = {}; // {title, id, sections: {id, crn}}
const cartData = document.querySelector('#cart-data');
const courses = Array.from(cartData.content.children);
for (const course of courses) {
const { id, title } = course.dataset;
const sections = Array.from(course.children).map(node => ({ ...node.dataset }));
this._courses[id] = { id, title, sections };
}
document.getElementById('course-counter').innerText = Object.keys(this._courses).length;
this._ids = Array.from(document.getElementById('schedule').children).map(e => e.dataset.crn);
}
get ids() {
return this._ids;
get crns() {
return Object.keys(this._courses)
.map(cid => this._courses[cid].sections.map(s => s.crn))
.reduce((prev, curr) => [...prev, ...curr], []);
}
set ids(ids) {
this._ids = ids;
document.getElementById('course-counter').innerText = ids.length;
fetch('/sessions/update?crns=' + ids.join(','), { cache: 'no-store' });
get ids() {
return Object.keys(this._courses)
.map(cid => this._courses[cid].sections.map(s => s.id))
.reduce((prev, curr) => [...prev, ...curr], []);
}
toggle() {
......@@ -30,36 +42,111 @@ class Schedule {
this.isOpen = !this.isOpen;
}
addToSchedule(section) {
if (this.ids.includes(section.dataset.crn)) return;
addCourse(course) {
this._courses[course.id] = course;
const parent = document.querySelector('#schedule');
const current = parent.querySelector(`#schedule-${course.id}`);
const newNode = this._constructCourseNode(course);
if (current !== null) parent.replaceChild(newNode, current);
else parent.appendChild(newNode);
document.getElementById('course-counter').innerText = Object.keys(this._courses).length;
fetch(`/sessions/update?section_ids=${this.ids.join(',')}`, { cache: 'no-store' });
}
removeCourse(id) {
const sectionIds = this._courses[id].sections.map(s => s.id);
for (const sectionId of sectionIds) {
const sectionCard = document.querySelector(`#section-${sectionId}`);
sectionCard && sectionCard.classList.remove('selected');
}
delete this._courses[id];
const parent = document.querySelector('#schedule');
const current = parent.querySelector(`#schedule-${id}`);
parent.removeChild(current);
document.getElementById('course-counter').innerText = Object.keys(this._courses).length;
fetch(`/sessions/update?section_ids=${this.ids.join(',')}`, { cache: 'no-store' });
}
courseContainingSection(id) {
for (const courseId in this._courses) {
const course = this._courses[courseId];
for (const section of course.sections) {
if (section.id == id) return course;
}
}
return undefined;
}
includesSection(id) {
return !!this.courseContainingSection(id);
}
// section: { id, crn }
addSection(section) {
const course = this._courses[section.cid];
if (course) {
course.sections.push(section);
this.ids = [...this.ids, section.dataset.crn];
const courseNode = document.querySelector('#schedule').querySelector(`#schedule-${course.id}`);
const crnList = courseNode.querySelector('.crns');
crnList.innerText = course.sections.map(s => `#${s.crn}`);
section.classList.remove('section-item');
section.classList.remove('selected');
section.classList.add('schedule-section-card');
section.onclick = () => removeFromSchedule(section);
fetch(`/sessions/update?section_ids=${this.ids.join(',')}`, { cache: 'no-store' });
} else {
const courseCard = document.getElementById(`course-${section.cid}`);
const title = courseCard.querySelector('#title').innerText;
document.getElementById('schedule').appendChild(section);
this.addCourse({ title, id: section.cid, sections: [section] });
}
}
removeFromSchedule(id) {
const cart = document.getElementById('schedule');
const section = cart.querySelector(`#section-${id}`);
cart.removeChild(section);
removeSection(section) {
const course = this.courseContainingSection(section.id);
course.sections = course.sections.filter(s => s.id !== section.id);
const schedule = document.querySelector('#schedule');
const courseNode = schedule.querySelector(`#schedule-${course.id}`);
const crnList = courseNode.querySelector('.crns');
if (course.sections.length === 0) {
this.removeCourse(section.cid);
} else {
crnList.innerText = course.sections.map(s => `#${s.crn}`);
}
this.ids = this.ids.filter(_id => _id != id);
fetch(`/sessions/update?section_ids=${this.ids.join(',')}`, { cache: 'no-store' });
}
async downloadIcs() {
const cal = await fetch(`/api/schedules?crns=${this.ids.join(',')}`);
const cal = await fetch(`/api/schedules?crns=${this.crns.join(',')}`);
const text = await cal.text();
var blob = new Blob([text], { type: 'text/calendar;charset=utf-8' });
saveAs(blob, 'test.ics');
}
async addToSystemCalendar() {
const url = `webcal://${window.location.hostname}/api/schedule?crns=${this.ids.join(',')}`;
const url = `webcal://${window.location.hostname}/api/schedule?crns=${this.crns.join(',')}`;
window.open(url, '_self');
}
_constructCourseNode(course) {
let html = `<li id="schedule-${course.id}" class="list-group-item schedule-section-card" onclick="removeCourse(${course.id})">`;
html += `<div style="display: flex; justify-content: space-between;">`;
html += `<b style="min-width: 15%">${course.title}</b>`;
html += `<span class="crns" style="color: gray; font-size: 10pt;">`;
html += course.sections.map(s => `#${s.crn}`).join(', ');
html += `</span>`;
html += `</div>`;
html += `</li>`;
return elementFromString(html);
}
}
const removeCourse = id => {
this.schedule.removeCourse(id);
};
......@@ -3,17 +3,30 @@
const sectionWithCrn = crn => document.getElementById('search-list').querySelector(`[data-crn="${crn}"]`);
const addCourse = (event, id) => {
const courseCard = document.getElementById(`course-${id}`);
const title = courseCard.querySelector('#title').innerText;
const sectionsItems = Array.from(courseCard.querySelectorAll('li'));
const sections = sectionsItems.map(li => ({ ...li.dataset }));
this.schedule.addCourse({ title, id, sections });
sectionsItems.forEach(s => s.classList.add('selected'));
event.stopPropagation();
};
/**
* Either adds or removes a section from the schedule depending on
* if it is currently in the schedule.
*/
const addOrRemoveFromSchedule = (event, section) => {
if (this.schedule.ids.includes(section.dataset.crn)) {
this.schedule.removeFromSchedule(section.dataset.crn);
section.classList.remove('selected');
const addOrRemoveFromSchedule = (event, sectionNode) => {
const section = { ...sectionNode.dataset };
if (this.schedule.includesSection(section.id)) {
this.schedule.removeSection(section);
sectionNode.classList.remove('selected');
} else {
this.schedule.addToSchedule(section.cloneNode(true));
section.classList.add('selected');
this.schedule.addSection(section);
sectionNode.classList.add('selected');
}
event.stopPropagation();
......
......@@ -99,3 +99,12 @@ body {
margin-bottom: 16px;
padding: 12px;
}
#add-course-btn:hover {
background-color: rgba(0,0,0,0.2);
}
#calendar {
background-color: white;
padding: 16px;
}
......@@ -12,15 +12,17 @@ class ApplicationController < ActionController::Base
end
def set_cart
@cart = cookies[:crns].split(',').map do |crn|
s = CourseSection.find_by_crn(crn)
s if s.course.semester == @semester
sections = cookies[:section_ids].split(',').map do |id|
CourseSection.find_by_id(id)
end
@cart.compact!
@cart = sections.group_by do |s|
s.course.id
end
end
def set_cookies
cookies[:crns] = "" if cookies[:crns].nil?
cookies[:section_ids] = "" if cookies[:section_ids].nil?
end
end
......@@ -15,4 +15,19 @@ class SchedulesController < ApplicationController
@schedule = Schedule.new crns
render plain: @schedule.to_ical # render a plaintext iCal file
end
def show
@events = @cart.map do |_cid, sections|
s = sections.first
formatted_date = Date.today.to_s.tr('-', '')
formatted_time = Time.parse(s.start_time).strftime("%H%M%S")
formatted_endtime = Time.parse(s.end_time).strftime("%H%M%S")
{
title: s.name,
start: "#{formatted_date}T#{formatted_time}",
end: "#{formatted_date} #{formatted_endtime}"
}
end
end
end
class SessionsController < ApplicationController
def update
update_cookie :crns
update_cookie :section_ids
update_cookie :semester_id
head :ok
......
module ApplicationHelper
def in_cart?(id)
@cart.select { |_cid, sections| sections.select { |s| s.id == id }.count.positive? }.count.positive?
end
end
......@@ -5,10 +5,13 @@
<%= csrf_meta_tags %>
<%= javascript_include_tag 'masonstrap.min' %>
<%= javascript_include_tag 'moment.min' %>
<%= javascript_include_tag 'fullcalendar.min' %>
<%= javascript_include_tag 'FileSaver' %>
<%= javascript_include_tag 'application' %>
<%= stylesheet_link_tag 'masonstrap.min' %>
<%= stylesheet_link_tag 'masonstrap.min' %>
<%= stylesheet_link_tag 'fullcalendar.min' %>
<%= stylesheet_link_tag 'application' %>
</head>
......
<div id="calendar"></div>
<template id="events" data-events="<%= @events.to_json %>"></template>
<script>
/* const cal = document.querySelector('#calendar');
* var calendar = new Calendar(cal, {
* defaultView: 'agendaWeek'
* }); */
document.addEventListener('DOMContentLoaded', () => {
const eventsJSON = document.querySelector('#events').dataset.events;
const events = JSON.parse(eventsJSON);
console.log(events);
$('#calendar').fullCalendar({
defaultView: 'agendaWeek',
header: { right: 'next'},
events: events
});
});
</script>
......@@ -5,8 +5,3 @@
<p>Please try again!</p>
<% end %>
<%= javascript_tag do %>
document.addEventListener('DOMContentLoaded', () => {
this.search = new Search();
});
<% end %>
......@@ -4,10 +4,20 @@
<div class="col order-1 order-lg-1" id="cart">
<div class="card">
<div class="card-body">
<h3 class="card-title">Your Schedule</h3>
<h3 class="card-title"><%= link_to 'Your Schedule', schedule_path %></h3>
</div>
<ul class="list-group list-group-flush" id="schedule">
<%= render partial: 'shared/section', collection: @cart, locals: { in_cart: true } %>
<% @cart.each do |cid, sections| %>
<% course = Course.find_by_id(cid) %>
<li id="schedule-<%= cid %>" class="list-group-item schedule-section-card" onclick="removeCourse(<%= cid %>)">
<div style="display: flex; justify-content: space-between;">
<b style="min-width: 15%"><%= "#{course.subject} #{course.course_number}" %></b>
<span class="crns" style="color: gray; font-size: 10pt;">
<%= sections.map { |s| "##{s.crn}" }.join(', ') %>
</span>
</div>
</li>
<% end %>
</ul>
<div class="card-body">
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#exportModal" onclick="setUrlInModal()">
......@@ -17,6 +27,16 @@
</div>
</div>
<template id="cart-data">
<% @cart.each do |cid, sections| %>
<% course = Course.find_by_id cid %>
<div data-id="<%= cid %>" data-title="<%= "#{course.subject} #{course.course_number}" %>">
<% sections.each do |s| %>
<div data-id="<%= s.id %>" data-crn="<%= s.crn %>"></div>
<% end %>
</div>
<% end %>
</template>
</div>
</div>
......
<% expanded = false unless defined? expanded %>
<div class="card" id="course-<%= course.id %>" onclick="toggleSections(this)">
<div class="card-body">
<div>
<h3 style="float: left"><%= "#{course.subject} #{course.course_number}" %></h3>
<h5 style="float: right"><em><%= course.title %></em>. <%= course.credits %> credits.</h5>
</div>
<div class="card-body">
<div style="display: flex; justify-content: space-between">
<h3 id="title"><%= "#{course.subject} #{course.course_number}" %></h3>
<div style="display: flex; flex-direction: column; justify-content: center;">
<div style="display: flex">
<h5><em><%= course.title %></em>. <%= course.credits %> credits.</h5>
&nbsp;&nbsp;&nbsp;
<h4 id="add-course-btn" onclick="addCourse(event, '<%= course.id %>');">
<i class="fas fa-plus" style="color: green"></i>
</h4>
</div>
</div>
</div>
<div style="clear: both"> </div>
<div style="clear: both"> </div>
<p class="description"><%= course.description %></p>
<p class="description"><%= course.description %></p>
<% unless course.prereqs.nil? || course.prereqs.empty? %>
<% first, rest = course.prereqs.split(':') %>
<% prereqs, note = rest.split('.') %>
<p><strong><%= first %>:</strong> <%= prereqs %> <sub><%= note %></sub></p>
<% end %>
<% unless course.prereqs.nil? || course.prereqs.empty? %>
<% first, rest = course.prereqs.split(':') %>
<% prereqs, note = rest.split('.') %>
<p><strong><%= first %>:</strong> <%= prereqs %> <sub><%= note %></sub></p>
<% end %>
<div class="d-block" style="text-align: center">
<p style="margin-bottom:-4px; font-size: 10px;">Expand</p>
<i class="fas fa-chevron-down"></i>
</div>
<div class="d-block" style="text-align: center">
<p style="margin-bottom:-4px; font-size: 10px;">Expand</p>
<i class="fas fa-chevron-down"></i>
</div>
<!-- List of Course Sections -->
<ul class="list-group list-group-flush" id="sections" style="display: <%= expanded ? "block" : "none" %>">
<% if defined?(@instructor) %>
<%= render partial: 'shared/section', collection: course.course_sections.where(instructor: @instructor), locals: { in_cart: false } %>
<% else %>
<%= render partial: 'shared/section', collection: course.course_sections, locals: { in_cart: false } %>
<% end %>
</ul>
</div>
<!-- List of Course Sections -->
<ul class="list-group list-group-flush" id="sections" style="display: <%= expanded ? "block" : "none" %>">
<% if defined?(@instructor) %>
<%= render partial: 'shared/section', collection: course.course_sections.where(instructor: @instructor), locals: {course: course} %>
<% else %>
<%= render partial: 'shared/section', collection: course.course_sections, locals: {course: course} %>
<% end %>
</ul>
</div>
</div>
......@@ -7,7 +7,7 @@
Schedules
</a>
<select onchange="setSemester(this)">
<% for semester in Semester.all %>
<% Semester.all.each do |semester| %>
<option value="<%= semester.id %>" <% if @semester == semester %> selected <% end %> >
<%= "#{semester.season} #{semester.year}" %>
</option>
......
<% if in_cart %>
<li id="section-<%= section.crn %>"
class="list-group-item schedule-section-card"
data-crn="<%= section.crn %>"
onclick="removeFromSchedule(this)"
>
<span style="float:left"><b class="subj"><%= "#{section.name}" %></b>: <%= section.title %></span>
<span style="float:right"><i class="fas fa-map-marker-alt"></i> <%= section.location %></span>
<div style="clear: both"></div>
<span style="float:left"><i class="fas fa-chalkboard-teacher"></i> <%= link_to section.instructor.name, instructor_path(section.instructor) %> </span>
<span style="float:right"><i class="fas fa-clock"></i> <%= "#{section.days}, #{section.start_time}-#{section.end_time}" %></span>
<div style="clear: both"></div>
</li>
<% else %>
<li id="section-<%= section.crn %>"
class="list-group-item section-item <%= "selected" if @cart.include? section %>"
data-crn="<%= section.crn %>"
onclick="addOrRemoveFromSchedule(event, this)"
>
<span style="float:left"><b class="subj"><%= "#{section.name}" %></b>: <%= section.title %></span>
<span style="float:right"><i class="fas fa-map-marker-alt"></i> <%= section.location %></span>
<div style="clear: both"></div>
<span style="float:left"><i class="fas fa-chalkboard-teacher"></i> <%= link_to section.instructor.name, instructor_path(section.instructor) %></span>
<span style="float:right"><i class="fas fa-clock"></i> <%= "#{section.days}, #{section.start_time}-#{section.end_time}" %></span>
<div style="clear: both"></div>
</li>
<% end %>
<li id="section-<%= section.id %>"
class="list-group-item section-item <%= "selected" if in_cart? section.id %>"
data-crn="<%= section.crn %>"
data-id="<%= section.id %>"
data-cid="<%= course.id %>"
onclick="addOrRemoveFromSchedule(event, this)"
>
<span style="float:left"><b class="subj"><%= "#{section.name}" %></b>: <%= section.title %></span>
<span style="float:right"><i class="fas fa-map-marker-alt"></i> <%= section.location %></span>
<div style="clear: both"></div>
<span style="float:left"><i class="fas fa-chalkboard-teacher"></i> <%= link_to section.instructor.name, instructor_path(section.instructor) %></span>
<span style="float:right"><i class="fas fa-clock"></i> <%= "#{section.days}, #{section.start_time}-#{section.end_time}" %></span>
<div style="clear: both"></div>
</li>
......@@ -16,4 +16,7 @@ Rails.application.config.assets.precompile += %w(
schedule.js
masonstrap.min.css
masonstrap.min.js
moment.min.js
fullcalendar.min.js
fullcalendar.min.css
)
......@@ -4,6 +4,7 @@ Rails.application.routes.draw do
get 'sessions/update', as: 'update_session'
resources :instructors, only: [:index, :show]
get 'schedule', to: 'schedules#show', as: 'schedule'
scope :api do # Register /api routes
resources :courses, only: [:index, :show]
......
This source diff could not be displayed because it is too large. You can view the blob instead.
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.moment=t()}(this,function(){"use strict";var e,i;function c(){return e.apply(null,arguments)}function o(e){return e instanceof Array||"[object Array]"===Object.prototype.toString.call(e)}function u(e){return null!=e&&"[object Object]"===Object.prototype.toString.call(e)}function l(e){return void 0===e}function d(e){return"number"==typeof e||"[object Number]"===Object.prototype.toString.call(e)}function h(e){return e instanceof Date||"[object Date]"===Object.prototype.toString.call(e)}function f(e,t){var n,s=[];for(n=0;n<e.length;++n)s.push(t(e[n],n));return s}function m(e,t){return Object.prototype.hasOwnProperty.call(e,t)}function _(e,t){for(var n in t)m(t,n)&&(e[n]=t[n]);return m(t,"toString")&&(e.toString=t.toString),m(t,"valueOf")&&(e.valueOf=t.valueOf),e}function y(e,t,n,s){return Ot(e,t,n,s,!0).utc()}function g(e){return null==e._pf&&(e._pf={empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1,parsedDateParts:[],meridiem:null,rfc2822:!1,weekdayMismatch:!1}),e._pf}function p(e){if(null==e._isValid){var t=g(e),n=i.call(t.parsedDateParts,function(e){return null!=e}),s=!isNaN(e._d.getTime())&&t.overflow<0&&!t.empty&&!t.invalidMonth&&!t.invalidWeekday&&!t.weekdayMismatch&&!t.nullInput&&!t.invalidFormat&&!t.userInvalidated&&(!t.meridiem||t.meridiem&&n);if(e._strict&&(s=s&&0===t.charsLeftOver&&0===t.unusedTokens.length&&void 0===t.bigHour),null!=Object.isFrozen&&Object.isFrozen(e))return s;e._isValid=s}return e._isValid}function v(e){var t=y(NaN);return null!=e?_(g(t),e):g(t).userInvalidated=!0,t}i=Array.prototype.some?Array.prototype.some:function(e){for(var t=Object(this),n=t.length>>>0,s=0;s<n;s++)if(s in t&&e.call(this,t[s],s,t))return!0;return!1};var r=c.momentProperties=[];function w(e,t){var n,s,i;if(l(t._isAMomentObject)||(e._isAMomentObject=t._isAMomentObject),l(t._i)||(e._i=t._i),l(t._f)||(e._f=t._f),l(t._l)||(e._l=t._l),l(t._strict)||(e._strict=t._strict),l(t._tzm)||(e._tzm=t._tzm),l(t._isUTC)||(e._isUTC=t._isUTC),l(t._offset)||(e._offset=t._offset),l(t._pf)||(e._pf=g(t)),l(t._locale)||(e._locale=t._locale),0<r.length)for(n=0;n<r.length;n++)l(i=t[s=r[n]])||(e[s]=i);return e}var t=!1;function M(e){w(this,e),this._d=new Date(null!=e._d?e._d.getTime():NaN),this.isValid()||(this._d=new Date(NaN)),!1===t&&(t=!0,c.updateOffset(this),t=!1)}function S(e){return e instanceof M||null!=e&&null!=e._isAMomentObject}function D(e){return e<0?Math.ceil(e)||0:Math.floor(e)}function k(e){var t=+e,n=0;return 0!==t&&isFinite(t)&&(n=D(t)),n}function a(e,t,n){var s,i=Math.min(e.length,t.length),r=Math.abs(e.length-t.length),a=0;for(s=0;s<i;s++)(n&&e[s]!==t[s]||!n&&k(e[s])!==k(t[s]))&&a++;return a+r}function Y(e){!1===c.suppressDeprecationWarnings&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+e)}function n(i,r){var a=!0;return _(function(){if(null!=c.deprecationHandler&&c.deprecationHandler(null,i),a){for(var e,t=[],n=0;n<arguments.length;n++){if(e="","object"==typeof arguments[n]){for(var s in e+="\n["+n+"] ",arguments[0])e+=s+": "+arguments[0][s]+", ";e=e.slice(0,-2)}else e=arguments[n];t.push(e)}Y(i+"\nArguments: "+Array.prototype.slice.call(t).join("")+"\n"+(new Error).stack),a=!1}return r.apply(this,arguments)},r)}var s,O={};function T(e,t){null!=c.deprecationHandler&&c.deprecationHandler(e,t),O[e]||(Y(t),O[e]=!0)}function x(e){return e instanceof Function||"[object Function]"===Object.prototype.toString.call(e)}function b(e,t){var n,s=_({},e);for(n in t)m(t,n)&&(u(e[n])&&u(t[n])?(s[n]={},_(s[n],e[n]),_(s[n],t[n])):null!=t[n]?s[n]=t[n]:delete s[n]);for(n in e)m(e,n)&&!m(t,n)&&u(e[n])&&(s[n]=_({},s[n]));return s}function P(e){null!=e&&this.set(e)}c.suppressDeprecationWarnings=!1,c.deprecationHandler=null,s=Object.keys?Object.keys:function(e){var t,n=[];for(t in e)m(e,t)&&n.push(t);return n};var W={};function H(e,t){var n=e.toLowerCase();W[n]=W[n+"s"]=W[t]=e}function R(e){return"string"==typeof e?W[e]||W[e.toLowerCase()]:void 0}function C(e){var t,n,s={};for(n in e)m(e,n)&&(t=R(n))&&(s[t]=e[n]);return s}var F={};function L(e,t){F[e]=t}function U(e,t,n){var s=""+Math.abs(e),i=t-s.length;return(0<=e?n?"+":"":"-")+Math.pow(10,Math.max(0,i)).toString().substr(1)+s}var N=/(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,G=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,V={},E={};function I(e,t,n,s){var i=s;"string"==typeof s&&(i=function(){return this[s]()}),e&&(E[e]=i),t&&(E[t[0]]=function(){return U(i.apply(this,arguments),t[1],t[2])}),n&&(E[n]=function(){return this.localeData().ordinal(i.apply(this,arguments),e)})}function A(e,t){return e.isValid()?(t=j(t,e.localeData()),V[t]=V[t]||function(s){var e,i,t,r=s.match(N);for(e=0,i=r.length;e<i;e++)E[r[e]]?r[e]=E[r[e]]:r[e]=(t=r[e]).match(/\[[\s\S]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"");return function(e){var t,n="";for(t=0;t<i;t++)n+=x(r[t])?r[t].call(e,s):r[t];return n}}(t),V[t](e)):e.localeData().invalidDate()}function j(e,t){var n=5;function s(e){return t.longDateFormat(e)||e}for(G.lastIndex=0;0<=n&&G.test(e);)e=e.replace(G,s),G.lastIndex=0,n-=1;return e}var Z=/\d/,z=/\d\d/,$=/\d{3}/,q=/\d{4}/,J=/[+-]?\d{6}/,B=/\d\d?/,Q=/\d\d\d\d?/,X=/\d\d\d\d\d\d?/,K=/\d{1,3}/,ee=/\d{1,4}/,te=/[+-]?\d{1,6}/,ne=/\d+/,se=/[+-]?\d+/,ie=/Z|[+-]\d\d:?\d\d/gi,re=/Z|[+-]\d\d(?::?\d\d)?/gi,ae=/[0-9]{0,256}['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFF07\uFF10-\uFFEF]{1,256}|[\u0600-\u06FF\/]{1,256}(\s*?[\u0600-\u06FF]{1,256}){1,2}/i,oe={};function ue(e,n,s){oe[e]=x(n)?n:function(e,t){return e&&s?s:n}}function le(e,t){return m(oe,e)?oe[e](t._strict,t._locale):new RegExp(de(e.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(e,t,n,s,i){return t||n||s||i})))}function de(e){return e.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}var he={};function ce(e,n){var t,s=n;for("string"==typeof e&&(e=[e]),d(n)&&(s=function(e,t){t[n]=k(e)}),t=0;t<e.length;t++)he[e[t]]=s}function fe(e,i){ce(e,function(e,t,n,s){n._w=n._w||{},i(e,n._w,n,s)})}var me=0,_e=1,ye=2,ge=3,pe=4,ve=5,we=6,Me=7,Se=8;function De(e){return ke(e)?366:365}function ke(e){return e%4==0&&e%100!=0||e%400==0}I("Y",0,0,function(){var e=this.year();return e<=9999?""+e:"+"+e}),I(0,["YY",2],0,function(){return this.year()%100}),I(0,["YYYY",4],0,"year"),I(0,["YYYYY",5],0,"year"),I(0,["YYYYYY",6,!0],0,"year"),H("year","y"),L("year",1),ue("Y",se),ue("YY",B,z),ue("YYYY",ee,q),ue("YYYYY",te,J),ue("YYYYYY",te,J),ce(["YYYYY","YYYYYY"],me),ce("YYYY",function(e,t){t[me]=2===e.length?c.parseTwoDigitYear(e):k(e)}),ce("YY",function(e,t){t[me]=c.parseTwoDigitYear(e)}),ce("Y",function(e,t){t[me]=parseInt(e,10)}),c.parseTwoDigitYear=function(e){return k(e)+(68<k(e)?1900:2e3)};var Ye,Oe=Te("FullYear",!0);function Te(t,n){return function(e){return null!=e?(be(this,t,e),c.updateOffset(this,n),this):xe(this,t)}}function xe(e,t){return e.isValid()?e._d["get"+(e._isUTC?"UTC":"")+t]():NaN}function be(e,t,n){e.isValid()&&!isNaN(n)&&("FullYear"===t&&ke(e.year())&&1===e.month()&&29===e.date()?e._d["set"+(e._isUTC?"UTC":"")+t](n,e.month(),Pe(n,e.month())):e._d["set"+(e._isUTC?"UTC":"")+t](n))}function Pe(e,t){if(isNaN(e)||isNaN(t))return NaN;var n,s=(t%(n=12)+n)%n;return e+=(t-s)/12,1===s?ke(e)?29:28:31-s%7%2}Ye=Array.prototype.indexOf?Array.prototype.indexOf:function(e){var t;for(t=0;t<this.length;++t)if(this[t]===e)return t;return-1},I("M",["MM",2],"Mo",function(){return this.month()+1}),I("MMM",0,0,function(e){return this.localeData().monthsShort(this,e)}),I("MMMM",0,0,function(e){return this.localeData().months(this,e)}),H("month","M"),L("month",8),ue("M",B),ue("MM",B,z),ue("MMM",function(e,t){return t.monthsShortRegex(e)}),ue("MMMM",function(e,t){return t.monthsRegex(e)}),ce(["M","MM"],function(e,t){t[_e]=k(e)-1}),ce(["MMM","MMMM"],function(e,t,n,s){var i=n._locale.monthsParse(e,s,n._strict);null!=i?t[_e]=i:g(n).invalidMonth=e});var We=/D[oD]?(\[[^\[\]]*\]|\s)+MMMM?/,He="January_February_March_April_May_June_July_August_September_October_November_December".split("_");var Re="Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_");function Ce(e,t){var n;if(!e.isValid())return e;if("string"==typeof t)if(/^\d+$/.test(t))t=k(t);else if(!d(t=e.localeData().monthsParse(t)))return e;return n=Math.min(e.date(),Pe(e.year(),t)),e._d["set"+(e._isUTC?"UTC":"")+"Month"](t,n),e}function Fe(e){return null!=e?(Ce(this,e),c.updateOffset(this,!0),this):xe(this,"Month")}var Le=ae;var Ue=ae;function Ne(){function e(e,t){return t.length-e.length}var t,n,s=[],i=[],r=[];for(t=0;t<12;t++)n=y([2e3,t]),s.push(this.monthsShort(n,"")),i.push(this.months(n,"")),r.push(this.months(n,"")),r.push(this.monthsShort(n,""));for(s.sort(e),i.sort(e),r.sort(e),t=0;t<12;t++)s[t]=de(s[t]),i[t]=de(i[t]);for(t=0;t<24;t++)r[t]=de(r[t]);this._monthsRegex=new RegExp("^("+r.join("|")+")","i"),this._monthsShortRegex=this._monthsRegex,this._monthsStrictRegex=new RegExp("^("+i.join("|")+")","i"),this._monthsShortStrictRegex=new RegExp("^("+s.join("|")+")","i")}function Ge(e){var t=new Date(Date.UTC.apply(null,arguments));return e<100&&0<=e&&isFinite(t.getUTCFullYear())&&t.setUTCFullYear(e),t}function Ve(e,t,n){var s=7+t-n;return-((7+Ge(e,0,s).getUTCDay()-t)%7)+s-1}function Ee(e,t,n,s,i){var r,a,o=1+7*(t-1)+(7+n-s)%7+Ve(e,s,i);return o<=0?a=De(r=e-1)+o:o>De(e)?(r=e+1,a=o-De(e)):(r=e,a=o),{year:r,dayOfYear:a}}function Ie(e,t,n){var s,i,r=Ve(e.year(),t,n),a=Math.floor((e.dayOfYear()-r-1)/7)+1;return a<1?s=a+Ae(i=e.year()-1,t,n):a>Ae(e.year(),t,n)?(s=a-Ae(e.year(),t,n),i=e.year()+1):(i=e.year(),s=a),{week:s,year:i}}function Ae(e,t,n){var s=Ve(e,t,n),i=Ve(e+1,t,n);return(De(e)-s