Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UCalendar en RamosUC #104

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions front/assets/src/css/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,12 @@ input[type=number] {

/*when an element is selected and pointer re-enters the rating container, selected rate and siblings get semi transparent, as reminder of current selection*/
.rating:hover > input:checked ~ label:before{ opacity: 0.4; }

#ucalendar {
background: linear-gradient(140deg, #72dcdc 0%, #52abc6 100%);
transition: all 0.3s ease-in-out;
font-weight: bolder;
border: none;
color: white;
text-shadow: 0 0 1px #0c5564;
}
3 changes: 3 additions & 0 deletions front/assets/src/planner.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { add, remove, loadRamo, loadFromCookie } from "./utils/schedule"
import { loadInfo } from "./utils/info"
import { search } from "./utils/search"
import { loadQuota } from "./utils/quota"
import * as ucalendar from "./utils/ucalendar"

export {
toggle, toggleRow, toggleDay, clearSelects,
Expand All @@ -30,6 +31,8 @@ $(() => {
// Load ramos from cookie
wp.loadFromCookie()

$("#ucalendar").on("click", () => ucalendar.dowload_schedule())

// Load filters dropdowns
$("#campus").multipleSelect({ selectAll: false, showClear: true })
$("#formato").multipleSelect({ selectAll: false, showClear: true })
Expand Down
7 changes: 7 additions & 0 deletions front/assets/src/utils/schedule.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getCookie, setCookie } from "./cookies"
import ga_event from "./ga_event.js"
import * as ucalendar from "./ucalendar"


// Add ramo to cookie and load it to schedule
Expand All @@ -25,6 +26,9 @@ const remove = (id) => {
if (index != -1) {
saved.splice(index, 1)
}

ucalendar.remove_from_schedule_page(id)

setCookie("ramos", saved.join(","), 120)

var parents = $(`td > [name='ramo_${id}']`).parent().get()
Expand Down Expand Up @@ -64,6 +68,9 @@ const toCourseClassName = (type_of_course) => {

const loadRamoHandleResponse = (response, id, showDelete, ramos) => {
var ramo = response

ucalendar.add_from_schedule_page(id, ramo)

// Print in horario
for (let [key, value] of Object.entries(ramo.schedule)) {
var slot = $(`#${key.toUpperCase()}`)
Expand Down
59 changes: 59 additions & 0 deletions front/assets/src/utils/ucalendar/consts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
export const every_year_holidays = [
"05-01", // Día del trabajo
"05-21", // Días de las Glorias Navales
"08-15", // Asunción de la Virgen
"09-18", // Primera Junta Nacional de Gobierno
"09-19", // Glorias del Ejército
"10-11", // Celebración del Día del Encuentro de Dos Mundos
"10-31", // Día de las Iglesias Evangélicas y Protestantes
"11-01", // Día de Todos los Santos
"12-08", // Inmaculada Concepción de la Virgen
"12-25", // Navidad
]

export const period_range = {
"2023-1": [new Date(2023, 2, 6), new Date(2023, 5, 30)],
"2023-2": [new Date(2023, 7, 7), new Date(2023, 10, 1)],
}


export const holidays = [
// Semana Santa
new Date(2023, 3, 6),
new Date(2023, 3, 7),
new Date(2023, 3, 8),
new Date(2023, 3, 9),
// Receso
new Date(2023, 4, 2),
new Date(2023, 4, 3),
new Date(2023, 4, 4),
new Date(2023, 4, 5),
new Date(2023, 4, 6),
// San Pedro y San Pablo
new Date(2023, 5, 26),
// Asunción de la Virgen
new Date(2023, 7, 14),
// Receso
new Date(2023, 9, 2),
new Date(2023, 9, 3),
new Date(2023, 9, 4),
new Date(2023, 9, 5),
new Date(2023, 9, 6),
new Date(2023, 9, 7),
]

export const module_length = { hours: 1, minutes: 20 }

export const module_start_time = {
1: { hour: 8, minute: 30 },
2: { hour: 10, minute: 0 },
3: { hour: 11, minute: 30 },
4: { hour: 14, minute: 0 },
5: { hour: 15, minute: 30 },
6: { hour: 17, minute: 0 },
7: { hour: 18, minute: 30 },
8: { hour: 20, minute: 0 },
}

export const ics_day_names = { l: "MO", m: "TU", w: "WE", j: "TH", v: "FR", s: "SA", d: "SU" }
export const days = [...Object.keys(ics_day_names)]
125 changes: 125 additions & 0 deletions front/assets/src/utils/ucalendar/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/** @author @benjavicente */

/** @typedef {{[key: string]: string}} ScheduleMap */
/** @typedef {{ id: number, initials: string, name: string, period: string, schedule: ScheduleMap }} Course */
/** @typedef {{ day: string, module: number, type: string, group: ScheduleElement[] }} ScheduleElement */
/** @typedef {{ days: string[], modules: number[], type: string }} ScheduleBlock */


import { module_start_time, holidays, every_year_holidays, module_length, ics_day_names, days, period_range } from "./consts"
import { calendarTemplate, eventTemplate, exDateTemplate } from "./templates"
import { dispatchDownload, eqSet, firstIndex } from "./utils"


// API

/** @type {Course[]} */
let courses = []
export const add_from_schedule_page = (id, course) => courses.push({ id, ...course })
export const remove_from_schedule_page = (id) => courses = courses.filter((c) => c.id !== id)
export const dowload_schedule = () => dispatchDownload(makeCalendarFromCoruses(courses), "horario.ics")


// UCalendar

/** @param {Course[]} courses */
function makeCalendarFromCoruses(courses, semester = "2023-1") {
return calendarTemplate(courses.flatMap(groupModules).map((e) => toEvent(e, semester)).map(eventTemplate).join("\n"))
}

/** @param {Date} dateToChange, @param {Date} dateWithTime */
function changeTimeOfDate(dateToChange, dateWithTime) {
dateToChange.setHours(dateWithTime.getHours(), dateWithTime.getMinutes(), dateWithTime.getSeconds())
return dateToChange
}

/** @param {Date} start */
function generateExDates(start) {
const ex_dates = holidays.map((h) => new Date(h)) // copy dates
for (const [month, day] of every_year_holidays.map(h => h.split("-")))
ex_dates.push(new Date(2023, month - 1, + day))
return ex_dates.map(d => changeTimeOfDate(d, start)).map(exDateTemplate).join("\n")
}

/** @param {Course} course */
function groupModules({ schedule, ...course }) {

const scheduleDays = Object.entries(schedule).map(([[day, m], type]) => {
/** @type {ScheduleElement} */
const self = { day, module: parseInt(m), type, group: [] }
self.group.push(self)
return self
})

// Vertical
for (const bs of scheduleDays) {
for (const rs of scheduleDays) {
if (bs.group === rs.group || bs.type !== rs.type) continue

// Ve si están juntos y en el mismo día
if (bs.day !== rs.day) continue
if (Math.abs(bs.module - rs.module) > 1) continue

// No se puede expandir entre 3 y 4 (almuerzo)
if (bs.module === 3 && rs.module === 4) continue
if (bs.module === 4 && rs.module === 3) continue

bs.group.push(rs)
rs.group = bs.group
}
}

// Horizontal
for (const bs of scheduleDays) {
for (const rs of scheduleDays) {
if (bs.group === rs.group || bs.type !== rs.type) continue

// Ver si los grupos tienen los mismos modulos
const bsModSet = new Set(bs.group.map((e) => e.module))
const rsModSet = new Set(rs.group.map((e) => e.module))
if (!eqSet(bsModSet, rsModSet)) continue

bs.group.push(rs)
rs.group = bs.group
}
}

// Entregar grupos
return [...new Set(scheduleDays.map((g) => g.group))].map((group) => {
const days = [... new Set(group.map((g) => g.day))]
const modules = [... new Set(group.map((g) => g.module))]
const type = group[0].type
return { days, modules, type, group, course }
})
}

/** @param {ScheduleBlock} block, @param {string} semester */
function toEvent(block, semester) {
const first_module = Math.min(...block.modules)
const last_module = Math.max(...block.modules)
const first_day_number = firstIndex(days, block.days)

const [initial_date, last_date] = period_range[semester]

// Se necesita mover el primer dia de clases para que este en el primer día del bloque
// en específico, así podemos decir que el evento se repite de ahi hasta el ultimo dia
const initial_date_day = new Date(initial_date).getDay() // 0: Domingo, 1: Lunes, ...
const initial_date_offset = ((first_day_number + 1) - initial_date_day + 7) % 7
const initial_date_offsetted = new Date(initial_date)
initial_date_offsetted.setDate(initial_date.getDate() + initial_date_offset)

const { hour, minute } = module_start_time[first_module]
const start = new Date(initial_date_offsetted)
start.setHours(hour, minute, 0, 0) // Parte en el minuto y segundo 0

const end_delta = module_start_time[last_module]
const end = new Date(start)
end.setHours(end_delta.hour + module_length.hours, end_delta.minute + module_length.minutes)

const block_days = block.days.map((d) => ics_day_names[d]).join(",")

const summary = `${block.type === "CLAS" ? "" : `${block.type} `}${block.course.name}`
const ex_dates = generateExDates(start)

return { start, end, last_date, block_days, summary, ex_dates }
}
53 changes: 53 additions & 0 deletions front/assets/src/utils/ucalendar/templates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
const toICSDatetime = (date) => {
const year = date.getFullYear()
const month = `0${date.getMonth() + 1}`.slice(-2)
const day = `0${date.getDate()}`.slice(-2)
const hours = `0${date.getHours()}`.slice(-2)
const minutes = `0${date.getMinutes()}`.slice(-2)
const seconds = `0${date.getSeconds()}`.slice(-2)
return `${year}${month}${day}T${hours}${minutes}${seconds}`
}


export const calendarTemplate = (calendar) => `
BEGIN:VCALENDAR
PRODID:-//remos-uc//ucalendar//CL
VERSION:2.0
CALSCALE:GREGORIAN
X-WR-TIMEZONE:America/Santiago
BEGIN:VTIMEZONE
TZID:America/Santiago
X-LIC-LOCATION:America/Santiago
BEGIN:STANDARD
TZOFFSETFROM:-0300
TZOFFSETTO:-0400
TZNAME:-04
DTSTART:19700405T000000
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU
END:STANDARD
BEGIN:DAYLIGHT
TZOFFSETFROM:-0400
TZOFFSETTO:-0300
TZNAME:-03
RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=1SU
END:DAYLIGHT
END:VTIMEZONE
${calendar}
END:VCALENDAR
`

let event_id = 0
export const eventTemplate = (e) => `
BEGIN:VEVENT
DTSTART;TZID=America/Santiago:${toICSDatetime(e.start)}
DTEND;TZID=America/Santiago:${toICSDatetime(e.end)}
RRULE:FREQ=WEEKLY;UNTIL=${toICSDatetime(e.last_date)};BYDAY=${e.block_days}
${e.ex_dates}
DTSTAMP:${toICSDatetime(new Date())}
UID:${event_id++}
DESCRIPTION:${e.description || ""}
SUMMARY:${e.summary}
END:VEVENT
`

export const exDateTemplate = (date) => `EXDATE;TZID=America/Santiago:${toICSDatetime(date)}`
15 changes: 15 additions & 0 deletions front/assets/src/utils/ucalendar/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const eqSet = (a, b) => a.size === b.size && [...a].every(value => b.has(value))

export function firstIndex(array_to_search, values) {
for (const value of array_to_search) if (values.includes(value)) return array_to_search.indexOf(value)
}

export function dispatchDownload(content, name = "horario.ics") {
const e = document.createElement("a")
e.setAttribute("href", `data:text/plain;charset=utf-8,${encodeURIComponent(content)}`)
e.setAttribute("download", name)
e.style.display = "none"
document.body.appendChild(e)
e.click()
document.body.removeChild(e)
}
1 change: 1 addition & 0 deletions front/templates/courses/planner.html
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ <h3 class="modal-title">Horarios guardados</h3>
<button onclick="wp.viewSaved()" type="button" class="btn btn-primary ms-3 mb-3" data-bs-toggle="modal" data-bs-target="#savedModal">Ver horarios guardados</button>
<button onclick="wp.share()" type="button" class="btn btn-primary ms-3 mb-3 text-white">Compartir</button>
<button onclick="wp.buscacursos()" type="button" class="btn btn-primary ms-3 mb-3">Ver en Buscacursos</button>
<button id="ucalendar" class="btn ms-3 mb-3">Descargar horario</button>
<button type="button" class="btn btn-primary ms-3 mb-3" data-bs-toggle="modal" data-bs-target="#saveModal">Guardar</button>
<table class="table table-sm" id="ramos"></table>
</div>
Expand Down