cvut/fittable

View on GitHub
src/callbacks/sirius.js

Summary

Maintainability
D
2 days
Test Coverage
/**
 * Fittable application
 *
 * This is the main javascript file for fittable app.
 * Implements callbacks using Sirius API as data source. Then initializes the fittable widget.
 *
 * @author      Marián Hlaváč
 * @version     1.0
 */

import ReactCookie from 'react-cookie'
import URL from 'url'
import R from 'ramda'
import camelize from 'camelize'
import { fmoment } from '../date'
import { renameKeys } from '../utils'

import { FACULTY_ID, SIRIUS_PROXY_PATH, OAUTH_PROXY_PATH } from '../config'

const emptyObject = (obj) => R.is(Object, obj) && R.pipe(R.keys, R.propEq('length', 0))

/**
 * Logged user's username.
 */
const username = ReactCookie.load('oauth_username')

// Base URI of proxy for Sirius API
const siriusAPIUrl = `${getBaseUri()}${SIRIUS_PROXY_PATH}`

// Base URI of OAuth proxy
const oauthAPIUrl = `${getBaseUri()}${OAUTH_PROXY_PATH}`

// TODO: This should be removed!
const defaultLimit = 200

const STATUS_ERROR_TYPES = {
  0: 'connection',
  401: 'unauthorized',
  403: 'access',
  404: 'notfound',
  500: 'servererror',
}

function generateError (status, message = 'No message specified') {
  const error = new Error(message)
  if (status in STATUS_ERROR_TYPES) {
    error.type = STATUS_ERROR_TYPES[status]
    /* if (error.type === 'notfound' && view === 'person' && parameter === username) {
      error.type = 'own' + error.type
    } */
  } else {
    error.type = 'generic'
  }
  return error
}

function redirectToLanding () {
  window.location.href = 'landing.html'
}

/**
 * @return {string} a base URI (aka relative URL root) of this site with
 * trailing slash omitted.
 */
function getBaseUri () {
  let baseUri = document.baseURI

  // IE doesn't support baseURI, workaround
  if (typeof baseURI === 'undefined') {
    const baseTag = document.getElementsByTagName('base')[0]
    baseUri = baseTag.href
  }

  return URL.parse(baseUri).pathname.replace(/\/$/, '')
}

function isUserLoggedIn () {
  return username && (
    // TODO: Remove oauth_access_token once we implement OAuth for dev-server.
    ReactCookie.load('oauth_refresh_token') || ReactCookie.load('oauth_access_token')
  )
}

const makeRequest = R.curry((method, url, requestHandler) => {
  const request = new XMLHttpRequest()

  request.onreadystatechange = () => {
    if (request.readyState === 4) {
      // Bail out early on 401
      if (request.status === 401) {
        redirectToLanding()
        return
      }

      requestHandler(request)
    }
  }
  request.open(method, encodeURI(url), true)
  request.send(null)

  return request
})

const ajaxGet = makeRequest('GET')
const ajaxPost = makeRequest('POST')

/**
 * Data callback, requesting the events from Sirius API, depending on view type and range.
 * @param params   Object with request parameters
 * @param callback Callback to be called after successful request
 */
function dataCallback ({calendarType, dateFrom, dateTo, calendarId}, callback) {
  // FIXME: until `me` is a valid shortcut on Sirius
  if (calendarId === 'me' && calendarType === 'people') {
    calendarId = username
  }

  const path = `${calendarType}/${calendarId}/events` +
    `?from=${dateFrom}&to=${dateTo}&limit=${defaultLimit}` +
    `&include=courses,teachers,schedule_exceptions&deleted=true`

  function requestHandler (request) {
    if (request.readyState === XMLHttpRequest.DONE) {
      if (request.status === 200) {
        // Request successful
        const ajaxresult = JSON.parse(request.responseText)

        const linknames = { teachers: [], courses: [], exceptions: [] }

        const responseEvents = ajaxresult.events || []
        const data = responseEvents.map(event => {
          // And add new event to array
          const newEvent = {
            id: event.id,
            name: event.name,
            note: event.note,
            course: event.links.course,
            startsAt: event.starts_at,
            endsAt: event.ends_at,
            sequenceNumber: event.sequence_number,
            type: event.event_type,
            room: event.links.room,
            flag: null,
            notification: false,
            cancelled: event.deleted,
            replacement: false,
            teachers: event.links.teachers,
            details: {
              students: event.links.students || [],  // this is null for students!
              capacity: event.capacity,
              occupied: event.occupied,
              parallel: event.parallel,
              appliedExceptions: event.links.applied_exceptions,
            },
          }

          // If the original data are present, insert one reverted event
          if (!emptyObject(event.original_data)) {
            // Convert times to milliseconds
            const rangeFromMs = (new Date(dateFrom)).getMilliseconds()
            const rangeToMs = (new Date(dateTo)).getMilliseconds()
            const originalFromMs = (new Date(event.original_data.starts_at)).getMilliseconds()

            // Check if the original start is between the range
            if (rangeFromMs <= originalFromMs && originalFromMs >= rangeToMs) {
              // The event is cancelled, show with original data
              newEvent.startsAt = event.original_data.starts_at
              newEvent.endsAt = event.original_data.ends_at
              newEvent.room = event.original_data.room_id
              newEvent.cancelled = true
              newEvent.replacedAt = event.starts_at
            } else {
              // The event is replacement
              newEvent.replacement = true
              newEvent.replaces = event.original_data.starts_at
            }
          }
          return newEvent
        })

        // Add teachers links full names
        if ('linked' in ajaxresult) {
          if ('teachers' in ajaxresult.linked) {
            for (let i = 0; i < ajaxresult.linked.teachers.length; i++) {
              // Add teacher link full name
              linknames.teachers.push({
                id: ajaxresult.linked.teachers[i].id,
                name: {
                  cs: ajaxresult.linked.teachers[i].full_name,
                  en: ajaxresult.linked.teachers[i].full_name,
                },
              })
            }
          }

          // Add courses links full names
          if ('courses' in ajaxresult.linked) {
            for (let i = 0; i < ajaxresult.linked.courses.length; i++) {
              // Add course link full name
              linknames.courses.push({
                id: ajaxresult.linked.courses[i].id,
                name: {
                  cs: ajaxresult.linked.courses[i].name.cs,
                  en: ajaxresult.linked.courses[i].name.en,
                },
              })
            }
          }

          // Add exceptions links full names
          if ('schedule_exceptions' in ajaxresult.linked) {
            for (let i = 0; i < ajaxresult.linked.schedule_exceptions.length; i++) {
              // Add exceptions link full name
              linknames.exceptions.push({
                id: ajaxresult.linked.schedule_exceptions[i].id,
                type: ajaxresult.linked.schedule_exceptions[i].exception_type,
                name: ajaxresult.linked.schedule_exceptions[i].name,
              })
            }
          }
        }

        // Send data to fittable
        callback(null, {events: data, linkNames: linknames})
      } else {
        // Request failed
        callback(generateError(request.status, 'dataCallback failed'))
      }
    }
  }

  ajaxGet(`${siriusAPIUrl}/${path}`, requestHandler)
}

