Compare commits
121 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8dd7d35abe | |||
| 2b989fb137 | |||
| e2a13e90e0 | |||
| 3546a57711 | |||
| 770e26b0cf | |||
| fff60f5754 | |||
| a46e011c90 | |||
| 48224d9e34 | |||
| eb311e9f2d | |||
| 7b53e65a1b | |||
| 065d6d31a3 | |||
| d8eb160123 | |||
| 8e924d8e33 | |||
| 1e45b418b8 | |||
| e42c8be76f | |||
| b6b3f921a2 | |||
| d3234be6db | |||
| 50a5f9dcc5 | |||
| 62d40dd0a7 | |||
| 9e8e44b91f | |||
| df85e1084f | |||
| 0b41f8ac5d | |||
| 7b11537e0f | |||
| 830f55441c | |||
| 0a49d791ae | |||
| c675fc5650 | |||
| ad8496e661 | |||
| 72b9e4d214 | |||
| fa3fc44526 | |||
| 419cced592 | |||
| 352fd58fb5 | |||
| f043d77e5e | |||
| 9e88f3efe1 | |||
| cadd328958 | |||
| 8c4759d497 | |||
| ac8f488ab4 | |||
| df639a360c | |||
| 24328508c0 | |||
| ebd4849703 | |||
| 80d72bb5c0 | |||
| 204e7dc04a | |||
| cef0b31dba | |||
| 22ba3acab0 | |||
| 88bdcd8cf8 | |||
| 8559a93b9c | |||
| f8db85dca3 | |||
| b4bd24d884 | |||
| f6d30a750a | |||
| 988d6f5463 | |||
| 608132ad6c | |||
| 6cffb745a7 | |||
| 82212cf10c | |||
| 9656b3184b | |||
| 08b665a1fa | |||
| 500e13ef9d | |||
| b95220ba1a | |||
| b8dfb8f732 | |||
| 002aa84444 | |||
| e7c513695d | |||
| e51dbd2c5b | |||
| 9157232d45 | |||
| 17ef8f4380 | |||
| c31dbf0e50 | |||
| 9c2c1c3195 | |||
| 820c3c1148 | |||
| 6efb832212 | |||
| c885137ed7 | |||
| bb4b34722f | |||
| 2beca45e38 | |||
| cbec0add3b | |||
| 0ebce968b9 | |||
| 4095041925 | |||
| 0e618ceb13 | |||
| 042e72abb6 | |||
| 517235982b | |||
| 6e7041edcd | |||
| 67cf33b48c | |||
| 4c822fd207 | |||
| 68afbb8c76 | |||
| 5da0e89e08 | |||
| b2db05d5e2 | |||
| 3cb74083a7 | |||
| d3072ccaf6 | |||
| d566b53c22 | |||
| 7bad961f70 | |||
| 13e307f8d2 | |||
| 448e699cd3 | |||
| 1ca32b5ebd | |||
| 5ac3183ec1 | |||
| a7384753c8 | |||
| 393fc1cc71 | |||
| 06c265159b | |||
| 53a0884d0b | |||
| 7b0f11a1f4 | |||
| 4dc5966e51 | |||
| be0a657ba3 | |||
| 3287c14fa3 | |||
| 7af20f806b | |||
| bd8d7fd113 | |||
| 76cf8c4c53 | |||
| eaa1609b77 | |||
| 1324afe978 | |||
| e86d2fd4f5 | |||
| cf34db2c61 | |||
| 9038a73678 | |||
| 446ed686bd | |||
| 4782870d94 | |||
| 3ce21623d8 | |||
| 2338e4f6af | |||
| 38089c74b5 | |||
| 8860c88b1a | |||
| 75f9dc9849 | |||
| 805ffaa50e | |||
| c7d16a00e6 | |||
| 67ec2b85b2 | |||
| 9a752305e3 | |||
| c95a5b343e | |||
| 5029555c21 | |||
| 8eb2080f14 | |||
| 0296b2151a | |||
| 3575db8182 |
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 HyDEV
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,6 +1,38 @@
|
||||
# veracross-analyzer
|
||||
<h1 align="center"><br><br>
|
||||
VeracrossAnalyzer UI
|
||||
</h1>
|
||||
|
||||
## Project setup
|
||||
<h4 align="center">
|
||||
A Website, A Visual Representation of Students' Grade Data on Veracross
|
||||
</h4>
|
||||
|
||||
<h5 align="center">
|
||||
<a href="#intro">Introduction</a>
|
||||
<a href="#setup">Project Setup</a>
|
||||
<a href="#license">License</a>
|
||||
</h5><br><br><br>
|
||||
|
||||
|
||||
|
||||
<a name="intro"></a>
|
||||
Introduction:
|
||||
--------
|
||||
|
||||
This is a website that generates visual representation of students' grade data on Veracross. Currently there is only one graph and one numerical data representing the GPA. But also it just released yesterday! (Yay!) What do you expect this soon lol?
|
||||
|
||||
**Here's how it looks like right now:** *(Now all of you know my grades ;-;)*
|
||||
|
||||

