img

Season Study has officially been released and is maintained as open source!

1
https://github.com/cykzht/season-zhixue

Introduction

Season Study – a beautiful grade query website.

Just like its name suggests, it is a program that changes with the seasons, randomly selecting a new season as the theme each time it starts.

img

img

img

img

Program Principles

The overall framework of the website is developed using Python + Flask + HTML + layui. The birth of this project is inseparable from open-source libraries such as zhixuewang, layui, Flask, etc. Thank you to these developers for their contributions to the open-source community.

Web Services

Using Python’s Flask framework to return page information.

1
2
3
4
5
6
7
8
9
from flask import *

app = Flask(__name__, static_url_path='')

@app.route('/', methods=['GET', 'POST'])
def index():
"""Return the login page"""
return render_template('index.html')
app.run(host='0.0.0.0', port=5000)

Season Update

Randomly select a season and return the corresponding colors and background images.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def get_season():
'''
Randomly select a season
slog: Season name
pic: Background image
color: Theme color
'''
ran = random.randint(1, 4)
if (ran == 1):
slog = 'Graduation Network – Autumn'
color = '#e5bc00'
pic = 'images/web_login_bg1.webp'
elif (ran == 2):
slog = 'Graduation Network – Summer'
color = '#50e45c'
pic = 'images/web_login_bg2.webp'
elif (ran == 3):
slog = 'Graduation Network – Spring'
color = '#f1beea'
pic = 'images/web_login_bg3.webp'
elif (ran == 4):
slog = 'Graduation Network – Winter'
color = '#27A9E3'
pic = 'images/web_login_bg4.webp'
return slog, pic, color

User Login

Retrieve the website’s session and user information (from the zhixuewang library).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
from zhixuewang.student import StudentAccount
from zhixuewang.exceptions import UserNotFoundError, UserOrPassError, LoginError, RoleError
import json
import requests

def get_session(username: str, password: str, _type: str = "auto") -> requests.Session:
"""Get session through username and password"""
if len(password) != 32:
password = encode_password(password)
session = get_basic_session()
r = session.get(Url.SSO_URL)
json_obj = json.loads(r.text.strip().replace("\\", "").replace("'", "")[1:-1])
if json_obj["code"] != 1000:
raise LoginError(json_obj["data"])
lt = json_obj["data"]["lt"]
execution = json_obj["data"]["execution"]
r = session.get(Url.SSO_URL,
params={
"encode": "true",
"sourceappname": "tkyh,tkyh",
"_eventId": "submit",
"appid": "zx-container-client",
"client": "web",
"type": "loginByNormal",
"key": _type,
"lt": lt,
"execution": execution,
"customLogoutUrl": "https://www.zhixue.com/login.html",
"username": username,
"password": password
})
json_obj = json.loads(r.text.strip().replace("\\", "").replace("'", "")[1:-1])
if json_obj["code"] != 1001:
if json_obj["code"] == 1002:
raise UserOrPassError()
if json_obj["code"] == 2009:
raise UserNotFoundError()
raise LoginError(json_obj["data"])
ticket = json_obj["data"]["st"]
session.post(Url.SERVICE_URL, data={
"action": "login",
"ticket": ticket,
})
session.cookies.set("uname", base64_encode(username))
session.cookies.set("pwd", base64_encode(password))
return session



def login_student(username: str, password: str) -> StudentAccount:
"""Log in to the student account through username and password"""
session = get_session(username, password)
student = StudentAccount(session)
return student.set_base_info()

zxw = login_student(username, password)

Login Implementation

Using HTML’s form submission to POST requests, returning to Flask for login.

Frontend

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<form class="layui-form" method="post" action="{{url_for('index')}}" autocomplete="on">
<div class="layui-form-item">
<input name="username" class="layui-input" style="height: 45px;" placeholder="Username" required="" type="text">
</div>
<div class="layui-form-item">
<input name="password" class="layui-input" style="height: 45px;" placeholder="Password" required="" type="password">
</div>
<label>Remember username</label>
<input type="checkbox" name="remember_user" id="remember_user" lay-skin="primary">
<div class="layui-form-item">
<p align="center" style="font-size:20px;color:#FF0000">{{ message }}</p>
</div>
<div class="layui-form-item">
<input lay-submit lay-filter="login" class="layui-btn layui-btn-lg layui-btn-normal" value="Login"
style="width:100%;font-size: 18px;height: 48px;" type="submit" name="login">
</div>
</form>