/**
 * Search callback, returning search results from Sirius API.
 * @param query
 * @param callback
 */
function searchCallback (query, callback) {
  function requestHandler (request) {
    if (request.readyState === XMLHttpRequest.DONE) {
      if (request.status === 200) {
        // Request successful
        const ajaxresult = JSON.parse(request.responseText)

        // Send data to fittable
        callback(ajaxresult.results)
      } else {
        // Request failed
        // fittable.onError(generateError(request.status).type)
        console.error(generateError(request.status))
      }
    }
  }

  ajaxGet(`${siriusAPIUrl}/search?q=${query}&limit=${defaultLimit}`, requestHandler)
}

function semesterDataCallback (callback) {
  function requestHandler (request) {
    if (request.readyState === XMLHttpRequest.DONE) {

      // Request successful
      if (request.status === 200) {
        const data = camelize(JSON.parse(request.responseText))
        const semesters = R.map(convertSemester, data.semesters || [])

        // Send semester data to fittable
        callback(semesters)

      // Request failed
      } else {
        // fittable.onError(generateError(request.status).type)
        console.error(generateError(request.status))
      }
    }
  }

  ajaxGet(`${siriusAPIUrl}/semesters?faculty=${FACULTY_ID}&limit=${defaultLimit}`, requestHandler)
}

const convertInterval = R.pipe(
  // Badly named in Sirius API; "at" marks a specific time, "on" is for date.
  renameKeys({ startsAt: 'startsOn', endsAt: 'endsOn' }),
  // Convert string dates to frozen moment.
  R.evolve({ startsOn: fmoment, endsOn: fmoment })
)

const convertSemester = R.pipe(
  (semester) => ({
    ...semester,
    periods: R.map(convertInterval, semester.periods),
    breakDuration: 15,  // FIXME: replace this and that two below with semester.hourStarts
    dayStartsAtHour: 7.5,
    dayEndsAtHour: 21.25,
  }),
  convertInterval
)

function fetchUserCallback (cb) {
  function requestHandler (request) {
    if (request.readyState === XMLHttpRequest.DONE) {
      if (request.status === 200) {
        // Request successful
        const rawData = JSON.parse(request.responseText)

        if (!rawData || !rawData.people) {
          cb(generateError('generic'))
          return
        }

        const rawUser = rawData.people

        const data = {
          publicAccessToken: rawUser['access_token'],
          id: rawUser.id,
          name: rawUser.name,
        }

        cb(null, data)
      } else {
        // Request failed
        cb(generateError(request.status).type)
      }
    }
  }

  ajaxGet(`${siriusAPIUrl}/people/${username}`, requestHandler)
}

function logoutUserCallback (cb) {
  ajaxPost(`${oauthAPIUrl}/logout`, (request) => {
    if (request.readyState === XMLHttpRequest.DONE) {
      if (request.status < 400) {
        cb(null)
      } else {
        cb(generateError(request.status, 'Logout failed'))
      }
    }
  })
}

// If the user is not logged in, redirect him to the landing page
if (!isUserLoggedIn()) {
  redirectToLanding()
}

export {
  dataCallback as data,
  searchCallback as search,
  semesterDataCallback as semesterData,
  fetchUserCallback,
  logoutUserCallback,
}