├── .gitignore ├── dev ├── rmp_server │ ├── requirements.txt │ ├── .gitignore │ ├── README.md │ ├── __init__.py │ └── rate_my_professor │ │ ├── README.md │ │ └── __init__.py ├── schedule_of_courses │ ├── README.md │ └── scrape.py └── templates │ └── calendar.html ├── app ├── src │ ├── index.css │ ├── scripts │ │ ├── soc │ │ │ ├── index.ts │ │ │ ├── course.ts │ │ │ ├── section.ts │ │ │ ├── meet.ts │ │ │ ├── calendar.ts │ │ │ └── soc.tsx │ │ ├── api.tsx │ │ ├── utils.ts │ │ ├── scheduleGenerator.tsx │ │ └── apiTypes.ts │ ├── constants │ │ ├── scheduleGenerator.ts │ │ ├── schedule.ts │ │ ├── frontend.ts │ │ └── soc.ts │ ├── components │ │ ├── MultipleCourseDisplay.tsx │ │ ├── MultipleSelectionDisplay.tsx │ │ ├── MultipleScheduleDisplay.tsx │ │ ├── CourseDisplay.tsx │ │ ├── SelectionDisplay.tsx │ │ ├── SectionDisplay.tsx │ │ ├── SectionPicker.tsx │ │ ├── ScheduleBuilder.tsx │ │ └── ScheduleDisplay.tsx │ └── main.tsx ├── public │ └── logo.png ├── postcss.config.js ├── tailwind.config.js ├── .prettierrc.js ├── tsconfig.node.json ├── .gitignore ├── .eslintrc.cjs ├── vite.config.ts ├── index.html ├── tsconfig.json └── package.json ├── README.md └── LICENSE.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /dev/rmp_server/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | Flask_Cors 3 | gql 4 | requests 5 | -------------------------------------------------------------------------------- /app/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /app/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ufosc/SwampScheduler/HEAD/app/public/logo.png -------------------------------------------------------------------------------- /app/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /app/src/scripts/soc/index.ts: -------------------------------------------------------------------------------- 1 | export * from "@scripts/soc/soc"; 2 | export * from "@scripts/soc/course"; 3 | export * from "@scripts/soc/section"; 4 | export * from "@scripts/soc/meet"; 5 | -------------------------------------------------------------------------------- /dev/schedule_of_courses/README.md: -------------------------------------------------------------------------------- 1 | # UF's Schedule of Courses Scraper 2 | 3 | ## Usage 4 | 5 | Change the desired term and category (program) in `scrape.py` and then run it. 6 | It's that easy! 7 | -------------------------------------------------------------------------------- /app/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /app/.prettierrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import("prettier").Options} */ 2 | const config = { 3 | trailingComma: "all", 4 | tabWidth: 4, 5 | semi: true, 6 | singleQuote: false, 7 | }; 8 | 9 | export default config; 10 | -------------------------------------------------------------------------------- /app/src/constants/scheduleGenerator.ts: -------------------------------------------------------------------------------- 1 | export const LIMIT_VALUES = [1e3, 5e4, 2e5, 1e6, 2e6, Infinity]; 2 | 3 | export const LIMITS: Array<[number, string]> = LIMIT_VALUES.map((val) => [ 4 | val, 5 | val == Infinity ? "∞" : val.toLocaleString(), 6 | ]); 7 | -------------------------------------------------------------------------------- /app/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /dev/rmp_server/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | .flaskenv 4 | *.pyc 5 | *.pyo 6 | env/ 7 | venv/ 8 | .venv/ 9 | env* 10 | dist/ 11 | build/ 12 | *.egg 13 | *.egg-info/ 14 | .tox/ 15 | .cache/ 16 | .pytest_cache/ 17 | .idea/ 18 | docs/_build/ 19 | .vscode 20 | 21 | # Coverage reports 22 | htmlcov/ 23 | .coverage 24 | .coverage.* 25 | *,cover 26 | -------------------------------------------------------------------------------- /dev/rmp_server/README.md: -------------------------------------------------------------------------------- 1 | # Schedule Helper Severver 2 | 3 | ## API Endpoints 4 | 5 | ### /teacher-ratings 6 | 7 | This endpoint will return a teachers rating data from RateMyProfessor.com when parameter "name" is passed with teacher's full name. 8 | 9 | ### /schedule/ 10 | 11 | This simply proxied to UF SOC Api with all the parameters that are passed to this endpoint. -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | .env 11 | node_modules 12 | dist 13 | dist-ssr 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /app/src/components/MultipleCourseDisplay.tsx: -------------------------------------------------------------------------------- 1 | import { Course } from "@scripts/soc"; 2 | import CourseDisplay from "@components/CourseDisplay"; 3 | 4 | interface Props { 5 | courses: Course[]; 6 | } 7 | 8 | export default function MultipleCourseDisplay(props: Props) { 9 | const courses = props.courses.map((course: Course) => ( 10 | 11 | )); 12 | 13 | return
{courses}
; 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import { QueryClient, QueryClientProvider } from "react-query"; 4 | import ScheduleBuilder from "@components/ScheduleBuilder.tsx"; 5 | import "./index.css"; 6 | 7 | const queryClient = new QueryClient(); 8 | ReactDOM.createRoot(document.getElementById("root")!).render( 9 | 10 | 11 | 12 | 13 | , 14 | ); 15 | -------------------------------------------------------------------------------- /app/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "plugin:react-hooks/recommended", 8 | ], 9 | ignorePatterns: ["dist", ".eslintrc.cjs"], 10 | parser: "@typescript-eslint/parser", 11 | plugins: ["react-refresh"], 12 | rules: { 13 | "react-refresh/only-export-components": [ 14 | "warn", 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /app/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react-swc"; 3 | import path from "path"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react()], 8 | resolve: { 9 | alias: { 10 | "@": path.resolve(__dirname, "./src"), 11 | "@components": path.resolve(__dirname, "./src/components"), 12 | "@scripts": path.resolve(__dirname, "./src/scripts"), 13 | "@constants": path.resolve(__dirname, "./src/constants"), 14 | }, 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Schedule Helper 8 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /dev/templates/calendar.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 21 | 22 | Calendar Template 23 | 24 | 25 |

26 | Hello world! 27 |

28 | 29 | -------------------------------------------------------------------------------- /app/src/constants/schedule.ts: -------------------------------------------------------------------------------- 1 | import { Term } from "@constants/soc"; 2 | 3 | export interface PeriodCount { 4 | regular: number; 5 | extra: number; 6 | all: number; 7 | } 8 | 9 | const FallSpringCount: PeriodCount = { 10 | regular: 11, 11 | extra: 3, 12 | all: 14, 13 | }; 14 | 15 | const SummerCount: PeriodCount = { 16 | regular: 7, 17 | extra: 2, 18 | all: 9, 19 | }; 20 | 21 | export const PERIOD_COUNTS: Record = { 22 | [Term.Fall]: FallSpringCount, 23 | [Term.Spring]: FallSpringCount, 24 | [Term.Summer]: SummerCount, 25 | [Term.Summer_A]: SummerCount, 26 | [Term.Summer_B]: SummerCount, 27 | [Term.Summer_C]: SummerCount, 28 | }; 29 | -------------------------------------------------------------------------------- /app/src/constants/frontend.ts: -------------------------------------------------------------------------------- 1 | const sectionColors: string[] = [ 2 | "bg-red-200", 3 | "bg-lime-200", 4 | "bg-cyan-200", 5 | "bg-fuchsia-200", 6 | "bg-amber-200", 7 | "bg-green-200", 8 | "bg-orange-200", 9 | ]; 10 | 11 | export function getSectionColor(sectionInd: number): string { 12 | return sectionColors[sectionInd % sectionColors.length]; 13 | } 14 | 15 | const SearchByStringExampleMap = new Map([ 16 | ["course-code", "MAS3114"], 17 | ["course-title", "Linear Algebra"], 18 | ["instructor", "Huang"], 19 | ]); 20 | 21 | export function getSearchByStringExample(searchByStr: string): string { 22 | const val = SearchByStringExampleMap.get(searchByStr); 23 | if (val === undefined) 24 | throw new Error( 25 | `SearchBy string "${searchByStr}" does not map to an example`, 26 | ); 27 | return val; 28 | } 29 | -------------------------------------------------------------------------------- /app/src/scripts/soc/course.ts: -------------------------------------------------------------------------------- 1 | import { API_Course } from "@scripts/apiTypes"; 2 | import { Section } from "@scripts/soc"; 3 | import { Term } from "@constants/soc"; 4 | import { MinMax } from "@scripts/utils.ts"; 5 | 6 | export class Course { 7 | uid: string; 8 | term: Term; 9 | code: string; 10 | id: string; 11 | name: string; 12 | description: string; 13 | sections: Section[]; 14 | 15 | constructor(uid: string, term: Term, courseJSON: API_Course) { 16 | this.uid = uid; 17 | this.term = term; 18 | this.code = courseJSON.code; 19 | this.id = courseJSON.courseId; 20 | this.name = courseJSON.name; 21 | this.description = courseJSON.description; 22 | this.sections = []; 23 | } 24 | 25 | get credits(): MinMax { 26 | const credits = this.sections.map((s) => s.credits), 27 | minimums = credits.map((c) => c.min), 28 | maximums = credits.map((c) => c.max); 29 | 30 | return new MinMax(Math.min(...minimums), Math.max(...maximums)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "types": ["vite/client"], 8 | "skipLibCheck": true, 9 | 10 | /* Path Aliases */ 11 | "baseUrl": "./", 12 | "paths": { 13 | "@/*": ["src/*"], 14 | "@components/*": ["src/components/*"], 15 | "@scripts/*": ["src/scripts/*"], 16 | "@constants/*": ["src/constants/*"] 17 | }, 18 | 19 | /* Bundler mode */ 20 | "moduleResolution": "bundler", 21 | "allowImportingTsExtensions": true, 22 | "resolveJsonModule": true, 23 | "isolatedModules": true, 24 | "noEmit": true, 25 | "jsx": "react-jsx", 26 | 27 | /* Linting */ 28 | "strict": true, 29 | "noUnusedLocals": true, 30 | "noUnusedParameters": true, 31 | "noFallthroughCasesInSwitch": true 32 | }, 33 | "include": ["src"], 34 | "references": [{ "path": "./tsconfig.node.json" }] 35 | } 36 | -------------------------------------------------------------------------------- /app/src/scripts/api.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { API_Filters } from "@scripts/apiTypes"; 3 | import { fetchCORS } from "@scripts/utils"; 4 | 5 | export class CampusMap { 6 | static getLocationURL(locationID: string): string { 7 | const baseURL: string = "https://campusmap.ufl.edu/#/index/"; 8 | return baseURL + locationID; 9 | } 10 | 11 | static createLink( 12 | locationID: string | null, 13 | locationStr: string | null, 14 | inner: React.JSX.Element, 15 | _target: string = "_blank", 16 | ): React.JSX.Element { 17 | if (locationID && locationStr) 18 | return ( 19 | 20 | {inner} 21 | 22 | ); 23 | return {inner}; // Don't add a link, and don't add 24 | } 25 | } 26 | 27 | enum UF_SOC_API_URL { 28 | FILTERS = "https://one.uf.edu/apix/soc/filters", 29 | } 30 | 31 | export class UF_SOC_API { 32 | static async fetchFilters(): Promise { 33 | return await fetchCORS(UF_SOC_API_URL.FILTERS).then((r) => r.json()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /dev/rmp_server/__init__.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from flask import Flask, request, jsonify 3 | from flask_cors import cross_origin 4 | 5 | import rate_my_professor 6 | 7 | rmp = rate_my_professor.RateMyProfessor() 8 | app = Flask(__name__) 9 | 10 | 11 | @app.route('/teacher-ratings/', methods=['GET']) 12 | @cross_origin() 13 | def get_teacher_ratings(): 14 | """ 15 | Returns a teacher's rating data based on their name. 16 | """ 17 | teacher_name = request.args.get('name') 18 | search_result = rmp.search_teachers(teacher_name) 19 | 20 | if teacher_name in search_result: 21 | teacher_id = search_result[teacher_name] 22 | return jsonify(rmp.get_teacher_ratings(teacher_id)) 23 | else: 24 | return jsonify({}) 25 | 26 | 27 | @app.route('/schedule/', methods=['GET']) 28 | @cross_origin() 29 | def get_schedule(): 30 | """ 31 | Returns schedule data by proxying UF SOC API 32 | using same query parameters as accessed with this URL. 33 | """ 34 | payload = request.args 35 | response = requests.get( 36 | "https://one.ufl.edu/apix/soc/schedule/", 37 | params=payload, 38 | timeout=10, 39 | ) 40 | return jsonify(response.json()) 41 | 42 | 43 | if __name__ == '__main__': 44 | app.run() 45 | -------------------------------------------------------------------------------- /app/src/scripts/utils.ts: -------------------------------------------------------------------------------- 1 | export function fetchCORS(input: RequestInfo | URL, init?: RequestInit) { 2 | // noinspection JSUnresolvedReference 3 | const { VITE_CORS_API_KEY: cors_api_key } = import.meta.env; 4 | if (!cors_api_key) throw new Error("CORS API key is missing"); 5 | 6 | return fetch(`https://proxy.cors.sh/${input}`, { 7 | ...init, 8 | headers: { 9 | ...init?.headers, 10 | "x-cors-api-key": cors_api_key, 11 | }, 12 | }); 13 | } 14 | 15 | export function* take(max: number, iterable: Iterable): Generator { 16 | for (const item of iterable) { 17 | if (max-- <= 0) return; 18 | yield item; 19 | } 20 | } 21 | 22 | interface Countable { 23 | length: number; 24 | } 25 | 26 | export function notEmpty(val: T): boolean { 27 | return val.length > 0; 28 | } 29 | 30 | export function notNullish(value: T | null | undefined): value is T { 31 | return value !== null && value !== undefined; 32 | } 33 | 34 | export function arrayEquals(a: Array, b: Array) { 35 | return a.length === b.length && a.every((val, ind) => val === b[ind]); 36 | } 37 | 38 | export class MinMax { 39 | readonly min: T; 40 | readonly max: T; 41 | 42 | constructor(min: T, max: T) { 43 | this.min = min; 44 | this.max = max; 45 | } 46 | 47 | get display(): string { 48 | return this.min == this.max ? `${this.min}` : `${this.min}-${this.max}`; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "schedule-helper", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "npm run check-format && tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview", 11 | "check-format": "prettier . --check", 12 | "fix-format": "prettier . --write" 13 | }, 14 | "dependencies": { 15 | "classnames": "^2.3.2", 16 | "ical-generator": "^6.0.1", 17 | "ics": "^3.7.2", 18 | "moment": "^2.30.1", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "react-drag-and-drop": "^3.0.0", 22 | "react-fitty": "^1.0.1", 23 | "react-icons": "^4.10.1", 24 | "react-query": "^3.39.3", 25 | "real-cancellable-promise": "^1.2.0", 26 | "rrule": "^2.8.1" 27 | }, 28 | "devDependencies": { 29 | "@types/react": "^18.2.15", 30 | "@types/react-dom": "^18.2.7", 31 | "@typescript-eslint/eslint-plugin": "^6.0.0", 32 | "@typescript-eslint/parser": "^6.0.0", 33 | "@vitejs/plugin-react-swc": "^3.3.2", 34 | "autoprefixer": "^10.4.14", 35 | "eslint": "^8.45.0", 36 | "eslint-plugin-react-hooks": "^4.6.0", 37 | "eslint-plugin-react-refresh": "^0.4.3", 38 | "postcss": "^8.4.27", 39 | "prettier": "3.0.0", 40 | "tailwindcss": "^3.3.3", 41 | "typescript": "^5.0.2", 42 | "vite": "^4.4.5" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/components/MultipleSelectionDisplay.tsx: -------------------------------------------------------------------------------- 1 | import { Selection } from "@scripts/scheduleGenerator"; 2 | import { Section } from "@scripts/soc"; 3 | import SelectionDisplay from "@components/SelectionDisplay"; 4 | 5 | interface Props { 6 | selections: Selection[]; 7 | handleDrop: (ind: number, uid: string) => Promise; 8 | newSelection: () => void; 9 | handleRemove: (sectionToRemove: Section) => void; 10 | handleDeleteSelection: (ind: number) => void; 11 | } 12 | 13 | export default function MultipleSelectionDisplay(props: Props) { 14 | // TODO: don't use index? 15 | const selectionDisplays = props.selections.map((sel, i) => ( 16 | // TODO: add a unique to each selection and use the id as the key 17 | 25 | )); 26 | 27 | return ( 28 |
29 |
{selectionDisplays}
30 | {/* TODO: reconsider all these classes */} 31 | 39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /app/src/components/MultipleScheduleDisplay.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Schedule } from "@scripts/scheduleGenerator"; 3 | import ScheduleDisplay from "@components/ScheduleDisplay"; 4 | 5 | const NUM_PER_PAGE = 25; 6 | 7 | interface Props { 8 | schedules: Schedule[]; 9 | numPerPage?: number; 10 | } 11 | 12 | export default function MultipleScheduleDisplay(props: Props) { 13 | const [numPages, setNumPages] = useState(1); 14 | 15 | const maxSchedulesToShow = (props.numPerPage ?? NUM_PER_PAGE) * numPages; 16 | const schedulesToShow = props.schedules.slice(0, maxSchedulesToShow); 17 | return ( 18 |
19 |

20 | 21 | {props.schedules.length.toLocaleString()} Schedules 22 | Generated 23 | 24 |

25 | 26 | {schedulesToShow.map((schedule: Schedule, s) => ( 27 |
28 | Schedule #{s + 1} 29 | 30 |
31 | ))} 32 | 33 | {maxSchedulesToShow < props.schedules.length && ( 34 |
35 | 43 |
44 | )} 45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /app/src/components/CourseDisplay.tsx: -------------------------------------------------------------------------------- 1 | import { Course, Section } from "@scripts/soc"; 2 | import SectionDisplay from "@components/SectionDisplay"; 3 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 4 | // @ts-ignore 5 | import { Draggable } from "react-drag-and-drop"; 6 | 7 | interface Props { 8 | course: Course; 9 | } 10 | 11 | export default function CourseDisplay(props: Props) { 12 | const sectionDisplays = props.course.sections.map((section: Section) => ( 13 | 14 | )); 15 | 16 | return ( 17 |
18 | {/* COURSE INFORMATION */} 19 | 20 |
21 |
22 | {/* Course Code & Name */} 23 |

24 | {props.course.code} {props.course.name} 25 |

26 | 27 | {/* Description */} 28 |
29 |

30 | {props.course.description} 31 |

32 |
33 | 34 | {/* Additional Information */} 35 |
36 | ({props.course.credits.display} Credits) 37 |
38 |
39 |
40 |
41 | 42 | {/* SECTIONS */} 43 |
{sectionDisplays}
44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /app/src/components/SelectionDisplay.tsx: -------------------------------------------------------------------------------- 1 | import { Section } from "@scripts/soc"; 2 | import SectionDisplay from "@components/SectionDisplay"; 3 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 4 | // @ts-ignore 5 | import { Droppable } from "react-drag-and-drop"; 6 | import { Selection } from "@scripts/scheduleGenerator"; 7 | import { GrClose } from "react-icons/gr"; 8 | 9 | interface Props { 10 | ind: number; 11 | selection: Selection; 12 | handleDrop: (ind: number, uid: string) => Promise; 13 | handleRemove: (sectionToRemove: Section) => void; 14 | handleDeleteSelection: (ind: number) => void; 15 | } 16 | 17 | export default function SelectionDisplay(props: Props) { 18 | const doDrop = ({ uid }: { uid: string }) => { 19 | props.handleDrop(props.ind, uid).then(); 20 | }; 21 | 22 | const sectionDisplays = props.selection.map((section: Section, idx) => ( 23 | 28 | )); 29 | 30 | return ( 31 |
32 | 33 |
34 |
35 | Course {props.ind + 1} 36 | 44 |
45 | 46 |
{sectionDisplays}
47 |
48 |
49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /dev/rmp_server/rate_my_professor/README.md: -------------------------------------------------------------------------------- 1 | # Python Wrapper For Rate My Professor's API 2 | 3 | ## Classes 4 | 5 | ### Teacher 6 | 7 | - uid -> ID of the teacher 8 | - first_name -> First name of teacher 9 | - last_name -> Last name of teacher 10 | - avg_difficulty -> Float value of difficulty rating of the teacher 11 | - avg_rating -> Float value of rating of the teacher 12 | - would_take_again_count -> Number of students who would take again 13 | - would_take_again_percent -> Percentage of students who would take again 14 | - ratings_count -> Number of ratings 15 | - rating -> A list of ratings of Rating dataclass 16 | 17 | ### Rating 18 | 19 | - class_name -> Name of the class 20 | - is_attendance_required -> Is attendance required in the class 21 | - is_for_credit -> Is the class for credit 22 | - is_online -> Is the class online 23 | - is_textbook_required -> Is textbook required in the class 24 | - would_take_again -> Will the student take class again 25 | - quality_rating -> Float value of quality rating 26 | - clarity_rating -> Float value of clarity rating 27 | - helpful_rating -> Float value of helpful rating 28 | - thumbs_up -> Number of thumbs up received on this rating 29 | - thumbs_down -> Number of thumbs down received on this rating 30 | - comment -> Comment by the user 31 | - flag_status -> Flag status of this rating 32 | - grade -> Grade received by the student 33 | 34 | ## Functions 35 | 36 | ### Search 37 | 38 | Searches for teacher based on their name. It returns a dictionary with teacher's full name to teacher's ID. 39 | 40 | ```python 41 | 42 | from dev import rate_my_professor 43 | 44 | rms = rate_my_professor.RateMyProfessor() 45 | teacher = rms.search_teachers("John Doe") 46 | ``` 47 | 48 | `search_teachers` returns something like this: 49 | 50 | ``` 51 | { 52 | "John Doe": "jkdhue#8bcdcj", 53 | "Johnie Doe": "djfdwdlkdk" 54 | } 55 | ``` 56 | 57 | ### Get Ratings 58 | 59 | Gets teacher's ratings based on teacher's ID. 60 | 61 | ```python 62 | 63 | from dev import rate_my_professor 64 | 65 | rms = rate_my_professor.RateMyProfessor() 66 | teacher = rms.get_teacher_ratings("3478Vdhd") # Returns Teacher dataclass 67 | ``` -------------------------------------------------------------------------------- /app/src/scripts/scheduleGenerator.tsx: -------------------------------------------------------------------------------- 1 | import { Section, SOC_Generic } from "@scripts/soc"; 2 | import { Term } from "@constants/soc"; 3 | 4 | export class Selection extends Array
{} 5 | 6 | export class Schedule extends Array
{ 7 | term: Term; 8 | 9 | constructor(term: Term, sections: Section[] = []) { 10 | super(); 11 | if (sections.length > 0) this.push(...sections); 12 | this.term = term; 13 | } 14 | 15 | fits(sectionToAdd: Section, gap: number): boolean { 16 | return this.every( 17 | (sec: Section) => !sectionToAdd.conflictsWith(sec, gap), 18 | ); 19 | } 20 | } 21 | 22 | export class ScheduleGenerator { 23 | soc: SOC_Generic; 24 | selections: Selection[] = []; 25 | gap: number = 0; 26 | 27 | constructor(soc: SOC_Generic) { 28 | this.soc = soc; 29 | } 30 | 31 | loadSelections(selections: Selection[]) { 32 | this.selections = selections; 33 | console.log("Loaded selections", this.selections); 34 | } 35 | 36 | setGap(gap: number) { 37 | this.gap = gap; 38 | } 39 | 40 | *yieldSchedules( 41 | selectionInd: number = 0, 42 | currSchedule: Schedule = new Schedule(this.soc.info.term), 43 | ): Generator { 44 | // Return if there are no selections 45 | if (this.selections.length < 1) { 46 | console.log("No selections, not generating"); 47 | return undefined; 48 | } 49 | 50 | // Return if schedule is complete 51 | if (selectionInd == this.selections.length) return currSchedule; 52 | 53 | // Go through all the sections for the selection, and see if each could generate a new schedule 54 | const selection: Selection = this.selections[selectionInd]; 55 | for (const sectionToAdd of selection) { 56 | if (currSchedule.fits(sectionToAdd, this.gap)) { 57 | // If it fits the current schedule, add it and keep going 58 | const gen = this.yieldSchedules( 59 | selectionInd + 1, 60 | new Schedule(currSchedule.term, [ 61 | ...currSchedule, 62 | sectionToAdd, 63 | ]), 64 | ); 65 | 66 | let newSchedule: IteratorResult; 67 | do { 68 | newSchedule = gen.next(); 69 | if (newSchedule.value) yield newSchedule.value; 70 | } while (!newSchedule.done); 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/src/scripts/apiTypes.ts: -------------------------------------------------------------------------------- 1 | export enum API_Day { 2 | Mon = "M", 3 | Tue = "T", 4 | Wed = "W", 5 | Thu = "R", 6 | Fri = "F", 7 | Sat = "S", 8 | } 9 | 10 | export const API_Days: API_Day[] = [ 11 | API_Day.Mon, 12 | API_Day.Tue, 13 | API_Day.Wed, 14 | API_Day.Thu, 15 | API_Day.Fri, 16 | API_Day.Sat, 17 | ]; 18 | 19 | export interface API_MeetTime { 20 | meetNo: number; 21 | meetDays: API_Day[]; 22 | meetTimeBegin: string; 23 | meetTimeEnd: string; 24 | meetPeriodBegin: string; 25 | meetPeriodEnd: string; 26 | meetBuilding: string; 27 | meetBldgCode: string; 28 | meetRoom: string; 29 | } 30 | 31 | export interface API_Instructor { 32 | name: string; 33 | } 34 | 35 | export interface API_Waitlist { 36 | isEligible: string; 37 | cap: number; 38 | total: number; 39 | } 40 | 41 | export enum API_Section_Type { 42 | PrimarilyClassroom = "PC", 43 | Hybrid = "HB", 44 | MostlyOnline = "PD", 45 | Online = "AD", 46 | } 47 | 48 | export interface API_Section { 49 | number: string; 50 | classNumber: number; 51 | gradBasis: number; 52 | acadCareer: number; 53 | display: string; 54 | credits: number | "VAR"; 55 | credits_min: number; 56 | credits_max: number; 57 | note: string; 58 | dNote: string; 59 | genEd: string[]; 60 | quest: string[]; 61 | sectWeb: API_Section_Type; 62 | rotateTitle: string; 63 | deptCode: number; 64 | deptName: string; 65 | openSeats: number; 66 | courseFee: number; 67 | lateFlag: string; 68 | EEP: string; 69 | LMS: string; 70 | instructors: API_Instructor[]; 71 | meetTimes: API_MeetTime[]; 72 | addEligible: string; 73 | grWriting: string; 74 | finalExam: string; 75 | dropaddDeadline: string; 76 | pastDeadline: boolean; 77 | startDate: string; 78 | endDate: string; 79 | waitList: API_Waitlist; 80 | } 81 | 82 | export interface API_Course { 83 | code: string; 84 | courseId: string; 85 | name: string; 86 | openSeats: number; 87 | termInd: string; 88 | description: string; 89 | prerequisites: string; 90 | sections: API_Section[]; 91 | } 92 | 93 | export interface API_Filter { 94 | CODE: C; 95 | DESC: string; 96 | } 97 | 98 | export interface API_Filter_Sortable extends API_Filter { 99 | SORT_TERM: number; 100 | } 101 | 102 | export interface API_Filters { 103 | categories: API_Filter[]; 104 | progLevels: API_Filter[]; 105 | terms: API_Filter_Sortable[]; 106 | departments: API_Filter[]; 107 | } 108 | -------------------------------------------------------------------------------- /app/src/scripts/soc/section.ts: -------------------------------------------------------------------------------- 1 | import { 2 | API_Days, 3 | API_Instructor, 4 | API_Section, 5 | API_Section_Type, 6 | } from "@scripts/apiTypes"; 7 | import { Meetings, MeetTime, noMeetings } from "@scripts/soc"; 8 | import { Term } from "@constants/soc"; 9 | import { MinMax } from "@scripts/utils.ts"; 10 | 11 | export class Section { 12 | uid: string; 13 | term: Term; 14 | type: API_Section_Type; 15 | number: number; 16 | courseCode: string; // Only for display TODO: consider using getCourse with UID 17 | displayName: string; 18 | deptControlled: boolean = false; 19 | instructors: string[]; 20 | credits: MinMax; 21 | meetings: Meetings = noMeetings(); 22 | finalExamDate: string; 23 | startDate: string; 24 | endDate: string; 25 | 26 | constructor( 27 | uid: string, 28 | term: Term, 29 | sectionJSON: API_Section, 30 | courseCode: string, 31 | ) { 32 | this.uid = uid; 33 | this.term = term; 34 | this.type = sectionJSON.sectWeb; 35 | this.number = sectionJSON.classNumber; 36 | this.courseCode = courseCode; 37 | this.displayName = sectionJSON.display; 38 | this.instructors = []; 39 | this.credits = new MinMax( 40 | sectionJSON.credits_min, 41 | sectionJSON.credits_max, 42 | ); 43 | // Add every meeting 44 | for (const api_meetTime of sectionJSON.meetTimes) { 45 | // Go through meetTimes 46 | for (const day of api_meetTime.meetDays) // Add a MeetTime for each day with the same schedule 47 | this.meetings[day].push( 48 | new MeetTime(term, api_meetTime, this.isOnline), 49 | ); 50 | } 51 | this.instructors = sectionJSON.instructors.map( 52 | (i: API_Instructor) => i.name, 53 | ); 54 | this.finalExamDate = sectionJSON.finalExam; 55 | 56 | // Check if is a controlled section, if so change displayName to something "identifiable" 57 | if (this.displayName == "Departmentally Controlled") { 58 | this.deptControlled = true; 59 | this.displayName = courseCode; 60 | } 61 | this.startDate = sectionJSON.startDate; 62 | this.endDate = sectionJSON.endDate; 63 | } 64 | 65 | // Returns true if any of the meet times conflict 66 | conflictsWith(other: Section, gap: number): boolean { 67 | return API_Days.some((day) => 68 | this.meetings[day].some((mT1) => 69 | other.meetings[day].some((mT2) => mT1.conflictsWith(mT2, gap)), 70 | ), 71 | ); 72 | } 73 | 74 | get isOnline(): boolean { 75 | return ( 76 | this.type == API_Section_Type.Online || 77 | this.type == API_Section_Type.MostlyOnline 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/src/constants/soc.ts: -------------------------------------------------------------------------------- 1 | /* TERM */ 2 | 3 | export enum Term { 4 | Fall = "Fall", 5 | Spring = "Spring", 6 | Summer = "Summer", 7 | Summer_A = "Summer A", 8 | Summer_B = "Summer B", 9 | Summer_C = "Summer C", 10 | } 11 | 12 | const TermStringMap: Map = new Map([ 13 | ["1", Term.Spring], 14 | ["5", Term.Summer], 15 | ["56W1", Term.Summer_A], 16 | ["56W2", Term.Summer_B], 17 | ["51", Term.Summer_C], 18 | ["8", Term.Fall], 19 | ]); 20 | 21 | export function getTerm(termStr: string): Term { 22 | const val = TermStringMap.get(termStr); 23 | if (val === undefined) 24 | throw new Error(`Term string "${termStr}" does not map to a Program`); 25 | return val; 26 | } 27 | 28 | /* PROGRAM */ 29 | 30 | export enum Program { 31 | Campus = "Campus + Web", 32 | Online = "UF Online", 33 | Innovation = "Innovation Academy", 34 | } 35 | 36 | const ProgramStringMap: Map = new Map([ 37 | ["CWSP", Program.Campus], 38 | ["UFOL", Program.Online], 39 | ["IA", Program.Innovation], 40 | ]); 41 | 42 | export function getProgram(programStr: string): Program { 43 | const val = ProgramStringMap.get(programStr); 44 | if (val === undefined) 45 | throw new Error( 46 | `Program string "${programStr}" does not map to a Program`, 47 | ); 48 | return val; 49 | } 50 | 51 | const ProgramStringReverseMap: Map = new Map( 52 | [...ProgramStringMap.entries()].map(([k, v]) => [v, k]), 53 | ); 54 | 55 | export function getProgramString(program: Program): string { 56 | const val = ProgramStringReverseMap.get(program); 57 | if (val === undefined) 58 | throw new Error(`Program "${program}" does not map to a string`); 59 | return val; 60 | } 61 | 62 | /* SEARCH */ 63 | 64 | export enum SearchBy { 65 | COURSE_CODE = "Course Code", 66 | COURSE_TITLE = "Course Title", 67 | INSTRUCTOR = "Instructor", 68 | } 69 | 70 | export const SearchBys = [ 71 | SearchBy.COURSE_CODE, 72 | SearchBy.COURSE_TITLE, 73 | SearchBy.INSTRUCTOR, 74 | ]; 75 | 76 | const SearchByStringMap = new Map([ 77 | ["course-code", SearchBy.COURSE_CODE], 78 | ["course-title", SearchBy.COURSE_TITLE], 79 | ["instructor", SearchBy.INSTRUCTOR], 80 | ]); 81 | 82 | export function getSearchBy(searchByStr: string): SearchBy { 83 | const val = SearchByStringMap.get(searchByStr); 84 | if (val === undefined) 85 | throw new Error( 86 | `SearchBy string "${searchByStr}" does not map to a SearchBy`, 87 | ); 88 | return val; 89 | } 90 | 91 | const SearchByStringReverseMap: Map = new Map( 92 | [...SearchByStringMap.entries()].map(([k, v]) => [v, k]), 93 | ); 94 | 95 | export function getSearchByString(searchBy: SearchBy): string { 96 | const val = SearchByStringReverseMap.get(searchBy); 97 | if (val === undefined) 98 | throw new Error(`SearchBy "${searchBy} does not map to a string"`); 99 | return val; 100 | } 101 | -------------------------------------------------------------------------------- /dev/schedule_of_courses/scrape.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | from typing import TypedDict, Any, List 4 | 5 | import requests 6 | 7 | BASE_URL = "https://one.uf.edu/apix/soc/schedule" 8 | APP_DIR = "../../app/" 9 | 10 | 11 | def generate_soc_request_url(term: str, program: str, last_control_number: int = 0) -> str: 12 | """ 13 | @rtype: str 14 | @param term: a string that corresponds to the term ('2228' --> Fall 2022) 15 | @param program: which UF program (ie. on campus/online/innovation) 16 | @param last_control_number: effectively, the number of courses to skip before scraping 17 | @return: the URL for the SOC request 18 | """ 19 | parameters = { 20 | "term": term, 21 | "category": program, 22 | "last-control-number": last_control_number 23 | } 24 | 25 | parameters_str = '&'.join([f'{k}={v}' for k, v in parameters.items()]) 26 | 27 | return f'{BASE_URL}?{parameters_str}' 28 | 29 | 30 | class SOCInfo(TypedDict): 31 | term: str 32 | program: str 33 | scraped_at: int 34 | 35 | 36 | class SOC(TypedDict): 37 | info: SOCInfo 38 | courses: List[Any] 39 | 40 | 41 | def fetch_soc(term: str, program: str, last_control_number: int = 0, num_results_per_request: int = 50) -> SOC: 42 | """ 43 | Fetches UF's schedule of courses. 44 | @rtype: list 45 | @param term: a string that corresponds to the term ('2228' --> Fall 2022) 46 | @param program: which UF program (ie. on campus/online/innovation) 47 | @param last_control_number: effectively, the number of courses to skip before downloading 48 | @param num_results_per_request: must be between 1 and 50 49 | @return: a list of the courses and their relevant information (from UF API) 50 | """ 51 | 52 | assert last_control_number >= 0, \ 53 | "Last control number must be at least 0, {last_control_number} was given." 54 | assert 1 <= num_results_per_request <= 50, \ 55 | f"Number of results per request must be between 1 and 50, {num_results_per_request} was given." 56 | 57 | soc: SOC = { 58 | "info": { 59 | "term": term, 60 | "program": program, 61 | "scraped_at": int(time.time()) 62 | }, 63 | "courses": [] 64 | } 65 | last = None 66 | while last is None or last['RETRIEVEDROWS'] == num_results_per_request: 67 | # If retrieved less than asked for we have gotten the final page of results 68 | 69 | next_last_control_num = last['LASTCONTROLNUMBER'] if (last is not None) else last_control_number 70 | url = generate_soc_request_url(term, program, last_control_number=next_last_control_num) 71 | 72 | request = requests.get(url) 73 | last = json.loads(request.text)[0] 74 | soc['courses'].extend(last['COURSES']) 75 | 76 | print('.', end='') 77 | return soc 78 | 79 | 80 | if __name__ == "__main__": 81 | print('Fetching SOC...', end='') 82 | soc_scraped = fetch_soc('2238', 'CWSP') # Scrape the schedule of courses for Fall 2023 (On-campus) 83 | print(' DONE') 84 | 85 | print("Converting to JSON and writing to 'soc_scraped.json'...", end=' ') 86 | soc_str = json.dumps(soc_scraped) # Convert to JSON 87 | with open(APP_DIR + 'src/json/soc_scraped.json', 'w') as f: # Save JSON as file 88 | f.write(soc_str) 89 | print('DONE') 90 | -------------------------------------------------------------------------------- /app/src/scripts/soc/meet.ts: -------------------------------------------------------------------------------- 1 | import { API_Day, API_MeetTime } from "@scripts/apiTypes"; 2 | import { Term } from "@constants/soc"; 3 | import { PERIOD_COUNTS } from "@constants/schedule"; 4 | 5 | export class MeetTime { 6 | term: Term; 7 | periodBegin: number; 8 | periodEnd: number; 9 | timeBegin: string; 10 | timeEnd: string; 11 | bldg: string; 12 | room: string; 13 | isOnline: boolean; 14 | locationID: string | null; 15 | 16 | constructor(term: Term, meetTimeJSON: API_MeetTime, isOnline: boolean) { 17 | this.term = term; 18 | this.periodBegin = this.parsePeriod(meetTimeJSON.meetPeriodBegin); 19 | this.periodEnd = this.parsePeriod(meetTimeJSON.meetPeriodEnd); 20 | this.timeBegin = meetTimeJSON.meetTimeBegin; 21 | this.timeEnd = meetTimeJSON.meetTimeEnd; 22 | this.bldg = meetTimeJSON.meetBuilding; 23 | this.room = meetTimeJSON.meetRoom; 24 | this.isOnline = isOnline; 25 | this.locationID = meetTimeJSON.meetBldgCode; 26 | 27 | // Assume length is one period if either periodBegin or periodEnd is NaN 28 | if (isNaN(this.periodBegin)) this.periodBegin = this.periodEnd; 29 | if (isNaN(this.periodEnd)) this.periodEnd = this.periodBegin; 30 | 31 | // If the meeting is online, there is no location 32 | if (this.locationID == "WEB") this.locationID = null; 33 | } 34 | 35 | private parsePeriod(period: string): number { 36 | if (period) { 37 | if (period.charAt(0) == "E") { 38 | const periodCounts = PERIOD_COUNTS[this.term]; 39 | return periodCounts.regular + parseInt(period.substring(1)); 40 | } 41 | return parseInt(period); 42 | } 43 | return NaN; 44 | } 45 | 46 | static formatPeriod(p: number, term: Term) { 47 | const periodCounts = PERIOD_COUNTS[term]; 48 | return p > periodCounts.regular 49 | ? `E${p - periodCounts.regular}` 50 | : `${p}`; 51 | } 52 | 53 | formatPeriods(): string { 54 | return this.periodBegin == this.periodEnd 55 | ? MeetTime.formatPeriod(this.periodBegin, this.term) 56 | : `${MeetTime.formatPeriod( 57 | this.periodBegin, 58 | this.term, 59 | )}-${MeetTime.formatPeriod(this.periodEnd, this.term)}`; 60 | } 61 | 62 | get location(): string | null { 63 | if (this.bldg && this.room) return `${this.bldg} ${this.room}`; 64 | if (this.isOnline) return "Online"; 65 | return null; 66 | } 67 | 68 | // Returns true if the meet times conflict (overlap) 69 | conflictsWith(other: MeetTime, gap: number): boolean { 70 | return ( 71 | //this after other 72 | (this.periodBegin > other.periodBegin && 73 | other.periodEnd + gap >= this.periodBegin) || 74 | //this before other 75 | (this.periodBegin < other.periodBegin && 76 | this.periodEnd + gap >= other.periodBegin) || 77 | //this at other 78 | this.periodBegin == other.periodBegin 79 | ); 80 | } 81 | } 82 | 83 | export type Meetings = Record; 84 | 85 | export function noMeetings(): Meetings { 86 | return { 87 | [API_Day.Mon]: [], 88 | [API_Day.Tue]: [], 89 | [API_Day.Wed]: [], 90 | [API_Day.Thu]: [], 91 | [API_Day.Fri]: [], 92 | [API_Day.Sat]: [], 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /app/src/components/SectionDisplay.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Section } from "@scripts/soc"; 3 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 4 | // @ts-ignore 5 | import { Draggable } from "react-drag-and-drop"; 6 | import { GrClose, GrLock } from "react-icons/gr"; 7 | import { CampusMap } from "@scripts/api"; 8 | import { API_Days } from "@scripts/apiTypes.ts"; 9 | import { notNullish } from "@scripts/utils.ts"; 10 | 11 | interface Props { 12 | section: Section; 13 | draggable?: boolean; 14 | handleRemove?: (sectionToRemove: Section) => void; 15 | } 16 | 17 | //Courses Display 18 | 19 | export default function SectionDisplay({ 20 | section, 21 | draggable = false, 22 | handleRemove, 23 | }: Props) { 24 | const allTimes: React.JSX.Element[] = API_Days.map((day, d) => 25 | section.meetings[day].length > 0 ? ( 26 |
27 | {day}:{" "} 28 | {section.meetings[day].map((mT, m) => ( 29 | 30 | {CampusMap.createLink( 31 | mT.locationID, 32 | `${mT.location}`, 33 | <>{mT.formatPeriods()}, 34 | )}{" "} 35 | 36 | ))} 37 |
38 | ) : null, 39 | ).filter(notNullish); 40 | 41 | return ( 42 | 48 |
49 | {" "} 50 | {/* SECTION */} 51 |
52 |
53 |
54 | {section.number} 55 | 56 | ({section.credits.display} Credits) 57 | 58 |
59 | 68 |
69 | 70 |
71 |

72 | {section.deptControlled && ( 73 | 74 | 75 | 76 | )} 77 | {section.displayName} 78 |

79 |

80 | {section.instructors.join(", ")} 81 |

82 |
83 | {allTimes} 84 |
85 |
86 |
87 |
88 |
89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🐊 Swamp Scheduler 📆 2 | 3 | An open-source web app developed to help students at the University of Florida plan for classes next 4 | semester. [It's available now!](https://osc.rconde.xyz/) 5 | 6 | Made with :heart: by [UF's Open Source Club](https://ufosc.org) ([@ufosc](https://github.com/ufosc/)). 7 | 8 | ## Table of Contents 9 | 10 | - [Features](#Features) 11 | - [Setup](#Setup) 12 | - [Usage](#Usage) 13 | - [Contribution](#Contribution) 14 | - [Maintainers](#Maintainers) 15 | - [License](#License) 16 | 17 | ## Features 18 | 19 | - **Course Explorer:** Explore courses offered at the University of Florida 20 | - Find courses by *course code*, *title*, or *instructor* 21 | - **Course Selections:** Make multiple courses selections with backup options. 22 | - Pick which classes you want (and remove which sections you don't) 23 | - **Schedule Generator:** View and compare all the possible schedules and pick the one that fits your needs and wants. 24 | - View color-coded schedules that show what your day-to-day ~~struggle~~ workload will be 25 | 26 | ## Installation 27 | 28 | ### Prerequisites 29 | 30 | Make sure to have `npm` installed. 31 | 32 | ### Setup 33 | 34 | #### Clone the Repo 35 | 36 | Clone the repository to your local machine: 37 | 38 | ```shell 39 | git clone 40 | ``` 41 | 42 | #### Install the Dependencies 43 | 44 | Enter the web-app directory (`/app`) and install the dependencies (React, Tailwind CSS, etc.): 45 | 46 | ```shell 47 | cd app 48 | npm install 49 | ``` 50 | 51 | ### Usage: 52 | 53 | - In the web-app directory: 54 | - **Development:** Run `npm run dev` to run the development server locally (with hot reloading). 55 | - **Production:** Run `npm run build` to build the app to `/app/dist`. 56 | 57 | ## Contribution 58 | 59 | Before you can make good contributions, you need to know a little bit about what we're using and how the web-app works. 60 | After that, you should be ready to get your hands dirty! 61 | 62 | ### What We're Using (our Tech Stack) 63 | 64 | This project is built using a variety of exciting technologies, including: 65 | 66 | - **TypeScript:** The JavaScript programming language with a typing system (for `Course` objects, etc.) 67 | - Familiarize yourself with [TypeScript’s documentation](https://www.typescriptlang.org/docs/) to understand the 68 | basics and best practices. 69 | - **React:** A JavaScript library for building dynamic user interfaces. 70 | - The [official React documentation](https://reactjs.org/docs/getting-started.html) is a great resource for learning 71 | about component-based architecture and state management. 72 | - **Tailwind CSS:** A utility-first CSS framework. 73 | - Review the [Tailwind CSS documentation](https://tailwindcss.com/docs) for understanding utility-first styling and 74 | theming. 75 | - **Vite:** Simply used as a build tool and development server. 76 | - Learn how to set up, configure, and use Vite from [Vite’s official guide](https://vitejs.dev/guide/). 77 | 78 | ### How It All Works 79 | 80 | Be sure to read (yes, read) some of our code. Everything works better when we all understand what we're talking about. 81 | 82 | [SwampScheduler's documentation](https://docs.ufosc.org/docs/swamp-scheduler) is a work-in-progress. 83 | 84 | ### Give Me Something To Do! 85 | 86 | There are lots of things that can be done, and a lot of them are on our back-burner. 87 | 88 | Take a look at what [issues (enhancements, bug fixes, and ideas)](https://github.com/ufosc/SwampScheduler/issues) are 89 | open. If you find one you like, assign yourself and 90 | be sure to talk to other people about what you're doing (it helps us, the [maintainers](#Maintainers) best allocate our 91 | resources). 92 | 93 | ## Maintainers 94 | 95 | We're your Technical Leads, Product Managers, and Mentors all-in-one: 96 | 97 | - [Robert Conde](https://github.com/RobertConde) 98 | - [Brian Nielsen](https://github.com/bnielsen1) 99 | 100 | ## License 101 | 102 | GNU Affero General Public License v3.0 103 | -------------------------------------------------------------------------------- /app/src/components/SectionPicker.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Course, SOC_API, SOC_Generic } from "@scripts/soc"; 3 | import MultipleCourseDisplay from "@components/MultipleCourseDisplay"; 4 | import { getSearchBy, getSearchByString, SearchBys } from "@constants/soc"; 5 | import { useQuery } from "react-query"; 6 | import { getSearchByStringExample } from "@constants/frontend"; 7 | 8 | //Search Type Drop Down 9 | //Searh Bar 10 | //Search Button 11 | 12 | interface Props { 13 | soc: SOC_Generic; 14 | } 15 | 16 | export default function SectionPicker(props: Props) { 17 | const [searchByString, setSearchByString] = useState( 18 | getSearchByString(SearchBys[0]), 19 | ); 20 | const [searchText, setSearchText] = useState(""); 21 | const searchBy = getSearchBy(searchByString); 22 | 23 | /* https://github.com/apollographql/apollo-client/issues/9583 */ 24 | const [abortRef, setAbortRef] = useState(new AbortController()); 25 | const { 26 | isFetching, 27 | data: courses, 28 | refetch: fetchCourses, 29 | } = useQuery({ 30 | initialData: [], 31 | queryFn: () => { 32 | setAbortRef(new AbortController()); 33 | return props.soc instanceof SOC_API 34 | ? props.soc.fetchSearchCourses(searchBy, searchText, abortRef) 35 | : props.soc.searchCourses(searchBy, searchText); 36 | }, 37 | enabled: false, 38 | notifyOnChangeProps: ["data", "isFetching"], // Must re-render on isFetching change to update cursor 39 | refetchOnWindowFocus: false, // Turned off to prevent queries while debugging using console 40 | refetchOnReconnect: false, // Not needed 41 | }); 42 | 43 | let coursesToDisplay =

Loading...

; 44 | if (!isFetching) 45 | coursesToDisplay = ; 46 | 47 | return ( 48 |
52 |
53 | 70 | 71 | setSearchText(e.target.value)} 80 | autoComplete={"off"} 81 | onKeyDown={(e) => { 82 | if (e.key === "Enter") { 83 | fetchCourses(); 84 | } 85 | }} 86 | /> 87 | 88 | 94 |
95 | 96 | {coursesToDisplay} 97 |
98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /app/src/scripts/soc/calendar.ts: -------------------------------------------------------------------------------- 1 | import moment from "moment/moment"; 2 | import { API_Day, API_Days } from "@scripts/apiTypes.ts"; 3 | import ical, { ICalEventRepeatingFreq, ICalWeekday } from "ical-generator"; 4 | import { Schedule } from "@scripts/scheduleGenerator.tsx"; 5 | 6 | export function getDate(datePart: string, timePart: string) { 7 | return moment( 8 | `${datePart} ${timePart.toLowerCase()}`, 9 | "MM/DD/YYYY hh:mm a", 10 | ); 11 | } 12 | 13 | const weekdays: Record = { 14 | [API_Day.Mon]: ICalWeekday.MO, 15 | [API_Day.Tue]: ICalWeekday.TU, 16 | [API_Day.Wed]: ICalWeekday.WE, 17 | [API_Day.Thu]: ICalWeekday.TH, 18 | [API_Day.Fri]: ICalWeekday.FR, 19 | [API_Day.Sat]: ICalWeekday.SA, 20 | }; 21 | export function handleExportScheduleClick(schedule: Schedule) { 22 | try { 23 | // summary: class name + course code 24 | // description: section # 25 | // startTime = DATE OF DOWNLOAD and the timeBegin of the course 26 | // endTime = DATE OF DOWNLOAD and the timeEnd of the course 27 | // Location = bldg + room e.g. CAR0100 28 | // Online classes have empty meet arrays 29 | const cal = ical(); 30 | for (const section of schedule) { 31 | console.log(section); 32 | for (const day of API_Days) { 33 | const summary = `${section.displayName}, ${section.courseCode}`; 34 | const description = `Section #${section.number}`; 35 | for (const meeting of section.meetings[day]) { 36 | const firstStartDate = getDate( 37 | section.startDate, 38 | meeting.timeBegin, 39 | ), 40 | firstEndDate = getDate( 41 | section.startDate, 42 | meeting.timeEnd, 43 | ); 44 | 45 | const indexOfDay = API_Days.indexOf(day); 46 | const startDayOfWeek = 47 | (firstStartDate.weekday() - 48 | moment().day("Monday").weekday() + 49 | 7) % 50 | 7; 51 | 52 | console.log( 53 | day, 54 | indexOfDay, 55 | section.startDate, 56 | startDayOfWeek, 57 | ); 58 | const dayOffset = (indexOfDay - startDayOfWeek + 7) % 7; 59 | firstStartDate.add(dayOffset, "days"); 60 | firstEndDate.add(dayOffset, "days"); 61 | 62 | const location = meeting.location; 63 | 64 | const until = getDate(section.endDate, "11:59 PM"); 65 | 66 | cal.createEvent({ 67 | start: firstStartDate.toDate(), 68 | end: firstEndDate.toDate(), 69 | summary, 70 | description, 71 | location, 72 | }).repeating({ 73 | freq: ICalEventRepeatingFreq.WEEKLY, 74 | byDay: weekdays[day], 75 | until, 76 | }); 77 | } 78 | } 79 | } 80 | 81 | // Convert the calendar to an iCalendar string 82 | const icalContent = cal.toString(); 83 | 84 | // Create a Blob from the iCalendar content 85 | const file = new File([icalContent], "swampschedule.ics", { 86 | type: "text/calendar", 87 | }); 88 | 89 | // Create a URL for the Blob 90 | const url = URL.createObjectURL(file); 91 | 92 | // Create a temporary anchor element and trigger the download 93 | const anchor = document.createElement("a"); 94 | anchor.href = url; 95 | anchor.download = "swampschedule.ics"; 96 | document.body.appendChild(anchor); 97 | anchor.click(); 98 | document.body.removeChild(anchor); 99 | 100 | // Revoke the URL to release memory 101 | URL.revokeObjectURL(url); 102 | } catch (error) { 103 | console.error("Error exporting schedule:", error); 104 | // Handle the error appropriately 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /dev/rmp_server/rate_my_professor/__init__.py: -------------------------------------------------------------------------------- 1 | """ Module to fetch data from RateMyProfessor.com """ 2 | from dataclasses import dataclass 3 | 4 | from gql import gql, Client 5 | from gql.transport.aiohttp import AIOHTTPTransport 6 | 7 | _AUTH_TOKEN = "dGVzdDp0ZXN0" 8 | _UF_SCHOOL_ID = "U2Nob29sLTExMDA=" 9 | 10 | 11 | @dataclass 12 | class Rating: 13 | """ Data class for a rating """ 14 | class_name: str 15 | is_attendance_required: bool 16 | is_for_credit: bool 17 | is_online: bool 18 | is_textbook_required: bool 19 | would_take_again: bool 20 | quality_rating: float 21 | clarity_rating: float 22 | difficulty_rating: float 23 | helpful_rating: float 24 | thumbs_up: int 25 | thumbs_down: int 26 | comment: str 27 | flag_status: str 28 | grade: str 29 | 30 | 31 | @dataclass 32 | class Teacher: 33 | """ Data class for a teacher """ 34 | uid: str 35 | first_name: str | None 36 | last_name: str | None 37 | avg_difficulty: float 38 | avg_rating: float 39 | would_take_again_count: int 40 | would_take_again_percent: float 41 | ratings_count: int 42 | ratings: list[Rating] 43 | 44 | 45 | class RateMyProfessor: 46 | """ Class to fetch data from RateMyProfessor.com """ 47 | 48 | def __init__(self): 49 | self._transport = AIOHTTPTransport( 50 | url="https://www.ratemyprofessors.com/graphql", 51 | headers={"authorization": f"Basic {_AUTH_TOKEN}"} 52 | ) 53 | self._client = Client(transport=self._transport, fetch_schema_from_transport=True) 54 | 55 | def get_teacher_ratings(self, teacher_id): 56 | """Returns a teacher's ratings based on @teacher_id.""" 57 | query = gql( 58 | r""" 59 | query TeacherRatingsPageQuery($id: ID!) { 60 | node(id: $id) { 61 | ... on Teacher { 62 | avgDifficultyRounded 63 | avgRatingRounded 64 | wouldTakeAgainCount 65 | wouldTakeAgainPercentRounded 66 | numRatings 67 | ratings { 68 | edges { 69 | node { 70 | attendanceMandatory 71 | clarityRatingRounded 72 | class 73 | comment 74 | difficultyRatingRounded 75 | flagStatus 76 | grade 77 | helpfulRatingRounded 78 | isForCredit 79 | isForOnlineClass 80 | iWouldTakeAgain 81 | qualityRating 82 | textbookIsUsed 83 | thumbsUpTotal 84 | thumbsDownTotal 85 | } 86 | } 87 | } 88 | } 89 | } 90 | } 91 | """ 92 | ) 93 | 94 | params = {"id": teacher_id} 95 | results = self._client.execute(query, variable_values=params) 96 | 97 | teacher = Teacher( 98 | uid=teacher_id, 99 | first_name=None, 100 | last_name=None, 101 | avg_difficulty=results["node"]["avgDifficultyRounded"], 102 | avg_rating=results["node"]["avgRatingRounded"], 103 | would_take_again_count=results["node"]["wouldTakeAgainCount"], 104 | would_take_again_percent=results["node"]["wouldTakeAgainPercentRounded"], 105 | ratings_count=results["node"]["numRatings"], 106 | ratings=[], 107 | ) 108 | 109 | for element in results["node"]["ratings"]["edges"]: 110 | teacher.ratings.append( 111 | Rating( 112 | class_name=element["node"]["class"], 113 | is_attendance_required=element["node"]["attendanceMandatory"], 114 | is_for_credit=element["node"]["isForCredit"], 115 | is_online=element["node"]["isForOnlineClass"], 116 | is_textbook_required=element["node"]["textbookIsUsed"], 117 | quality_rating=element["node"]["qualityRating"], 118 | difficulty_rating=element["node"]["difficultyRatingRounded"], 119 | helpful_rating=element["node"]["helpfulRatingRounded"], 120 | comment=element["node"]["comment"], 121 | flag_status=element["node"]["flagStatus"], 122 | grade=element["node"]["grade"], 123 | thumbs_down=element["node"]["thumbsUpTotal"], 124 | thumbs_up=element["node"]["thumbsDownTotal"], 125 | clarity_rating=element["node"]["clarityRatingRounded"], 126 | would_take_again=element["node"]["iWouldTakeAgain"], 127 | ) 128 | ) 129 | 130 | return teacher 131 | 132 | def search_teachers(self, name: str) -> dict: 133 | """ 134 | Searches for teachers based on @name. 135 | Returns a dictionary that maps a teacher's their ID. 136 | """ 137 | query = gql( 138 | r""" 139 | query SearchTeacher($text: String!, $schoolID: ID!) { 140 | newSearch { 141 | teachers(query: {text: $text, schoolID: $schoolID}) { 142 | edges { 143 | node { 144 | id 145 | firstName 146 | lastName 147 | } 148 | } 149 | } 150 | } 151 | } 152 | """ 153 | ) 154 | 155 | params = {"text": name, "schoolID": _UF_SCHOOL_ID} 156 | results = self._client.execute(query, variable_values=params) 157 | 158 | teachers = {} 159 | for x in results["newSearch"]["teachers"]["edges"]: 160 | full_name = f"{x['node']['firstName']} {x['node']['lastName']}" 161 | teachers[full_name] = x["node"]["id"] 162 | 163 | return teachers 164 | -------------------------------------------------------------------------------- /app/src/scripts/soc/soc.tsx: -------------------------------------------------------------------------------- 1 | import { API_Course, API_Section } from "@scripts/apiTypes"; 2 | import { 3 | getProgram, 4 | getProgramString, 5 | getSearchByString, 6 | getTerm, 7 | Program, 8 | SearchBy, 9 | Term, 10 | } from "@constants/soc"; 11 | import { CancellablePromise, Cancellation } from "real-cancellable-promise"; 12 | import { Course, Section } from "@scripts/soc"; 13 | import { fetchCORS } from "@scripts/utils"; 14 | 15 | interface SOCInfo { 16 | termStr: string; 17 | term: Term; 18 | year: number; 19 | program: Program; 20 | scraped_at: Date; 21 | } 22 | 23 | interface UID { 24 | courseUID: string; 25 | sectionUID: string | null; 26 | } 27 | 28 | export abstract class SOC_Generic { 29 | info: SOCInfo; 30 | courses: Course[]; 31 | 32 | /* CONSTRUCT & INITIALIZE */ 33 | 34 | protected constructor(info: SOCInfo, courses: Course[]) { 35 | this.info = info; 36 | this.courses = courses; 37 | } 38 | 39 | static async initialize(): Promise { 40 | throw new Error("SOC initializer not implemented."); 41 | } 42 | 43 | /* UID */ 44 | 45 | /** 46 | * Used to form a course/section's UID from the SOC. 47 | * @param courseInd -- The course/section's associated course index. 48 | * @param sectionInd -- The section's section index. 49 | * @returns The UID. 50 | */ 51 | static formUID( 52 | courseInd: number, 53 | sectionInd: number | undefined = undefined, 54 | ) { 55 | return sectionInd === undefined 56 | ? `${courseInd}` 57 | : `${courseInd}#${sectionInd}`; 58 | } 59 | 60 | /** 61 | * Used to split a course/section's UID. 62 | * @param uid -- The course/section's UID. 63 | * @returns The split UID. 64 | */ 65 | static splitUID(uid: string): UID { 66 | const sepInd = uid.indexOf("#"); 67 | if (sepInd < 0) 68 | // No separator => Course 69 | return { courseUID: uid, sectionUID: null }; 70 | 71 | // Separator => Section (in a Course) 72 | return { 73 | courseUID: uid.substring(0, sepInd), 74 | sectionUID: uid.substring(sepInd + 1), 75 | }; 76 | } 77 | 78 | /** 79 | * Used to get a course/section from the SOC. 80 | * @param uid -- The course/section's UID. 81 | * @returns The course/section; null if there are no matches. 82 | */ 83 | get(uid: string): Course | Section | null { 84 | const splitUID = SOC_Generic.splitUID(uid); 85 | if (!splitUID.sectionUID) 86 | // Resolve course 87 | return this.courses.at(parseInt(uid)) ?? null; 88 | else { 89 | // Resolve section 90 | const course = this.get(splitUID.courseUID); 91 | if (course instanceof Course) 92 | return ( 93 | course.sections.at(parseInt(splitUID.sectionUID)) ?? null 94 | ); // Section resolve 95 | return null; 96 | } 97 | } 98 | 99 | /* UID UTILS */ 100 | 101 | /** 102 | * Used to get the course that a section belongs to. 103 | * @param sectionUID -- A `Section`'s UID. 104 | * @returns The course; null if it doesn't exist. 105 | */ 106 | getCourseBySectionUID(sectionUID: string): Course | null { 107 | const splitUID = SOC_Generic.splitUID(sectionUID), 108 | course = this.get(splitUID.courseUID); 109 | if (course instanceof Course) return course; 110 | return null; 111 | } 112 | 113 | /* SEARCHING */ 114 | 115 | /** 116 | * Note: Search for courses by a SearchBy and phrase. 117 | * @param searchBy -- The SearchBy to use. 118 | * @param phrase -- Ex. COT3100 (not case-sensitive) 119 | * @returns A promise for all courses that match; null if one doesn't exist. 120 | */ 121 | searchCourses(searchBy: SearchBy, phrase: string): Course[] { 122 | console.log(`Searching by ${searchBy} for "${phrase}"`); 123 | if (!phrase) 124 | // Empty search phrase should not return all courses 125 | return []; 126 | 127 | const upperPhrase: string = phrase.toUpperCase(); 128 | if (searchBy === SearchBy.COURSE_CODE) { 129 | return this.courses.filter((c) => c.code.includes(upperPhrase)); 130 | } else if (searchBy === SearchBy.COURSE_TITLE) { 131 | return this.courses.filter((c) => 132 | c.name.toUpperCase().includes(upperPhrase), 133 | ); 134 | } else if (searchBy === SearchBy.INSTRUCTOR) { 135 | return this.courses.filter((c) => 136 | c.sections.some((s) => 137 | s.instructors.some((inst) => 138 | inst.toUpperCase().includes(upperPhrase), 139 | ), 140 | ), 141 | ); 142 | } 143 | throw new Error("Unhandled SearchBy."); 144 | } 145 | 146 | /* UTILS */ 147 | 148 | static decodeTermString(termStr: string): { term: Term; year: number } { 149 | return { 150 | term: getTerm(termStr.substring(3)), 151 | year: parseInt(termStr.charAt(0) + "0" + termStr.substring(1, 3)), 152 | }; 153 | } 154 | 155 | protected existsCourse(courseID: string, courseName: string): boolean { 156 | // There exists courses that are under the same course ID, but have different names (ex. MUN2800) 157 | return this.courses.some( 158 | (c) => c.id == courseID && c.name == courseName, 159 | ); 160 | } 161 | } 162 | 163 | export class SOC_API extends SOC_Generic { 164 | static async initialize( 165 | { 166 | termStr, 167 | programStr, 168 | }: { 169 | termStr: string | undefined; 170 | programStr: string | undefined; 171 | } = { 172 | termStr: undefined, 173 | programStr: undefined, 174 | }, 175 | ): Promise { 176 | if (!termStr || !programStr) 177 | throw new Error("Term or program string was not provided."); 178 | 179 | const termStringInfo = this.decodeTermString(termStr); 180 | return new SOC_API( 181 | { 182 | termStr: termStr, 183 | term: termStringInfo.term, 184 | year: termStringInfo.year, 185 | program: getProgram(programStr), 186 | scraped_at: new Date(), 187 | }, 188 | [], 189 | ); 190 | } 191 | 192 | fetchSearchCourses( 193 | searchBy: SearchBy, 194 | phrase: string, 195 | cont: AbortController, 196 | ): CancellablePromise { 197 | const fetchPromise = this.fetchCourses(searchBy, phrase, cont); 198 | return new CancellablePromise( 199 | fetchPromise 200 | .then(() => this.searchCourses(searchBy, phrase)) 201 | .catch((e) => { 202 | if (e instanceof Cancellation) 203 | console.log("Fetch was aborted."); 204 | else throw e; 205 | return []; 206 | }), 207 | () => cont.abort(), 208 | ); 209 | } 210 | 211 | private fetchCache = new Set(); 212 | 213 | private async fetchCourses( 214 | searchBy: SearchBy, 215 | phrase: string, 216 | controller: AbortController, 217 | lcn = 0, 218 | ): Promise { 219 | if (!phrase) return Promise.resolve(); 220 | 221 | const search = JSON.stringify({ by: searchBy, phrase }); 222 | if (this.fetchCache.has(search)) { 223 | console.log("Search has already already fetched. Not fetching."); 224 | return Promise.resolve(); 225 | } 226 | console.log( 227 | `Fetching by ${searchBy} for "${phrase}"${ 228 | lcn > 0 ? ` @ #${lcn}` : "" 229 | }`, 230 | ); 231 | 232 | const searchURL = 233 | "https://one.uf.edu/apix/soc/schedule" + 234 | `?term=${this.info.termStr}` + 235 | `&category=${getProgramString(this.info.program)}` + 236 | `&last-control-number=${lcn}` + 237 | `&${getSearchByString(searchBy)}=${phrase}`; 238 | return fetchCORS(searchURL, { signal: controller.signal }) 239 | .then(async (r) => await r.json()) 240 | .then(async (j) => { 241 | const { COURSES, LASTCONTROLNUMBER, RETRIEVEDROWS } = j[0]; 242 | 243 | // Add each course, if appropriate 244 | COURSES.forEach((courseJson: API_Course) => { 245 | const courseID: string = courseJson.courseId, 246 | courseCode: string = courseJson.code, 247 | courseInd: number = this.courses.length; 248 | 249 | // Prevent duplicates 250 | const courseName = courseJson.name; 251 | if (!this.existsCourse(courseID, courseName)) { 252 | const course: Course = new Course( 253 | SOC_API.formUID(courseInd), 254 | this.info.term, 255 | courseJson, 256 | ); 257 | courseJson.sections.forEach( 258 | (sectionJson: API_Section, sectionInd: number) => { 259 | course.sections.push( 260 | new Section( 261 | SOC_API.formUID(courseInd, sectionInd), 262 | this.info.term, 263 | sectionJson, 264 | courseCode, 265 | ), 266 | ); 267 | }, 268 | ); 269 | this.courses.push(course); // Add the course to the courses array 270 | } 271 | }); 272 | 273 | if (RETRIEVEDROWS == 50) 274 | await this.fetchCourses( 275 | searchBy, 276 | phrase, 277 | controller, 278 | LASTCONTROLNUMBER, 279 | ); 280 | }) 281 | .then(() => { 282 | this.fetchCache.add(search); 283 | }) 284 | .catch((e) => { 285 | if (e.name === "AbortError") throw new Cancellation(); 286 | throw e; 287 | }); 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /app/src/components/ScheduleBuilder.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | import { Course, Section, SOC_API, SOC_Generic } from "@scripts/soc"; 3 | import { 4 | Schedule, 5 | ScheduleGenerator, 6 | Selection, 7 | } from "@scripts/scheduleGenerator"; 8 | import SectionPicker from "@components/SectionPicker"; 9 | import MultipleSelectionDisplay from "@components/MultipleSelectionDisplay"; 10 | import MultipleScheduleDisplay from "@components/MultipleScheduleDisplay"; 11 | import { UF_SOC_API } from "@scripts/api"; 12 | import { API_Filters } from "@scripts/apiTypes"; 13 | import { arrayEquals, notEmpty, take } from "@scripts/utils"; 14 | import { LIMIT_VALUES, LIMITS } from "@constants/scheduleGenerator"; 15 | 16 | const getDefaultSelections = () => [new Selection()]; 17 | const defaultProgram = "CWSP"; 18 | 19 | interface Props {} 20 | 21 | interface States { 22 | filters: API_Filters | null; 23 | soc: SOC_Generic | null; 24 | generator: ScheduleGenerator | null; 25 | limit: number; 26 | selections: Selection[]; 27 | schedules: Schedule[]; 28 | showAddCourse: boolean; 29 | gap: number; 30 | } 31 | 32 | const defaultState: States = { 33 | filters: null, 34 | soc: null, 35 | generator: null, 36 | limit: LIMIT_VALUES[0], 37 | selections: getDefaultSelections(), 38 | schedules: [], 39 | showAddCourse: false, 40 | gap: 0, 41 | }; 42 | 43 | export default class ScheduleBuilder extends Component { 44 | constructor(props: Props) { 45 | super(props); 46 | this.state = defaultState; 47 | } 48 | 49 | reset() { 50 | console.log("Resetting Schedule Builder"); 51 | this.setState({ 52 | selections: getDefaultSelections(), 53 | schedules: [], 54 | }); 55 | } 56 | 57 | componentDidMount() { 58 | // TODO: implement retry upon fetch error 59 | UF_SOC_API.fetchFilters().then(async (filters) => { 60 | this.setState({ filters }); 61 | await this.setSOC(filters.terms[0].CODE, defaultProgram); 62 | }); 63 | } 64 | 65 | componentDidUpdate( 66 | _prevProps: Readonly, 67 | prevState: Readonly, 68 | ) { 69 | // If limit was changed or a section was added/removed from a section, generate new schedules 70 | if ( 71 | this.state.limit != prevState.limit || 72 | this.state.gap != prevState.gap || 73 | !arrayEquals( 74 | this.state.selections.filter(notEmpty), 75 | prevState.selections.filter(notEmpty), 76 | ) 77 | ) { 78 | if (this.state.generator) { 79 | // Make sure generator is not null 80 | this.state.generator.loadSelections( 81 | // Generate schedules from non-empty selections 82 | this.state.selections.filter( 83 | (sel: Selection) => sel.length > 0, 84 | ), 85 | ); 86 | this.state.generator.setGap(this.state.gap); 87 | const newSchedules: Schedule[] = [ 88 | ...take( 89 | this.state.limit, 90 | this.state.generator.yieldSchedules(), 91 | ), 92 | ]; 93 | this.setState({ schedules: newSchedules }); 94 | console.log( 95 | "Selections were changed, so schedules have been regenerated", 96 | newSchedules, 97 | ); 98 | 99 | // If schedules changed, log schedules 100 | if (prevState.schedules != newSchedules) 101 | console.log("New schedules", newSchedules); 102 | else console.log("Same schedules"); 103 | } 104 | } 105 | } 106 | 107 | async setSOC(termStr: string, programStr: string) { 108 | console.log(`Setting SOC to "${termStr}" for "${programStr}"`); 109 | await SOC_API.initialize({ termStr, programStr }).then((soc) => 110 | this.setState({ 111 | soc: soc, 112 | generator: new ScheduleGenerator(soc), 113 | }), 114 | ); 115 | this.reset(); // Make sure to only show info from the current SOC 116 | } 117 | 118 | async handleDrop(ind: number, uid: string) { 119 | if (this.state.soc) { 120 | // Make sure SOC exists 121 | const item: Section | Course | null = this.state.soc.get(uid); 122 | console.log("Handling drop; will try to add", item); 123 | 124 | if (item) { 125 | // Make sure a match was found (not null) 126 | // Get the section(s) to try to add to selection 127 | let sectionsToTryAdd: Section[]; 128 | if (item instanceof Course) sectionsToTryAdd = item.sections; 129 | else sectionsToTryAdd = [item]; 130 | 131 | // Get the sections that have not been added to a selection 132 | const sectionsToAdd: Section[] = sectionsToTryAdd.filter( 133 | (section) => 134 | !this.state.selections.some( 135 | // TODO: extract to a Selections class 136 | (sel) => sel.includes(section), 137 | ), 138 | ); 139 | this.newSelection(ind, sectionsToAdd); // Add the section that have not been added 140 | } 141 | } 142 | } 143 | 144 | // TODO: make separate functions 145 | newSelection(ind: number = -1, sectionsToAdd: Section[] = []) { 146 | if (ind == -1) { 147 | this.setState({ 148 | selections: [...this.state.selections, new Selection()], 149 | }); 150 | return; 151 | } 152 | 153 | const newSelections = this.state.selections.map((sel, i) => { 154 | if (i == ind) return [...sel, ...sectionsToAdd]; 155 | return sel; 156 | }); 157 | this.setState({ selections: newSelections }); 158 | } 159 | 160 | handleDeleteSelection(ind: number) { 161 | let newSelections = this.state.selections.filter((_sel, i) => i != ind); 162 | if (newSelections.length == 0) newSelections = getDefaultSelections(); 163 | 164 | this.setState({ selections: newSelections }); 165 | } 166 | 167 | handleRemove(sectionToRemove: Section) { 168 | const newSelections = this.state.selections.map((sel) => 169 | sel.filter((sec) => sec != sectionToRemove), 170 | ); 171 | this.setState({ selections: newSelections }); 172 | } 173 | 174 | render() { 175 | // Show loading screen if filters/terms haven't been fetched yet 176 | if (this.state.filters === null) 177 | return ( 178 |
179 |

Fetching latest semester information...

180 |
181 | ); 182 | if (this.state.soc === null) 183 | // Make sure SOC is set 184 | return ( 185 |
186 |

Setting latest Schedule of Courses...

187 |
188 | ); 189 | //Main Over-Head Bar 190 | return ( 191 |
192 | {/* Title & Term Selector */} 193 |
194 |

195 | 🐊 Swamp Scheduler 📆 196 |

197 | 198 |
199 | 200 |
{Gap Between Sections ({this.state.gap})}
201 | 202 | 208 | this.setState({ gap: parseInt(e.target.value) }) 209 | } 210 | /> 211 | 212 | 232 | 233 | 248 |
249 | 250 |
251 | 252 | {/* Main of Builder */} 253 |
254 | {/* Picker */} 255 |
256 | 257 |
258 | 259 | {/* Selected */} 260 |
261 | 271 |
272 | 273 | {/* Generated Schedules */} 274 |
275 | 279 |
280 |
281 |
282 | ); 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /app/src/components/ScheduleDisplay.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import classNames from "classnames"; 3 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 4 | // @ts-ignore 5 | import { ReactFitty } from "react-fitty"; 6 | import { API_Day, API_Days } from "@scripts/apiTypes"; 7 | import { MeetTime, Section } from "@scripts/soc"; 8 | import { Schedule } from "@scripts/scheduleGenerator"; 9 | import { getSectionColor } from "@constants/frontend"; 10 | import { PERIOD_COUNTS } from "@constants/schedule"; 11 | import { GrPersonalComputer } from "react-icons/gr"; 12 | import { handleExportScheduleClick } from "@scripts/soc/calendar.ts"; 13 | 14 | interface Props { 15 | schedule: Schedule; 16 | } 17 | 18 | interface States {} 19 | 20 | // TODO: reconsider what to store 21 | type MeetTimeInfo = { 22 | meetTime: MeetTime; 23 | courseColor: string; 24 | courseNum: number; 25 | sectionIsOnline: boolean; 26 | }; 27 | 28 | export default class ScheduleDisplay extends Component { 29 | // TODO: redo this (it is *disgusting*); maybe there is a library that does the work 30 | 31 | render() { 32 | const schedule = this.props.schedule, 33 | periodCounts = PERIOD_COUNTS[schedule.term]; 34 | 35 | // TODO: this is suspiciously similar to Meetings class 36 | const blockSchedule: Record = { 37 | [API_Day.Mon]: new Array(periodCounts.all).fill(null), 38 | [API_Day.Tue]: new Array(periodCounts.all).fill(null), 39 | [API_Day.Wed]: new Array(periodCounts.all).fill(null), 40 | [API_Day.Thu]: new Array(periodCounts.all).fill(null), 41 | [API_Day.Fri]: new Array(periodCounts.all).fill(null), 42 | [API_Day.Sat]: new Array(periodCounts.all).fill(null), 43 | }; 44 | 45 | schedule.forEach((section: Section, s: number) => 46 | API_Days.forEach((day) => 47 | section.meetings[day].forEach((mT) => { 48 | const info: MeetTimeInfo = { 49 | meetTime: mT, 50 | courseColor: getSectionColor(s), 51 | courseNum: s + 1, 52 | sectionIsOnline: section.isOnline, 53 | }; 54 | for ( 55 | let p = mT.periodBegin ?? periodCounts.all; 56 | p <= mT.periodEnd ?? -1; 57 | ++p 58 | ) 59 | blockSchedule[day][p - 1] = info; 60 | }), 61 | ), 62 | ); 63 | 64 | const divs = []; 65 | for (let p = 0; p < periodCounts.all; ++p) { 66 | for (const day of API_Days) { 67 | // TODO: make this a checkbox or automatically change format to 6 days if schedule has a Saturday course 68 | if (day == API_Day.Sat) continue; 69 | 70 | //TODO: make this not absolutely horrible :) 71 | const meetTimeInfo: MeetTimeInfo | null = blockSchedule[day][p]; 72 | 73 | if (meetTimeInfo == null) { 74 | // No course 75 | divs.push( 76 |
, 87 | ); 88 | continue; 89 | } 90 | 91 | const mT = meetTimeInfo.meetTime, 92 | color = meetTimeInfo.courseColor, 93 | courseNum = meetTimeInfo.courseNum; 94 | 95 | let location: React.JSX.Element = TBD; 96 | if (mT.location) location = <>{mT.location}; 97 | 98 | if ( 99 | mT.periodBegin != mT.periodEnd && 100 | (p == 0 || 101 | blockSchedule[day][p - 1] == null || 102 | blockSchedule[day][p - 1]!.meetTime != mT) 103 | ) { 104 | // TODO: why do I have to do this garbage?? 105 | const spanMap: Map = new Map< 106 | number, 107 | string 108 | >([ 109 | [2, "row-span-2"], 110 | [3, "row-span-3"], 111 | [4, "row-span-4"], 112 | [5, "row-span-5"], 113 | [6, "row-span-6"], 114 | ]); 115 | const span: string = spanMap.get( 116 | Math.min(1 + (mT.periodEnd - mT.periodBegin), 6), 117 | )!; // TODO: error handling for NaN 118 | 119 | divs.push( 120 |
132 |
133 | 138 | {location} 139 | 140 | {courseNum} 141 | 142 | 143 |
144 |
, 145 | ); 146 | } else if ( 147 | !( 148 | p > 0 && 149 | mT != null && 150 | blockSchedule[day][p - 1] != null && 151 | blockSchedule[day][p - 1]!.meetTime == mT 152 | ) 153 | ) 154 | divs.push( 155 |
167 | 172 | {location} 173 | 174 | {courseNum} 175 | 176 | 177 |
, 178 | ); 179 | } 180 | } 181 | 182 | const onlineSections: Section[] = schedule.filter((s) => s.isOnline); 183 | 184 | return ( 185 |
186 | 196 |
197 |
198 | {schedule.map((sec: Section, s: number) => ( 199 |
210 | ({s + 1}) Sec. {sec.number} [ 211 | {sec.courseCode}] 212 |
213 | ))} 214 |
215 |
216 | 217 |
218 |
219 |
220 | {[...Array(periodCounts.all).keys()] 221 | .map((p) => p + 1) 222 | .map((p) => ( 223 |
228 | 229 | {MeetTime.formatPeriod( 230 | p, 231 | schedule.term, 232 | )} 233 | 234 |
235 | ))} 236 | 237 | {onlineSections.length > 0 && ( 238 |
243 |
248 | ️ 249 |
250 |
251 | )} 252 |
253 |
254 | 255 |
256 |
257 | {divs} 258 | {onlineSections.length > 0 && ( 259 |
260 |
261 |
262 | {onlineSections.map( 263 | (sec: Section, ind: number) => ( 264 |
277 | {sec.displayName} 278 | 279 | {1 + ind} 280 | 281 |
282 | ), 283 | )} 284 |
285 |
286 |
287 | )} 288 |
289 |
290 |
291 |
292 | ); 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ### GNU AFFERO GENERAL PUBLIC LICENSE 2 | 3 | Version 3, 19 November 2007 4 | 5 | Copyright (C) 2007 Free Software Foundation, Inc. 6 | 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | ### Preamble 12 | 13 | The GNU Affero General Public License is a free, copyleft license for 14 | software and other kinds of works, specifically designed to ensure 15 | cooperation with the community in the case of network server software. 16 | 17 | The licenses for most software and other practical works are designed 18 | to take away your freedom to share and change the works. By contrast, 19 | our General Public Licenses are intended to guarantee your freedom to 20 | share and change all versions of a program--to make sure it remains 21 | free software for all its users. 22 | 23 | When we speak of free software, we are referring to freedom, not 24 | price. Our General Public Licenses are designed to make sure that you 25 | have the freedom to distribute copies of free software (and charge for 26 | them if you wish), that you receive source code or can get it if you 27 | want it, that you can change the software or use pieces of it in new 28 | free programs, and that you know you can do these things. 29 | 30 | Developers that use our General Public Licenses protect your rights 31 | with two steps: (1) assert copyright on the software, and (2) offer 32 | you this License which gives you legal permission to copy, distribute 33 | and/or modify the software. 34 | 35 | A secondary benefit of defending all users' freedom is that 36 | improvements made in alternate versions of the program, if they 37 | receive widespread use, become available for other developers to 38 | incorporate. Many developers of free software are heartened and 39 | encouraged by the resulting cooperation. However, in the case of 40 | software used on network servers, this result may fail to come about. 41 | The GNU General Public License permits making a modified version and 42 | letting the public access it on a server without ever releasing its 43 | source code to the public. 44 | 45 | The GNU Affero General Public License is designed specifically to 46 | ensure that, in such cases, the modified source code becomes available 47 | to the community. It requires the operator of a network server to 48 | provide the source code of the modified version running there to the 49 | users of that server. Therefore, public use of a modified version, on 50 | a publicly accessible server, gives the public access to the source 51 | code of the modified version. 52 | 53 | An older license, called the Affero General Public License and 54 | published by Affero, was designed to accomplish similar goals. This is 55 | a different license, not a version of the Affero GPL, but Affero has 56 | released a new version of the Affero GPL which permits relicensing 57 | under this license. 58 | 59 | The precise terms and conditions for copying, distribution and 60 | modification follow. 61 | 62 | ### TERMS AND CONDITIONS 63 | 64 | #### 0. Definitions. 65 | 66 | "This License" refers to version 3 of the GNU Affero General Public 67 | License. 68 | 69 | "Copyright" also means copyright-like laws that apply to other kinds 70 | of works, such as semiconductor masks. 71 | 72 | "The Program" refers to any copyrightable work licensed under this 73 | License. Each licensee is addressed as "you". "Licensees" and 74 | "recipients" may be individuals or organizations. 75 | 76 | To "modify" a work means to copy from or adapt all or part of the work 77 | in a fashion requiring copyright permission, other than the making of 78 | an exact copy. The resulting work is called a "modified version" of 79 | the earlier work or a work "based on" the earlier work. 80 | 81 | A "covered work" means either the unmodified Program or a work based 82 | on the Program. 83 | 84 | To "propagate" a work means to do anything with it that, without 85 | permission, would make you directly or secondarily liable for 86 | infringement under applicable copyright law, except executing it on a 87 | computer or modifying a private copy. Propagation includes copying, 88 | distribution (with or without modification), making available to the 89 | public, and in some countries other activities as well. 90 | 91 | To "convey" a work means any kind of propagation that enables other 92 | parties to make or receive copies. Mere interaction with a user 93 | through a computer network, with no transfer of a copy, is not 94 | conveying. 95 | 96 | An interactive user interface displays "Appropriate Legal Notices" to 97 | the extent that it includes a convenient and prominently visible 98 | feature that (1) displays an appropriate copyright notice, and (2) 99 | tells the user that there is no warranty for the work (except to the 100 | extent that warranties are provided), that licensees may convey the 101 | work under this License, and how to view a copy of this License. If 102 | the interface presents a list of user commands or options, such as a 103 | menu, a prominent item in the list meets this criterion. 104 | 105 | #### 1. Source Code. 106 | 107 | The "source code" for a work means the preferred form of the work for 108 | making modifications to it. "Object code" means any non-source form of 109 | a work. 110 | 111 | A "Standard Interface" means an interface that either is an official 112 | standard defined by a recognized standards body, or, in the case of 113 | interfaces specified for a particular programming language, one that 114 | is widely used among developers working in that language. 115 | 116 | The "System Libraries" of an executable work include anything, other 117 | than the work as a whole, that (a) is included in the normal form of 118 | packaging a Major Component, but which is not part of that Major 119 | Component, and (b) serves only to enable use of the work with that 120 | Major Component, or to implement a Standard Interface for which an 121 | implementation is available to the public in source code form. A 122 | "Major Component", in this context, means a major essential component 123 | (kernel, window system, and so on) of the specific operating system 124 | (if any) on which the executable work runs, or a compiler used to 125 | produce the work, or an object code interpreter used to run it. 126 | 127 | The "Corresponding Source" for a work in object code form means all 128 | the source code needed to generate, install, and (for an executable 129 | work) run the object code and to modify the work, including scripts to 130 | control those activities. However, it does not include the work's 131 | System Libraries, or general-purpose tools or generally available free 132 | programs which are used unmodified in performing those activities but 133 | which are not part of the work. For example, Corresponding Source 134 | includes interface definition files associated with source files for 135 | the work, and the source code for shared libraries and dynamically 136 | linked subprograms that the work is specifically designed to require, 137 | such as by intimate data communication or control flow between those 138 | subprograms and other parts of the work. 139 | 140 | The Corresponding Source need not include anything that users can 141 | regenerate automatically from other parts of the Corresponding Source. 142 | 143 | The Corresponding Source for a work in source code form is that same 144 | work. 145 | 146 | #### 2. Basic Permissions. 147 | 148 | All rights granted under this License are granted for the term of 149 | copyright on the Program, and are irrevocable provided the stated 150 | conditions are met. This License explicitly affirms your unlimited 151 | permission to run the unmodified Program. The output from running a 152 | covered work is covered by this License only if the output, given its 153 | content, constitutes a covered work. This License acknowledges your 154 | rights of fair use or other equivalent, as provided by copyright law. 155 | 156 | You may make, run and propagate covered works that you do not convey, 157 | without conditions so long as your license otherwise remains in force. 158 | You may convey covered works to others for the sole purpose of having 159 | them make modifications exclusively for you, or provide you with 160 | facilities for running those works, provided that you comply with the 161 | terms of this License in conveying all material for which you do not 162 | control copyright. Those thus making or running the covered works for 163 | you must do so exclusively on your behalf, under your direction and 164 | control, on terms that prohibit them from making any copies of your 165 | copyrighted material outside their relationship with you. 166 | 167 | Conveying under any other circumstances is permitted solely under the 168 | conditions stated below. Sublicensing is not allowed; section 10 makes 169 | it unnecessary. 170 | 171 | #### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 172 | 173 | No covered work shall be deemed part of an effective technological 174 | measure under any applicable law fulfilling obligations under article 175 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 176 | similar laws prohibiting or restricting circumvention of such 177 | measures. 178 | 179 | When you convey a covered work, you waive any legal power to forbid 180 | circumvention of technological measures to the extent such 181 | circumvention is effected by exercising rights under this License with 182 | respect to the covered work, and you disclaim any intention to limit 183 | operation or modification of the work as a means of enforcing, against 184 | the work's users, your or third parties' legal rights to forbid 185 | circumvention of technological measures. 186 | 187 | #### 4. Conveying Verbatim Copies. 188 | 189 | You may convey verbatim copies of the Program's source code as you 190 | receive it, in any medium, provided that you conspicuously and 191 | appropriately publish on each copy an appropriate copyright notice; 192 | keep intact all notices stating that this License and any 193 | non-permissive terms added in accord with section 7 apply to the code; 194 | keep intact all notices of the absence of any warranty; and give all 195 | recipients a copy of this License along with the Program. 196 | 197 | You may charge any price or no price for each copy that you convey, 198 | and you may offer support or warranty protection for a fee. 199 | 200 | #### 5. Conveying Modified Source Versions. 201 | 202 | You may convey a work based on the Program, or the modifications to 203 | produce it from the Program, in the form of source code under the 204 | terms of section 4, provided that you also meet all of these 205 | conditions: 206 | 207 | - a) The work must carry prominent notices stating that you modified 208 | it, and giving a relevant date. 209 | - b) The work must carry prominent notices stating that it is 210 | released under this License and any conditions added under 211 | section 7. This requirement modifies the requirement in section 4 212 | to "keep intact all notices". 213 | - c) You must license the entire work, as a whole, under this 214 | License to anyone who comes into possession of a copy. This 215 | License will therefore apply, along with any applicable section 7 216 | additional terms, to the whole of the work, and all its parts, 217 | regardless of how they are packaged. This License gives no 218 | permission to license the work in any other way, but it does not 219 | invalidate such permission if you have separately received it. 220 | - d) If the work has interactive user interfaces, each must display 221 | Appropriate Legal Notices; however, if the Program has interactive 222 | interfaces that do not display Appropriate Legal Notices, your 223 | work need not make them do so. 224 | 225 | A compilation of a covered work with other separate and independent 226 | works, which are not by their nature extensions of the covered work, 227 | and which are not combined with it such as to form a larger program, 228 | in or on a volume of a storage or distribution medium, is called an 229 | "aggregate" if the compilation and its resulting copyright are not 230 | used to limit the access or legal rights of the compilation's users 231 | beyond what the individual works permit. Inclusion of a covered work 232 | in an aggregate does not cause this License to apply to the other 233 | parts of the aggregate. 234 | 235 | #### 6. Conveying Non-Source Forms. 236 | 237 | You may convey a covered work in object code form under the terms of 238 | sections 4 and 5, provided that you also convey the machine-readable 239 | Corresponding Source under the terms of this License, in one of these 240 | ways: 241 | 242 | - a) Convey the object code in, or embodied in, a physical product 243 | (including a physical distribution medium), accompanied by the 244 | Corresponding Source fixed on a durable physical medium 245 | customarily used for software interchange. 246 | - b) Convey the object code in, or embodied in, a physical product 247 | (including a physical distribution medium), accompanied by a 248 | written offer, valid for at least three years and valid for as 249 | long as you offer spare parts or customer support for that product 250 | model, to give anyone who possesses the object code either (1) a 251 | copy of the Corresponding Source for all the software in the 252 | product that is covered by this License, on a durable physical 253 | medium customarily used for software interchange, for a price no 254 | more than your reasonable cost of physically performing this 255 | conveying of source, or (2) access to copy the Corresponding 256 | Source from a network server at no charge. 257 | - c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | - d) Convey the object code by offering access from a designated 263 | place (gratis or for a charge), and offer equivalent access to the 264 | Corresponding Source in the same way through the same place at no 265 | further charge. You need not require recipients to copy the 266 | Corresponding Source along with the object code. If the place to 267 | copy the object code is a network server, the Corresponding Source 268 | may be on a different server (operated by you or a third party) 269 | that supports equivalent copying facilities, provided you maintain 270 | clear directions next to the object code saying where to find the 271 | Corresponding Source. Regardless of what server hosts the 272 | Corresponding Source, you remain obligated to ensure that it is 273 | available for as long as needed to satisfy these requirements. 274 | - e) Convey the object code using peer-to-peer transmission, 275 | provided you inform other peers where the object code and 276 | Corresponding Source of the work are being offered to the general 277 | public at no charge under subsection 6d. 278 | 279 | A separable portion of the object code, whose source code is excluded 280 | from the Corresponding Source as a System Library, need not be 281 | included in conveying the object code work. 282 | 283 | A "User Product" is either (1) a "consumer product", which means any 284 | tangible personal property which is normally used for personal, 285 | family, or household purposes, or (2) anything designed or sold for 286 | incorporation into a dwelling. In determining whether a product is a 287 | consumer product, doubtful cases shall be resolved in favor of 288 | coverage. For a particular product received by a particular user, 289 | "normally used" refers to a typical or common use of that class of 290 | product, regardless of the status of the particular user or of the way 291 | in which the particular user actually uses, or expects or is expected 292 | to use, the product. A product is a consumer product regardless of 293 | whether the product has substantial commercial, industrial or 294 | non-consumer uses, unless such uses represent the only significant 295 | mode of use of the product. 296 | 297 | "Installation Information" for a User Product means any methods, 298 | procedures, authorization keys, or other information required to 299 | install and execute modified versions of a covered work in that User 300 | Product from a modified version of its Corresponding Source. The 301 | information must suffice to ensure that the continued functioning of 302 | the modified object code is in no case prevented or interfered with 303 | solely because modification has been made. 304 | 305 | If you convey an object code work under this section in, or with, or 306 | specifically for use in, a User Product, and the conveying occurs as 307 | part of a transaction in which the right of possession and use of the 308 | User Product is transferred to the recipient in perpetuity or for a 309 | fixed term (regardless of how the transaction is characterized), the 310 | Corresponding Source conveyed under this section must be accompanied 311 | by the Installation Information. But this requirement does not apply 312 | if neither you nor any third party retains the ability to install 313 | modified object code on the User Product (for example, the work has 314 | been installed in ROM). 315 | 316 | The requirement to provide Installation Information does not include a 317 | requirement to continue to provide support service, warranty, or 318 | updates for a work that has been modified or installed by the 319 | recipient, or for the User Product in which it has been modified or 320 | installed. Access to a network may be denied when the modification 321 | itself materially and adversely affects the operation of the network 322 | or violates the rules and protocols for communication across the 323 | network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | #### 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders 351 | of that material) supplement the terms of this License with terms: 352 | 353 | - a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | - b) Requiring preservation of specified reasonable legal notices or 356 | author attributions in that material or in the Appropriate Legal 357 | Notices displayed by works containing it; or 358 | - c) Prohibiting misrepresentation of the origin of that material, 359 | or requiring that modified versions of such material be marked in 360 | reasonable ways as different from the original version; or 361 | - d) Limiting the use for publicity purposes of names of licensors 362 | or authors of the material; or 363 | - e) Declining to grant rights under trademark law for use of some 364 | trade names, trademarks, or service marks; or 365 | - f) Requiring indemnification of licensors and authors of that 366 | material by anyone who conveys the material (or modified versions 367 | of it) with contractual assumptions of liability to the recipient, 368 | for any liability that these contractual assumptions directly 369 | impose on those licensors and authors. 370 | 371 | All other non-permissive additional terms are considered "further 372 | restrictions" within the meaning of section 10. If the Program as you 373 | received it, or any part of it, contains a notice stating that it is 374 | governed by this License along with a term that is a further 375 | restriction, you may remove that term. If a license document contains 376 | a further restriction but permits relicensing or conveying under this 377 | License, you may add to a covered work material governed by the terms 378 | of that license document, provided that the further restriction does 379 | not survive such relicensing or conveying. 380 | 381 | If you add terms to a covered work in accord with this section, you 382 | must place, in the relevant source files, a statement of the 383 | additional terms that apply to those files, or a notice indicating 384 | where to find the applicable terms. 385 | 386 | Additional terms, permissive or non-permissive, may be stated in the 387 | form of a separately written license, or stated as exceptions; the 388 | above requirements apply either way. 389 | 390 | #### 8. Termination. 391 | 392 | You may not propagate or modify a covered work except as expressly 393 | provided under this License. Any attempt otherwise to propagate or 394 | modify it is void, and will automatically terminate your rights under 395 | this License (including any patent licenses granted under the third 396 | paragraph of section 11). 397 | 398 | However, if you cease all violation of this License, then your license 399 | from a particular copyright holder is reinstated (a) provisionally, 400 | unless and until the copyright holder explicitly and finally 401 | terminates your license, and (b) permanently, if the copyright holder 402 | fails to notify you of the violation by some reasonable means prior to 403 | 60 days after the cessation. 404 | 405 | Moreover, your license from a particular copyright holder is 406 | reinstated permanently if the copyright holder notifies you of the 407 | violation by some reasonable means, this is the first time you have 408 | received notice of violation of this License (for any work) from that 409 | copyright holder, and you cure the violation prior to 30 days after 410 | your receipt of the notice. 411 | 412 | Termination of your rights under this section does not terminate the 413 | licenses of parties who have received copies or rights from you under 414 | this License. If your rights have been terminated and not permanently 415 | reinstated, you do not qualify to receive new licenses for the same 416 | material under section 10. 417 | 418 | #### 9. Acceptance Not Required for Having Copies. 419 | 420 | You are not required to accept this License in order to receive or run 421 | a copy of the Program. Ancillary propagation of a covered work 422 | occurring solely as a consequence of using peer-to-peer transmission 423 | to receive a copy likewise does not require acceptance. However, 424 | nothing other than this License grants you permission to propagate or 425 | modify any covered work. These actions infringe copyright if you do 426 | not accept this License. Therefore, by modifying or propagating a 427 | covered work, you indicate your acceptance of this License to do so. 428 | 429 | #### 10. Automatic Licensing of Downstream Recipients. 430 | 431 | Each time you convey a covered work, the recipient automatically 432 | receives a license from the original licensors, to run, modify and 433 | propagate that work, subject to this License. You are not responsible 434 | for enforcing compliance by third parties with this License. 435 | 436 | An "entity transaction" is a transaction transferring control of an 437 | organization, or substantially all assets of one, or subdividing an 438 | organization, or merging organizations. If propagation of a covered 439 | work results from an entity transaction, each party to that 440 | transaction who receives a copy of the work also receives whatever 441 | licenses to the work the party's predecessor in interest had or could 442 | give under the previous paragraph, plus a right to possession of the 443 | Corresponding Source of the work from the predecessor in interest, if 444 | the predecessor has it or can get it with reasonable efforts. 445 | 446 | You may not impose any further restrictions on the exercise of the 447 | rights granted or affirmed under this License. For example, you may 448 | not impose a license fee, royalty, or other charge for exercise of 449 | rights granted under this License, and you may not initiate litigation 450 | (including a cross-claim or counterclaim in a lawsuit) alleging that 451 | any patent claim is infringed by making, using, selling, offering for 452 | sale, or importing the Program or any portion of it. 453 | 454 | #### 11. Patents. 455 | 456 | A "contributor" is a copyright holder who authorizes use under this 457 | License of the Program or a work on which the Program is based. The 458 | work thus licensed is called the contributor's "contributor version". 459 | 460 | A contributor's "essential patent claims" are all patent claims owned 461 | or controlled by the contributor, whether already acquired or 462 | hereafter acquired, that would be infringed by some manner, permitted 463 | by this License, of making, using, or selling its contributor version, 464 | but do not include claims that would be infringed only as a 465 | consequence of further modification of the contributor version. For 466 | purposes of this definition, "control" includes the right to grant 467 | patent sublicenses in a manner consistent with the requirements of 468 | this License. 469 | 470 | Each contributor grants you a non-exclusive, worldwide, royalty-free 471 | patent license under the contributor's essential patent claims, to 472 | make, use, sell, offer for sale, import and otherwise run, modify and 473 | propagate the contents of its contributor version. 474 | 475 | In the following three paragraphs, a "patent license" is any express 476 | agreement or commitment, however denominated, not to enforce a patent 477 | (such as an express permission to practice a patent or covenant not to 478 | sue for patent infringement). To "grant" such a patent license to a 479 | party means to make such an agreement or commitment not to enforce a 480 | patent against the party. 481 | 482 | If you convey a covered work, knowingly relying on a patent license, 483 | and the Corresponding Source of the work is not available for anyone 484 | to copy, free of charge and under the terms of this License, through a 485 | publicly available network server or other readily accessible means, 486 | then you must either (1) cause the Corresponding Source to be so 487 | available, or (2) arrange to deprive yourself of the benefit of the 488 | patent license for this particular work, or (3) arrange, in a manner 489 | consistent with the requirements of this License, to extend the patent 490 | license to downstream recipients. "Knowingly relying" means you have 491 | actual knowledge that, but for the patent license, your conveying the 492 | covered work in a country, or your recipient's use of the covered work 493 | in a country, would infringe one or more identifiable patents in that 494 | country that you have reason to believe are valid. 495 | 496 | If, pursuant to or in connection with a single transaction or 497 | arrangement, you convey, or propagate by procuring conveyance of, a 498 | covered work, and grant a patent license to some of the parties 499 | receiving the covered work authorizing them to use, propagate, modify 500 | or convey a specific copy of the covered work, then the patent license 501 | you grant is automatically extended to all recipients of the covered 502 | work and works based on it. 503 | 504 | A patent license is "discriminatory" if it does not include within the 505 | scope of its coverage, prohibits the exercise of, or is conditioned on 506 | the non-exercise of one or more of the rights that are specifically 507 | granted under this License. You may not convey a covered work if you 508 | are a party to an arrangement with a third party that is in the 509 | business of distributing software, under which you make payment to the 510 | third party based on the extent of your activity of conveying the 511 | work, and under which the third party grants, to any of the parties 512 | who would receive the covered work from you, a discriminatory patent 513 | license (a) in connection with copies of the covered work conveyed by 514 | you (or copies made from those copies), or (b) primarily for and in 515 | connection with specific products or compilations that contain the 516 | covered work, unless you entered into that arrangement, or that patent 517 | license was granted, prior to 28 March 2007. 518 | 519 | Nothing in this License shall be construed as excluding or limiting 520 | any implied license or other defenses to infringement that may 521 | otherwise be available to you under applicable patent law. 522 | 523 | #### 12. No Surrender of Others' Freedom. 524 | 525 | If conditions are imposed on you (whether by court order, agreement or 526 | otherwise) that contradict the conditions of this License, they do not 527 | excuse you from the conditions of this License. If you cannot convey a 528 | covered work so as to satisfy simultaneously your obligations under 529 | this License and any other pertinent obligations, then as a 530 | consequence you may not convey it at all. For example, if you agree to 531 | terms that obligate you to collect a royalty for further conveying 532 | from those to whom you convey the Program, the only way you could 533 | satisfy both those terms and this License would be to refrain entirely 534 | from conveying the Program. 535 | 536 | #### 13. Remote Network Interaction; Use with the GNU General Public License. 537 | 538 | Notwithstanding any other provision of this License, if you modify the 539 | Program, your modified version must prominently offer all users 540 | interacting with it remotely through a computer network (if your 541 | version supports such interaction) an opportunity to receive the 542 | Corresponding Source of your version by providing access to the 543 | Corresponding Source from a network server at no charge, through some 544 | standard or customary means of facilitating copying of software. This 545 | Corresponding Source shall include the Corresponding Source for any 546 | work covered by version 3 of the GNU General Public License that is 547 | incorporated pursuant to the following paragraph. 548 | 549 | Notwithstanding any other provision of this License, you have 550 | permission to link or combine any covered work with a work licensed 551 | under version 3 of the GNU General Public License into a single 552 | combined work, and to convey the resulting work. The terms of this 553 | License will continue to apply to the part which is the covered work, 554 | but the work with which it is combined will remain governed by version 555 | 3 of the GNU General Public License. 556 | 557 | #### 14. Revised Versions of this License. 558 | 559 | The Free Software Foundation may publish revised and/or new versions 560 | of the GNU Affero General Public License from time to time. Such new 561 | versions will be similar in spirit to the present version, but may 562 | differ in detail to address new problems or concerns. 563 | 564 | Each version is given a distinguishing version number. If the Program 565 | specifies that a certain numbered version of the GNU Affero General 566 | Public License "or any later version" applies to it, you have the 567 | option of following the terms and conditions either of that numbered 568 | version or of any later version published by the Free Software 569 | Foundation. If the Program does not specify a version number of the 570 | GNU Affero General Public License, you may choose any version ever 571 | published by the Free Software Foundation. 572 | 573 | If the Program specifies that a proxy can decide which future versions 574 | of the GNU Affero General Public License can be used, that proxy's 575 | public statement of acceptance of a version permanently authorizes you 576 | to choose that version for the Program. 577 | 578 | Later license versions may give you additional or different 579 | permissions. However, no additional obligations are imposed on any 580 | author or copyright holder as a result of your choosing to follow a 581 | later version. 582 | 583 | #### 15. Disclaimer of Warranty. 584 | 585 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 586 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 587 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT 588 | WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT 589 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 590 | A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND 591 | PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE 592 | DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR 593 | CORRECTION. 594 | 595 | #### 16. Limitation of Liability. 596 | 597 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 598 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR 599 | CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 600 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES 601 | ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT 602 | NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR 603 | LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM 604 | TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER 605 | PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 606 | 607 | #### 17. Interpretation of Sections 15 and 16. 608 | 609 | If the disclaimer of warranty and limitation of liability provided 610 | above cannot be given local legal effect according to their terms, 611 | reviewing courts shall apply local law that most closely approximates 612 | an absolute waiver of all civil liability in connection with the 613 | Program, unless a warranty or assumption of liability accompanies a 614 | copy of the Program in return for a fee. 615 | 616 | END OF TERMS AND CONDITIONS 617 | 618 | ### How to Apply These Terms to Your New Programs 619 | 620 | If you develop a new program, and you want it to be of the greatest 621 | possible use to the public, the best way to achieve this is to make it 622 | free software which everyone can redistribute and change under these 623 | terms. 624 | 625 | To do so, attach the following notices to the program. It is safest to 626 | attach them to the start of each source file to most effectively state 627 | the exclusion of warranty; and each file should have at least the 628 | "copyright" line and a pointer to where the full notice is found. 629 | 630 | 631 | Copyright (C) 632 | 633 | This program is free software: you can redistribute it and/or modify 634 | it under the terms of the GNU Affero General Public License as 635 | published by the Free Software Foundation, either version 3 of the 636 | License, or (at your option) any later version. 637 | 638 | This program is distributed in the hope that it will be useful, 639 | but WITHOUT ANY WARRANTY; without even the implied warranty of 640 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 641 | GNU Affero General Public License for more details. 642 | 643 | You should have received a copy of the GNU Affero General Public License 644 | along with this program. If not, see . 645 | 646 | Also add information on how to contact you by electronic and paper 647 | mail. 648 | 649 | If your software can interact with users remotely through a computer 650 | network, you should also make sure that it provides a way for users to 651 | get its source. For example, if your program is a web application, its 652 | interface could display a "Source" link that leads users to an archive 653 | of the code. There are many ways you could offer source, and different 654 | solutions will be better for different programs; see section 13 for 655 | the specific requirements. 656 | 657 | You should also get your employer (if you work as a programmer) or 658 | school, if any, to sign a "copyright disclaimer" for the program, if 659 | necessary. For more information on this, and how to apply and follow 660 | the GNU AGPL, see . 661 | --------------------------------------------------------------------------------