Backend

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from zhixuewang import login_student

if (request.method == 'POST'):
username = request.form.get("username")
password = request.form.get("password")
session['username'] = username
session['password'] = password

username = session.get('username')
password = session.get('password')
exam = request.args.get('exam')
try:
zxw = login_student(username, password)
exams = zxw.get_exams()
except Exception as e:
session.clear()
return render_template("index.html", season=season, message=e)

Exam Retrieval

Call the API to retrieve all exams.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import json
from typing import List, Tuple, Union
from zhixuewang.models import (ExtendedList, Exam, Mark, Subject, SubjectScore,
StuClass, School, Sex, Grade, Phase, ExtraRank, ExamInfo,
StuPerson, StuPersonList)
from zhixuewang.exceptions import UserDefunctError, PageConnectionError, PageInformationError
from json import JSONDecodeError

def get_page_exam(self, page_index: int) -> Tuple[ExtendedList[Exam], bool]:
"""Get the exam list for the specified page number"""
self.update_login_status()
exams: ExtendedList[Exam] = ExtendedList()
r = self._session.get(Url.GET_EXAM_URL,
params={
"pageIndex": page_index,
"pageSize": 10
},
headers=self._get_auth_header())
if not r.ok:
raise PageConnectionError(
f"get_page_exam出错, 状态码为{r.status_code}")
try:
json_data = r.json()["result"]
for exam_data in json_data["examList"]:
exam = Exam(
id=exam_data["examId"],
name=exam_data["examName"]
)
exam.create_time = exam_data["examCreateDateTime"]
exams.append(exam)
hasNextPage: bool = json_data["hasNextPage"]
except (JSONDecodeError, KeyError) as e:
raise PageInformationError(
f"get_page_exam中网页内容发生改变, 错误为{e}, 内容为\n{r.text}")
return exams, hasNextPage

def get_exams(self) -> ExtendedList[Exam]:
"""Get all exams"""

# Cache
if len(self.exams) > 0:
latest_exam = self.get_latest_exam()
if self.exams[0].id == latest_exam.id:
return self.exams

exams: ExtendedList[Exam] = ExtendedList()
i = 1
check = True
while check:
cur_exams, check = self.get_page_exam(i)
exams.extend(cur_exams)
i += 1
self.exams = exams
return exams

Grade Retrieval

Call the API to retrieve the latest grade or the grade for a specified exam (this version is a modified version).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import json
from typing import List, Tuple, Union
from zhixuewang.models import (ExtendedList, Exam, Mark, Subject, SubjectScore,
StuClass, School, Sex, Grade, Phase, ExtraRank, ExamInfo,
StuPerson, StuPersonList)
from zhixuewang.exceptions import UserDefunctError, PageConnectionError, PageInformationError
from json import JSONDecodeError

def __get_self_mark(self, exam: Exam, has_total_score: bool) -> Mark:
self.update_login_status()
mark = Mark(exam=exam, person=self)
r = self._session.get(Url.GET_MARK_URL,
params={"examId": exam.id},
headers=self._get_auth_header())
if not r.ok:
raise PageConnectionError(
f"__get_self_mark中出错, 状态码为{r.status_code}")
try:
json_data = r.json()
json_data = json_data["result"]
# exam.name = json_data["total_score"]["examName"]
# exam.id = json_data["total_score"]["examId"]
for subject in json_data["paperList"]:
subject_score = SubjectScore(
score=subject["userScore"],
subject=Subject(
id=subject["paperId"],
name=subject["subjectName"],
code=subject["subjectCode"],
standard_score=subject["standardScore"],
exam_id=exam.id),
person=StuPerson()
)
# subject_score.create_time = 0
mark.append(subject_score)
total_score = json_data.get("totalScore")
if has_total_score and total_score:
subject_score = SubjectScore(
score=total_score["userScore"],
subject=Subject(
id="",
name=total_score["subjectName"],
code="99",
standard_score=total_score["standardScore"],
exam_id=exam.id,
),
person=StuPerson(),
class_rank=exam.class_rank,
grade_rank=exam.grade_rank
)
# subject_score.create_time = 0
mark.append(subject_score)
self._set_exam_rank(mark)
except (JSONDecodeError, KeyError) as e:
if(not mark):
raise PageInformationError("The grades for this exam cannot be queried")
return mark
return mark

