Compare commits
66 Commits
optimization
...
UI
| Author | SHA1 | Date | |
|---|---|---|---|
| 9656b3184b | |||
| 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 |
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": {
|
||||
|
||||
+77
-15
@@ -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[]
|
||||
}
|
||||
@@ -52,39 +60,93 @@ export default class App extends Vue
|
||||
// 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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<login v-if="showLogin" v-on:login:courses="onLogin"></login>
|
||||
<login v-if="showLogin" v-on:login:token="onLogin" :http="http"></login>
|
||||
<navigation :courses="courses" v-on:navigation:select="onNavigate"></navigation>
|
||||
|
||||
<div id="app-content">
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
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.
|
||||
@@ -13,6 +14,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 +40,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 =>
|
||||
{
|
||||
|
||||
@@ -3,9 +3,22 @@
|
||||
<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 !== ''}">
|
||||
</el-input>
|
||||
|
||||
<el-input v-model="password"
|
||||
placeholder="Veracross Password"
|
||||
show-password=""
|
||||
:class="{'input-error': error !== ''}">
|
||||
</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;
|
||||
}
|
||||
}
|
||||
|
||||
+8
-1
@@ -7,5 +7,12 @@ 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' +
|
||||
' v1.1.0 `---\' '
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
|
||||
// Add some margins
|
||||
.el-card
|
||||
{
|
||||
margin: 10px;
|
||||
height: 494px;
|
||||
padding: 0;
|
||||
|
||||
// Vertical center
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.span-gpa-header
|
||||
{
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.span-gpa
|
||||
{
|
||||
font-size: 35px;
|
||||
font-family: 'Avenir', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.gpa-time
|
||||
{
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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}
|
||||
@@ -18,6 +19,9 @@ export default class Overall extends Vue
|
||||
let columns = ['date'];
|
||||
this.courses.forEach((course: Course) =>
|
||||
{
|
||||
// Ignore non-important courses
|
||||
if (course.status != 'active') return;
|
||||
|
||||
columns.push(course.name);
|
||||
});
|
||||
|
||||
@@ -25,6 +29,10 @@ export default class Overall extends Vue
|
||||
let minDate: Date = new Date();
|
||||
this.courses.forEach((course: Course) =>
|
||||
{
|
||||
// Ignore non-important courses
|
||||
if (course.status != 'active') return;
|
||||
|
||||
if (course.assignments.length == 0) return;
|
||||
let date = new Date(course.assignments[course.assignments.length - 1].date);
|
||||
if (date < minDate) minDate = date;
|
||||
});
|
||||
@@ -43,6 +51,9 @@ export default class Overall extends Vue
|
||||
let courseIndexes: {[index: string]: any} = {};
|
||||
this.courses.forEach((course: Course) =>
|
||||
{
|
||||
// Ignore non-important courses
|
||||
if (course.status != 'active') return;
|
||||
|
||||
courseScores[course.name] = 0;
|
||||
courseMaxScores[course.name] = 0;
|
||||
courseIndexes[course.name] = course.assignments.length - 1;
|
||||
@@ -58,6 +69,9 @@ export default class Overall extends Vue
|
||||
// Loop through courses
|
||||
this.courses.forEach((course: Course) =>
|
||||
{
|
||||
// Ignore non-important courses
|
||||
if (course.status != 'active') return;
|
||||
|
||||
// Reversed loop through the assignments
|
||||
for (let r = courseIndexes[course.name]; r >= 0; r--)
|
||||
{
|
||||
@@ -95,4 +109,18 @@ export default class Overall extends Vue
|
||||
rows: rows
|
||||
}
|
||||
}
|
||||
|
||||
public getGPA()
|
||||
{
|
||||
let gpa = GPAUtils.getGPA(this.courses);
|
||||
let result = '' + gpa.gpa;
|
||||
|
||||
/* Not accurate
|
||||
if (!gpa.accurate)
|
||||
{
|
||||
result = `(${result})`;
|
||||
}*/
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
<template>
|
||||
<div id="overall">
|
||||
<p>这是 Overall</p>
|
||||
<graph-overall :chart="convertCharts"></graph-overall>
|
||||
<el-row>
|
||||
<el-col :span="4">
|
||||
<el-card style="margin-left: 20px">
|
||||
<div style="padding: 14px;">
|
||||
<span class="span-gpa-header">GPA:</span>
|
||||
<br>
|
||||
<span class="span-gpa">{{getGPA()}}</span>
|
||||
<div class="bottom clearfix gpa-time">
|
||||
<time class="time">{{ new Date().toDateString() }}</time>
|
||||
<br>
|
||||
<el-button type="text" class="button">Button</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="20">
|
||||
<el-card style="margin-right: 20px">
|
||||
<p>Your average score graph all time:</p>
|
||||
<graph-overall :chart="convertCharts"></graph-overall>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div class=""></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* 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}
|
||||
{
|
||||
// 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.numericGrade == null)
|
||||
{
|
||||
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};
|
||||
}
|
||||
|
||||
// Count total GPA
|
||||
let totalGPA = 0;
|
||||
courses.forEach(course =>
|
||||
{
|
||||
totalGPA += this.getGP(course);
|
||||
});
|
||||
|
||||
// Get average GPA, round to two decimal places
|
||||
let gpa = Math.round(totalGPA / courses.length * 100) / 100;
|
||||
|
||||
// Return results
|
||||
return {gpa: gpa, accurate: accurate};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate GPA for a course
|
||||
*
|
||||
* @param course Course
|
||||
*/
|
||||
public static getGP(course: Course): number
|
||||
{
|
||||
// Find the GPA for this course.
|
||||
for (let scale of this.SCALE)
|
||||
{
|
||||
// Letter grades are the same
|
||||
if (scale[this.LETTER] == course.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)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
devServer: {
|
||||
disableHostCheck: true,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user