Compare commits
308 Commits
0.3.5.697
...
0.4.3.1006
| Author | SHA1 | Date | |
|---|---|---|---|
| 6204efd453 | |||
| db083732b0 | |||
| 4974049c0b | |||
| 857192ee6d | |||
| 90d07b1faa | |||
| 4abe02da94 | |||
| adca4b41e2 | |||
| 4d9d8e0be5 | |||
| 546ad81f7c | |||
| 1eff27ad26 | |||
| f540e03a56 | |||
| af925741b4 | |||
| 08bb24cac4 | |||
| 8a9ca83e68 | |||
| e6b6a73f1f | |||
| ceca351b07 | |||
| 4e14730db6 | |||
| 3b0e291df4 | |||
| 6bcb2577f7 | |||
| 54e54b89e9 | |||
| 0085d384fd | |||
| 3b4e40261f | |||
| eb8715867e | |||
| 1bcfdf5648 | |||
| 429de6553b | |||
| fce47648c8 | |||
| f6d44dd1f2 | |||
| 4fe0a54277 | |||
| e8bf21e60c | |||
| 472df39ac8 | |||
| ae94f54a7b | |||
| 8c9f0a0e83 | |||
| cb22baf120 | |||
| 0525aae98a | |||
| 4c87cb2947 | |||
| 076a1ee52e | |||
| 7bd9e97396 | |||
| aa014bcdab | |||
| ee308d29ff | |||
| 35ef2144b7 | |||
| 6dc2a21e8f | |||
| 343c921eb2 | |||
| f54dfba058 | |||
| a7d7ef44a6 | |||
| 793d7444b2 | |||
| eaf4d2ce7b | |||
| 79e615ee46 | |||
| 40353cfd35 | |||
| 2be2ce98e6 | |||
| 6659f65763 | |||
| 5b3ba4db07 | |||
| a05281f4e4 | |||
| 2f95548fb3 | |||
| 96ee9e9265 | |||
| 9026b9d3a9 | |||
| 67e38dd554 | |||
| 7ffef67e42 | |||
| 0d195cfb7f | |||
| 0d3e9c0840 | |||
| 6c600f31f8 | |||
| a05a44aaef | |||
| 31d3a5a09e | |||
| 30293cd261 | |||
| 9a279b3417 | |||
| 4a7ff0ea55 | |||
| fd341e9d33 | |||
| bdacc8dd9e | |||
| ad2c8a1ee8 | |||
| 1f58818a1a | |||
| 09442cbfba | |||
| 91e10d1fa8 | |||
| 80267feb54 | |||
| f4ee2dadb6 | |||
| d698f3d13a | |||
| 82cb845061 | |||
| 940738307b | |||
| df011787e1 | |||
| 0af393a1e4 | |||
| 6504cc033b | |||
| c6ccc5e311 | |||
| 3a8899507f | |||
| 2b7026c4ce | |||
| a3fd822252 | |||
| f9d7fa398f | |||
| 7ffc445bba | |||
| 8e9f6a4bb7 | |||
| 05cb560c8c | |||
| 53a57234a0 | |||
| 6af6bb0959 | |||
| 089aad7398 | |||
| 937e89ce5f | |||
| b0685ffd6b | |||
| 18dee50b96 | |||
| 8dbef09ec9 | |||
| 73b71f56a5 | |||
| ce702405d0 | |||
| 21744a1bef | |||
| 82064f2f02 | |||
| adfebc8c44 | |||
| 2f30e67671 | |||
| 0a80d534eb | |||
| a77c495843 | |||
| 5503aff6b1 | |||
| 7cc4567245 | |||
| e7f29ad0bf | |||
| cdbd101428 | |||
| da650ef16b | |||
| 8abfdd7f8e | |||
| 3c66f99363 | |||
| 251f87a072 | |||
| e15c95561c | |||
| f4ed39401c | |||
| c3fb998254 | |||
| d9e9bcc731 | |||
| 110ff6daec | |||
| 7d83344e73 | |||
| 06d93398fb | |||
| b05328888e | |||
| 19598f4a10 | |||
| cd7102a5df | |||
| 9bf1f6b624 | |||
| f1681ad405 | |||
| e0a70a23b0 | |||
| 677dfb491a | |||
| 07a9b38c8d | |||
| 53b78c4a94 | |||
| 2057f08e40 | |||
| 7bc843aa9a | |||
| 0a8855e020 | |||
| 445d07c5e4 | |||
| 125e7d14de | |||
| 4917f5cb0c | |||
| 2504b37eb7 | |||
| b0d3bf4bd2 | |||
| e640384fd8 | |||
| adf7af6b84 | |||
| beb4155aab | |||
| 3337766ae6 | |||
| dc135dc78b | |||
| b39c51beea | |||
| 971339e49c | |||
| 9db0431e96 | |||
| d667f2ab34 | |||
| 9b895a8fd7 | |||
| 66d7e18a71 | |||
| 4643155908 | |||
| c6c3303a99 | |||
| 1aea558c40 | |||
| 316c7e1f63 | |||
| 8364befc91 | |||
| 47c25d0e71 | |||
| 422d574c6c | |||
| 3400c07e77 | |||
| 79bb10b14e | |||
| e7563fcfb5 | |||
| 43f0046827 | |||
| ef424dd9e3 | |||
| 37902b6d1f | |||
| 92e692f69a | |||
| b6e0e12cab | |||
| 6eb7e421e0 | |||
| fbb9352546 | |||
| 6f5c4f3a09 | |||
| 1e91cec8d2 | |||
| 17262e12af | |||
| 56a954c235 | |||
| da5eced769 | |||
| 71f2eccab4 | |||
| a5162c1f5b | |||
| 53c82fd477 | |||
| 2867f8d09c | |||
| 5b902171c8 | |||
| c40c5b6b94 | |||
| b66c313b05 | |||
| 48287cdc4b | |||
| 750c768848 | |||
| 4b512d64d9 | |||
| e121210e96 | |||
| 5a87608fa6 | |||
| e819abe789 | |||
| eb718d14d1 | |||
| 564896e940 | |||
| d91c3875b0 | |||
| 1fe2edb9f4 | |||
| 91977e1226 | |||
| 948018c7de | |||
| f2350680c8 | |||
| 4cc424e079 | |||
| 49d26fcf61 | |||
| 9fde3b21d4 | |||
| 6952649058 | |||
| b12a717cea | |||
| 8e1e222656 | |||
| 8f7775bb69 | |||
| 0477161af2 | |||
| 3b387db5b5 | |||
| d85d0e4e36 | |||
| b74b086f98 | |||
| e1d20b822f | |||
| e98997f230 | |||
| 82e17030d4 | |||
| d1905ee2b1 | |||
| 310b2bcee1 | |||
| d55eebce19 | |||
| 03507968ee | |||
| 56769e4518 | |||
| fba589d6fe | |||
| ff5b22b5dc | |||
| 14de2e0b23 | |||
| 880e331c99 | |||
| cf794588df | |||
| d362598d83 | |||
| 536e98642d | |||
| baae0d088c | |||
| 202feb12d7 | |||
| d2ab7c059f | |||
| e1ef117dcd | |||
| fb3186d575 | |||
| b913873951 | |||
| bbdcb42316 | |||
| 5438637224 | |||
| 970a058ba3 | |||
| 5d295db1b7 | |||
| b22aac7ca2 | |||
| eef29a4611 | |||
| 131952ed37 | |||
| 2e95c2550e | |||
| 0db6e0d693 | |||
| 27198ad4e0 | |||
| 3cbb6ebee6 | |||
| 2308df65b5 | |||
| c8f82cc991 | |||
| a6e1b905ed | |||
| 9432e9a806 | |||
| d90280a10f | |||
| db30b7f807 | |||
| 20910b1562 | |||
| 5659a049e5 | |||
| 085812d859 | |||
| e60a4669ac | |||
| ac84907a98 | |||
| 04ee69e8e2 | |||
| a2445aca6a | |||
| 336a58b23d | |||
| 5a5cf9bd4d | |||
| 44d262f457 | |||
| 27fda43373 | |||
| b3de3b8405 | |||
| 633918aa40 | |||
| 3bc59e87d3 | |||
| af7d9e9dca | |||
| 3b8884dc84 | |||
| 92158684c0 | |||
| b37de9cf24 | |||
| cd2dff5559 | |||
| d6a85af15c | |||
| 0d333879ca | |||
| 905db3c73a | |||
| c5dfad8be8 | |||
| c9454a3832 | |||
| c732d475f1 | |||
| a4b7e0fd46 | |||
| bcee069b32 | |||
| 182208f8c3 | |||
| 24befed17b | |||
| 07991b2a0e | |||
| 51ea0c7a80 | |||
| 8b01428208 | |||
| f69e2617d4 | |||
| ddfdb47b93 | |||
| 14849f4211 | |||
| 067c599cb1 | |||
| 33ceaa38c0 | |||
| 0a0288c2ee | |||
| 90f888bc4b | |||
| 9bb34fb2a4 | |||
| 22b75a6b30 | |||
| 6af8410698 | |||
| f023c724fa | |||
| 6bbbe9cece | |||
| e6a4a04bb4 | |||
| 5c4a391d96 | |||
| a75b15d840 | |||
| 1322bd6326 | |||
| e2997c345c | |||
| 923a7e824f | |||
| 10ac0b5330 | |||
| bfeba9da40 | |||
| 752a865334 | |||
| 5eda771070 | |||
| 0826080f82 | |||
| 6fab785a49 | |||
| ecbcca5f14 | |||
| 861de56f10 | |||
| 3ce66e1201 | |||
| 7e8ea73363 | |||
| 9142525d21 | |||
| 62577ff1bb | |||
| 953556ccee | |||
| cc8621f304 | |||
| baae26d244 | |||
| cbf70cbeef | |||
| 1e28191e67 | |||
| ef4d9d38e7 | |||
| 52626406bf | |||
| fec6c77dc2 | |||
| 4db8ee7837 | |||
| 9bfde0e39d |
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>404: Redirecting...</title>
|
||||
<meta http-equiv = "refresh" content = "0; url = https://vera.hydev.org/" />
|
||||
</head>
|
||||
<body>
|
||||
404 Not Found! Redirecting to (<a href="https://vera.hydev.org">https://vera.hydev.org</a>)...
|
||||
<script>
|
||||
window.location.href = 'https://vera.hydev.org';
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+2
-1
@@ -3,7 +3,8 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<!--meta name="viewport" content="width=device-width,initial-scale=1.0"-->
|
||||
<meta name="viewport" content="width=1024">
|
||||
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title>Veracross Analyzer</title>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
|
||||
#app-content
|
||||
@@ -71,3 +72,14 @@ div.el-card.course-card > div.el-card__body
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// Non-selectable text
|
||||
.unselectable
|
||||
{
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
+23
-60
@@ -1,64 +1,16 @@
|
||||
import {Component, Vue} from 'vue-property-decorator';
|
||||
import Login from '@/components/login/login';
|
||||
import Navigation from '@/components/navigation/navigation';
|
||||
import Overall from '@/pages/overall/overall';
|
||||
import Overall from '@/pages/overall/overall.vue';
|
||||
import Constants from '@/constants';
|
||||
import JsonUtils from '@/utils/json-utils';
|
||||
import pWaitFor from 'p-wait-for';
|
||||
import {HttpUtils} from '@/utils/http-utils';
|
||||
import {CourseUtils} from '@/utils/course-utils';
|
||||
import {GPAUtils} from '@/utils/gpa-utils';
|
||||
import {HttpUtils} from '@/logic/utils/http-utils';
|
||||
import {GPAUtils} from '@/logic/utils/gpa-utils';
|
||||
import Loading from '@/components/loading/loading.vue';
|
||||
import CoursePage from '@/pages/course/course-page';
|
||||
import CoursePage from '@/pages/course/course-page.vue';
|
||||
import Course from '@/logic/course';
|
||||
|
||||
|
||||
/**
|
||||
* Objects of this interface represent assignment grades.
|
||||
*/
|
||||
export interface Assignment
|
||||
{
|
||||
id: number,
|
||||
scoreId: number,
|
||||
type: string,
|
||||
typeId: number,
|
||||
description: string,
|
||||
date: string,
|
||||
complete: string,
|
||||
include: boolean,
|
||||
display: boolean,
|
||||
|
||||
unread: boolean,
|
||||
|
||||
scoreMax: number,
|
||||
score: number
|
||||
}
|
||||
|
||||
/**
|
||||
* A course
|
||||
*/
|
||||
export interface Course
|
||||
{
|
||||
assignmentsId: number,
|
||||
id: number,
|
||||
name: string,
|
||||
teacherName: string,
|
||||
status: string,
|
||||
|
||||
letterGrade?: string,
|
||||
numericGrade?: number,
|
||||
|
||||
level: string,
|
||||
scaleUp: number,
|
||||
|
||||
grading:
|
||||
{
|
||||
method: string,
|
||||
weightingMap: {[index: string]: number}
|
||||
}
|
||||
|
||||
assignments: Assignment[]
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: {Login, Navigation, Overall, Loading, CoursePage},
|
||||
})
|
||||
@@ -144,7 +96,7 @@ export default class App extends Vue
|
||||
if (response.success)
|
||||
{
|
||||
// Save courses
|
||||
this.courses = response.data;
|
||||
this.courses = response.data.map((courseJson: any) => new Course(courseJson));
|
||||
|
||||
// Load assignments
|
||||
this.loadAssignments();
|
||||
@@ -171,9 +123,7 @@ export default class App extends Vue
|
||||
// Check success
|
||||
if (response.success)
|
||||
{
|
||||
// Load assignments
|
||||
// Parse json and filter it
|
||||
course.assignments = JsonUtils.filterAssignments(response.data);
|
||||
course.loadAssignments(response.data);
|
||||
}
|
||||
else throw new Error(response.data);
|
||||
})
|
||||
@@ -181,10 +131,10 @@ export default class App extends Vue
|
||||
});
|
||||
|
||||
// Wait for assignments to be ready.
|
||||
pWaitFor(() => this.courses.every(c => c.assignments != null)).then(() =>
|
||||
pWaitFor(() => this.courses.every(c => c.rawAssignments != null)).then(() =>
|
||||
{
|
||||
// Filter courses
|
||||
this.filteredCourses = CourseUtils.getGradedCourses(this.courses);
|
||||
this.filteredCourses = this.courses.filter(c => c.isGraded);
|
||||
|
||||
// Check grading algorithms
|
||||
this.checkGradingAlgorithms();
|
||||
@@ -202,8 +152,10 @@ export default class App extends Vue
|
||||
// Loop through all the courses
|
||||
for (const course of this.filteredCourses)
|
||||
{
|
||||
let termGrade = +GPAUtils.getTotalMeanAverage(course.computed.termAssignments[Constants.CURRENT_TERM]).toFixed(2);
|
||||
|
||||
// Check if total-average grade is the same with percent-type grade
|
||||
if (course.numericGrade == +GPAUtils.getTotalMeanAverage(course).toFixed(2))
|
||||
if (course.rawNumericGrade == termGrade)
|
||||
{
|
||||
course.grading = {method: 'TOTAL_MEAN', weightingMap: {}};
|
||||
}
|
||||
@@ -268,4 +220,15 @@ export default class App extends Vue
|
||||
// Refresh
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Select time (Eg. Term 1, Term 2, All Year, etc.)
|
||||
*
|
||||
* @param code
|
||||
*/
|
||||
public selectTime(code: number)
|
||||
{
|
||||
// TODO: Optimize
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<login v-if="showLogin" v-on:login:token="onLogin"></login>
|
||||
<navigation :courses="filteredCourses"
|
||||
:activeIndex.sync="selectedTab"
|
||||
v-on:sign-out="signOut">
|
||||
@sign-out="signOut" @select-time="selectTime">
|
||||
</navigation>
|
||||
|
||||
<div id="app-content" v-if="assignmentsReady && loading === ''">
|
||||
|
||||
@@ -34,11 +34,9 @@
|
||||
})
|
||||
export default class Loading extends Vue
|
||||
{
|
||||
// @ts-ignore
|
||||
@Prop() text: string;
|
||||
@Prop({required: true}) text: string;
|
||||
|
||||
// @ts-ignore
|
||||
@Prop() error: boolean;
|
||||
@Prop({required: true}) error: boolean;
|
||||
|
||||
getText()
|
||||
{
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import {Component, Vue} from 'vue-property-decorator';
|
||||
import Constants from '@/constants';
|
||||
import {HttpUtils} from '@/utils/http-utils';
|
||||
import App from '@/components/app/app';
|
||||
import VersionUtils from '@/utils/version-utils';
|
||||
import VersionUtils from '@/logic/utils/version-utils';
|
||||
|
||||
/**
|
||||
* This component handles user login, and obtains data from the server.
|
||||
* TODO: Press enter to login
|
||||
*/
|
||||
@Component({
|
||||
components: {},
|
||||
@@ -24,20 +22,27 @@ export default class Login extends Vue
|
||||
*/
|
||||
public created()
|
||||
{
|
||||
// Check cookies version
|
||||
if (this.needToUpdateCookies())
|
||||
{
|
||||
console.log('Version Updated! Clearing cookies...');
|
||||
|
||||
// Clear all cookies
|
||||
this.$cookies.keys().forEach(key => this.$cookies.remove(key));
|
||||
}
|
||||
|
||||
// Check login cookies
|
||||
if (this.$cookies.isKey('va.token'))
|
||||
{
|
||||
// Already contains valid token / TODO: Validate
|
||||
this.$emit('login:token', this.$cookies.get('va.token'));
|
||||
// Check cookies version
|
||||
if (this.needToUpdateCookies())
|
||||
{
|
||||
console.log('Version Updated! Clearing cookies...');
|
||||
|
||||
// Clear all cookies
|
||||
this.$cookies.keys().forEach(key => this.$cookies.remove(key));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Already contains valid token / TODO: Validate
|
||||
// TODO: Update token each access
|
||||
this.$emit('login:token', this.$cookies.get('va.token'));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log('Cookies doesn\'t exist');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +77,7 @@ export default class Login extends Vue
|
||||
{
|
||||
// Save token to cookies
|
||||
this.$cookies.set('va.token', response.data, '27d');
|
||||
this.$cookies.set('va.version', Constants.VERSION);
|
||||
this.$cookies.set('va.version', Constants.VERSION, '27d');
|
||||
|
||||
// Call custom event with token
|
||||
this.$emit('login:token', response.data);
|
||||
|
||||
@@ -5,23 +5,24 @@
|
||||
<img alt="Vue logo" src="../../assets/logo.png">
|
||||
|
||||
<h1>Veracross Analyzer</h1>
|
||||
<form id="login-form">
|
||||
<el-input v-model="username"
|
||||
placeholder="School Username"
|
||||
:class="{'input-error': error !== ''}"
|
||||
@keyup.enter.native="onEnter">
|
||||
</el-input>
|
||||
|
||||
<el-input v-model="username"
|
||||
placeholder="School Username"
|
||||
:class="{'input-error': error !== ''}"
|
||||
@keyup.enter.native="onEnter">
|
||||
</el-input>
|
||||
<el-input v-model="password"
|
||||
placeholder="Veracross Password"
|
||||
show-password=""
|
||||
:class="{'input-error': error !== ''}"
|
||||
@keyup.enter.native="onEnter">
|
||||
</el-input>
|
||||
|
||||
<el-input v-model="password"
|
||||
placeholder="Veracross Password"
|
||||
show-password=""
|
||||
:class="{'input-error': error !== ''}"
|
||||
@keyup.enter.native="onEnter">
|
||||
</el-input>
|
||||
<div class="el-form-item__error custom">{{error}}</div>
|
||||
|
||||
<div class="el-form-item__error custom">{{error}}</div>
|
||||
|
||||
<el-button plain type="primary" @click="onLoginClick" :loading="loading">Login</el-button>
|
||||
<el-button plain type="primary" @click="onLoginClick" :loading="loading">Login</el-button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,6 +27,17 @@
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
#nav-grading-period
|
||||
{
|
||||
// Float right
|
||||
position: absolute;
|
||||
right: 110px;
|
||||
|
||||
// Margins
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
#nav-title
|
||||
{
|
||||
// Float left
|
||||
@@ -40,11 +51,63 @@
|
||||
align-items: center;
|
||||
|
||||
// Margins
|
||||
margin-left: 14px;
|
||||
margin-left: 20px;
|
||||
margin-right: 8px;
|
||||
|
||||
// Font
|
||||
font-size: 1.25rem;
|
||||
font: bold 16px arial;
|
||||
display: inline-flex;
|
||||
|
||||
// Shadow effect
|
||||
-webkit-background-clip: text;
|
||||
background-color: #b1b1b1;
|
||||
color: transparent;
|
||||
text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.85);
|
||||
|
||||
// Make it non-clickable
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#next-course
|
||||
{
|
||||
// Down center
|
||||
width: 50%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 25%;
|
||||
padding-top: 2px;
|
||||
box-shadow: 0 -2px 9px 0 #ecebeb;
|
||||
}
|
||||
|
||||
footer
|
||||
{
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#prev-course
|
||||
{
|
||||
// Up center
|
||||
width: 50%;
|
||||
position: absolute;
|
||||
top: 61px;
|
||||
left: 25%;
|
||||
padding-bottom: 2px;
|
||||
box-shadow: 0 2px 9px 0 #ecebeb;
|
||||
}
|
||||
|
||||
.nav-course-operations
|
||||
{
|
||||
// Background
|
||||
background-color: rgba(214, 214, 214, 0.67);
|
||||
opacity: 0.85;
|
||||
|
||||
// Font
|
||||
font-size: 14px;
|
||||
color: #ab8585;
|
||||
|
||||
// Cursor
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import Constants from '@/constants';
|
||||
import {Course} from '@/components/app/app';
|
||||
import {CourseUtils} from '@/utils/course-utils';
|
||||
import App from '@/components/app/app';
|
||||
import {CourseUtils} from '@/logic/utils/course-utils';
|
||||
import {FormatUtils} from '@/logic/utils/format-utils';
|
||||
import pWaitFor from 'p-wait-for';
|
||||
import Course from '@/logic/course';
|
||||
|
||||
/**
|
||||
* This component is the top navigation bar
|
||||
@@ -11,23 +13,46 @@ import {CourseUtils} from '@/utils/course-utils';
|
||||
})
|
||||
export default class Navigation extends Vue
|
||||
{
|
||||
// @ts-ignore
|
||||
@Prop() activeIndex: string;
|
||||
@Prop({required: true}) activeIndex: string;
|
||||
|
||||
@Prop() courses: any;
|
||||
@Prop({required: true}) courses: Course[];
|
||||
|
||||
private gradingPeriod: string = 'All Year';
|
||||
|
||||
// Instance
|
||||
public static instance: Navigation;
|
||||
|
||||
/**
|
||||
* This is called when the instance is created.
|
||||
*/
|
||||
public created()
|
||||
{
|
||||
// Check selected time
|
||||
if (!this.$cookies.isKey('va.grading-period'))
|
||||
{
|
||||
this.$cookies.set('va.grading-period', this.gradingPeriod, '10y');
|
||||
}
|
||||
this.gradingPeriod = this.$cookies.get('va.grading-period');
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called when the instance is loaded.
|
||||
*/
|
||||
public mounted()
|
||||
{
|
||||
// Set instance
|
||||
Navigation.instance = this;
|
||||
|
||||
// Set history state
|
||||
let url = window.location.pathname;
|
||||
if (url == '/' || url == '') url = '/overall';
|
||||
let url = '/' + window.location.hash;
|
||||
if (url == '/' || url == '') url = '/#overall';
|
||||
window.history.replaceState({lastTab: url.substring(1)}, '', url);
|
||||
|
||||
// Update initial index
|
||||
this.updateIndex(url.substring(1));
|
||||
// Update initial index after loading is done
|
||||
pWaitFor(() => this.courses.length > 1 && App.instance.loading != '').then(() =>
|
||||
{
|
||||
this.updateIndex(url.substring(2), false);
|
||||
});
|
||||
|
||||
// Create history state listener
|
||||
window.onpopstate = e =>
|
||||
@@ -36,7 +61,7 @@ export default class Navigation extends Vue
|
||||
{
|
||||
// Restore previous tab
|
||||
console.log(`onPopState: Current: ${this.activeIndex}, Previous: ${e.state.lastTab}`);
|
||||
this.updateIndex(e.state.lastTab);
|
||||
this.updateIndex(e.state.lastTab, false);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -56,33 +81,106 @@ export default class Navigation extends Vue
|
||||
{
|
||||
// Update active index
|
||||
this.updateIndex(index);
|
||||
|
||||
// Debug output TODO: Remove this
|
||||
console.log(`onNavigate: Previous: ${this.activeIndex}, New: ${index}`);
|
||||
|
||||
// Check url
|
||||
let url = `/${index}`;
|
||||
|
||||
// Push history state
|
||||
window.history.pushState({lastTab: index}, '', url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update index
|
||||
*
|
||||
* @param newIndex New index
|
||||
* @param history Record in history or not (Default true)
|
||||
*/
|
||||
private updateIndex(newIndex: string)
|
||||
public updateIndex(newIndex: string, history?: boolean)
|
||||
{
|
||||
// Call custom event
|
||||
this.$emit('update:activeIndex', newIndex);
|
||||
|
||||
// Record or not
|
||||
if (history == null || history)
|
||||
{
|
||||
// Check url
|
||||
let url = `/#${newIndex}`;
|
||||
|
||||
// Push history state
|
||||
window.history.pushState({lastTab: newIndex}, '', url);
|
||||
}
|
||||
|
||||
// Update title
|
||||
document.title = 'Veracross Analyzer - ' + this.getTitle(newIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called when the sign out button is clicked.
|
||||
* Get title for index
|
||||
*
|
||||
* @param index Index
|
||||
*/
|
||||
public signOut()
|
||||
public getTitle(index: string)
|
||||
{
|
||||
// Call custom event
|
||||
this.$emit('sign-out');
|
||||
// Course
|
||||
if (index.startsWith('course'))
|
||||
{
|
||||
return this.findCourse(index.split('/')[1], 0).name;
|
||||
}
|
||||
|
||||
// Others
|
||||
return FormatUtils.toTitleCase(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move to the next course
|
||||
*
|
||||
* @param indexOffset Index offset (Eg. 1 for next)
|
||||
*/
|
||||
public nextCourse(indexOffset: number)
|
||||
{
|
||||
// Set tab to the next index
|
||||
this.updateIndex(CourseUtils.formatTabIndex(this.findNextCourse(indexOffset)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the next course
|
||||
*
|
||||
* @param indexOffset Index offset (Eg. 1 for next)
|
||||
*/
|
||||
public findNextCourse(indexOffset: number)
|
||||
{
|
||||
return this.findCourse(this.activeIndex.split('/')[1], indexOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find course
|
||||
*
|
||||
* @param courseId Course ID
|
||||
* @param indexOffset Index offset (Eg. 1 for next)
|
||||
*/
|
||||
public findCourse(courseId: string, indexOffset: number)
|
||||
{
|
||||
// Find current course index
|
||||
let courseIndex = this.courses.findIndex(c => c.id == +courseId);
|
||||
|
||||
// Find next course
|
||||
return this.courses[courseIndex + indexOffset];
|
||||
}
|
||||
|
||||
/**
|
||||
* Select grading period
|
||||
*
|
||||
* @param command Term 1, Term 2, All Year, etc.
|
||||
*/
|
||||
public selectGradingPeriod(command: string)
|
||||
{
|
||||
this.gradingPeriod = command;
|
||||
this.$cookies.set('va.grading-period', command, '10y');
|
||||
|
||||
// Call event
|
||||
this.$emit('select-time', this.getSelectedGradingPeriod());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get code for selected time
|
||||
*/
|
||||
public getSelectedGradingPeriod(): number
|
||||
{
|
||||
if (this.gradingPeriod == 'All Year') return -1;
|
||||
else return +this.gradingPeriod.replace('Term ', '') - 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
<el-menu style="margin-bottom: 10px;" class="centered" mode="horizontal"
|
||||
:default-active="activeIndex" @select="onSelect">
|
||||
|
||||
<!--div id="nav-title">
|
||||
<div id="nav-title">
|
||||
Veracross Analyzer
|
||||
</div-->
|
||||
</div>
|
||||
|
||||
<el-menu-item index="overall">Overall</el-menu-item>
|
||||
|
||||
@@ -16,9 +16,34 @@
|
||||
:key="course.name">{{course.name}}</el-menu-item>
|
||||
</el-submenu>
|
||||
|
||||
<el-button @click="signOut" id="sign-out-button" type="text">Sign Out</el-button>
|
||||
<!-- Grading period selection -->
|
||||
<el-dropdown id="nav-grading-period" @command="selectGradingPeriod">
|
||||
<el-button type="primary" size="medium">
|
||||
{{gradingPeriod}}<i class="el-icon-arrow-down el-icon--right"></i>
|
||||
</el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item command="Term 1">Term 1</el-dropdown-item>
|
||||
<el-dropdown-item command="Term 2">Term 2</el-dropdown-item>
|
||||
<el-dropdown-item command="Term 3" disabled>Term 3</el-dropdown-item>
|
||||
<el-dropdown-item command="Term 4" disabled>Term 4</el-dropdown-item>
|
||||
<el-dropdown-item command="All Year" divided>All Year</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
|
||||
<el-button @click="$emit('sign-out')" id="sign-out-button" type="text">Sign Out</el-button>
|
||||
</el-menu>
|
||||
<div class="line"></div>
|
||||
|
||||
<!-- Previous course / Next course (Only when the page is courses) -->
|
||||
<div v-if="activeIndex.includes('course') && findNextCourse(-1) != null"
|
||||
@click="nextCourse(-1)" id="prev-course" class="nav-course-operations unselectable">
|
||||
▲ PREVIOUS COURSE ▲
|
||||
</div>
|
||||
<footer>
|
||||
<div v-if="activeIndex.includes('course') && findNextCourse(1) != null"
|
||||
@click="nextCourse(1)" id="next-course" class="nav-course-operations unselectable">
|
||||
▼ NEXT COURSE ▼
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
+13
-2
@@ -7,7 +7,7 @@ export default class Constants
|
||||
public static API_URL: string = 'https://va.hydev.org/api';
|
||||
|
||||
/** Current version */
|
||||
public static VERSION: string = '0.3.5.697';
|
||||
public static VERSION: string = '0.4.3.1006';
|
||||
|
||||
/** Minimum version that still supports the same cookies */
|
||||
public static MIN_SUPPORTED_VERSION: string = '0.3.4.561';
|
||||
@@ -53,5 +53,16 @@ export default class Constants
|
||||
'#724e58',
|
||||
'#4b565b'
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// Terms (TODO: Actually get the terms dynamically
|
||||
public static TERMS =
|
||||
[
|
||||
new Date('Sep 04 2019'),
|
||||
new Date('Nov 03 2019'),
|
||||
new Date('Jan 19 2020'),
|
||||
new Date('Mar 22 2020'),
|
||||
new Date('Jun 05 2020'),
|
||||
];
|
||||
public static CURRENT_TERM = 1;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
import JsonUtils from '@/logic/utils/json-utils';
|
||||
import {FormatUtils} from '@/logic/utils/format-utils';
|
||||
import {CourseUtils} from '@/logic/utils/course-utils';
|
||||
import Navigation from '@/components/navigation/navigation';
|
||||
import {GPAUtils} from '@/logic/utils/gpa-utils';
|
||||
|
||||
/**
|
||||
* Objects of this interface represent assignment grades.
|
||||
*/
|
||||
export interface Assignment
|
||||
{
|
||||
id: number
|
||||
scoreId: number
|
||||
type: string
|
||||
typeId: number
|
||||
description: string
|
||||
time: number
|
||||
complete: string
|
||||
include: boolean
|
||||
display: boolean
|
||||
|
||||
unread: boolean
|
||||
|
||||
scoreMax: number
|
||||
score: number
|
||||
|
||||
gradingPeriod: number
|
||||
}
|
||||
|
||||
export interface AssignmentType
|
||||
{
|
||||
id: number
|
||||
name: string
|
||||
|
||||
weight: number
|
||||
scoreMax: number
|
||||
score: number
|
||||
percent: number
|
||||
}
|
||||
|
||||
export default class Course
|
||||
{
|
||||
id: number;
|
||||
assignmentsId: number;
|
||||
name: string;
|
||||
teacherName: string;
|
||||
status: string;
|
||||
rawAssignments: Assignment[];
|
||||
|
||||
rawLetterGrade?: string;
|
||||
rawNumericGrade?: number;
|
||||
|
||||
level: string;
|
||||
scaleUp: number;
|
||||
|
||||
grading:
|
||||
{
|
||||
method: string
|
||||
weightingMap: {[index: string]: number}
|
||||
};
|
||||
|
||||
computed:
|
||||
{
|
||||
termAssignments: Assignment[][]
|
||||
allYearGrade: number
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct a course with a course json object
|
||||
*
|
||||
* @param courseJson Course json object
|
||||
*/
|
||||
constructor(courseJson: any)
|
||||
{
|
||||
this.id = courseJson.id;
|
||||
this.assignmentsId = courseJson.assignmentsId;
|
||||
this.name = FormatUtils.parseText(courseJson.name).trim();
|
||||
this.teacherName = courseJson.teacherName;
|
||||
this.status = courseJson.status;
|
||||
|
||||
this.rawLetterGrade = courseJson.letterGrade;
|
||||
this.rawNumericGrade = courseJson.numericGrade;
|
||||
|
||||
// Other api issue
|
||||
if (this.rawLetterGrade == '')
|
||||
{
|
||||
this.rawNumericGrade = undefined;
|
||||
this.rawLetterGrade = undefined;
|
||||
}
|
||||
|
||||
// Level and scaleUp
|
||||
let level = CourseUtils.detectLevel(this.name);
|
||||
if (level != undefined)
|
||||
{
|
||||
this.level = level.level;
|
||||
this.scaleUp = level.scaleUp;
|
||||
}
|
||||
else this.level = 'Unknown';
|
||||
|
||||
this.grading = courseJson.grading;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load in assignments data
|
||||
*
|
||||
* @param data Assignments data
|
||||
*/
|
||||
loadAssignments(data: any)
|
||||
{
|
||||
// Load assignments
|
||||
// Parse json and filter it
|
||||
this.rawAssignments = JsonUtils.filterAssignments(data);
|
||||
|
||||
// Sort by date (Latest is at 0)
|
||||
this.rawAssignments.sort((a, b) => b.time - a.time);
|
||||
|
||||
// Filter assignments into terms
|
||||
let termAssignments: Assignment[][] = [[], [], [], []];
|
||||
let currentTerm = 0;
|
||||
|
||||
// Loop through it by time order
|
||||
this.rawAssignments.forEach(a => termAssignments[a.gradingPeriod].push(a));
|
||||
|
||||
// Set computed data
|
||||
this.computed = {termAssignments: termAssignments, allYearGrade: -1};
|
||||
}
|
||||
|
||||
/**
|
||||
* Is graded or not
|
||||
*/
|
||||
get isGraded(): boolean
|
||||
{
|
||||
// Skip future or past courses
|
||||
if (this.status != 'active') return false;
|
||||
|
||||
// Skip courses without levels TODO: Ask for user input
|
||||
if (this.level == 'None' || this.level == 'Unknown' || this.scaleUp == -1) return false;
|
||||
|
||||
// Skip courses without graded assignments
|
||||
if (this.assignments.length == 0) return false;
|
||||
|
||||
// Skip if there are no grading scale
|
||||
// if (course.grading.method == 'NOT_GRADED') return;
|
||||
|
||||
// Is graded
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get assignments of the selected time
|
||||
*/
|
||||
get rawSelectedAssignments(): Assignment[]
|
||||
{
|
||||
let timeCode = Navigation.instance.getSelectedGradingPeriod();
|
||||
|
||||
// All year
|
||||
if (timeCode == -1)
|
||||
{
|
||||
return this.rawAssignments;
|
||||
}
|
||||
|
||||
// Specific time
|
||||
return this.computed.termAssignments[timeCode];
|
||||
}
|
||||
|
||||
private _cacheAssignments: Assignment[];
|
||||
|
||||
/**
|
||||
* Get graded assignments
|
||||
*/
|
||||
get assignments(): Assignment[]
|
||||
{
|
||||
if (this._cacheAssignments == null)
|
||||
this._cacheAssignments = this.rawSelectedAssignments.filter(a => a.complete == 'Complete');
|
||||
|
||||
return this._cacheAssignments;
|
||||
}
|
||||
|
||||
private _cacheLetterGrade: string;
|
||||
|
||||
/**
|
||||
* Get letter grade
|
||||
*/
|
||||
get letterGrade(): string
|
||||
{
|
||||
if (this._cacheLetterGrade == null)
|
||||
{
|
||||
// Get scale
|
||||
let scale = GPAUtils.findScale(this.numericGrade);
|
||||
|
||||
// Scale not found
|
||||
if (scale == undefined) return this._cacheLetterGrade = '--';
|
||||
|
||||
// Cache
|
||||
this._cacheLetterGrade = scale.letter;
|
||||
}
|
||||
|
||||
return this._cacheLetterGrade;
|
||||
}
|
||||
|
||||
private _cacheNumericGrade: number;
|
||||
|
||||
/**
|
||||
* Get numeric grade
|
||||
*/
|
||||
get numericGrade(): number
|
||||
{
|
||||
// Cached
|
||||
if (this._cacheNumericGrade == null)
|
||||
{
|
||||
// Calculate
|
||||
if (this.grading.method == 'PERCENT_TYPE')
|
||||
{
|
||||
this._cacheNumericGrade = GPAUtils.getPercentTypeAverage(this, this.assignments);
|
||||
}
|
||||
else if (this.grading.method == 'TOTAL_MEAN')
|
||||
{
|
||||
this._cacheNumericGrade = GPAUtils.getTotalMeanAverage(this.assignments);
|
||||
}
|
||||
else this._cacheNumericGrade = -1;
|
||||
}
|
||||
|
||||
return this._cacheNumericGrade;
|
||||
}
|
||||
|
||||
private _cacheAssignmentTypes: AssignmentType[];
|
||||
|
||||
/**
|
||||
* Get assignment types
|
||||
*/
|
||||
get assignmentTypes(): AssignmentType[]
|
||||
{
|
||||
if (this._cacheAssignmentTypes == null)
|
||||
{
|
||||
// Get all types
|
||||
let types = this.assignments.map(a => a.type);
|
||||
|
||||
// Remove duplicates
|
||||
types = types.filter((type, i, a) => a.indexOf(type) == i);
|
||||
|
||||
// Get total possible score for weight calculation
|
||||
let totalScoreMax = this.assignments.reduce((sum, a) => sum + a.scoreMax, 0);
|
||||
|
||||
// For every type...
|
||||
this._cacheAssignmentTypes = types.map(type =>
|
||||
{
|
||||
// Get assignments of the type
|
||||
let typeAssignments = this.assignments.filter(a => a.type == type);
|
||||
|
||||
// Count scores and max scores
|
||||
let score = typeAssignments.reduce((sum, a) => sum + a.score, 0);
|
||||
let scoreMax = typeAssignments.reduce((sum, a) => sum + a.scoreMax, 0);
|
||||
|
||||
// Calculate weight
|
||||
let weight = this.grading.method == 'PERCENT_TYPE'
|
||||
? this.grading.weightingMap[type] : scoreMax / totalScoreMax;
|
||||
|
||||
// Return
|
||||
return {name: type, id: typeAssignments[0].typeId, weight: weight,
|
||||
scoreMax: scoreMax, score: score, percent: +(score / scoreMax * 100).toFixed(2)}
|
||||
})
|
||||
}
|
||||
|
||||
return this._cacheAssignmentTypes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import Course from '@/logic/course';
|
||||
import Navigation from '@/components/navigation/navigation';
|
||||
import Constants from '@/constants';
|
||||
|
||||
const LEVEL_AP = {level: 'AP', scaleUp: 1};
|
||||
const LEVEL_H = {level: 'H', scaleUp: 0.75};
|
||||
const LEVEL_A = {level: 'A', scaleUp: 0.5};
|
||||
const LEVEL_CP = {level: 'CP', scaleUp: 0.25};
|
||||
const LEVEL_CLUB = {level: 'Club', scaleUp: -1};
|
||||
|
||||
const UNKNOWN_COURSE_LIST = new Map();
|
||||
UNKNOWN_COURSE_LIST.set('Piano Masterclass', LEVEL_H);
|
||||
UNKNOWN_COURSE_LIST.set('Multivariable Calculus with Differential Equations', LEVEL_H);
|
||||
UNKNOWN_COURSE_LIST.set('Introduction to Algorithmic Thinking and Computational Technologies', LEVEL_A);
|
||||
UNKNOWN_COURSE_LIST.set('Ceramics 1', LEVEL_CP);
|
||||
UNKNOWN_COURSE_LIST.set('Ceramics 2', LEVEL_A);
|
||||
UNKNOWN_COURSE_LIST.set('Sculpture', LEVEL_CP);
|
||||
UNKNOWN_COURSE_LIST.set('Drawing', LEVEL_CP);
|
||||
UNKNOWN_COURSE_LIST.set('Painting', LEVEL_CP);
|
||||
|
||||
export class CourseUtils
|
||||
{
|
||||
/**
|
||||
* Format course to tab index string
|
||||
*
|
||||
* @param course Course object
|
||||
* @return string Tab index
|
||||
*/
|
||||
public static formatTabIndex(course: Course): string
|
||||
{
|
||||
return `course/${course.id}/${course.name.toLowerCase().split(' ').join('-')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect course level based on course name
|
||||
*
|
||||
* @param name Course name
|
||||
*/
|
||||
static detectLevel(name: string)
|
||||
{
|
||||
// Common ones
|
||||
if (name.startsWith('AP')) return LEVEL_AP;
|
||||
if (name.endsWith(' H')) return LEVEL_H;
|
||||
if (name.endsWith(' A')) return LEVEL_A;
|
||||
if (name.endsWith(' CP')) return LEVEL_CP;
|
||||
if (name.startsWith('HS ')) return LEVEL_CLUB;
|
||||
if (name.startsWith('MS ')) return LEVEL_CLUB;
|
||||
|
||||
// Uncommon ones
|
||||
let lower = name.toLowerCase();
|
||||
|
||||
if (name.startsWith('Pre-AP')) return LEVEL_AP;
|
||||
if (lower.endsWith(' acc')) return LEVEL_A;
|
||||
if (name.endsWith('H')) return LEVEL_H;
|
||||
if (name.endsWith('A')) return LEVEL_A;
|
||||
if (name.endsWith('CP')) return LEVEL_CP;
|
||||
|
||||
// Even more uncommon
|
||||
if (lower.includes('honors')) return LEVEL_H;
|
||||
if (lower.includes('accelerated')) return LEVEL_A;
|
||||
if (name.includes('Advanced')) return LEVEL_A;
|
||||
|
||||
// Unknown course list
|
||||
if (UNKNOWN_COURSE_LIST.has(name)) return UNKNOWN_COURSE_LIST.get(name);
|
||||
|
||||
// Really unknown
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the begin date of the selected term
|
||||
*/
|
||||
static getTermBeginDate()
|
||||
{
|
||||
let selected = Navigation.instance.getSelectedGradingPeriod();
|
||||
|
||||
return selected == -1 ? Constants.TERMS[0] : Constants.TERMS[selected];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the end date of the selected term
|
||||
*/
|
||||
static getTermEndDate()
|
||||
{
|
||||
let selected = Navigation.instance.getSelectedGradingPeriod();
|
||||
|
||||
return selected == -1 ? Constants.TERMS[3] : Constants.TERMS[selected + 1];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import moment from 'moment';
|
||||
|
||||
export class FormatUtils
|
||||
{
|
||||
/**
|
||||
* Limit string length
|
||||
*
|
||||
* @param str String
|
||||
* @param length Max length
|
||||
*/
|
||||
public static limit(str: string, length: number): string
|
||||
{
|
||||
return str.length <= length ? str : str.substr(0, length - 2) + '...'
|
||||
}
|
||||
|
||||
/**
|
||||
* To Title Case
|
||||
*
|
||||
* @param str oRigInAL sTrING
|
||||
* @return string Original String
|
||||
*/
|
||||
public static toTitleCase(str: string)
|
||||
{
|
||||
return str.replace(/\w\S*/g, s => s.charAt(0).toUpperCase() + s.substr(1).toLowerCase())
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse html text
|
||||
*
|
||||
* @param str
|
||||
*/
|
||||
public static parseText(str: string): string
|
||||
{
|
||||
return str.replace(/&/g, '&');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
import Course, {Assignment} from '@/logic/course';
|
||||
|
||||
export interface Scale
|
||||
{
|
||||
min: number
|
||||
letter: string
|
||||
gp: number
|
||||
}
|
||||
|
||||
/**
|
||||
* This is an utility class to calculate GPA.
|
||||
*/
|
||||
export class GPAUtils
|
||||
{
|
||||
// [[Min score, Letter grade, Base GPA], ...]
|
||||
public static SCALE: Scale[] =
|
||||
[
|
||||
{min: 96.5, letter: 'A+', gp: 4.00},
|
||||
{min: 92.5, letter: 'A' , gp: 3.75},
|
||||
{min: 89.5, letter: 'A-', gp: 3.50},
|
||||
{min: 86.5, letter: 'B+', gp: 3.25},
|
||||
{min: 82.5, letter: 'B' , gp: 3.00},
|
||||
{min: 79.5, letter: 'B-', gp: 2.75},
|
||||
{min: 76.5, letter: 'C+', gp: 2.50},
|
||||
{min: 72.5, letter: 'C' , gp: 2.25},
|
||||
{min: 70.5, letter: 'C-', gp: 2.00},
|
||||
{min: 69.5, letter: 'D' , gp: 1.00},
|
||||
{min: 0 , letter: 'F' , gp: 0.00}
|
||||
];
|
||||
|
||||
/**
|
||||
* Calculate GPA for a list of couses
|
||||
*
|
||||
* @param coursesOriginal List of courses
|
||||
*/
|
||||
public static getGPA(coursesOriginal: Course[]): {gpa: number, accurate: boolean, max: number}
|
||||
{
|
||||
// Clone array
|
||||
let courses: Course[] = [];
|
||||
|
||||
// Accurate or not
|
||||
let accurate: boolean = true;
|
||||
|
||||
// Remove all courses that does not have a grade
|
||||
coursesOriginal.forEach(course =>
|
||||
{
|
||||
if (course.letterGrade == null || course.letterGrade == '')
|
||||
{
|
||||
accurate = false;
|
||||
}
|
||||
else if (course.level != 'none')
|
||||
{
|
||||
courses.push(course);
|
||||
}
|
||||
});
|
||||
|
||||
// If no course have grade, return -1
|
||||
if (courses.length == 0)
|
||||
{
|
||||
return {gpa: -1, accurate: false, max: -1};
|
||||
}
|
||||
|
||||
// Count total GPA
|
||||
let totalGPA = 0;
|
||||
let maxTotal = 0;
|
||||
courses.forEach(course =>
|
||||
{
|
||||
totalGPA += this.getGP(course, course.letterGrade);
|
||||
maxTotal += this.getGP(course, 'A+');
|
||||
});
|
||||
|
||||
// Get average GPA, round to two decimal places
|
||||
let gpa = Math.round(totalGPA / courses.length * 100) / 100;
|
||||
let maxGPA = Math.round(maxTotal / courses.length * 100) / 100;
|
||||
|
||||
// Return results
|
||||
return {gpa: gpa, accurate: accurate, max: maxGPA};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate GPA for a course
|
||||
*
|
||||
* @param course Course
|
||||
* @param letterGrade Letter grade
|
||||
*/
|
||||
public static getGP(course: Course, letterGrade: string): number
|
||||
{
|
||||
// Get scale
|
||||
let scale = this.findScale(letterGrade);
|
||||
|
||||
// No scale
|
||||
if (scale == undefined) return -1;
|
||||
|
||||
// Add scaleUp if not failed.
|
||||
return scale.gp == 0 ? 0 : scale.gp + course.scaleUp;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the scale for a grade
|
||||
*
|
||||
* @param grade Letter grade or numeric grade
|
||||
*/
|
||||
public static findScale(grade: string | number): Scale | undefined
|
||||
{
|
||||
// Letter grade
|
||||
if (typeof grade == 'string')
|
||||
{
|
||||
return this.SCALE.find(scale => scale.letter == grade);
|
||||
}
|
||||
|
||||
// Numeric grade
|
||||
return this.SCALE.find(scale => grade >= scale.min);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the total-mean (total/max) average
|
||||
*
|
||||
* @param assignments
|
||||
*/
|
||||
public static getTotalMeanAverage(assignments: Assignment[])
|
||||
{
|
||||
let score = 0;
|
||||
let max = 0;
|
||||
|
||||
// Loop through assignments
|
||||
assignments.forEach(assignment =>
|
||||
{
|
||||
// If assignment should be displayed
|
||||
if (assignment.complete != 'Complete') return;
|
||||
|
||||
// Record scores
|
||||
score += assignment.score;
|
||||
max += assignment.scoreMax;
|
||||
});
|
||||
|
||||
// Return
|
||||
return +(score / max * 100).toFixed(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the percent type
|
||||
* TODO: Combine it with overall-line
|
||||
*
|
||||
* @param course
|
||||
* @param assignments
|
||||
*/
|
||||
public static getPercentTypeAverage(course: Course, assignments: Assignment[])
|
||||
{
|
||||
let typeScores: {[index: string]: any} = {};
|
||||
let typeCounts: {[index: string]: any} = {};
|
||||
|
||||
// Loop through assignments
|
||||
assignments.forEach(assignment =>
|
||||
{
|
||||
// If assignment should be displayed
|
||||
if (assignment.complete != 'Complete') return;
|
||||
|
||||
// Record scores
|
||||
if (typeScores[assignment.type] == undefined) typeScores[assignment.type] = 0;
|
||||
typeScores[assignment.type] += assignment.score / assignment.scoreMax;
|
||||
|
||||
if (typeCounts[assignment.type] == undefined) typeCounts[assignment.type] = 0;
|
||||
typeCounts[assignment.type] ++;
|
||||
});
|
||||
|
||||
// Count total percentage (This is to avoid less than expected cases)
|
||||
// Eg. If HW = 25% and Quiz = 75%, I have 1 hw and 0 quiz
|
||||
// Without total percentage, the avg grade I get is 25%.
|
||||
let totalPercentage = 0;
|
||||
for (let type in course.grading.weightingMap)
|
||||
{
|
||||
if (typeScores[type] != undefined)
|
||||
{
|
||||
totalPercentage += course.grading.weightingMap[type];
|
||||
}
|
||||
}
|
||||
|
||||
// Count
|
||||
let score = 0;
|
||||
for (let type in typeScores)
|
||||
{
|
||||
let typeFactor = course.grading.weightingMap[type] / totalPercentage;
|
||||
score += typeScores[type] * typeFactor / typeCounts[type];
|
||||
}
|
||||
|
||||
// Add average to the row
|
||||
return +(score * 100).toFixed(2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import Constants from '@/constants';
|
||||
|
||||
export default class GraphUtils
|
||||
{
|
||||
static DOT = '<span style="display:inline-block;margin-right:5px;border-radius:10px;width:9px;height:9px;background-color:{color}"></span>';
|
||||
|
||||
/**
|
||||
* Base settings
|
||||
*
|
||||
* @param title
|
||||
* @param subtitle
|
||||
*/
|
||||
static getBaseSettings(title?: String, subtitle?: String)
|
||||
{
|
||||
return {
|
||||
// Color
|
||||
color: Constants.THEME.colors,
|
||||
|
||||
// Title
|
||||
title:
|
||||
{
|
||||
show: title != null,
|
||||
textStyle:
|
||||
{
|
||||
fontSize: 13
|
||||
},
|
||||
text: title,
|
||||
subtext: subtitle,
|
||||
x: 'center'
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get term mark lines
|
||||
*/
|
||||
static getTermLines()
|
||||
{
|
||||
return {
|
||||
silent: true,
|
||||
symbol: 'none',
|
||||
lineStyle: {color: Constants.THEME.colors[2]},
|
||||
animationDuration: 500,
|
||||
data: Constants.TERMS.map((term, index) =>
|
||||
{
|
||||
return {xAxis: term.getTime(), label: {formatter: `Term ${index + 1}`}}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mark areas for percentage scores
|
||||
*/
|
||||
static getGradeMarkAreas(opacity: number)
|
||||
{
|
||||
return {
|
||||
silent: true,
|
||||
data:
|
||||
[
|
||||
// Above 100
|
||||
[{itemStyle: {color: 'rgba(230,253,255)', opacity: opacity}, yAxis: 120}, {yAxis: 100}],
|
||||
// 90 to 100
|
||||
[{itemStyle: {color: 'rgba(241,255,237)', opacity: opacity}, yAxis: 100}, {yAxis: 90}],
|
||||
// 80 to 90
|
||||
[{itemStyle: {color: 'rgba(255,250,216)', opacity: opacity}, yAxis: 90}, {yAxis: 80}],
|
||||
// 70 to 80
|
||||
[{itemStyle: {color: 'rgba(255,225,199)', opacity: opacity}, yAxis: 80}, {yAxis: 70}],
|
||||
// Below 70 (Fail)
|
||||
[{itemStyle: {color: 'rgb(255,190,184)', opacity: opacity}, yAxis: 70}, {yAxis: -100}]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Assignment} from '@/components/app/app';
|
||||
import {Assignment} from '@/logic/course';
|
||||
|
||||
export default class JsonUtils
|
||||
{
|
||||
@@ -10,18 +10,15 @@ export default class JsonUtils
|
||||
*/
|
||||
public static filterAssignments(assignments: any): Assignment[]
|
||||
{
|
||||
let result: Assignment[] = [];
|
||||
|
||||
assignments.assignments.forEach((assignment: any) =>
|
||||
return assignments.assignments.map((assignment: any) =>
|
||||
{
|
||||
result.push(
|
||||
{
|
||||
return {
|
||||
id: assignment.assignment_id,
|
||||
scoreId: assignment.score_id,
|
||||
type: assignment.assignment_type,
|
||||
typeId: assignment.assignment_type_id,
|
||||
description: assignment.assignment_description,
|
||||
date: assignment._date,
|
||||
time: new Date(assignment._date).getTime(),
|
||||
complete: assignment.completion_status,
|
||||
include: assignment.include_in_calculated_grade == 1,
|
||||
display: assignment.display_grade == 1,
|
||||
@@ -29,10 +26,10 @@ export default class JsonUtils
|
||||
unread: assignment.is_unread == 1,
|
||||
|
||||
scoreMax: assignment.maximum_score,
|
||||
score: +assignment.raw_score
|
||||
});
|
||||
});
|
||||
score: +assignment.raw_score,
|
||||
|
||||
return result;
|
||||
gradingPeriod: +assignment.grading_period.replace('Quarter ', '') - 1
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
import Vue from 'vue';
|
||||
import ElementUI from 'element-ui';
|
||||
const VCharts = require('v-charts');
|
||||
|
||||
import App from './components/app/app.vue';
|
||||
import VueCookies from 'vue-cookies';
|
||||
|
||||
const VCharts = require('v-charts');
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
// Use Element UI
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div id="assignment-type-head">
|
||||
<el-card :body-style="{padding: '0px'}">
|
||||
<div id="type-info-card">
|
||||
<span id="type-name">{{type.name}}</span>
|
||||
<span id="type-average">Average: {{type.percent}}%</span>
|
||||
</div>
|
||||
|
||||
<AssignmentEntry v-for="assignment of filteredAssignments" :key="assignment.id"
|
||||
:assignment="assignment" :unread="false"
|
||||
backgroundColor="#ffffff" narrow="true">
|
||||
</AssignmentEntry>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import AssignmentEntry from '@/pages/overall/overall-course/assignment-entry/assignment-entry.vue';
|
||||
import {Assignment, AssignmentType} from '@/logic/course';
|
||||
|
||||
@Component({
|
||||
components: {AssignmentEntry}
|
||||
})
|
||||
export default class AssignmentTypeHead extends Vue
|
||||
{
|
||||
@Prop({required: true}) type: AssignmentType;
|
||||
@Prop({required: true}) assignments: Assignment[];
|
||||
|
||||
get filteredAssignments()
|
||||
{
|
||||
// Filter assignments to only this type
|
||||
return this.assignments.filter(a => a.typeId == this.type.id);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#type-info-card
|
||||
{
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
#type-name
|
||||
{
|
||||
// Font
|
||||
font-size: 22px;
|
||||
color: var(--main);
|
||||
|
||||
// Center
|
||||
height: 60px;
|
||||
line-height: 60px;
|
||||
|
||||
// Alignment
|
||||
padding-left: 20px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
#type-average
|
||||
{
|
||||
// Font
|
||||
font-size: 14px;
|
||||
color: #8db3e4;
|
||||
|
||||
// Center
|
||||
height: 60px;
|
||||
line-height: 64px;
|
||||
|
||||
// Alignment
|
||||
float: left;
|
||||
margin-left: 15px;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
@@ -1,29 +0,0 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import {Assignment, Course} from '@/components/app/app';
|
||||
import CourseHead from '@/pages/overall/overall-course/course-head/course-head.vue';
|
||||
import CourseScatter from '@/pages/course/course-scatter/course-scatter';
|
||||
|
||||
@Component({
|
||||
components: {CourseHead, CourseScatter}
|
||||
})
|
||||
export default class CoursePage extends Vue
|
||||
{
|
||||
// @ts-ignore
|
||||
@Prop({required: true}) course: Course;
|
||||
|
||||
private unread: number = -1;
|
||||
private unreadAssignments: Assignment[] = [];
|
||||
|
||||
/**
|
||||
* Count the number of unread assignments with cache
|
||||
*/
|
||||
countUnread(): number
|
||||
{
|
||||
if (this.unread == -1)
|
||||
{
|
||||
this.unreadAssignments = this.course.assignments.filter(a => a.unread);
|
||||
return this.unread = this.unreadAssignments.length;
|
||||
}
|
||||
else return this.unread;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<el-card id="course-card" class="course-card">
|
||||
<course-head :course="course" :unread="countUnread()"></course-head>
|
||||
<course-head :clickable="false" :course="course" :unread="countUnread()"></course-head>
|
||||
|
||||
<div class="course-card-content expand">
|
||||
<el-row>
|
||||
@@ -13,9 +13,58 @@
|
||||
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-card class="large overall-line-card vertical-center" body-style="padding: 0">
|
||||
<CourseTypeRadar :course="course"></CourseTypeRadar>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
Hi
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<AssignmentTypeHead v-for="type in course.assignmentTypes" :key="type.id"
|
||||
:type="type" :assignments="course.assignments">
|
||||
</AssignmentTypeHead>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script src="./course-page.ts" lang="ts"></script>
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import CourseHead from '@/pages/overall/overall-course/course-head/course-head.vue';
|
||||
import CourseScatter from '@/pages/course/course-scatter/course-scatter';
|
||||
import AssignmentEntry from '@/pages/overall/overall-course/assignment-entry/assignment-entry.vue';
|
||||
import AssignmentTypeHead from '@/pages/course/assignment-type-head/assignment-type-head.vue';
|
||||
import Course, {Assignment} from '@/logic/course';
|
||||
import TypeRadar from '@/pages/course/course-type-radar/type-radar';
|
||||
|
||||
@Component({
|
||||
components: {CourseTypeRadar: TypeRadar, AssignmentEntry, CourseHead, CourseScatter, AssignmentTypeHead}
|
||||
})
|
||||
export default class CoursePage extends Vue
|
||||
{
|
||||
@Prop({required: true}) course: Course;
|
||||
|
||||
private unread: number = -1;
|
||||
private unreadAssignments: Assignment[] = [];
|
||||
|
||||
/**
|
||||
* Count the number of unread assignments with cache
|
||||
*/
|
||||
countUnread(): number
|
||||
{
|
||||
if (this.unread == -1)
|
||||
{
|
||||
this.unreadAssignments = this.course.assignments.filter(a => a.unread);
|
||||
return this.unread = this.unreadAssignments.length;
|
||||
}
|
||||
else return this.unread;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style src="./course-page.scss" lang="scss" scoped></style>
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import {Assignment, Course} from '@/components/app/app';
|
||||
import {GPAUtils} from '@/utils/gpa-utils';
|
||||
import Constants from '@/constants';
|
||||
import {FormatUtils} from '@/utils/format-utils';
|
||||
import {FormatUtils} from '@/logic/utils/format-utils';
|
||||
import moment from 'moment';
|
||||
import Course, {Assignment} from '@/logic/course';
|
||||
import GraphUtils from '@/logic/utils/graph-utils';
|
||||
|
||||
@Component({
|
||||
})
|
||||
export default class CourseScatter extends Vue
|
||||
{
|
||||
// @ts-ignore
|
||||
@Prop({required: true}) course: Course;
|
||||
|
||||
/**
|
||||
@@ -27,39 +26,11 @@ export default class CourseScatter extends Vue
|
||||
*/
|
||||
get chartSettings()
|
||||
{
|
||||
// Map assignments
|
||||
let map = this.mapAssignments();
|
||||
|
||||
let itemStyle =
|
||||
{
|
||||
normal:
|
||||
{
|
||||
opacity: 0.8,
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.2)'
|
||||
}
|
||||
};
|
||||
|
||||
// Create settings
|
||||
let settings =
|
||||
{
|
||||
// Color
|
||||
color: Constants.THEME.colors,
|
||||
|
||||
// Title
|
||||
title:
|
||||
{
|
||||
show: true,
|
||||
textStyle:
|
||||
{
|
||||
fontSize: 13
|
||||
},
|
||||
text: 'Assignments',
|
||||
subtext: 'Assignment scores for ' + this.course.name,
|
||||
x: 'center'
|
||||
},
|
||||
// Base settings
|
||||
...GraphUtils.getBaseSettings('Assignments', 'Assignment scores for ' + this.course.name),
|
||||
|
||||
// X axis represents course names
|
||||
xAxis:
|
||||
@@ -69,7 +40,7 @@ export default class CourseScatter extends Vue
|
||||
{
|
||||
formatter: (name: any) => moment(name).format('MMM DD')
|
||||
},
|
||||
max: FormatUtils.toChartDate(new Date())
|
||||
max: new Date().getTime()
|
||||
},
|
||||
|
||||
// Y axis represents GPAs and MaxGPAs
|
||||
@@ -83,8 +54,8 @@ export default class CourseScatter extends Vue
|
||||
{
|
||||
formatter: (name: any) => name + '%'
|
||||
},
|
||||
max: 100,
|
||||
min: (value: any) => Math.floor(value.min) - 5
|
||||
min: (value: any) => Math.floor(value.min) - 5,
|
||||
max: (value: any) => Math.min(Math.ceil(value.max), 110)
|
||||
},
|
||||
|
||||
// Tooltip
|
||||
@@ -94,9 +65,13 @@ export default class CourseScatter extends Vue
|
||||
axisPointer:
|
||||
{
|
||||
type: 'cross'
|
||||
}
|
||||
},
|
||||
formatter: (ps: any[]) => moment(ps[0].data[0]).format('MMM DD, YYYY') + '<br>' + ps.map(p =>
|
||||
`${GraphUtils.DOT.replace('{color}', p.color)}
|
||||
${FormatUtils.limit(p.data[2], 22)}: ${p.data[1]}%<br>`).join('')
|
||||
},
|
||||
|
||||
// Legend
|
||||
legend:
|
||||
{
|
||||
bottom: 24,
|
||||
@@ -109,20 +84,53 @@ export default class CourseScatter extends Vue
|
||||
},
|
||||
|
||||
// Data
|
||||
series: Array.from(map, ([type, assignments]) =>
|
||||
{
|
||||
return {
|
||||
type: 'scatter',
|
||||
name: type,
|
||||
data: CourseScatter.assignmentsData(assignments),
|
||||
itemStyle: itemStyle
|
||||
}
|
||||
})
|
||||
series: this.series()
|
||||
};
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get series data
|
||||
*/
|
||||
private series()
|
||||
{
|
||||
// Scatter data point style
|
||||
let itemStyle =
|
||||
{
|
||||
normal:
|
||||
{
|
||||
opacity: 0.8,
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.2)'
|
||||
}
|
||||
};
|
||||
|
||||
// Create scatter plots
|
||||
let map = this.mapAssignments();
|
||||
let series: any[] = Array.from(map, ([type, assignments]) =>
|
||||
{
|
||||
return {
|
||||
type: 'scatter',
|
||||
name: type,
|
||||
data: CourseScatter.assignmentsData(assignments),
|
||||
itemStyle: itemStyle
|
||||
}
|
||||
});
|
||||
|
||||
// Push other stuff
|
||||
series.push(
|
||||
{
|
||||
type: 'line',
|
||||
markLine: GraphUtils.getTermLines(),
|
||||
markArea: GraphUtils.getGradeMarkAreas(0.4)
|
||||
});
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map assignments to {assignmentType, [assignment]} format.
|
||||
*/
|
||||
@@ -152,6 +160,6 @@ export default class CourseScatter extends Vue
|
||||
private static assignmentsData(assignments: Assignment[])
|
||||
{
|
||||
return assignments.filter(a => a.complete == 'Complete')
|
||||
.map(a => [FormatUtils.toChartDate(a.date), (a.score / a.scoreMax * 100).toFixed(2)]);
|
||||
.map(a => [a.time, (a.score / a.scoreMax * 100).toFixed(2), a.description]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div id="course-scatter">
|
||||
<ve-scatter height="450px" class="graph" :extend="{heyIUsedCourseObject: this.course.name}" :after-config="afterConfig"></ve-scatter>
|
||||
<ve-scatter height="450px" class="graph" :extend="{a: this.course.name}" :after-config="afterConfig"></ve-scatter>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import Constants from '@/constants';
|
||||
import {FormatUtils} from '@/logic/utils/format-utils';
|
||||
import moment from 'moment';
|
||||
import Course, {Assignment} from '@/logic/course';
|
||||
import GraphUtils from '@/logic/utils/graph-utils';
|
||||
|
||||
@Component
|
||||
export default class TypeRadar extends Vue
|
||||
{
|
||||
@Prop({required: true}) course: Course;
|
||||
|
||||
/**
|
||||
* Override options
|
||||
*
|
||||
* @param options Original options (Unused)
|
||||
*/
|
||||
afterConfig(options: any)
|
||||
{
|
||||
return this.chartSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate settings
|
||||
*/
|
||||
get chartSettings()
|
||||
{
|
||||
// Create settings
|
||||
let settings =
|
||||
{
|
||||
...GraphUtils.getBaseSettings('Type Radar',
|
||||
'Assignment type grades for ' + this.course.name),
|
||||
|
||||
// Radar settings
|
||||
radar:
|
||||
{
|
||||
// shape: 'circle',
|
||||
name:
|
||||
{
|
||||
textStyle:
|
||||
{
|
||||
fontSize: 14,
|
||||
textShadowColor: '#cfcfcf',
|
||||
textShadowBlur: 2,
|
||||
textShadowOffsetX: 1,
|
||||
textShadowOffsetY: 1,
|
||||
color: '#fff',
|
||||
backgroundColor: '#f6f6f6',
|
||||
borderRadius: 3,
|
||||
padding: [3, 5]
|
||||
}
|
||||
},
|
||||
splitArea:
|
||||
{
|
||||
areaStyle:
|
||||
{
|
||||
color:
|
||||
[
|
||||
'rgb(255,161,151)',
|
||||
'rgb(255,190,184)',
|
||||
'rgba(255,225,199)',
|
||||
'rgba(255,250,216)',
|
||||
'rgba(241,255,237)',
|
||||
],
|
||||
opacity: 0.4
|
||||
}
|
||||
},
|
||||
indicator: this.course.assignmentTypes.map((t, i) => {return {
|
||||
name: `${t.name}\n${t.percent}%`,
|
||||
max: 100,
|
||||
color: Constants.THEME.colors[i]
|
||||
}}),
|
||||
radius: '60%',
|
||||
center: ['50%', '60%']
|
||||
},
|
||||
|
||||
tooltip: {},
|
||||
|
||||
// Data
|
||||
series:
|
||||
{
|
||||
type: 'radar',
|
||||
data:
|
||||
[
|
||||
{
|
||||
name: 'Score',
|
||||
symbol: 'circle',
|
||||
areaStyle:
|
||||
{
|
||||
color:
|
||||
{
|
||||
type: 'radial',
|
||||
x: 0.5, y: 0.6, r: 0.5,
|
||||
colorStops:
|
||||
[
|
||||
{offset: 0, color: '#ffa0a0'},
|
||||
{offset: 0.5, color: '#fffead'},
|
||||
{offset: 1, color: '#d1ffde'}
|
||||
],
|
||||
global: false // 缺省为 false
|
||||
},
|
||||
opacity: 0.2
|
||||
},
|
||||
value: this.course.assignmentTypes.map(t => t.percent)
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
color: '#6771c1'
|
||||
};
|
||||
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div id="course-scatter">
|
||||
<ve-radar height="450px" class="graph" :extend="{a: this.course.name}" :after-config="afterConfig"></ve-radar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./type-radar.ts" lang="ts"></script>
|
||||
<style lang="scss" scoped>
|
||||
#overall-bar
|
||||
{
|
||||
.graph
|
||||
{
|
||||
margin-top: 50px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +1,13 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import {Course} from '@/components/app/app';
|
||||
import {GPAUtils} from '@/utils/gpa-utils';
|
||||
import Course from '@/logic/course';
|
||||
import {GPAUtils} from '@/logic/utils/gpa-utils';
|
||||
import Constants from '@/constants';
|
||||
import {FormatUtils} from '@/logic/utils/format-utils';
|
||||
|
||||
@Component({
|
||||
})
|
||||
export default class OverallBar extends Vue
|
||||
{
|
||||
// @ts-ignore
|
||||
@Prop({required: true}) courses: Course[];
|
||||
|
||||
/**
|
||||
@@ -40,7 +40,7 @@ export default class OverallBar extends Vue
|
||||
rotate: 90,
|
||||
|
||||
// Truncate text length
|
||||
formatter: (value: string) => value.length <= 16 ? value : value.substr(0, 14) + '...'
|
||||
formatter: (value: string) => FormatUtils.limit(value, 16)
|
||||
},
|
||||
},
|
||||
|
||||
@@ -53,15 +53,16 @@ export default class OverallBar extends Vue
|
||||
// Data
|
||||
series:
|
||||
[
|
||||
// Max GP
|
||||
{
|
||||
type: 'bar',
|
||||
barGap: '-100%',
|
||||
data: this.courses.map(course =>
|
||||
{
|
||||
return {value: [course.name, GPAUtils.getGP(course, 'A+')],
|
||||
itemStyle: {color: '#d8d8d8'}}
|
||||
return {value: [course.name, GPAUtils.getGP(course, 'A+')], itemStyle: {color: '#d8d8d8'}}
|
||||
}),
|
||||
},
|
||||
// Current GP
|
||||
{
|
||||
type: 'bar',
|
||||
barGap: '-100%',
|
||||
@@ -92,14 +93,21 @@ export default class OverallBar extends Vue
|
||||
{
|
||||
let data: any = [];
|
||||
|
||||
this.courses.forEach(course =>
|
||||
this.courses.forEach((course, index) =>
|
||||
{
|
||||
// Get GP
|
||||
let gp = GPAUtils.getGP(course, course.letterGrade);
|
||||
|
||||
// No grade cases
|
||||
if (gp == -1) return;
|
||||
|
||||
// Push data
|
||||
data.push(
|
||||
{
|
||||
value: [course.name, GPAUtils.getGP(course, course.letterGrade)],
|
||||
value: [course.name, gp],
|
||||
itemStyle:
|
||||
{
|
||||
color: Constants.THEME.colors[data.length]
|
||||
color: Constants.THEME.colors[index]
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+37
-2
@@ -1,6 +1,6 @@
|
||||
|
||||
// Row
|
||||
.unread-entry
|
||||
.assignment-entry
|
||||
{
|
||||
height: 40px;
|
||||
padding: 0 10px 0 20px;
|
||||
@@ -16,6 +16,10 @@
|
||||
span.month
|
||||
{
|
||||
margin-right: 5px;
|
||||
|
||||
// Unified width
|
||||
display: inline-block;
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
span.now
|
||||
@@ -95,9 +99,40 @@
|
||||
height: 22px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
// Unified width
|
||||
.entry-box.score, .entry-box.max
|
||||
{
|
||||
min-width: 30px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// Unified width
|
||||
.entry-box.percent
|
||||
{
|
||||
min-width: 60px;
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.unread-entry:first-child
|
||||
// Narrow layout
|
||||
.assignment-entry.narrow
|
||||
{
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
// Unread
|
||||
.no-unread
|
||||
{
|
||||
visibility: hidden !important;
|
||||
width: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
padding: 0 0 0 10px !important;
|
||||
}
|
||||
|
||||
.assignment-entry:first-child
|
||||
{
|
||||
padding-top: 3px;
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div class="assignment-entry vertical-center"
|
||||
:class="narrow ? 'narrow' : ''"
|
||||
:style="`background: ${backgroundColor}`">
|
||||
|
||||
<el-row class="unread-row">
|
||||
<el-col :span="3" class="date">
|
||||
<span class="month">{{getMoment(assignment.time).format("MMM D")}}</span>
|
||||
<span class="now">({{getMoment(assignment.time).fromNow()}})</span>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="15" class="description">
|
||||
<span class="type entry-box"
|
||||
:style="`border-color: var(--assignment-type-${assignment.typeId})`">
|
||||
{{assignment.type}}
|
||||
</span>
|
||||
<span class="text">{{assignment.description}}</span>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6" class="grade">
|
||||
<span class="percent entry-box">
|
||||
{{(assignment.score / assignment.scoreMax * 100).toFixed(1)}}
|
||||
<span class="symbol">%</span>
|
||||
</span>
|
||||
<span class="score entry-box">{{assignment.score}}</span>
|
||||
<span class="max entry-box">{{assignment.scoreMax}}</span>
|
||||
|
||||
<el-button class="mark-as-read" :class="unread ? 'unread' : 'no-unread'"
|
||||
size="mini" type="text" icon="el-icon-close"
|
||||
@click="markAsRead">
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import moment from 'moment';
|
||||
import {Assignment} from '@/logic/course';
|
||||
|
||||
@Component
|
||||
export default class AssignmentEntry extends Vue
|
||||
{
|
||||
@Prop({required: true}) assignment: Assignment;
|
||||
|
||||
@Prop({default: false}) unread: boolean;
|
||||
@Prop({default: '#f5f7fa'}) backgroundColor: string;
|
||||
@Prop({default: false}) narrow: boolean;
|
||||
|
||||
/**
|
||||
* Format a date to the displayed format
|
||||
*
|
||||
* @param date Date
|
||||
*/
|
||||
getMoment(date: number)
|
||||
{
|
||||
return moment(new Date(date));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this unread assignment as read
|
||||
*/
|
||||
markAsRead()
|
||||
{
|
||||
// Call custom event
|
||||
this.$emit('mark-as-read', this.assignment)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style src="./assignment-entry.scss" lang="scss" scoped></style>
|
||||
@@ -0,0 +1,92 @@
|
||||
// Main card content
|
||||
.course-card-content.main
|
||||
{
|
||||
padding: 0 20px 0 20px;
|
||||
height: 90px;
|
||||
|
||||
// Main color
|
||||
background: white;
|
||||
}
|
||||
|
||||
.course-col-name
|
||||
{
|
||||
// Align left
|
||||
text-align: left;
|
||||
float: left;
|
||||
|
||||
.course-name
|
||||
{
|
||||
overflow: hidden;
|
||||
font-size: 22px;
|
||||
color: var(--main);
|
||||
}
|
||||
|
||||
.course-teacher
|
||||
{
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.course-col-grade
|
||||
{
|
||||
// Align right
|
||||
text-align: right;
|
||||
float: right;
|
||||
|
||||
// Adjust position
|
||||
margin-top: -2px;
|
||||
|
||||
.course-grade
|
||||
{
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
.course-updates
|
||||
{
|
||||
font-size: 14px;
|
||||
|
||||
.unread-number
|
||||
{
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.unread-text
|
||||
{
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.course-updates.unread
|
||||
{
|
||||
.unread-number
|
||||
{
|
||||
background: var(--unread);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.unread-text
|
||||
{
|
||||
color: var(--unread);
|
||||
}
|
||||
}
|
||||
|
||||
.course-updates.none
|
||||
{
|
||||
color: #999999;
|
||||
|
||||
.unread-number
|
||||
{
|
||||
background: #eeeeee;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,8 @@
|
||||
<el-col :span="12" class="course-col-grade">
|
||||
<div class="course-grade">
|
||||
<span class="letter">{{course.letterGrade}} </span>
|
||||
<span class="numeric">{{course.numericGrade.toFixed(2)}}</span>
|
||||
<span class="percent">%</span>
|
||||
<span class="numeric">{{course.numericGrade === undefined ? '--' : course.numericGrade.toFixed(2)}}</span>
|
||||
<span class="percent" v-if="course.numericGrade !== undefined">%</span>
|
||||
</div>
|
||||
<div class="course-updates" @click="redirect" :class="unread === 0 ? 'none' : 'unread'">
|
||||
<span class="unread-number">{{unread}}</span>
|
||||
@@ -25,22 +25,18 @@
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import App, {Course} from '@/components/app/app';
|
||||
import {CourseUtils} from '@/utils/course-utils';
|
||||
import Course from '@/logic/course';
|
||||
import {CourseUtils} from '@/logic/utils/course-utils';
|
||||
import Navigation from '@/components/navigation/navigation';
|
||||
|
||||
@Component({
|
||||
components: {}
|
||||
})
|
||||
@Component
|
||||
export default class CourseHead extends Vue
|
||||
{
|
||||
// @ts-ignore
|
||||
@Prop() unread: number;
|
||||
@Prop({required: true}) unread: number;
|
||||
|
||||
// @ts-ignore
|
||||
@Prop() course: Course;
|
||||
@Prop({required: true}) course: Course;
|
||||
|
||||
// @ts-ignore
|
||||
@Prop() clickable: boolean;
|
||||
@Prop({required: true}) clickable: boolean;
|
||||
|
||||
/**
|
||||
* Redirect to the course page
|
||||
@@ -48,102 +44,9 @@
|
||||
redirect()
|
||||
{
|
||||
if (!this.clickable) return;
|
||||
App.instance.selectedTab = CourseUtils.formatTabIndex(this.course);
|
||||
Navigation.instance.updateIndex(CourseUtils.formatTabIndex(this.course));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
// Main card content
|
||||
.course-card-content.main
|
||||
{
|
||||
padding: 0 20px 0 20px;
|
||||
height: 90px;
|
||||
|
||||
// Main color
|
||||
background: white;
|
||||
}
|
||||
|
||||
.course-col-name
|
||||
{
|
||||
// Align left
|
||||
text-align: left;
|
||||
float: left;
|
||||
|
||||
.course-name
|
||||
{
|
||||
overflow: hidden;
|
||||
font-size: 22px;
|
||||
color: var(--main);
|
||||
}
|
||||
|
||||
.course-teacher
|
||||
{
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.course-col-grade
|
||||
{
|
||||
// Align right
|
||||
text-align: right;
|
||||
float: right;
|
||||
|
||||
// Adjust position
|
||||
margin-top: -2px;
|
||||
|
||||
.course-grade
|
||||
{
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
.course-updates
|
||||
{
|
||||
font-size: 14px;
|
||||
|
||||
.unread-number
|
||||
{
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.unread-text
|
||||
{
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.course-updates.unread
|
||||
{
|
||||
.unread-number
|
||||
{
|
||||
background: var(--unread);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.unread-text
|
||||
{
|
||||
color: var(--unread);
|
||||
}
|
||||
}
|
||||
|
||||
.course-updates.none
|
||||
{
|
||||
color: #999999;
|
||||
|
||||
.unread-number
|
||||
{
|
||||
background: #eeeeee;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style src="./course-head.scss" lang="scss"></style>
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import App, {Assignment, Course} from '@/components/app/app';
|
||||
import {GPAUtils} from '@/utils/gpa-utils';
|
||||
import Constants from '@/constants';
|
||||
import UnreadEntry from '@/pages/overall/overall-course/unread-entry/unread-entry';
|
||||
import App from '@/components/app/app';
|
||||
import AssignmentEntry from '@/pages/overall/overall-course/assignment-entry/assignment-entry.vue';
|
||||
import CourseHead from '@/pages/overall/overall-course/course-head/course-head.vue';
|
||||
import Course, {Assignment} from '@/logic/course';
|
||||
|
||||
@Component({
|
||||
components: {UnreadEntry, CourseHead}
|
||||
components: {UnreadEntry: AssignmentEntry, CourseHead}
|
||||
})
|
||||
export default class OverallCourse extends Vue
|
||||
{
|
||||
// @ts-ignore
|
||||
@Prop({required: true}) course: Course;
|
||||
|
||||
private unread: number = -1;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<unread-entry v-for="assignment in unreadAssignments"
|
||||
:assignment="assignment"
|
||||
:key="assignment.id"
|
||||
unread="true"
|
||||
v-on:mark-as-read="markAsRead">
|
||||
</unread-entry>
|
||||
</div>
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import {Assignment, Course} from '@/components/app/app';
|
||||
import moment from 'moment';
|
||||
|
||||
@Component({
|
||||
})
|
||||
export default class UnreadEntry extends Vue
|
||||
{
|
||||
// @ts-ignore
|
||||
@Prop({required: true}) assignment: Assignment;
|
||||
|
||||
/**
|
||||
* Format a date to the displayed format
|
||||
*
|
||||
* @param date Date
|
||||
*/
|
||||
getMoment(date: string)
|
||||
{
|
||||
return moment(new Date(date));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this unread assignment as read
|
||||
*/
|
||||
markAsRead()
|
||||
{
|
||||
// Call custom event
|
||||
this.$emit('mark-as-read', this.assignment)
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
<template>
|
||||
<div class="unread-entry vertical-center">
|
||||
<el-row class="unread-row">
|
||||
<el-col :span="3" class="date">
|
||||
<span class="month">{{getMoment(assignment.date).format("MMM Do")}}</span>
|
||||
<span class="now">({{getMoment(assignment.date).fromNow()}})</span>
|
||||
</el-col>
|
||||
<el-col :span="15" class="description">
|
||||
<span class="type entry-box"
|
||||
:style="`border-color: var(--assignment-type-${assignment.typeId})`">
|
||||
{{assignment.type}}
|
||||
</span>
|
||||
<span class="text">{{assignment.description}}</span>
|
||||
</el-col>
|
||||
<el-col :span="6" class="grade">
|
||||
<span class="percent entry-box">
|
||||
{{(assignment.score / assignment.scoreMax * 100).toFixed(1)}}
|
||||
<span class="symbol">%</span>
|
||||
</span>
|
||||
<span class="score entry-box">{{assignment.score}}</span>
|
||||
<span class="max entry-box">{{assignment.scoreMax}}</span>
|
||||
<el-button class="mark-as-read" size="mini" type="text"
|
||||
icon="el-icon-close" @click="markAsRead">
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./unread-entry.ts" lang="ts"></script>
|
||||
<style src="./unread-entry.scss" lang="scss" scoped></style>
|
||||
@@ -1,180 +1,164 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import {Course} from '@/components/app/app';
|
||||
import moment from 'moment';
|
||||
import Course, {Assignment} from '@/logic/course';
|
||||
import Constants from '@/constants';
|
||||
import Navigation from '@/components/navigation/navigation';
|
||||
import {CourseUtils} from '@/logic/utils/course-utils';
|
||||
import GraphUtils from '@/logic/utils/graph-utils';
|
||||
import {GPAUtils} from '@/logic/utils/gpa-utils';
|
||||
|
||||
@Component({
|
||||
})
|
||||
@Component
|
||||
export default class OverallLine extends Vue
|
||||
{
|
||||
// @ts-ignore
|
||||
@Prop({required: true}) courses: Course[];
|
||||
|
||||
private settings =
|
||||
{
|
||||
// Title
|
||||
title:
|
||||
{
|
||||
show: true,
|
||||
textStyle:
|
||||
{
|
||||
fontSize: 12
|
||||
},
|
||||
text: 'Average Grade',
|
||||
subtext: 'Average score trend for every course',
|
||||
x: 'center'
|
||||
},
|
||||
// Legend
|
||||
legend:
|
||||
{
|
||||
show: false,
|
||||
//left: 'auto',
|
||||
//align: 'left',
|
||||
//orient: 'vertical'
|
||||
textStyle:
|
||||
{
|
||||
fontSize: 11
|
||||
},
|
||||
icon: 'circle'
|
||||
},
|
||||
// Zoom bar
|
||||
dataZoom:
|
||||
[
|
||||
// TODO: Calculate real value for startValue
|
||||
{
|
||||
startValue: '9/13/2019'
|
||||
},
|
||||
{
|
||||
type: 'inside'
|
||||
}
|
||||
],
|
||||
series:
|
||||
{
|
||||
smooth: true
|
||||
},
|
||||
xAxis:
|
||||
{
|
||||
//type: 'time'
|
||||
},
|
||||
yAxis:
|
||||
{
|
||||
min: (value: any) => Math.floor(value.min),
|
||||
max: (value: any) => value.max
|
||||
}
|
||||
};
|
||||
filteredCourses: Course[];
|
||||
settings: any;
|
||||
|
||||
/**
|
||||
* Convert assignments list to a graph dataset.
|
||||
* When this component is created
|
||||
*/
|
||||
get convertChart()
|
||||
created()
|
||||
{
|
||||
let courses = this.courses;
|
||||
// Filter courses
|
||||
this.filteredCourses = this.courses.filter(c => c.isGraded && c.assignments.length > 0);
|
||||
|
||||
// Compute the column names
|
||||
let columns = courses.map(course => course.name);
|
||||
columns.unshift('date');
|
||||
// Generate settings
|
||||
this.settings =
|
||||
{
|
||||
...GraphUtils.getBaseSettings('Average Grade', 'Average score trend for every course'),
|
||||
|
||||
// Zoom bar
|
||||
dataZoom:
|
||||
[
|
||||
{
|
||||
type: 'slider',
|
||||
startValue: Math.max(moment().subtract(30, 'days').toDate().getTime(),
|
||||
CourseUtils.getTermBeginDate().getTime()),
|
||||
|
||||
// Minimum zoom: 1 week
|
||||
minValueSpan: 7 * 24 * 60 * 60 * 1000
|
||||
}
|
||||
],
|
||||
|
||||
// Tooltip
|
||||
tooltip:
|
||||
{
|
||||
trigger: 'axis'
|
||||
},
|
||||
|
||||
// Axis
|
||||
xAxis:
|
||||
{
|
||||
type: 'time',
|
||||
axisLabel:
|
||||
{
|
||||
formatter: (name: any) => moment(name).format('MMM DD')
|
||||
},
|
||||
},
|
||||
yAxis:
|
||||
{
|
||||
axisLabel:
|
||||
{
|
||||
formatter: (name: any) => name + '%'
|
||||
},
|
||||
min: (value: any) => Math.floor(value.min),
|
||||
max: (value: any) => Math.min(Math.ceil(value.max), 110)
|
||||
},
|
||||
|
||||
// Series data
|
||||
series: this.series()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override options
|
||||
*
|
||||
* @param options Original options (Unused)
|
||||
*/
|
||||
afterConfig(options: any)
|
||||
{
|
||||
return this.settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate series data
|
||||
*/
|
||||
private series()
|
||||
{
|
||||
// Each course
|
||||
let series: any[] = this.filteredCourses.map(course => this.getCourseSeries(course));
|
||||
|
||||
// Push other stuff
|
||||
series.push(
|
||||
{
|
||||
type: 'line',
|
||||
markLine: GraphUtils.getTermLines(),
|
||||
markArea: GraphUtils.getGradeMarkAreas(0.4)
|
||||
});
|
||||
|
||||
return series
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate series data for a course
|
||||
*
|
||||
* @param course
|
||||
*/
|
||||
private getCourseSeries(course: Course)
|
||||
{
|
||||
// Graded assignments
|
||||
let assignments = course.assignments.slice().reverse();
|
||||
|
||||
// Create series
|
||||
return {
|
||||
name: course.name,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle', // circle, diamond, emptyCircle, none
|
||||
data: this.toDateRange([...assignments.map(a => a.time)].map((time, i) =>
|
||||
{
|
||||
// Find subset before this assignment
|
||||
let subset = assignments.filter(a => a.time <= time);
|
||||
|
||||
// Find grade
|
||||
if (course.grading.method == 'PERCENT_TYPE')
|
||||
return [time, GPAUtils.getPercentTypeAverage(course, subset)];
|
||||
if (course.grading.method == 'TOTAL_MEAN')
|
||||
return [time, GPAUtils.getTotalMeanAverage(subset)];
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert point data to date range data.
|
||||
* Eg. [[Mon, 10], [Wed, 5]] to [[Mon, 10], [Tue, 10], [Wed, 5]]
|
||||
*
|
||||
* @param data
|
||||
*/
|
||||
private toDateRange(data: any[])
|
||||
{
|
||||
// Find the min date
|
||||
let minDates = courses.map(course => new Date(course.assignments[course.assignments.length - 1].date).getTime());
|
||||
let minDates = this.courses.map(course => course.assignments[course.assignments.length - 1].time);
|
||||
let minDate: Date = new Date(Math.min.apply(null, minDates));
|
||||
|
||||
// Find the dates in between
|
||||
let now = new Date();
|
||||
let dates = [];
|
||||
let now = new Date(Math.min(new Date().getTime(), CourseUtils.getTermEndDate().getTime()));
|
||||
let dates: number[] = [];
|
||||
for (let date = minDate; date <= now; date.setDate(date.getDate() + 1))
|
||||
{
|
||||
dates.push(new Date(date));
|
||||
dates.push(new Date(date).getTime());
|
||||
}
|
||||
|
||||
// Compute the rows data
|
||||
let rows: {[index: string]: any}[] = [];
|
||||
dates.forEach(date =>
|
||||
let lastValue: any = null;
|
||||
return dates.map(date =>
|
||||
{
|
||||
// Define row object
|
||||
let row: {[index: string]:any} = {'date': date.toLocaleDateString('en-US')};
|
||||
// Data point on this specific date
|
||||
let thisValue = data.find(a => a[0] == date);
|
||||
|
||||
// Loop through courses
|
||||
courses.forEach(course =>
|
||||
{
|
||||
// Total Mean
|
||||
if (course.grading.method == 'TOTAL_MEAN')
|
||||
{
|
||||
let score = 0;
|
||||
let max = 0;
|
||||
|
||||
// Loop through assignments
|
||||
course.assignments.forEach(assignment =>
|
||||
{
|
||||
// If assignment should be displayed
|
||||
if (assignment.complete != 'Complete') return;
|
||||
|
||||
// Date is being looked at
|
||||
let assignmentDate = new Date(assignment.date);
|
||||
if (assignmentDate.getTime() < date.getTime())
|
||||
{
|
||||
// Record scores
|
||||
score += assignment.score;
|
||||
max += assignment.scoreMax;
|
||||
}
|
||||
});
|
||||
|
||||
// Add average to the row
|
||||
row[course.name] = score / max * 100;
|
||||
}
|
||||
else if (course.grading.method == 'PERCENT_TYPE')
|
||||
{
|
||||
let typeScores: {[index: string]: any} = {};
|
||||
let typeCounts: {[index: string]: any} = {};
|
||||
|
||||
// Loop through assignments
|
||||
course.assignments.forEach(assignment =>
|
||||
{
|
||||
// If assignment should be displayed
|
||||
if (assignment.complete != 'Complete') return;
|
||||
|
||||
// Date is being looked at
|
||||
let assignmentDate = new Date(assignment.date);
|
||||
if (assignmentDate.getTime() < date.getTime())
|
||||
{
|
||||
// Record scores
|
||||
if (typeScores[assignment.type] == undefined) typeScores[assignment.type] = 0;
|
||||
typeScores[assignment.type] += assignment.score / assignment.scoreMax;
|
||||
|
||||
if (typeCounts[assignment.type] == undefined) typeCounts[assignment.type] = 0;
|
||||
typeCounts[assignment.type] ++;
|
||||
}
|
||||
});
|
||||
|
||||
// Count total percentage (This is to avoid less than expected cases)
|
||||
// Eg. If HW = 25% and Quiz = 75%, I have 1 hw and 0 quiz
|
||||
// Without total percentage, the avg grade I get is 25%.
|
||||
let totalPercentage = 0;
|
||||
for (let type in course.grading.weightingMap)
|
||||
{
|
||||
if (typeScores[type] != undefined)
|
||||
{
|
||||
totalPercentage += course.grading.weightingMap[type];
|
||||
}
|
||||
}
|
||||
|
||||
// Count
|
||||
let score = 0;
|
||||
for (let type in typeScores)
|
||||
{
|
||||
let typeFactor = course.grading.weightingMap[type] / totalPercentage;
|
||||
score += typeScores[type] * typeFactor / typeCounts[type];
|
||||
}
|
||||
|
||||
// Add average to the row
|
||||
if (score != 0) row[course.name] = score * 100;
|
||||
}
|
||||
});
|
||||
|
||||
// Add it to the array
|
||||
rows.push(row);
|
||||
// None
|
||||
if (thisValue == null) return [date, lastValue == null ? null : lastValue[1]];
|
||||
else return [date, (lastValue = thisValue)[1]];
|
||||
});
|
||||
|
||||
return {
|
||||
columns: columns,
|
||||
rows: rows
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div id="overall-line">
|
||||
<ve-line :data="convertChart" :extend="settings"></ve-line>
|
||||
<ve-line :extend="{a: this.courses}" :after-config="afterConfig"></ve-line>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -34,6 +34,17 @@
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.no-grade
|
||||
{
|
||||
font-size: 30px;
|
||||
color: #b1b1b1;
|
||||
|
||||
// Disable selecting
|
||||
display:block;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
// Cards
|
||||
.el-card.overall-bar-card
|
||||
{
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import OverallLine from '@/pages/overall/overall-line/overall-line';
|
||||
import OverallBar from '@/pages/overall/overall-bar/overall-bar';
|
||||
import OverallCourse from '@/pages/overall/overall-course/overall-course';
|
||||
import {Course} from '@/components/app/app';
|
||||
import {GPAUtils} from '@/utils/gpa-utils';
|
||||
|
||||
@Component({
|
||||
components: {OverallLine, OverallBar, OverallCourse}
|
||||
})
|
||||
export default class Overall extends Vue
|
||||
{
|
||||
// @ts-ignore
|
||||
@Prop({required: true}) courses: Course[];
|
||||
|
||||
/**
|
||||
* This function is called to get gpa since I can't import another
|
||||
* class in the Vue file.
|
||||
*/
|
||||
public getGPA()
|
||||
{
|
||||
return GPAUtils.getGPA(this.courses);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div id="overall">
|
||||
<el-row>
|
||||
<el-row v-if="getGPA().gpa !== -1">
|
||||
<el-col :span="4">
|
||||
<el-card class="large gpa-card vertical-center">
|
||||
<div style="padding: 14px;">
|
||||
@@ -25,6 +25,12 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row v-if="getGPA().gpa === -1">
|
||||
<el-card class="large gpa-card vertical-center">
|
||||
<div class="no-grade">This quarter has no grades yet...</div>
|
||||
</el-card>
|
||||
</el-row>
|
||||
|
||||
<overall-course v-for="course in courses"
|
||||
:course="course"
|
||||
:key="course.id">
|
||||
@@ -32,5 +38,30 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./overall.ts" lang="ts"></script>
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import OverallLine from '@/pages/overall/overall-line/overall-line';
|
||||
import OverallBar from '@/pages/overall/overall-bar/overall-bar';
|
||||
import OverallCourse from '@/pages/overall/overall-course/overall-course';
|
||||
import Course from '@/logic/course';
|
||||
import {GPAUtils} from '@/logic/utils/gpa-utils';
|
||||
|
||||
@Component({
|
||||
components: {OverallLine, OverallBar, OverallCourse}
|
||||
})
|
||||
export default class Overall extends Vue
|
||||
{
|
||||
@Prop({required: true}) courses: Course[];
|
||||
|
||||
/**
|
||||
* This function is called to get gpa since I can't import another
|
||||
* class in the Vue file.
|
||||
*/
|
||||
public getGPA()
|
||||
{
|
||||
return GPAUtils.getGPA(this.courses);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style src="./overall.scss" lang="scss" scoped></style>
|
||||
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
import Vue, { VNode } from 'vue';
|
||||
import Vue, {VNode} from 'vue';
|
||||
|
||||
declare global {
|
||||
namespace JSX {
|
||||
|
||||
Vendored
+4
-3
@@ -1,4 +1,5 @@
|
||||
declare module '*.vue' {
|
||||
import Vue from 'vue';
|
||||
export default Vue;
|
||||
declare module '*.vue'
|
||||
{
|
||||
import Vue from 'vue';
|
||||
export default Vue;
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import {Course} from '@/components/app/app';
|
||||
|
||||
export class CourseUtils
|
||||
{
|
||||
/**
|
||||
* Return a list of courses that are graphed
|
||||
*
|
||||
* @param original Original course list
|
||||
* @return Course[] Filtered course list
|
||||
*/
|
||||
public static getGradedCourses(original: Course[]): Course[]
|
||||
{
|
||||
// Define result
|
||||
let result: Course[] = [];
|
||||
|
||||
// Filter through courses
|
||||
original.forEach(course =>
|
||||
{
|
||||
// Skip future or past courses
|
||||
if (course.status != 'active') return;
|
||||
|
||||
// Skip courses without levels
|
||||
if (course.level == 'None') return;
|
||||
|
||||
// Skip courses without graded assignments
|
||||
if (course.assignments.filter(a => a.complete == 'Complete').length == 0) return;
|
||||
|
||||
// Skip if there are no grading scale
|
||||
// if (course.grading.method == 'NOT_GRADED') return;
|
||||
|
||||
// Add it to the list
|
||||
result.push(course);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format course to tab index string
|
||||
*
|
||||
* @param course Course object
|
||||
* @return string Tab index
|
||||
*/
|
||||
public static formatTabIndex(course: Course): string
|
||||
{
|
||||
return `course/${course.id}/${course.name.toLowerCase().split(' ').join('-')}`;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import moment from 'moment';
|
||||
|
||||
export class FormatUtils
|
||||
{
|
||||
/**
|
||||
* Convert date format to yyyy-mm-dd
|
||||
*
|
||||
* @param _date Date
|
||||
*/
|
||||
public static toChartDate(_date: string | Date)
|
||||
{
|
||||
// Convert to Date
|
||||
let date: Date = _date instanceof Date ? _date : new Date(_date);
|
||||
|
||||
// Convert to yyyy-mm-dd
|
||||
return moment(date).format('YYYY-MM-DD');
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
/**
|
||||
* This is an utility class to calculate GPA.
|
||||
*/
|
||||
import {Course} from '@/components/app/app';
|
||||
|
||||
export class GPAUtils
|
||||
{
|
||||
// [[Min score, Letter grade, Base GPA], ...]
|
||||
public static SCALE =
|
||||
[
|
||||
[96.5, 'A+', 4.00],
|
||||
[92.5, 'A' , 3.75],
|
||||
[89.5, 'A-', 3.50],
|
||||
[86.5, 'B+', 3.25],
|
||||
[82.5, 'B' , 3.00],
|
||||
[79.5, 'B-', 2.75],
|
||||
[76.5, 'C+', 2.50],
|
||||
[72.5, 'C' , 2.25],
|
||||
[70.5, 'C-', 2.00],
|
||||
[69.5, 'D' , 1.00],
|
||||
[0 , 'F' , 0.00]
|
||||
];
|
||||
|
||||
// Keywords
|
||||
public static MIN = 0;
|
||||
public static LETTER = 1;
|
||||
public static GPA = 2;
|
||||
|
||||
/**
|
||||
* Calculate GPA for a list of couses
|
||||
*
|
||||
* @param coursesOriginal List of courses
|
||||
*/
|
||||
public static getGPA(coursesOriginal: Course[]): {gpa: number, accurate: boolean, max: number}
|
||||
{
|
||||
// Clone array
|
||||
let courses: Course[] = [];
|
||||
|
||||
// Accurate or not
|
||||
let accurate: boolean = true;
|
||||
|
||||
// Remove all courses that does not have a grade
|
||||
coursesOriginal.forEach(course =>
|
||||
{
|
||||
if (course.letterGrade == null || course.letterGrade == '')
|
||||
{
|
||||
accurate = false;
|
||||
}
|
||||
else if (course.level != 'none')
|
||||
{
|
||||
courses.push(course);
|
||||
}
|
||||
});
|
||||
|
||||
// If no course have grade, return -1
|
||||
if (courses.length == 0)
|
||||
{
|
||||
return {gpa: -1, accurate: false, max: -1};
|
||||
}
|
||||
|
||||
// Count total GPA
|
||||
let totalGPA = 0;
|
||||
let maxTotal = 0;
|
||||
courses.forEach(course =>
|
||||
{
|
||||
totalGPA += this.getGP(course, course.letterGrade);
|
||||
maxTotal += this.getGP(course, 'A+');
|
||||
});
|
||||
|
||||
// Get average GPA, round to two decimal places
|
||||
let gpa = Math.round(totalGPA / courses.length * 100) / 100;
|
||||
let maxGPA = Math.round(maxTotal / courses.length * 100) / 100;
|
||||
|
||||
// Return results
|
||||
return {gpa: gpa, accurate: accurate, max: maxGPA};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate GPA for a course
|
||||
*
|
||||
* @param course Course
|
||||
* @param letterGrade Letter grade
|
||||
*/
|
||||
public static getGP(course: Course, letterGrade?: string): number
|
||||
{
|
||||
// Find the GPA for this course.
|
||||
for (let scale of this.SCALE)
|
||||
{
|
||||
// Letter grades are the same
|
||||
if (scale[this.LETTER] == letterGrade)
|
||||
{
|
||||
// Get grade and add it
|
||||
let grade = <number> scale[this.GPA];
|
||||
|
||||
// Add scaleUp if not failed.
|
||||
if (grade != 0) grade += course.scaleUp;
|
||||
|
||||
// That's it
|
||||
return grade;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the total-mean (total/max) average
|
||||
*
|
||||
* @param course Course
|
||||
*/
|
||||
public static getTotalMeanAverage(course: Course)
|
||||
{
|
||||
let score = 0;
|
||||
let max = 0;
|
||||
|
||||
// Loop through assignments
|
||||
course.assignments.forEach(assignment =>
|
||||
{
|
||||
// If assignment should be displayed
|
||||
if (assignment.complete != 'Complete') return;
|
||||
|
||||
// Record scores
|
||||
score += assignment.score;
|
||||
max += assignment.scoreMax;
|
||||
});
|
||||
|
||||
// Return
|
||||
return score / max * 100;
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -24,7 +24,10 @@
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
],
|
||||
|
||||
// Custom
|
||||
"strictPropertyInitialization": false
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
|
||||
Reference in New Issue
Block a user