def get_self_mark(self,
exam_data: Union[Exam, str] = "",
has_total_score: bool = True) -> Mark:
"""Get the grade for the specified exam"""
exam = self.get_exam(exam_data)
if exam is None:
return Mark()
return self.__get_self_mark(exam, has_total_score)

exams = zxw.get_exams()
a = zxw.get_self_mark(exams[0])

Table Presentation

Generate a table based on the returned grades.

Frontend

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div class="login">
<center>
<table border="1px" width="300">
<caption style="font-size: 23px">
{{title}}
</caption>
<tr style="background: #ee0000">
<th style="font-size: 20px">Subject</th>
<th style="font-size: 20px">Score</th>
</tr>
{% for row in data %}
<tr>
<th style="font-size: 20px">{{ row[0] }}</th>
<th style="font-size: 20px">{{ row[1] }}</th>
</tr>
{% endfor %}
</table>
</center>
</div>

Backend

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
if len(exams) != 0:
data = []
examlist = []
for i in exams:
data.append(i.id)
examlist.append((i.id, i.name))
if (exam):
if (exam in data):
try:
a = zxw.get_self_mark(exam)
except Exception as e:
return render_template("examlist.html", data=examlist, season=season, avatar=avatar, message="The exam has not been scanned for this session")
msg = a.person.name+'-'+a.exam.name
data = []
for i in a:
if (i.class_rank):
data.append((i.subject.name, i.score, i.class_rank))
else:
data.append((i.subject.name, i.score))
return render_template("zhixue.html", data=data, season=season, title=msg, avatar=avatar)
else:
return render_template("examlist.html", data=examlist, season=season, avatar=avatar, message="This exam cannot be queried")
else:
return render_template("examlist.html", data=examlist, season=season, avatar=avatar)
else:
return render_template("index.html", season=season, message='No exams detected')

Automatic Login

Using layui framework for secondary development. Store the account and password in the user’s device via cookies, and automatically fill them in each time you log in (some browsers do not support cookies).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<script src="js/jquery-3.6.0.min.js"></script>
<script src="js/jquery.cookie.js"></script>
<script src="js/layui.js"></script>

<script>
layui.use(['form'], function () {
var form = layui.form,
layer = layui.layer;

/*Remember username and password*/
if ($.cookie("remember_user")) {
$("#remember_user").prop("checked", true);
form.val("add_form", {
"username": $.cookie("user_name"),
"password": $.cookie("user_password")
})
}
// Perform login operation
form.on('submit(login)', function (data) {
data = data.field;
if (data.username == '') {
layer.msg('Username cannot be empty');
return false;
}
if (data.password == '') {
layer.msg('Password cannot be empty');
return false;
}
//Check remember password
if (data.remember_user == "on") {
var user_name = data.username;
var user_password = data.password;
$.cookie("remember_user", "true", {
expires: 7
}); // Store a cookie with a 7-day expiration
$.cookie("user_name", user_name, {
expires: 7
}); // Store a cookie with a 7-day expiration
$.cookie("user_password", user_password, {
expires: 7
}); // Store a cookie with a 7-day expiration
} else {
$.cookie("remember_user", "false", {
expires: -1
}); // Delete cookie
$.cookie("user_name", '', {
expires: -1
});
$.cookie("user_password", '', {
expires: -1
});
}
return true;
});
});

</script>

Usage Instructions

Required Environment

Install using pip commands.

1
2
pip3 install flask
pip3 install zhixuewang

Enable the Program

  1. After installing the required libraries, run the Python program (ready to use out of the box).

  2. Accesshttp://127.0.0.1:5000/ to visit the Season Study website.

  3. Enter the account and password for a school’s website to query the latest exam results.

Deploy the Program

If you want to run the program on a server, it is recommended to start it using WSGI method.

1
pip3 install gevent

Add the following to the code:

1
2
3
4
5
from gevent import pywsgi

#app.run(host='0.0.0.0', port=5000)
server = pywsgi.WSGIServer(('0.0.0.0', 5000), app)
server.serve_forever()

Or use gunicorn to start.

1
pip3 install gunicorn
1
gunicorn app:app
  • app is the flask startup python file, we use app.py here,
  • app is the flask application instance, we named it app in app.py.