|
||||
|
||||
<br>
|
||||
|
||||
<a name="setup"></a>
|
||||
Project Setup:
|
||||
--------
|
||||
|
||||
TODO: Actually write a project setup tutorial that's not generated by Vue on initialization ;-;.
|
||||
|
||||
### Install
|
||||
```
|
||||
npm install
|
||||
```
|
||||
@@ -15,15 +47,10 @@ npm run serve
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Run your tests
|
||||
```
|
||||
npm run test
|
||||
```
|
||||
<br>
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
<a name="license"></a>
|
||||
License: [MIT](https://choosealicense.com/licenses/mit/)
|
||||
--------
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
The MIT license basically means that this project is open-soucred and you can do whatever you want with it, as long as you include a copy of this license in your distribution. You don't have to ask for permissions to use or anything. However, if you do bad things with it, I'm not responsible.
|
||||
|
||||
Generated
+5
@@ -10848,6 +10848,11 @@
|
||||
"resolved": "https://registry.npm.taobao.org/vue-class-component/download/vue-class-component-7.1.0.tgz",
|
||||
"integrity": "sha1-sz78sQ4XI21oT3Cx6W8ZRux5Poc="
|
||||
},
|
||||
"vue-cookies": {
|
||||
"version": "1.5.13",
|
||||
"resolved": "https://registry.npmjs.org/vue-cookies/-/vue-cookies-1.5.13.tgz",
|
||||
"integrity": "sha512-8pjpXnvbNWx1Lft0t3MJnW+ylv0Wa2Tb6Ch617u/pQah3+SfXUZZdkh3EL3bSpe/Sw2Wdw3+DhycgQsKANSxXg=="
|
||||
},
|
||||
"vue-hot-reload-api": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npm.taobao.org/vue-hot-reload-api/download/vue-hot-reload-api-2.3.3.tgz",
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"v-charts": "^1.19.0",
|
||||
"vue": "^2.6.10",
|
||||
"vue-class-component": "^7.0.2",
|
||||
"vue-cookies": "^1.5.13",
|
||||
"vue-property-decorator": "^8.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# abort on errors
|
||||
set -e
|
||||
|
||||
# build
|
||||
npm run build
|
||||
|
||||
# navigate into the build output directory
|
||||
cd dist
|
||||
|
||||
# if you are deploying to a custom domain
|
||||
echo 'vera.hydev.org' > CNAME
|
||||
|
||||
git init
|
||||
git add -A
|
||||
git commit -m 'deploy'
|
||||
|
||||
# if you are deploying to https://<USERNAME>.github.io/<REPO>
|
||||
git push -f git@github.com:HyDevelop/VeracrossAnalyzer.Client.git master:gh-pages
|
||||
|
||||
cd -
|
||||
+90
-16
@@ -5,6 +5,7 @@ import Overall from '@/pages/overall/overall';
|
||||
import Constants from '@/constants';
|
||||
import JsonUtils from '@/utils/json-utils';
|
||||
import pWaitFor from 'p-wait-for';
|
||||
import {HttpUtils} from '@/utils/http-utils';
|
||||
|
||||
/**
|
||||
* Objects of this interface represent assignment grades.
|
||||
@@ -31,6 +32,13 @@ export interface Course
|
||||
id: number,
|
||||
name: string,
|
||||
teacherName: string,
|
||||
status: string,
|
||||
|
||||
letterGrade?: string,
|
||||
numericGrade?: number,
|
||||
|
||||
level: string,
|
||||
scaleUp: number,
|
||||
|
||||
assignments: Grade[]
|
||||
}
|
||||
@@ -47,44 +55,98 @@ export default class App extends Vue
|
||||
public courses: Course[] = [];
|
||||
|
||||
// Currently selected tab
|
||||
public selectedTab: string = "overall";
|
||||
public selectedTab: string = 'overall';
|
||||
|
||||
// Are the course assignments loaded from the server.
|
||||
public assignmentsReady: boolean = false;
|
||||
|
||||
// Token
|
||||
public token: string = '';
|
||||
|
||||
// Http Client
|
||||
public http: HttpUtils = new HttpUtils('');
|
||||
|
||||
/**
|
||||
* This is called when the instance is created.
|
||||
*/
|
||||
public created()
|
||||
{
|
||||
// Show splash
|
||||
console.log(Constants.SPLASH);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called when the user logs in.
|
||||
*
|
||||
* @param courses Courses Json
|
||||
* @param token Authorization token
|
||||
*/
|
||||
public onLogin(courses: Course[])
|
||||
public onLogin(token: string)
|
||||
{
|
||||
// Hide login bar
|
||||
this.showLogin = false;
|
||||
|
||||
// Assign courses
|
||||
this.courses = courses;
|
||||
// Store token
|
||||
this.token = token;
|
||||
|
||||
// Debug output TODO: Remove this
|
||||
console.log(courses);
|
||||
// Assign token to http client
|
||||
this.http.token = token;
|
||||
|
||||
// Load data
|
||||
this.loadCoursesAfterLogin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load courses data after login.
|
||||
*/
|
||||
public loadCoursesAfterLogin()
|
||||
{
|
||||
this.http.post('/courses', {}).then(response =>
|
||||
{
|
||||
// Check success
|
||||
if (response.success)
|
||||
{
|
||||
// Save courses
|
||||
this.courses = response.data;
|
||||
|
||||
// Load assignments
|
||||
this.loadAssignments();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Show error message TODO: Show it properly
|
||||
alert(response.data);
|
||||
}
|
||||
})
|
||||
.catch(alert);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the assignments of the courses
|
||||
*
|
||||
* @param courses Courses Json
|
||||
*/
|
||||
public loadAssignments()
|
||||
{
|
||||
// Get assignments for all the courses
|
||||
this.courses.forEach(course =>
|
||||
{
|
||||
// Send request to get assignments
|
||||
fetch(`${Constants.API_URL}/veracross/assignments?id=${course.assignmentsId}`).then(res =>
|
||||
this.http.post('/assignments', {id: course.assignmentsId}).then(response =>
|
||||
{
|
||||
// Get response body text
|
||||
res.text().then(text =>
|
||||
// Check success
|
||||
if (response.success)
|
||||
{
|
||||
// Load assignments
|
||||
// Parse json and filter it
|
||||
course.assignments = JsonUtils.filterAssignments(JSON.parse(text));
|
||||
})
|
||||
course.assignments = JsonUtils.filterAssignments(response.data);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Show error message TODO: Show it properly
|
||||
alert(response.data);
|
||||
}
|
||||
})
|
||||
.catch(err =>
|
||||
{
|
||||
alert(err);
|
||||
});
|
||||
.catch(alert);
|
||||
});
|
||||
|
||||
// Wait for assignments to be ready.
|
||||
@@ -123,4 +185,16 @@ export default class App extends Vue
|
||||
// Update selected tab
|
||||
this.selectedTab = tab;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign out
|
||||
*/
|
||||
public signOut()
|
||||
{
|
||||
// Clear all cookies
|
||||
this.$cookies.keys().forEach(key => this.$cookies.remove(key));
|
||||
|
||||
// Refresh
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<login v-if="showLogin" v-on:login:courses="onLogin"></login>
|
||||
<navigation :courses="courses" v-on:navigation:select="onNavigate"></navigation>
|
||||
<login v-if="showLogin" v-on:login:token="onLogin" :http="http"></login>
|
||||
<navigation :courses="courses"
|
||||
v-on:sign-out="signOut()"
|
||||
v-on:navigation:select="onNavigate">
|
||||
</navigation>
|
||||
|
||||
<div id="app-content">
|
||||
<overall :courses="courses" v-if="selectedTab === 'overall' && assignmentsReady"></overall>
|
||||
|
||||
@@ -72,3 +72,28 @@
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// Error
|
||||
.input-error
|
||||
{
|
||||
.el-input__inner
|
||||
{
|
||||
color: #ff3a3a6b !important;
|
||||
border-color: #ff3a3a6b !important;
|
||||
background-color: #ffdddd3b !important;
|
||||
}
|
||||
|
||||
.el-input__inner:focus
|
||||
{
|
||||
background-color: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Fix error message
|
||||
.el-form-item__error.custom
|
||||
{
|
||||
padding-top: 0;
|
||||
position: relative;
|
||||
top: auto;
|
||||
float: left;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import {Component, Vue} from 'vue-property-decorator';
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import Constants from '@/constants';
|
||||
import {HttpUtils} from '@/utils/http-utils';
|
||||
|
||||
/**
|
||||
* This component handles user login, and obtains data from the server.
|
||||
* TODO: Press enter to login
|
||||
*/
|
||||
@Component({
|
||||
components: {},
|
||||
@@ -13,6 +15,23 @@ export default class Login extends Vue
|
||||
public password: any = '';
|
||||
|
||||
public loading: boolean = false;
|
||||
public error: String = '';
|
||||
|
||||
@Prop()
|
||||
public http?: HttpUtils;
|
||||
|
||||
/**
|
||||
* This is called when the instance is created.
|
||||
*/
|
||||
public created()
|
||||
{
|
||||
// Check login cookies
|
||||
if (this.$cookies.isKey('va.token'))
|
||||
{
|
||||
// Already contains valid token / TODO: Validate
|
||||
this.$emit('login:token', this.$cookies.get('va.token'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On click, sends username and password to the server.
|
||||
@@ -22,15 +41,27 @@ export default class Login extends Vue
|
||||
// Make login button loading
|
||||
this.loading = true;
|
||||
|
||||
// Fetch request TODO: Add username and password when the https server is ready.
|
||||
fetch(`${Constants.API_URL}/veracross/courses`).then(res =>
|
||||
// Fetch request
|
||||
(<HttpUtils> this.http).post('/login', {username: this.username, password: this.password})
|
||||
.then(response =>
|
||||
{
|
||||
// Get response body text
|
||||
res.text().then(text =>
|
||||
// Check success
|
||||
if (response.success)
|
||||
{
|
||||
// Call custom event with courses info
|
||||
this.$emit('login:courses', JSON.parse(text));
|
||||
})
|
||||
// Save token to cookies
|
||||
this.$cookies.set('va.token', response.data, '7d');
|
||||
|
||||
// Call custom event with token
|
||||
this.$emit('login:token', response.data);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Show error message
|
||||
this.error = response.data;
|
||||
|
||||
// Allow the user to retry
|
||||
this.loading = false;
|
||||
}
|
||||
})
|
||||
.catch(err =>
|
||||
{
|
||||
@@ -40,4 +71,12 @@ export default class Login extends Vue
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called when the user hits enter in the input boxes.
|
||||
*/
|
||||
public onEnter()
|
||||
{
|
||||
this.onLoginClick();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,24 @@
|
||||
<div class="login-vertical-center">
|
||||
<div class="login-panel">
|
||||
<img alt="Vue logo" src="../../assets/logo.png">
|
||||
|
||||
<h1>Veracross Analyzer</h1>
|
||||
<el-input v-model="username" placeholder="School Username"></el-input>
|
||||
<el-input v-model="password" placeholder="Veracross Password" show-password=""></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>
|
||||
|
||||
<div class="el-form-item__error custom">{{error}}</div>
|
||||
|
||||
<el-button plain type="primary" @click="onLoginClick" :loading="loading">Login</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
|
||||
.el-menu.centered li
|
||||
{
|
||||
display: inline-block !important;
|
||||
float: none !important;
|
||||
}
|
||||
|
||||
// Borders
|
||||
#navigation
|
||||
{
|
||||
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
|
||||
|
||||
ul
|
||||
{
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,4 +27,13 @@ export default class Navigation extends Vue
|
||||
// Call custom event
|
||||
this.$emit('navigation:select', this.activeIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called when the sign out button is clicked.
|
||||
*/
|
||||
public signOut()
|
||||
{
|
||||
// Call custom event
|
||||
this.$emit('sign-out');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
:key="course.name">{{course.name}}</el-menu-item>
|
||||
</el-submenu>
|
||||
|
||||
<el-button @click="signOut" id="sign-out-button" type="text">Sign Out</el-button>
|
||||
</el-menu>
|
||||
<div class="line"></div>
|
||||
</div>
|
||||
|
||||
+8
-2
@@ -5,7 +5,13 @@ export default class Constants
|
||||
{
|
||||
/**
|
||||
* Base url for api access
|
||||
* TODO: Use https for actual usage
|
||||
*/
|
||||
public static API_URL: string = 'http://cn2.hydev.org:24021/api';
|
||||
public static API_URL: string = 'https://va.hydev.org/api';
|
||||
|
||||
public static SPLASH: string =
|
||||
'. , ,---. | \n' +
|
||||
'| |. , |---|,---.,---.| , .,---,,---.,---.\n' +
|
||||
' \\ / >< | || |,---|| | | .-\' |---\'| \n' +
|
||||
' `\' \' ` ` \'` \'`---^`---\'`---|\'---\'`---\'` \n' +
|
||||
' v0.1.2.236 `---\' '
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import ElementUI from 'element-ui';
|
||||
const VCharts = require('v-charts');
|
||||
|
||||
import App from './components/app/app.vue';
|
||||
import VueCookies from 'vue-cookies';
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
@@ -12,6 +13,9 @@ Vue.use(ElementUI, {locale: 'en-us'});
|
||||
// Use VCharts
|
||||
Vue.use(VCharts);
|
||||
|
||||
// Use Cookies
|
||||
Vue.use(VueCookies);
|
||||
|
||||
// Init app
|
||||
new Vue({
|
||||
render: (h) => h(App),
|
||||
|
||||
@@ -7,16 +7,15 @@ export default class GraphOverall extends Vue
|
||||
{
|
||||
@Prop({required: true}) chart: any;
|
||||
|
||||
public chartData =
|
||||
private settings =
|
||||
{
|
||||
columns: ['日期', '访问用户', '下单用户', '下单率'],
|
||||
rows: [
|
||||
{ '日期': '1/1', '访问用户': 1393, '下单用户': 1093, '下单率': 0.32 },
|
||||
{ '日期': '1/2', '访问用户': 3530, '下单用户': 3230, '下单率': 0.26 },
|
||||
{ '日期': '1/3', '访问用户': 2923, '下单用户': 2623, '下单率': 0.76 },
|
||||
{ '日期': '1/4', '访问用户': 1723, '下单用户': 1423, '下单率': 0.49 },
|
||||
{ '日期': '1/5', '访问用户': 3792, '下单用户': 3492, '下单率': 0.323 },
|
||||
{ '日期': '1/6', '访问用户': 4593, '下单用户': 4293, '下单率': 0.78 }
|
||||
]
|
||||
};
|
||||
series:
|
||||
{
|
||||
smooth: false
|
||||
},
|
||||
yAxis:
|
||||
{
|
||||
min: 70
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div id="graph-overall">
|
||||
<ve-line :data="chart" :extend="{series: {smooth: false}}"></ve-line>
|
||||
<p>Your average score graph all time:</p>
|
||||
<ve-line :data="chart" :extend="settings"></ve-line>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
|
||||
// Add some margins
|
||||
.el-card
|
||||
{
|
||||
margin: 10px;
|
||||
height: 494px;
|
||||
padding: 0;
|
||||
|
||||
// Vertical center
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.gpa-card
|
||||
{
|
||||
margin-left: 20px;
|
||||
min-width: 136px;
|
||||
}
|
||||
|
||||
.gpa
|
||||
{
|
||||
display: block;
|
||||
}
|
||||
|
||||
.gpa.header
|
||||
{
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.gpa.text
|
||||
{
|
||||
font-size: 35px;
|
||||
font-family: 'Avenir', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.gpa.max
|
||||
{
|
||||
margin-top: -10px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 12px;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.gpa.time
|
||||
{
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
@@ -1,30 +1,39 @@
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import GraphOverall from '@/pages/overall/graph-overall/graph-overall';
|
||||
import {Course} from '@/components/app/app';
|
||||
import {GPAUtils} from '@/utils/gpa-utils';
|
||||
|
||||
@Component({
|
||||
components: {GraphOverall}
|
||||
})
|
||||
export default class Overall extends Vue
|
||||
{
|
||||
@Prop({required: true}) courses: any;
|
||||
// @ts-ignore
|
||||
@Prop({required: true}) courses: Course[];
|
||||
|
||||
/**
|
||||
* Convert assignments list to a graph dataset.
|
||||
*/
|
||||
get convertCharts()
|
||||
{
|
||||
// Null case
|
||||
if (this.courses == null) return [];
|
||||
|
||||
// Filter it
|
||||
let courses: Course[] = this.filterCourses();
|
||||
|
||||
// Compute the column names
|
||||
let columns = ['date'];
|
||||
this.courses.forEach((course: Course) =>
|
||||
courses.forEach(course =>
|
||||
{
|
||||
columns.push(course.name);
|
||||
});
|
||||
|
||||
// Find the min date
|
||||
let minDate: Date = new Date();
|
||||
this.courses.forEach((course: Course) =>
|
||||
courses.forEach(course =>
|
||||
{
|
||||
if (course.assignments.length == 0) return;
|
||||
let date = new Date(course.assignments[course.assignments.length - 1].date);
|
||||
if (date < minDate) minDate = date;
|
||||
});
|
||||
@@ -41,7 +50,7 @@ export default class Overall extends Vue
|
||||
let courseScores: {[index: string]: any} = {};
|
||||
let courseMaxScores: {[index: string]: any} = {};
|
||||
let courseIndexes: {[index: string]: any} = {};
|
||||
this.courses.forEach((course: Course) =>
|
||||
courses.forEach(course =>
|
||||
{
|
||||
courseScores[course.name] = 0;
|
||||
courseMaxScores[course.name] = 0;
|
||||
@@ -56,15 +65,18 @@ export default class Overall extends Vue
|
||||
let row: {[index: string]:any} = {'date': date.toLocaleDateString('en-US')};
|
||||
|
||||
// Loop through courses
|
||||
this.courses.forEach((course: Course) =>
|
||||
courses.forEach(course =>
|
||||
{
|
||||
// Reversed loop through the assignments
|
||||
for (let r = courseIndexes[course.name]; r >= 0; r--)
|
||||
{
|
||||
let assignment = course.assignments[r];
|
||||
let assignmentDate = new Date(assignment.date);
|
||||
|
||||
// If assignment should be displayed
|
||||
if (assignment.complete != 'Complete') continue;
|
||||
|
||||
// Date is being looked at
|
||||
let assignmentDate = new Date(assignment.date);
|
||||
if (assignmentDate.getTime() == date.getTime())
|
||||
{
|
||||
// Record scores
|
||||
@@ -95,4 +107,49 @@ export default class Overall extends Vue
|
||||
rows: rows
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of courses that are graphed
|
||||
*/
|
||||
private filterCourses(): Course[]
|
||||
{
|
||||
// Define result
|
||||
let result: Course[] = [];
|
||||
|
||||
// Filter through courses
|
||||
this.courses.forEach(course =>
|
||||
{
|
||||
// Skip future or past courses
|
||||
if (course.status != 'active') return;
|
||||
|
||||
// Skip courses without levels
|
||||
if (course.level == 'None') return;
|
||||
|
||||
// Skip courses without assignments
|
||||
if (course.assignments.length == 0) return;
|
||||
|
||||
// Add it to the list
|
||||
result.push(course);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called to get gpa since I can't import another
|
||||
* class in the Vue file.
|
||||
*/
|
||||
public getGPA()
|
||||
{
|
||||
let gpa = GPAUtils.getGPA(this.courses);
|
||||
// let result = '' + gpa.gpa;
|
||||
|
||||
/* Not accurate
|
||||
if (!gpa.accurate)
|
||||
{
|
||||
result = `(${result})`;
|
||||
}*/
|
||||
|
||||
return gpa;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
<template>
|
||||
<div id="overall">
|
||||
<p>这是 Overall</p>
|
||||
<graph-overall :chart="convertCharts"></graph-overall>
|
||||
<el-row>
|
||||
<el-col :span="4">
|
||||
<el-card class="gpa-card">
|
||||
<div style="padding: 14px;">
|
||||
<span class="gpa header">GPA:</span>
|
||||
<span class="gpa text">{{getGPA().gpa}}</span>
|
||||
<span class="gpa max">(Out of {{getGPA().max}})</span>
|
||||
<div class="bottom clearfix gpa time">
|
||||
<time>{{ new Date().toDateString() }}</time>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="20">
|
||||
<el-card style="margin-right: 20px">
|
||||
<graph-overall :chart="convertCharts"></graph-overall>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div class=""></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import Constants from '@/constants';
|
||||
|
||||
export class HttpUtils
|
||||
{
|
||||
public token: string = '';
|
||||
|
||||
constructor (token: string)
|
||||
{
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public post(node: string, body: any): Promise<any>
|
||||
{
|
||||
// Add token
|
||||
if (this.token != '') body['token'] = this.token;
|
||||
|
||||
// Create promise
|
||||
return new Promise<any>((resolve, reject) =>
|
||||
{
|
||||
// Fetch request
|
||||
fetch(`${Constants.API_URL}${node}`, {method: 'POST', body: JSON.stringify(body)}).then(res =>
|
||||
{
|
||||
// Get response body text
|
||||
res.text().then(text =>
|
||||
{
|
||||
// Parse response
|
||||
let response = JSON.parse(text);
|
||||
resolve(response);
|
||||
})
|
||||
.catch(reject)
|
||||
})
|
||||
.catch(reject)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ export default class JsonUtils
|
||||
type: assignment.assignment_type,
|
||||
description: assignment.assignment_description,
|
||||
date: assignment._date,
|
||||
complete: assignment.complete_status,
|
||||
complete: assignment.completion_status,
|
||||
include: assignment.include_in_calculated_grade == 1,
|
||||
display: assignment.display_grade == 1,
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports =
|
||||
{
|
||||
devServer:
|
||||
{
|
||||
disableHostCheck: true,
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user