[M] Merge Feature > Master: First release

[M] Merge Feature > Master: First release
This commit is contained in:
Hykilpikonna
2019-09-23 19:49:33 -04:00
committed by GitHub
21 changed files with 477 additions and 50 deletions
+5
View File
@@ -10848,6 +10848,11 @@
"resolved": "https://registry.npm.taobao.org/vue-class-component/download/vue-class-component-7.1.0.tgz", "resolved": "https://registry.npm.taobao.org/vue-class-component/download/vue-class-component-7.1.0.tgz",
"integrity": "sha1-sz78sQ4XI21oT3Cx6W8ZRux5Poc=" "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": { "vue-hot-reload-api": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npm.taobao.org/vue-hot-reload-api/download/vue-hot-reload-api-2.3.3.tgz", "resolved": "https://registry.npm.taobao.org/vue-hot-reload-api/download/vue-hot-reload-api-2.3.3.tgz",
+1
View File
@@ -15,6 +15,7 @@
"v-charts": "^1.19.0", "v-charts": "^1.19.0",
"vue": "^2.6.10", "vue": "^2.6.10",
"vue-class-component": "^7.0.2", "vue-class-component": "^7.0.2",
"vue-cookies": "^1.5.13",
"vue-property-decorator": "^8.1.0" "vue-property-decorator": "^8.1.0"
}, },
"devDependencies": { "devDependencies": {
+90 -16
View File
@@ -5,6 +5,7 @@ import Overall from '@/pages/overall/overall';
import Constants from '@/constants'; import Constants from '@/constants';
import JsonUtils from '@/utils/json-utils'; import JsonUtils from '@/utils/json-utils';
import pWaitFor from 'p-wait-for'; import pWaitFor from 'p-wait-for';
import {HttpUtils} from '@/utils/http-utils';
/** /**
* Objects of this interface represent assignment grades. * Objects of this interface represent assignment grades.
@@ -31,6 +32,13 @@ export interface Course
id: number, id: number,
name: string, name: string,
teacherName: string, teacherName: string,
status: string,
letterGrade?: string,
numericGrade?: number,
level: string,
scaleUp: number,
assignments: Grade[] assignments: Grade[]
} }
@@ -47,44 +55,98 @@ export default class App extends Vue
public courses: Course[] = []; public courses: Course[] = [];
// Currently selected tab // Currently selected tab
public selectedTab: string = "overall"; public selectedTab: string = 'overall';
// Are the course assignments loaded from the server. // Are the course assignments loaded from the server.
public assignmentsReady: boolean = false; 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. * 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 // Hide login bar
this.showLogin = false; this.showLogin = false;
// Assign courses // Store token
this.courses = courses; this.token = token;
// Debug output TODO: Remove this // Assign token to http client
console.log(courses); 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 // Get assignments for all the courses
this.courses.forEach(course => this.courses.forEach(course =>
{ {
// Send request to get assignments // 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 // Check success
res.text().then(text => if (response.success)
{ {
// Load assignments
// Parse json and filter it // 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 => .catch(alert);
{
alert(err);
});
}); });
// Wait for assignments to be ready. // Wait for assignments to be ready.
@@ -123,4 +185,16 @@ export default class App extends Vue
// Update selected tab // Update selected tab
this.selectedTab = tab; this.selectedTab = tab;
} }
/**
* Sign out
*/
public signOut()
{
// Clear all cookies
this.$cookies.keys().forEach(key => this.$cookies.remove(key));
// Refresh
window.location.reload();
}
} }
+5 -2
View File
@@ -1,7 +1,10 @@
<template> <template>
<div id="app"> <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> <navigation :courses="courses"
v-on:sign-out="signOut()"
v-on:navigation:select="onNavigate">
</navigation>
<div id="app-content"> <div id="app-content">
<overall :courses="courses" v-if="selectedTab === 'overall' && assignmentsReady"></overall> <overall :courses="courses" v-if="selectedTab === 'overall' && assignmentsReady"></overall>
+25
View File
@@ -72,3 +72,28 @@
width: 100%; 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;
}
+38 -8
View File
@@ -1,5 +1,6 @@
import {Component, Vue} from 'vue-property-decorator'; import {Component, Prop, Vue} from 'vue-property-decorator';
import Constants from '@/constants'; import Constants from '@/constants';
import {HttpUtils} from '@/utils/http-utils';
/** /**
* This component handles user login, and obtains data from the server. * 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 password: any = '';
public loading: boolean = false; 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. * On click, sends username and password to the server.
@@ -22,15 +40,27 @@ export default class Login extends Vue
// Make login button loading // Make login button loading
this.loading = true; this.loading = true;
// Fetch request TODO: Add username and password when the https server is ready. // Fetch request
fetch(`${Constants.API_URL}/veracross/courses`).then(res => (<HttpUtils> this.http).post('/login', {username: this.username, password: this.password})
.then(response =>
{ {
// Get response body text // Check success
res.text().then(text => if (response.success)
{ {
// Call custom event with courses info // Save token to cookies
this.$emit('login:courses', JSON.parse(text)); 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 => .catch(err =>
{ {
+15 -2
View File
@@ -3,9 +3,22 @@
<div class="login-vertical-center"> <div class="login-vertical-center">
<div class="login-panel"> <div class="login-panel">
<img alt="Vue logo" src="../../assets/logo.png"> <img alt="Vue logo" src="../../assets/logo.png">
<h1>Veracross Analyzer</h1> <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> <el-button plain type="primary" @click="onLoginClick" :loading="loading">Login</el-button>
</div> </div>
</div> </div>
+12
View File
@@ -1,5 +1,17 @@
.el-menu.centered li .el-menu.centered li
{ {
display: inline-block !important; display: inline-block !important;
float: none !important; float: none !important;
} }
// Borders
#navigation
{
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
ul
{
border-bottom-width: 0;
}
}
+9
View File
@@ -27,4 +27,13 @@ export default class Navigation extends Vue
// Call custom event // Call custom event
this.$emit('navigation:select', this.activeIndex); 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');
}
} }
+1
View File
@@ -11,6 +11,7 @@
:key="course.name">{{course.name}}</el-menu-item> :key="course.name">{{course.name}}</el-menu-item>
</el-submenu> </el-submenu>
<el-button @click="signOut" id="sign-out-button" type="text">Sign Out</el-button>
</el-menu> </el-menu>
<div class="line"></div> <div class="line"></div>
</div> </div>
+8 -1
View File
@@ -7,5 +7,12 @@ export default class Constants
* Base url for api access * Base url for api access
* TODO: Use https for actual usage * 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.1.0 `---\' '
} }
+4
View File
@@ -3,6 +3,7 @@ import ElementUI from 'element-ui';
const VCharts = require('v-charts'); const VCharts = require('v-charts');
import App from './components/app/app.vue'; import App from './components/app/app.vue';
import VueCookies from 'vue-cookies';
Vue.config.productionTip = false; Vue.config.productionTip = false;
@@ -12,6 +13,9 @@ Vue.use(ElementUI, {locale: 'en-us'});
// Use VCharts // Use VCharts
Vue.use(VCharts); Vue.use(VCharts);
// Use Cookies
Vue.use(VueCookies);
// Init app // Init app
new Vue({ new Vue({
render: (h) => h(App), render: (h) => h(App),
@@ -7,16 +7,15 @@ export default class GraphOverall extends Vue
{ {
@Prop({required: true}) chart: any; @Prop({required: true}) chart: any;
public chartData = private settings =
{ {
columns: ['日期', '访问用户', '下单用户', '下单率'], series:
rows: [ {
{ '日期': '1/1', '访问用户': 1393, '下单用户': 1093, '下单率': 0.32 }, smooth: false
{ '日期': '1/2', '访问用户': 3530, '下单用户': 3230, '下单率': 0.26 }, },
{ '日期': '1/3', '访问用户': 2923, '下单用户': 2623, '下单率': 0.76 }, yAxis:
{ '日期': '1/4', '访问用户': 1723, '下单用户': 1423, '下单率': 0.49 }, {
{ '日期': '1/5', '访问用户': 3792, '下单用户': 3492, '下单率': 0.323 }, min: 70
{ '日期': '1/6', '访问用户': 4593, '下单用户': 4293, '下单率': 0.78 } }
] }
};
} }
@@ -1,6 +1,7 @@
<template> <template>
<div id="graph-overall"> <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> </div>
</template> </template>
+29
View File
@@ -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;
}
+59 -6
View File
@@ -1,30 +1,36 @@
import {Component, Prop, Vue} from 'vue-property-decorator'; import {Component, Prop, Vue} from 'vue-property-decorator';
import GraphOverall from '@/pages/overall/graph-overall/graph-overall'; import GraphOverall from '@/pages/overall/graph-overall/graph-overall';
import {Course} from '@/components/app/app'; import {Course} from '@/components/app/app';
import {GPAUtils} from '@/utils/gpa-utils';
@Component({ @Component({
components: {GraphOverall} components: {GraphOverall}
}) })
export default class Overall extends Vue export default class Overall extends Vue
{ {
@Prop({required: true}) courses: any; // @ts-ignore
@Prop({required: true}) courses: Course[];
get convertCharts() get convertCharts()
{ {
// Null case // Null case
if (this.courses == null) return []; if (this.courses == null) return [];
// Filter it
let courses: Course[] = this.filterCourses();
// Compute the column names // Compute the column names
let columns = ['date']; let columns = ['date'];
this.courses.forEach((course: Course) => courses.forEach(course =>
{ {
columns.push(course.name); columns.push(course.name);
}); });
// Find the min date // Find the min date
let minDate: Date = new 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); let date = new Date(course.assignments[course.assignments.length - 1].date);
if (date < minDate) minDate = date; if (date < minDate) minDate = date;
}); });
@@ -41,7 +47,7 @@ export default class Overall extends Vue
let courseScores: {[index: string]: any} = {}; let courseScores: {[index: string]: any} = {};
let courseMaxScores: {[index: string]: any} = {}; let courseMaxScores: {[index: string]: any} = {};
let courseIndexes: {[index: string]: any} = {}; let courseIndexes: {[index: string]: any} = {};
this.courses.forEach((course: Course) => courses.forEach(course =>
{ {
courseScores[course.name] = 0; courseScores[course.name] = 0;
courseMaxScores[course.name] = 0; courseMaxScores[course.name] = 0;
@@ -56,15 +62,18 @@ export default class Overall extends Vue
let row: {[index: string]:any} = {'date': date.toLocaleDateString('en-US')}; let row: {[index: string]:any} = {'date': date.toLocaleDateString('en-US')};
// Loop through courses // Loop through courses
this.courses.forEach((course: Course) => courses.forEach(course =>
{ {
// Reversed loop through the assignments // Reversed loop through the assignments
for (let r = courseIndexes[course.name]; r >= 0; r--) for (let r = courseIndexes[course.name]; r >= 0; r--)
{ {
let assignment = course.assignments[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 // Date is being looked at
let assignmentDate = new Date(assignment.date);
if (assignmentDate.getTime() == date.getTime()) if (assignmentDate.getTime() == date.getTime())
{ {
// Record scores // Record scores
@@ -95,4 +104,48 @@ export default class Overall extends Vue
rows: rows 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 as a string.
*/
public getGPA()
{
let gpa = GPAUtils.getGPA(this.courses);
let result = '' + gpa.gpa;
/* Not accurate
if (!gpa.accurate)
{
result = `(${result})`;
}*/
return result;
}
} }
+22 -2
View File
@@ -1,7 +1,27 @@
<template> <template>
<div id="overall"> <div id="overall">
<p>这是 Overall</p> <el-row>
<graph-overall :chart="convertCharts"></graph-overall> <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">
<graph-overall :chart="convertCharts"></graph-overall>
</el-card>
</el-col>
</el-row>
<div class=""></div>
</div> </div>
</template> </template>
+101
View File
@@ -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 || course.numericGrade == 0)
{
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;
}
}
+35
View File
@@ -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)
});
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ export default class JsonUtils
type: assignment.assignment_type, type: assignment.assignment_type,
description: assignment.assignment_description, description: assignment.assignment_description,
date: assignment._date, date: assignment._date,
complete: assignment.complete_status, complete: assignment.completion_status,
include: assignment.include_in_calculated_grade == 1, include: assignment.include_in_calculated_grade == 1,
display: assignment.display_grade == 1, display: assignment.display_grade == 1,
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
devServer: {
disableHostCheck: true,
}
}