diff --git a/app.py b/app.py index 30bd1d5..724f993 100644 --- a/app.py +++ b/app.py @@ -1,227 +1,148 @@ from functions import * from manage import manage from upload import upload +from login import login load_dotenv() app = Flask(__name__) +babel = Babel(app) app.register_blueprint(manage) app.register_blueprint(upload) +app.register_blueprint(login) app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY') +app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://'+os.environ.get('MYSQL_USER')+':'+os.environ.get('MYSQL_PASSWORD')+'@'+os.environ.get('MYSQL_HOST')+'/attendance' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +app.config['BABEL_DEFAULT_LOCALE'] = 'zh_TW' +app.jinja_env.add_extension('jinja2.ext.loopcontrols') +sdb = SQLAlchemy(app) - -@app.after_request -def add_header(response): - response.headers['SameSite'] = "Strict" - return response - - -@ app.route('/', methods=['GET', 'POST']) -def index(): - if request.method == 'GET': - if check_login_status(): - return render_template('login.html') - return redirect('/select') - elif request.method == 'POST': - email = request.form['username'] - if check_login_status(): - try: - if (verify_recaptcha("")): - user = auth.sign_in_with_email_and_password( - email, request.form['password']) - usrData = db.child("Users").child(user['localId']).child("permission").get( - user['idToken']).val() - if (usrData == 'realPerson'): - print("RealPerson Login SUCC:", email, flush=True) - session['is_logged_in'] = True - session['email'] = user['email'] - session['uid'] = user['localId'] - session['token'] = user['idToken'] - session['refreshToken'] = user['refreshToken'] - session['loginTime'] = datetime.now(tz) - return redirect('/select') - if (usrData == 'admin'): - print("Admin Login SUCC:", email, flush=True) - session['subuser_type'] = 'admin' - session['is_logged_in'] = True - session['email'] = user['email'] - session['uid'] = user['localId'] - session['token'] = user['idToken'] - session['refreshToken'] = user['refreshToken'] - session['loginTime'] = datetime.now(tz) - session['showUpload'] = db.child("Users").child( - session['uid']).child("showUpload").get(session['token']).val() - return redirect('/manage') - raise Exception("not real person or admin") - else: - print("ReC Error:", email, flush=True) - flash( - 'reCAPTCHA 錯誤,請稍後再試一次
reCAPTCHA Failed. Please try again later.') - return redirect('/') - except Exception as e: - print("Error*Login:", email, str(e), flush=True) - flash( - '帳號或密碼錯誤,請重新輸入
Incorrect username or password') - return redirect('/') - else: - return redirect('/select') - - -@app.route('/select', methods=['GET', 'POST']) -def selSubUser(): - if check_login_status(): - session.clear() - flash("Timeout. 遇時,請重新登入") +class DefaultModelView(ModelView): + restricted = True + def __init__(self, model, session, restricted=True, name=None, category=None, endpoint=None, url=None, **kwargs): + self.restricted = restricted + for k, v in kwargs.items(): + setattr(self, k, v) + setattr(self, 'can_export', True) + super(DefaultModelView, self).__init__(model, session, name=name, category=category, endpoint=endpoint, url=url) + def is_accessible(self): + if self.restricted == True: + return ((not check_login_status()) and is_admin() and check_permission()) + return ((not check_login_status()) and is_admin()) + def inaccessible_callback(self, name, **kwargs): + return redirect('/') +class MyAdminIndexView(AdminIndexView): + def is_accessible(self): + return ((not check_login_status()) and is_admin()) + def inaccessible_callback(self, name, **kwargs): return redirect('/') - refresh_token() - if 'subuser_type' in session and session['subuser_type'] == 'admin': - return redirect('/manage') - if request.method == 'GET': - usrData = db.child("Users").child(session['uid']).get( - session['token']).val() - session['subuser_type'] = '' - return render_template('selSubUser.html', data=usrData['accounts'], name=usrData['name']) - else: - data = request.form['subuser_sel'].split('^') - try: - if (verify_recaptcha("")): - if (data[0] == 'homeroom'): - session['homeroom'] = data[1] + '^' + data[2] - session['subuser_type'] = 'homeroom' - elif (data[0] == 'group'): - session['category'] = data[1] - session['class'] = data[2] - session['subuser_type'] = 'group' - return redirect('/manage') - else: - print("ReC Error:", data, flush=True) - flash( - 'reCAPTCHA 錯誤,請稍後再試一次
reCAPTCHA Failed. Please try again later.') - return redirect('/select') - except Exception as e: - print("Error*select:", session['email'], str(json.loads(e.args[1])[ - 'error']['message']), flush=True) - flash(str(json.loads(e.args[1])[ - 'error']['message'])) - return redirect('/select') - - -@app.route('/chgPassword', methods=['POST', 'GET']) -def chgPassword(): - data = {} - if request.method == 'GET': - if not check_login_status(): - refresh_token() - return render_template('chgPassword.html') - else: - return abort(404) - elif request.method == 'POST': - oldEmail = session['email'] - delUser = False - if not check_login_status(): - refresh_token() - try: - if (verify_recaptcha("")): - oldUsr = auth.sign_in_with_email_and_password( - oldEmail, request.form['password']) - print("chgPwd oldUser:", oldEmail, flush=True) - old = {} - old['uid'] = oldUsr['localId'] - old['token'] = oldUsr['idToken'] - data = db.child("Users").child( - oldUsr['localId']).get(oldUsr['idToken']).val() - - auth.delete_user_account(oldUsr['idToken']) - delUser = True - - newUsr = auth.create_user_with_email_and_password( - request.form['new_username'], request.form['new_password']) - db.child("Users").child(newUsr['localId']).set( - data, newUsr['idToken']) - db.child("Users").child(oldUsr['localId']).remove(oldUsr['idToken']) - session.clear() - flash( - '修改密碼成功,請重新登入
Password changed successfully. Please login again.') - return redirect('/') - else: - print("ReC Error:", oldEmail, flush=True) - flash( - 'reCAPTCHA 錯誤,請稍後再試一次
reCAPTCHA Failed. Please try again later.') - return redirect('/chgPassword') - except Exception as e: - if delUser: - try: - usr = auth.create_user_with_email_and_password( - oldEmail, request.form['password']) - db.child("Users").child(usr['localId']).set( - data, usr['idToken']) - except: - pass - print("Error*chgPassword:", oldEmail, str(json.loads(e.args[1])[ - 'error']['message']), flush=True) - flash(str(json.loads(e.args[1])[ - 'error']['message'])) - return redirect('/chgPassword') - - -@app.route('/forgotPassword', methods=['GET', 'POST']) -def forgotPassword(): - if request.method == 'GET': - return render_template('forgotPassword.html') - elif request.method == 'POST': - email = request.form['username'] - try: - if (verify_recaptcha("")): - auth.send_password_reset_email(email) - print("forgotPassword email sent:", email, flush=True) - flash( - '重置密碼信件已寄出,請至信箱收取
Password reset email has been sent to your email. Please check your email.') - return redirect('/') - else: - print("ReC Error:", email, flush=True) - flash( - 'reCAPTCHA 錯誤,請稍後再試一次
reCAPTCHA Failed. Please try again later.') - return redirect('/forgotPassword') - except Exception as e: - print("Error*forgotPassword:", email, str(json.loads(e.args[1])[ - 'error']['message']), flush=True) - flash(str(json.loads(e.args[1])[ - 'error']['message'])) - return redirect('/forgotPassword') - - -@app.route('/resetPassword', methods=['GET', 'POST']) -def resetPassword(): - if request.args.get('oobCode') is None: - return abort(404) - if request.method == 'GET': - return render_template('verifiedChgPassword.html', oobCode=request.args.get('oobCode')) - else: - try: - if (verify_recaptcha("")): - auth.verify_password_reset_code( - request.args.get('oobCode'), request.form['password']) - print("resetPassword success:", flush=True) - session.clear() - flash('重置密碼成功,請重新登入
Password reset success. Please login again.') - return redirect('/') - else: - print("ReC Error:", flush=True) - flash( - 'reCAPTCHA 錯誤,請稍後再試一次
reCAPTCHA Failed. Please try again later.') - return redirect('/resetPassword') - except Exception as e: - print("Error*resetPassword:", request.args.get('oobCode'), str(json.loads(e.args[1])[ - 'error']['message']), flush=True) - flash(str(json.loads(e.args[1])[ - 'error']['message'])) - return redirect('/resetPassword?mode=resetPassword&oobCode=' + request.args.get('oobCode')) - - -@ app.route('/logout', methods=['GET']) -def logout(): - session.clear() - return redirect('/') +admin = Admin( + app, + name='Attendance 點名系統 後台管理', + template_mode='bootstrap3', + index_view=MyAdminIndexView(), + ) +class Users(sdb.Model): + id = sdb.Column(sdb.INT, primary_key=True) + email = sdb.Column(sdb.Text) + name = sdb.Column(sdb.Text) + oldUsername = sdb.Column(sdb.Text) + role = sdb.Column(sdb.CHAR) + password = sdb.Column(sdb.Text) +class Students(sdb.Model): + id = sdb.Column(sdb.INT, primary_key=True) + email = sdb.Column(sdb.INT) + grade = sdb.Column(sdb.INT) + class_ = sdb.Column(sdb.INT) + num = sdb.Column(sdb.INT) + name = sdb.Column(sdb.Text) + ename = sdb.Column(sdb.Text) + classes = sdb.Column(sdb.Text) + password = sdb.Column(sdb.Text) +class Schedule(sdb.Model): + id = sdb.Column(sdb.INT, primary_key=True) + grade = sdb.Column(sdb.INT) + class_ = sdb.Column(sdb.INT) + dow = sdb.Column(sdb.INT) + period = sdb.Column(sdb.CHAR) + subject = sdb.Column(sdb.Text) + teacher = sdb.Column(sdb.Text) +class SpecSchedule(sdb.Model): + __tablename__ = 'specschedule' + id = sdb.Column(sdb.INT, primary_key=True) + grade = sdb.Column(sdb.INT) + class_ = sdb.Column(sdb.INT) + date = sdb.Column(sdb.VARCHAR(11)) + period = sdb.Column(sdb.CHAR) + subject = sdb.Column(sdb.Text) + teacher = sdb.Column(sdb.Text) +class GPClasses(sdb.Model): + __tablename__ = 'gpclasses' + id = sdb.Column(sdb.INT, primary_key=True) + category = sdb.Column(sdb.Text) + subclass = sdb.Column(sdb.Text) + about = sdb.Column(sdb.Text) + accs = sdb.Column(sdb.Text) +class Homerooms(sdb.Model): + id = sdb.Column(sdb.INT, primary_key=True) + grade = sdb.Column(sdb.INT) + class_ = sdb.Column(sdb.INT) + accs = sdb.Column(sdb.Text) +class Submission(sdb.Model): + id = sdb.Column(sdb.INT, primary_key=True) + grade = sdb.Column(sdb.INT) + class_ = sdb.Column(sdb.INT) + date = sdb.Column(sdb.VARCHAR(11)) + period = sdb.Column(sdb.CHAR) + signature = sdb.Column(sdb.Text) + ds1 = sdb.Column(sdb.INT) + ds2 = sdb.Column(sdb.INT) + ds3 = sdb.Column(sdb.INT) + ds4 = sdb.Column(sdb.INT) + ds5 = sdb.Column(sdb.INT) + ds6 = sdb.Column(sdb.INT) + ds7 = sdb.Column(sdb.INT) + notes = sdb.Column(sdb.Text) +class DS(sdb.Model): + id = sdb.Column(sdb.INT, primary_key=True) + grade = sdb.Column(sdb.INT) + class_ = sdb.Column(sdb.INT) + num = sdb.Column(sdb.INT) + date = sdb.Column(sdb.VARCHAR(11)) + period = sdb.Column(sdb.CHAR) + note = sdb.Column(sdb.Text) + status = sdb.Column(sdb.CHAR, default='X') +class Dates(sdb.Model): + id = sdb.Column(sdb.INT, primary_key=True) + date = sdb.Column(sdb.VARCHAR(11)) + dow = sdb.Column(sdb.INT) +class Absent(sdb.Model): + id = sdb.Column(sdb.INT, primary_key=True) + grade = sdb.Column(sdb.INT) + class_ = sdb.Column(sdb.INT) + num = sdb.Column(sdb.INT) + date = sdb.Column(sdb.VARCHAR(11)) + period = sdb.Column(sdb.CHAR) + status = sdb.Column(sdb.CHAR) + note = sdb.Column(sdb.Text) +class Forgot(sdb.Model): + id = sdb.Column(sdb.INT, primary_key=True) + resetID = sdb.Column(sdb.VARCHAR(11)) + email = sdb.Column(sdb.Text) + reqTime = sdb.Column(sdb.VARCHAR(20)) +admin.add_view(DefaultModelView(Users, sdb.session, restricted=False, column_exclude_list = ['password'], column_searchable_list = ['name', 'email', 'role'])) +admin.add_view(DefaultModelView(Students, sdb.session, restricted=False, column_exclude_list = ['password'], column_searchable_list = ['grade', 'class_', 'num', 'email','name', 'ename', 'classes'])) +admin.add_view(DefaultModelView(Schedule, sdb.session, column_searchable_list = ['grade', 'class_', 'dow', 'period', 'subject', 'teacher'])) +admin.add_view(DefaultModelView(SpecSchedule, sdb.session, restricted=False, column_searchable_list = ['grade', 'class_', 'date', 'period', 'subject', 'teacher'])) +admin.add_view(DefaultModelView(GPClasses, sdb.session, column_searchable_list = ['category', 'subclass', 'about', 'accs'])) +admin.add_view(DefaultModelView(Homerooms, sdb.session, column_searchable_list = ['grade', 'class_', 'accs'])) +admin.add_view(DefaultModelView(Submission, sdb.session, column_exclude_list=['signature'], column_searchable_list = ['grade', 'class_', 'date', 'period', 'notes'])) +admin.add_view(DefaultModelView(DS, sdb.session, restricted=False, column_searchable_list = ['grade', 'class_', 'date', 'period', 'num', 'note', 'status'])) +admin.add_view(DefaultModelView(Dates, sdb.session, column_searchable_list = ['date', 'dow'])) +admin.add_view(DefaultModelView(Absent, sdb.session, restricted=False, column_searchable_list = ['grade', 'class_', 'date', 'period', 'num', 'status', 'note'])) +admin.add_view(DefaultModelView(Forgot, sdb.session, column_searchable_list = ['resetID', 'email', 'reqTime'])) +admin.add_link(MenuLink(name='Back to Home 返回一般管理', category='', url='/manage')) +admin.add_link(MenuLink(name='Logout 登出', category='', url='/logout')) if __name__ == '__main__': - app.run(debug=True) + app.run(debug=True, host='0.0.0.0', port=80) diff --git a/db.sql b/db.sql new file mode 100644 index 0000000..fae6e55 --- /dev/null +++ b/db.sql @@ -0,0 +1,152 @@ +USE attendance; +CREATE TABLE users ( + id INT NOT NULL AUTO_INCREMENT, + PRIMARY KEY (id), + email TEXT, + name TEXT, + oldUsername TEXT, + role CHAR, /* S: SuperAdmin / A: Admin / R: Regular User */ + password TEXT +); + +CREATE TABLE students ( + id INT NOT NULL AUTO_INCREMENT, + PRIMARY KEY (id), + email TEXT, + grade INT, + class_ INT, + num INT, + name TEXT, + ename TEXT, + classes TEXT, + password TEXT + /* update column as wishes for group classes + ALTER TABLE students ADD COLUMN IF NOT EXISTS "" VARCHAR(255); */ +); + +--- Schedule of different day for different class +CREATE TABLE schedule ( + id INT NOT NULL AUTO_INCREMENT, + PRIMARY KEY (id), + grade INT, + class_ INT, + dow INT, + period CHAR, + subject TEXT, + teacher TEXT +); + +CREATE TABLE specschedule ( + id INT NOT NULL AUTO_INCREMENT, + PRIMARY KEY (id), + grade INT, + class_ INT, + date TEXT, + period CHAR, + subject TEXT, + teacher TEXT +); + +CREATE TABLE gpclasses ( + id INT NOT NULL AUTO_INCREMENT, + PRIMARY KEY (id), + category TEXT, + subclass TEXT, + about TEXT, + accs TEXT + /* Save as JSON + { + 0: 'acc1', + 1: 'acc2' + } + */ +); + +CREATE TABLE homerooms ( + id INT NOT NULL AUTO_INCREMENT, + PRIMARY KEY (id), + grade INT, + class_ INT, + accs TEXT + /* Save as JSON + { + 0: 'acc1', + 1: 'acc2' + } + */ +); + +DELETE FROM submission WHERE grade=9; +CREATE TABLE submission ( + id INT NOT NULL AUTO_INCREMENT, + PRIMARY KEY (id), + grade INT, + class_ INT, + date VARCHAR(11), + period CHAR, + signature LONGTEXT, + /* + Save as JSON + { + subClass: "signature", + subClass2: "signature2" + } + or + plain text if not GP + */ + ds1 INT DEFAULT 5, + ds2 INT DEFAULT 5, + ds3 INT DEFAULT 5, + ds4 INT DEFAULT 5, + ds5 INT DEFAULT 5, + ds6 INT DEFAULT 5, + ds7 INT DEFAULT 5, + notes TEXT + /* + Save as JSON + { + 'num': 'whatevernote', + 'num2': 'morenote' + } + */ +); + +CREATE TABLE ds ( + id INT NOT NULL AUTO_INCREMENT, + PRIMARY KEY (id), + grade INT, + class_ INT, + date VARCHAR(11), + period CHAR, + num INT, + note TEXT, + status CHAR DEFAULT 'X' +); + +CREATE TABLE dates ( + id INT NOT NULL AUTO_INCREMENT, + PRIMARY KEY (id), + date VARCHAR(11), + dow INT +); + +CREATE TABLE absent ( + id INT NOT NULL AUTO_INCREMENT, + PRIMARY KEY (id), + grade INT, + class_ INT, + date VARCHAR(11), + period CHAR, + num INT, + status CHAR, /* L: 遲到 / K: 曠課 / G: 事假 / S: 病假 / F: 喪假 / P: 疫情假 / O: 公假*/ + note TEXT +); + +CREATE TABLE forgot ( + id INT NOT NULL AUTO_INCREMENT, + PRIMARY KEY (id), + userType CHAR, /* T: teacher / S: student */ + resetID VARCHAR(11), + email TEXT, + reqTime VARCHAR(20) +); \ No newline at end of file diff --git a/functions.py b/functions.py index 2be0eb9..dd9f39a 100644 --- a/functions.py +++ b/functions.py @@ -1,7 +1,6 @@ from flask import * from typing import OrderedDict from flask import * -import pyrebase from datetime import datetime import pytz import os @@ -9,32 +8,65 @@ import base64 import csv import os import pandas as pd -from random import randint +from random import randint, choices +import string from dotenv import load_dotenv +from passlib.hash import sha256_crypt +import mysql.connector +import requests +from pprint import pprint +from flask_sqlalchemy import SQLAlchemy +from flask_admin import Admin, AdminIndexView, expose +from flask_admin.menu import MenuLink +from flask_admin.contrib.sqla import ModelView +from flask_babelex import Babel load_dotenv() -config = { - "apiKey": os.environ.get('apiKey'), - "authDomain": os.environ.get('authDomain'), - "databaseURL": os.environ.get('databaseURL'), - "storageBucket": os.environ.get('storageBucket'), - "serviceAccount": os.environ.get('serviceAccount'), - "messagingSenderId": os.environ.get('messagingSenderId'), - "appId": os.environ.get('appId'), - "measurementId": os.environ.get('measurementId'), -} -firebase = pyrebase.initialize_app(config) -db = firebase.database() -auth = firebase.auth() -storage = firebase.storage() tz = pytz.timezone('Asia/Taipei') +DSBOARD = [ + "上課前秩序", + "上課前禮貌", + "課間秩序", + "板擦清潔", + "講桌乾淨", + "地板整齊", + "桌椅整齊" +] +DSTEXT = [ + "", + "定", + "心", + "", + "專", + "案", + "" +] +DSOFFENSES = { + 'A': "把玩物品、不專心聽講", + 'B': "書寫或傳遞紙條、物品", + 'C': "自言自語或與同學交談", + 'D': "接話、大聲笑、起哄、發出怪聲", + 'E': "亂動、逗弄同學、影響教學情境", + 'F': "閱讀與該堂課無關之書籍", + 'G': "不敬師長、態度傲慢", + 'H': "其他經任教老師糾正、制止之行為", + 'Z': "上課睡覺" +} + +def refresh_db(): + return mysql.connector.connect(user=os.environ.get('MYSQL_USER'), password=os.environ.get('MYSQL_PASSWORD'), + host=os.environ.get('MYSQL_HOST'), + database='attendance') + +def genHash(password): + return sha256_crypt.hash(password) + +def verifyPassword(password, hash): + return sha256_crypt.verify(password, hash) def refresh_token(): - user = auth.refresh(session['refreshToken']) session['is_logged_in'] = True - session['token'] = user['idToken'] - session['refreshToken'] = user['refreshToken'] session['loginTime'] = datetime.now(tz) @@ -47,6 +79,25 @@ def check_login_status(): session['is_logged_in'] == False or (datetime.now(tz) - session['loginTime']).total_seconds() > 3600) +def send_email(to, subject, text): + return requests.post( + "https://api.mailgun.net/v3/mg.aaronlee.tech/messages", + auth=("api", os.environ.get("MG_APIKEY")), + data={"from": "Attendance 點名系統 ", + "to": [to], + "subject": subject, + "html": text}) + +def getName(grade, class_, number): + db = refresh_db() + cursor = db.cursor() + print(grade, class_, number) + cursor.execute("SELECT name FROM students WHERE grade=%s AND class=%s AND number=%s", (grade, class_, number)) + name = cursor.fetchone() + cursor.close() + db.close() + return name[0] + # LOGIN @@ -63,20 +114,14 @@ def verify_recaptcha(response): return r.json()['success'] # UPLOAD - +def is_admin(): + return 'subuser_type' in session and session['subuser_type'] == 'admin' def check_permission(): - return (db.child('Users').child(session['uid']).child('permission').get(session['token']).val() == 'admin' and - db.child("Users").child(session['uid']).child("showUpload").get(session['token']).val() == '1') - - -def addZeroesUntil(str, number): - if len(str) >= number: - return str + if 'subuser_type' in session and session['subuser_type'] == 'admin': + return session['showUpload'] else: - str = str + '0' - return addZeroesUntil(str, number) - + return False # MANAGE def removeprefix(s, prefix): diff --git a/login.py b/login.py new file mode 100644 index 0000000..b72c6d2 --- /dev/null +++ b/login.py @@ -0,0 +1,329 @@ +from functions import * +login = Blueprint('login', __name__) +@login.after_request +def add_header(response): + response.headers['SameSite'] = "Strict" + return response + + +@login.route('/', methods=['GET', 'POST']) +def index(): + if request.method == 'GET': + if check_login_status(): + return render_template('login.html') + return redirect('/select') + elif request.method == 'POST': + email = request.form['username'] + if check_login_status(): + try: + if (verify_recaptcha("")): + if request.form['user_type'] == 'teacher': + db = refresh_db() + cursor = db.cursor(buffered=True) + cursor.execute("SELECT name, role, oldUsername, password FROM users WHERE email = %s", (email,)) + user = cursor.fetchone() + cursor.close() + if user == None or not verifyPassword(request.form['password'], user[3]): + raise Exception('Invalid Login') + usrRole = user[1] + if (usrRole == 'R'): + print("RealPerson Login SUCC:", email, flush=True) + session['is_logged_in'] = True + session['email'] = email + session['name'] = user[0] + session['oldUsername'] = user[2] + session['loginTime'] = datetime.now(tz) + return redirect('/select') + if (usrRole == 'A' or usrRole == 'S'): + print("Admin Login SUCC:", email, flush=True) + session['subuser_type'] = 'admin' + session['is_logged_in'] = True + session['email'] = email + session['oldUsername'] = user[2] + session['loginTime'] = datetime.now(tz) + session['showUpload'] = True if usrRole == 'S' else False + return redirect('/manage') + raise Exception("not real person or admin") + elif request.form['user_type'] == 'student': + db = refresh_db() + cursor = db.cursor(buffered=True) + cursor.execute("SELECT password, grade, class_, num, name FROM students WHERE email = %s", (email,)) + user = cursor.fetchone() + cursor.close() + if user == None or not verifyPassword(request.form['password'], user[0]): + raise Exception('Invalid Login') + print("Student Login SUCC:", email, flush=True) + session['is_logged_in'] = True + session['email'] = email + session['loginTime'] = datetime.now(tz) + session['user_type'] = 'student' + session['name'] = user[4] + session['grade'] = user[1] + session['class'] = user[2] + session['num'] = user[3] + return redirect('/manage') + else: + print("ReC Error:", email, flush=True) + flash( + 'reCAPTCHA 錯誤,請稍後再試一次
reCAPTCHA Failed. Please try again later.') + return redirect('/') + except Exception as e: + print("Error*Login:", email, str(e), flush=True) + flash( + '帳號或密碼錯誤,請重新輸入
Incorrect username or password') + return redirect('/') + else: + return redirect('/select') + + +@login.route('/select', methods=['GET', 'POST']) +def selSubUser(): + if check_login_status(): + session.clear() + flash("Timeout. 遇時,請重新登入") + return redirect('/') + refresh_token() + if 'subuser_type' in session and session['subuser_type'] == 'admin' or 'user_type' in session and session['user_type'] == 'student': + return redirect('/manage') + if request.method == 'GET': + db = refresh_db() + cursor = db.cursor(buffered=True) + cursor.execute("SELECT category, subclass FROM gpclasses WHERE accs LIKE %s LIMIT 1", ('%'+session['oldUsername']+'%',)) + classes = cursor.fetchone() + cursor.close() + hasGroup = False + if classes != None: + hasGroup = True + db = refresh_db() + cursor = db.cursor(buffered=True) + cursor.execute("SELECT grade, class_ FROM homerooms WHERE accs LIKE %s", ('%'+session['oldUsername']+'%',)) + homerooms = cursor.fetchall() + cursor.close() + hrC = {} + for h in homerooms: + hrC[h[0]] = [] + hrC[h[0]].append(h[1]) + return render_template('selSubUser.html', group=hasGroup, homeroom=hrC, name=session['name']) + else: + data = request.form['subuser_sel'].split('^') + if data == []: + return redirect('/select') + try: + if (verify_recaptcha("")): + if (data[0] == 'homeroom'): + session['homeroom'] = data[1] + '^' + data[2] + session['subuser_type'] = 'homeroom' + elif (data[0] == 'group'): + session['subuser_type'] = 'group' + return redirect('/manage') + else: + print("ReC Error:", data, flush=True) + flash( + 'reCAPTCHA 錯誤,請稍後再試一次
reCAPTCHA Failed. Please try again later.') + return redirect('/select') + except Exception as e: + print("Error*select:", session['email'], str(e), flush=True) + flash(str(e)) + return redirect('/select') + + +@login.route('/chgPassword', methods=['POST', 'GET']) +def chgPassword(): + data = {} + if request.method == 'GET': + if not check_login_status(): + refresh_token() + return render_template('chgPassword.html') + else: + return abort(404) + elif request.method == 'POST': + oldEmail = session['email'] + if not check_login_status(): + refresh_token() + try: + if (verify_recaptcha("")): + db = refresh_db() + cursor = db.cursor(buffered=True) + if ('user_type' in session and session['user_type'] == 'student'): + cursor.execute("SELECT password FROM students WHERE email = %s", (oldEmail,)) + else: + cursor.execute("SELECT password FROM users WHERE email = %s", (oldEmail,)) + user = cursor.fetchone() + cursor.close() + if user == None or not verifyPassword(request.form['password'], user[0]): + raise Exception('Invalid Login') + if len(request.form['new_password']) < 6: + raise Exception('密碼長度不足
Password not long enough') + db = refresh_db() + cursor = db.cursor(buffered=True) + if (request.form['new_username'] != oldEmail and request.form['new_username'] != ''): + if ('user_type' in session and session['user_type'] == 'student'): + cursor.execute("SELECT * FROM students WHERE email = %s", (request.form['new_username'],)) + else: + cursor.execute("SELECT * FROM users WHERE email = %s", (request.form['new_username'],)) + user = cursor.fetchone() + cursor.close() + if user != None: + raise Exception('帳號已被使用
Username already used') + db = refresh_db() + cursor = db.cursor(buffered=True) + if ('user_type' in session and session['user_type'] == 'student'): + cursor.execute("UPDATE students SET password = %s WHERE email = %s", (genHash(request.form['new_password']), oldEmail)) + if (request.form['new_username'] != oldEmail and request.form['new_username'] != ''): + cursor.execute("UPDATE students SET email = %s WHERE email = %s", (request.form['new_username'], oldEmail)) + else: + cursor.execute("UPDATE users SET password = %s WHERE email = %s", ( + genHash(request.form['new_password']), oldEmail)) + if (request.form['new_username'] != oldEmail and request.form['new_username'] != ''): + cursor.execute("UPDATE users SET email = %s WHERE email = %s", (request.form['new_username'], oldEmail)) + db.commit() + cursor.close() + session.clear() + if (request.form['new_username'] != oldEmail and request.form['new_username'] != ''): + send_email(oldEmail, "Email Changed 信箱已更改", + """
+ Your email was changed at %s to %s.
+ If you did not change your email, please contact the student affair's office immediately. +
+ 你的信箱已在 %s 更改為 %s。
+ 如果你沒有更改信箱,請立即聯絡學務處。 +
+ This email was sent automatically. Please do not reply.
+ 這個郵件是自動發送的,請不要回覆。
+ """ % (str(datetime.now(tz)), request.form['new_username'], str(datetime.now(tz)), request.form['new_username'])) + flash( + '修改密碼成功,請重新登入
Password changed successfully. Please login again.') + return redirect('/') + else: + print("ReC Error:", oldEmail, flush=True) + flash( + 'reCAPTCHA 錯誤,請稍後再試一次
reCAPTCHA Failed. Please try again later.') + return redirect('/chgPassword') + except Exception as e: + flash(str(e)) + return redirect('/chgPassword') + + +@login.route('/forgotPassword', methods=['GET', 'POST']) +def forgotPassword(): + if request.method == 'GET': + return render_template('forgotPassword.html') + elif request.method == 'POST': + email = request.form['username'] + try: + if (verify_recaptcha("")): + db = refresh_db() + cursor = db.cursor(buffered=True) + if (request.form['user_type'] == 'student'): + cursor.execute("SELECT * FROM students WHERE email = %s", (email,)) + elif (request.form['user_type'] == 'teacher'): + cursor.execute("SELECT email FROM users WHERE email = %s", (email,)) + user = cursor.fetchone() + cursor.close() + if user == None: + raise Exception('無此 Email
Invalid Email') + exists = True + while exists: + resetID = ''.join(choices(string.ascii_lowercase + string.digits, k=10)) + cursor = db.cursor(buffered=True) + cursor.execute("SELECT * FROM forgot WHERE resetID = %s", (resetID,)) + user = cursor.fetchone() + cursor.close() + exists = (user != None) + db = refresh_db() + cursor = db.cursor(buffered=True) + cursor.execute(""" + INSERT INTO forgot (resetID, email, reqTime, userType) + VALUES (%s, %s, %s, %s) + """, (resetID, email, datetime.strftime(datetime.now(tz), '%Y-%m-%d %H:%M:%S'), 'T' if request.form['user_type'] == 'teacher' else 'S')) + db.commit() + cursor.close() + send_email(email, "Password Reset 重置密碼", + """
+ Please go to the following link to reset your password:
+ https://abs.aaronlee.tech/resetPassword?resetCode=%s
+ If you did not request a password reset, please ignore this email. +
+ 請點選以下連結重置密碼:
+ https://abs.aaronlee.tech/resetPassword?resetCode=%s
+ 如果您沒有要求重置密碼,請忽略此郵件。 +
+ This email was sent automatically. Please do not reply.
+ 這個郵件是自動發送的,請不要回覆。
+ """ % (resetID, resetID)) + print("forgotPassword email sent:", email, flush=True) + flash( + '重置密碼信件已寄出,請至信箱收取
Password reset email has been sent to your email. Please check your email.') + return redirect('/') + else: + print("ReC Error:", email, flush=True) + flash( + 'reCAPTCHA 錯誤,請稍後再試一次
reCAPTCHA Failed. Please try again later.') + return redirect('/forgotPassword') + except Exception as e: + print("Error*forgotPassword:", email, str(escape), flush=True) + flash(str(e)) + return redirect('/forgotPassword') + + +@login.route('/resetPassword', methods=['GET', 'POST']) +def resetPassword(): + if request.args.get('resetCode') is None: + return abort(404) + if request.method == 'GET': + return render_template('verifiedChgPassword.html', resetCode=request.args.get('resetCode')) + else: + try: + if (verify_recaptcha("")): + db = refresh_db() + cursor = db.cursor(buffered=True) + cursor.execute(""" + SELECT resetID, email, reqTime, userType + FROM forgot + WHERE resetID = %s + """, (request.args.get('resetCode'),)) + user = cursor.fetchone() + cursor.close() + if user == None: + raise Exception('無此重置密碼代碼
Invalid reset password code') + if (datetime.now(tz) - datetime.strptime(user[2], '%Y-%m-%d %H:%M:%S')).seconds > 3600: + cursor.execute("DELETE FROM forgot WHERE resetID = %s", (user[0],)) + db.commit() + cursor.close() + raise Exception('重置密碼代碼已過期
Reset password code expired') + if len(request.form['password']) < 6: + raise Exception('密碼長度不足
Password not long enough') + db = refresh_db() + cursor = db.cursor(buffered=True) + if (user[3] == 'T'): + cursor.execute("UPDATE users SET password = %s WHERE email = %s", ( + genHash(request.form['password']), user[1])) + elif (user[3] == 'S'): + cursor.execute("UPDATE students SET password = %s WHERE email = %s", ( + genHash(request.form['password']), user[1])) + db.commit() + cursor.close() + db = refresh_db() + cursor = db.cursor(buffered=True) + cursor.execute("DELETE FROM forgot WHERE resetID = %s", (user[0],)) + db.commit() + cursor.close() + session.clear() + flash( + '重置密碼成功,請重新登入
Password changed successfully. Please login again.') + return redirect('/') + else: + print("ReC Error:", flush=True) + flash( + 'reCAPTCHA 錯誤,請稍後再試一次
reCAPTCHA Failed. Please try again later.') + return redirect('/resetPassword') + except Exception as e: + print("Error*resetPassword:", request.args.get('resetCode'), str(e), flush=True) + flash(str(e)) + return redirect('/resetPassword?resetCode=' + request.args.get('resetCode')) + + +@login.route('/logout', methods=['GET']) +def logout(): + session.clear() + return redirect('/') diff --git a/manage.py b/manage.py index 12f6b95..d3d3d1d 100644 --- a/manage.py +++ b/manage.py @@ -6,137 +6,230 @@ manage = Blueprint('manage', __name__) def manageProcess(fCommand, fData): if (check_login_status()): return redirect('/logout') - # this is to fix a bug where pyrebase doesnt load the first request - db.child("Users").child( - session['uid']).child("permission").get(session['token']).val() - # end bug fix refresh_token() + if 'user_type' in session and session['user_type'] == 'student': + return redirect('/student') pl = session['subuser_type'] if pl == 'admin': - homerooms = db.child("Homerooms").get(session['token']).val() + db = refresh_db() + cursor = db.cursor() + cursor.execute("SELECT grade, class_ FROM homerooms ORDER BY grade ASC, class_ ASC") + homeroomsSQL = cursor.fetchall() + homerooms = {} + for h in homeroomsSQL: + if h[0] in homerooms: + homerooms[h[0]].append(h[1]) + else: + homerooms[h[0]] = [h[1]] currRoom = [] if fCommand == "admin": currRoom = fData[0].split("^") else: - for i in homerooms: - currRoom.append(i) - for j in homerooms[i]: - currRoom.append(j) - break - break - homeroomData = homerooms[currRoom[0]][currRoom[1]] - absData = homeroomData["Absent"] - homeroomData.pop('Absent') - if 'placeholder' in homeroomData: - homeroomData.pop('placeholder') + currRoom = [homeroomsSQL[0][0], homeroomsSQL[0][1]] + cursor = db.cursor() + cursor.execute("SELECT num,name,ename,classes FROM students WHERE grade=%s AND class_=%s ORDER BY num ASC", (currRoom[0], currRoom[1])) + students = cursor.fetchall() + studGP = {} + for s in students: + studGP[s[0]] = json.loads(s[3]) + cursor = db.cursor() + cursor.execute("SELECT date FROM dates ORDER BY date ASC") + dates = cursor.fetchall() currDate = "" if fCommand != "": currDate = fData[1] else: - for i in absData: - currDate = i - if i >= datetime.now(tz).strftime("%Y-%m-%d"): + for i in dates: + currDate = i[0] + if i[0] >= datetime.now(tz).strftime("%Y-%m-%d"): break - return render_template('admin.html', homerooms=homerooms, absData=absData, - homeroomCode=currRoom, homeroomData=homeroomData, currDate=currDate, periods=['m', '1', '2', '3', '4', - 'n', '5', '6', '7', '8', '9'], showUpload=session['showUpload']) - elif pl == 'group': - cateData = db.child("Classes").child( - "GP_Class").child(session['category']).get(session['token']).val() - cclass = { - "name": cateData['Class'][session['class']]['name'], - "category": session['category'], - "class_id": session['class'] - } - homerooms = cateData['Homerooms'] - currDate = "" - confirmed = [] - absData = {} - for h in homerooms: - h = h.split('^') - hrData = db.child("Homerooms").child( - h[0]).child(h[1]).get(session['token']).val() - tmpAbsData = hrData['Absent'] - hrData.pop('Absent') - if 'placeholder' in hrData: - hrData.pop('placeholder') - periods = [] - dow = "" - if currDate == "": - if fCommand == 'date': - currDate = fData - for j in tmpAbsData[currDate]: - if j == "dow": - dow = tmpAbsData[currDate][j] - continue - elif j == "confirm": - confirmed.append([h[0], h[1]]) - continue - elif j == "notes": - continue - if (tmpAbsData[currDate][j]['name'] == 'GP' and - tmpAbsData[currDate][j]['teacher'] == cclass['category']): - periods.append(j) - - else: - for i in tmpAbsData: - currDate = i - if i >= datetime.now(tz).strftime("%Y-%m-%d"): - tmp = False - for j in tmpAbsData[i]: - if j == "dow": - dow = tmpAbsData[i][j] - continue - elif j == "confirm": - confirmed.append([h[0], h[1]]) - continue - elif j == 'notes': - continue - if (tmpAbsData[i][j]['name'] == 'GP' and - tmpAbsData[i][j]['teacher'] == cclass['category']): - periods.append(j) - tmp = True - if tmp == True: - break + cursor = db.cursor() + cursor.execute("SELECT dow FROM dates WHERE date=%s", (currDate, )) + dow = cursor.fetchone()[0] + cursor = db.cursor() + cursor.execute("SELECT period, subject, teacher FROM schedule WHERE grade=%s AND class_=%s AND dow=%s", (currRoom[0], currRoom[1], dow)) + scheduleSQL = cursor.fetchall() + schedule = {} + for i in scheduleSQL: + schedule[i[0]] = { + "subject": i[1], + "teacher": i[2], + } + cursor = db.cursor() + cursor.execute("SELECT period, subject, teacher FROM specschedule WHERE grade=%s AND class_=%s AND date=%s", (currRoom[0], currRoom[1], currDate)) + specScheduleSQL = cursor.fetchall() + for i in specScheduleSQL: + schedule[i[0]] = { + "subject": i[1], + "teacher": i[2], + "special": True + } + cursor = db.cursor() + cursor.execute("SELECT period, signature, notes, ds1,ds2,ds3,ds4,ds5,ds6,ds7 FROM submission WHERE grade=%s AND class_=%s AND date=%s", (currRoom[0], currRoom[1], currDate)) + submissionSQL = cursor.fetchall() + submission = {} + cursor = db.cursor() + cursor.execute("SELECT period, num, note FROM ds WHERE grade=%s AND class_=%s AND date=%s", (currRoom[0], currRoom[1], currDate)) + idvDSSQL = cursor.fetchall() + idvDS = {} + for i in idvDSSQL: + if i[0] not in idvDS: + idvDS[i[0]] = {} + idvDS[i[0]][i[1]]= i[2] + for i in submissionSQL: + if i[0] == 'c': + submission[i[0]] = { + "signature": i[1], + "notes": i[2] + } + elif schedule[i[0]]["subject"] == "GP": + submission[i[0]] = OrderedDict() + signatures = json.loads(i[1]) + for j in signatures: + submission[i[0]][j] = { + "signature": signatures[j], + } + submission[i[0]]["notes"] = i[2] else: - for j in tmpAbsData[currDate]: - if j == "dow": - dow = tmpAbsData[currDate][j] + submission[i[0]] = { + "signature": i[1], + "notes": i[2], + "ds1": i[3], + "ds2": i[4], + "ds3": i[5], + "ds4": i[6], + "ds5": i[7], + "ds6": i[8], + "ds7": i[9] + } + cursor = db.cursor() + cursor.execute("SELECT period, num, status, note FROM absent WHERE grade=%s AND class_=%s AND date=%s", (currRoom[0], currRoom[1], currDate)) + absentDataSQL = cursor.fetchall() + absentData = {} + for p in ['m', '1', '2', '3', '4', 'n', '5', '6', '7', '8', '9']: + absentData[p] = {} + for i in absentDataSQL: + absentData[i[0]][i[1]] = i[2] + for i in absentDataSQL: + absentData[i[0]][i[1]] = { + 'status': i[2], + 'note': i[3], + } + return render_template('admin.html', homerooms=homerooms, currRoom=currRoom, students=students, currDate=currDate, schedule=schedule, submission=submission, studGP=studGP, idvDS=idvDS, + dates=dates, absentData=absentData, periods=['m', '1', '2', '3', '4', 'n', '5', '6', '7', '8', '9'], showUpload=session['showUpload'], dsboard=DSBOARD, dstext=DSTEXT, dsoffenses=DSOFFENSES) + # 'n', '5', '6', '7', '8', '9'], showUpload=session['showUpload']) + elif pl == 'group': + db = refresh_db() + cursor = db.cursor() + cursor.execute("SELECT category, subclass FROM gpclasses WHERE accs LIKE %s", ('%'+session['oldUsername']+'%',)) + gpclasses = cursor.fetchall() + data = {} + currDate = "" + dow = "" + cursor = db.cursor() + cursor.execute("SELECT date FROM dates ORDER BY date ASC") + dates = cursor.fetchall() + if fCommand != "": + currDate = fData + else: + for i in dates: + currDate = i[0] + if i[0] >= datetime.now(tz).strftime("%Y-%m-%d"): + break + cursor = db.cursor() + cursor.execute("SELECT dow FROM dates WHERE date=%s", (currDate, )) + dow = cursor.fetchone()[0] + + for c in gpclasses: + cursor.execute("SELECT about FROM gpclasses WHERE subclass=%s AND category=%s", + (c[1], c[0])) + cclass = { + "name": cursor.fetchone()[0], + "category": c[0], + "class_id": c[1] + } + data[cclass['category'] + ' ' + cclass['class_id']] = { + "cdata": cclass, + } + # get student list + cursor.execute("SELECT grade,class_,num,name,ename FROM students WHERE classes LIKE " + '\'%\"'+ cclass['category'] + '\": \"' + cclass['class_id'] +'\"%\'' + " ORDER BY grade ASC,class_ ASC,num ASC") + students = cursor.fetchall() + # get student homerooms + homerooms = [] + for x in students: + if (str(x[0]) + '^' + str(x[1])) not in homerooms: + homerooms.append(str(x[0]) + '^' + str(x[1])) + # get periods + for h in homerooms: + hs = h.split('^') + cursor.execute("SELECT period FROM schedule WHERE grade=%s AND class_=%s AND dow=%s AND teacher=%s", (hs[0], hs[1], dow, cclass['category'])) + scheduleSQL = cursor.fetchall() + cursor.execute("SELECT period FROM specschedule WHERE grade=%s AND class_=%s AND date=%s AND teacher=%s", (hs[0], hs[1], currDate, cclass['category'])) + specNTPSQL = cursor.fetchall() + for s in specNTPSQL: + scheduleSQL.append(s) + cursor.execute("SELECT period FROM specschedule WHERE grade=%s AND class_=%s AND date=%s AND teacher!=%s", (hs[0], hs[1], currDate, cclass['category'])) + specNTDSQL = cursor.fetchall() + specNTD = {} + for i in specNTDSQL: + specNTD[i[0]] = True + print(h, specNTD, scheduleSQL) + for p in scheduleSQL: + if p[0] in specNTD and specNTD[p[0]] == True: continue - elif j == "confirm": - confirmed.append([h[0], h[1]]) - continue - elif j == "notes": - continue - if (tmpAbsData[currDate][j]['name'] == 'GP' and - tmpAbsData[currDate][j]['teacher'] == cclass['category']): - periods.append(j) - for p in periods: - if not p in absData: - absData[p] = {} - for p in periods: - if not h[0] in absData[p]: - absData[p][h[0]] = {} - absData[p][h[0]][h[1]] = {} - if 'notes' in tmpAbsData[currDate][p]: - absData[p][h[0]][h[1] - ]['notes'] = tmpAbsData[currDate][p]['notes'] - for num in hrData: - if (cclass['category'] in hrData[num]['GP_Class'] and - hrData[num]['GP_Class'][cclass['category']] == cclass['class_id']): - for p in periods: - absData[p][h[0]][h[1]][num] = { - "name": hrData[num]['name'], - "eng_name": hrData[num]['eng_name'], - "alr_fill": (('signature' in tmpAbsData[currDate][p]) and - (cclass['class_id'] in tmpAbsData[currDate][p]['signature'] or 'STUD_AFFAIR_OFFICE' in tmpAbsData[currDate][p]['signature'])), - "absent": False if not num in tmpAbsData[currDate][p] else tmpAbsData[currDate][p][num] - } - return render_template('group_teach.html', dateKeys=sorted(tmpAbsData.keys()), cclass=cclass, absData=absData, dow=dow, currDate=currDate, tmpAbsData=tmpAbsData, confirmed=confirmed) + if p[0] not in data[cclass['category'] + ' ' + cclass['class_id']]: + data[cclass['category'] + ' ' + cclass['class_id']][p[0]] = {} + if (h not in data[cclass['category'] + ' ' + cclass['class_id']][p[0]]): + data[cclass['category'] + ' ' + cclass['class_id']][p[0]][h] = {} + cursor = db.cursor() + cursor.execute("SELECT signature FROM submission WHERE grade=%s AND class_=%s AND date=%s AND period=%s", (hs[0], hs[1], currDate, p[0])) + submissionSQL = cursor.fetchone() + submitted = False + try: + if submissionSQL[0] == 'STUD_AFFAIR_OFFICE': + submitted = True + except: + pass + try: + signatures = json.loads(submissionSQL[0]) + if cclass['class_id'] in signatures: + submitted = True + except: + pass + hrCfrm = False + if not submitted: + cursor = db.cursor() + cursor.execute("SELECT signature FROM submission WHERE grade=%s AND class_=%s AND date=%s AND period='c'", (hs[0], hs[1], currDate)) + hrCfrm = True if cursor.fetchone() != None else submitted + cursor = db.cursor() + cursor.execute("SELECT num, status, note FROM absent WHERE grade=%s AND class_=%s AND date=%s AND period=%s", (hs[0], hs[1], currDate, p[0])) + absentDataSQL = cursor.fetchall() + for x in students: + if (str(x[0])==hs[0] and str(x[1])==hs[1]): + studStatus = [item for item in absentDataSQL if item[0] == x[2]] + status = "" + if submitted: + if studStatus == []: + status = 'present' + else: + status = studStatus[0][1] + else: + if studStatus == []: + if hrCfrm: + status = '--' + else: + status = 'na' + else: + status = studStatus[0][1] + data[cclass['category'] + ' ' + cclass['class_id']][p[0]][h][x[2]] = { + "name": x[3], + "ename": x[4], + "status": status, + "note": '' if studStatus == [] else studStatus[0][2], + } + return render_template('group_teach.html', dates=dates, currDate=currDate, data=data, dsoffenses=DSOFFENSES) elif pl == 'homeroom': - homeroom = session['homeroom'].split('^') - homeroomData = db.child("Homerooms").child(homeroom[0]).child( - homeroom[1]).get(session['token']).val() + db = refresh_db() times = OrderedDict({ 'm': '00:00', '1': '08:15', @@ -158,20 +251,96 @@ def manageProcess(fCommand, fData): currTime <= times[next_item(times, i)]): currPeriod = i break - absData = homeroomData["Absent"] - homeroomData.pop('Absent') - if 'placeholder' in homeroomData: - homeroomData.pop('placeholder') + currRoom = session['homeroom'].split('^') + cursor = db.cursor() + cursor.execute("SELECT num,name,ename,classes FROM students WHERE grade=%s AND class_=%s ORDER BY num ASC", (currRoom[0], currRoom[1])) + students = cursor.fetchall() + studGP = {} + for s in students: + studGP[s[0]] = json.loads(s[3]) + cursor = db.cursor() + cursor.execute("SELECT date FROM dates ORDER BY date ASC") + dates = cursor.fetchall() currDate = "" - if fCommand == 'date': + if fCommand != "": currDate = fData else: - for i in absData: - currDate = i - if i >= datetime.now(tz).strftime("%Y-%m-%d"): + for i in dates: + currDate = i[0] + if i[0] >= datetime.now(tz).strftime("%Y-%m-%d"): break - return render_template('homeroom.html', absData=absData, homeroomCode=homeroom, homeroomData=homeroomData, - currDate=currDate, dateKeys=sorted(absData.keys()), periods=['m', '1', '2', '3', '4', 'n', '5', '6', '7', '8', '9'], currPeriod=currPeriod) + cursor = db.cursor() + cursor.execute("SELECT dow FROM dates WHERE date=%s", (currDate, )) + dow = cursor.fetchone()[0] + cursor = db.cursor() + cursor.execute("SELECT period, subject, teacher FROM schedule WHERE grade=%s AND class_=%s AND dow=%s", (currRoom[0], currRoom[1], dow)) + scheduleSQL = cursor.fetchall() + schedule = {} + for i in scheduleSQL: + schedule[i[0]] = { + "subject": i[1], + "teacher": i[2], + } + cursor = db.cursor() + cursor.execute("SELECT period, subject, teacher FROM specschedule WHERE grade=%s AND class_=%s AND date=%s", (currRoom[0], currRoom[1], currDate)) + specScheduleSQL = cursor.fetchall() + for i in specScheduleSQL: + schedule[i[0]] = { + "subject": i[1], + "teacher": i[2], + "special": True + } + cursor = db.cursor() + cursor.execute("SELECT period, signature, notes, ds1,ds2,ds3,ds4,ds5,ds6,ds7 FROM submission WHERE grade=%s AND class_=%s AND date=%s", (currRoom[0], currRoom[1], currDate)) + submissionSQL = cursor.fetchall() + cursor = db.cursor() + cursor.execute("SELECT period, num, note FROM ds WHERE grade=%s AND class_=%s AND date=%s", (currRoom[0], currRoom[1], currDate)) + idvDSSQL = cursor.fetchall() + idvDS = {} + for i in idvDSSQL: + if i[0] not in idvDS: + idvDS[i[0]] = {} + idvDS[i[0]][i[1]]= i[2] + submission = {} + for i in submissionSQL: + if i[0] == 'c': + submission[i[0]] = { + "signature": i[1], + "notes": i[2] + } + elif schedule[i[0]]["subject"] == "GP": + submission[i[0]] = OrderedDict() + signatures = json.loads(i[1]) + for j in signatures: + submission[i[0]][j] = { + "signature": signatures[j], + } + submission[i[0]]["notes"] = i[2] + else: + submission[i[0]] = { + "signature": i[1], + "notes": i[2], + "ds1": i[3], + "ds2": i[4], + "ds3": i[5], + "ds4": i[6], + "ds5": i[7], + "ds6": i[8], + "ds7": i[9], + } + cursor = db.cursor() + cursor.execute("SELECT period, num, status, note FROM absent WHERE grade=%s AND class_=%s AND date=%s", (currRoom[0], currRoom[1], currDate)) + absentDataSQL = cursor.fetchall() + absentData = {} + for p in ['m', '1', '2', '3', '4', 'n', '5', '6', '7', '8', '9']: + absentData[p] = {} + for i in absentDataSQL: + absentData[i[0]][i[1]] = { + 'status': i[2], + 'note': i[3], + } + return render_template('homeroom.html', currRoom=currRoom, students=students, currDate=currDate, schedule=schedule, submission=submission, currPeriod=currPeriod, studGP=studGP, + dates=dates, absentData=absentData, periods=['m', '1', '2', '3', '4', 'n', '5', '6', '7', '8', '9'], dsboard=DSBOARD, dstext=DSTEXT, dsoffenses=DSOFFENSES, idvDS=idvDS) else: return redirect('/logout') @@ -194,102 +363,206 @@ def manage_admin(g, r, date): ] return manageProcess("admin", data) +@manage.route('/student', methods=['GET']) +def showStudentAbs(): + if (check_login_status()): + return redirect('/logout') + refresh_token() + if not ('user_type' in session and session['user_type'] == 'student'): + return redirect('/') + db = refresh_db() + cursor = db.cursor() + cursor.execute("SELECT date, period, num, status, note FROM absent WHERE grade=%s AND class_=%s AND num=%s ORDER BY date DESC, period DESC, num ASC", (session['grade'], session['class'], session['num'])) + absentDataSQL = cursor.fetchall() + return render_template("list.html", title="Student Absent List | 學生缺勤紀錄", mode='STUDABS', data=absentDataSQL, currRoom=[session['grade'],session['class']], name=session['name'], num=session['num']) + +@manage.route('/student/ds', methods=['GET']) +def showStudentDS(): + if (check_login_status()): + return redirect('/logout') + refresh_token() + if not ('user_type' in session and session['user_type'] == 'student'): + return redirect('/') + db = refresh_db() + cursor = db.cursor() + cursor.execute("SELECT date, period, num, note FROM ds WHERE grade=%s AND class_=%s AND num=%s ORDER BY date DESC, period DESC, num ASC", (session['grade'], session['class'], session['num'])) + dsDataSQL = cursor.fetchall() + print(dsDataSQL) + return render_template("list.html", title="Student DS List | 學生定心紀錄", mode='STUDDS', data=dsDataSQL, currRoom=[session['grade'],session['class']], name=session['name'], num=session['num']) + +@manage.route('/manage/abs', methods=['GET']) +def showAllAbs(): + if (check_login_status()): + return redirect('/logout') + refresh_token() + currRoom = session['homeroom'].split('^') + db = refresh_db() + cursor = db.cursor() + cursor.execute("SELECT num,name,ename FROM students WHERE grade=%s AND class_=%s ORDER BY num ASC", (currRoom[0], currRoom[1])) + studentsSQL = cursor.fetchall() + students = {} + for st in studentsSQL: + students[st[0]] = { + 'name': st[1], + 'ename': st[2], + } + cursor = db.cursor() + cursor.execute("SELECT date, period, num, status, note FROM absent WHERE grade=%s AND class_=%s ORDER BY date DESC, period DESC, num ASC", (currRoom[0], currRoom[1])) + absentDataSQL = cursor.fetchall() + return render_template("list.html", title="Absent List | 缺勤紀錄", mode='ABS', students=students, data=absentDataSQL, currRoom=currRoom) + +@manage.route('/manage/ds', methods=['GET']) +def showAllDS(): + if (check_login_status()): + return redirect('/logout') + refresh_token() + currRoom = session['homeroom'].split('^') + db = refresh_db() + cursor = db.cursor() + cursor.execute("SELECT num,name,ename FROM students WHERE grade=%s AND class_=%s ORDER BY num ASC", (currRoom[0], currRoom[1])) + studentsSQL = cursor.fetchall() + students = {} + for st in studentsSQL: + students[st[0]] = { + 'name': st[1], + 'ename': st[2], + } + cursor = db.cursor() + cursor.execute("SELECT date, period, num, note FROM ds WHERE grade=%s AND class_=%s ORDER BY date DESC, period DESC, num ASC", (currRoom[0], currRoom[1])) + dsDataSQL = cursor.fetchall() + return render_template("list.html", title="DS List | 定心紀錄", mode='DS', students=students, data=dsDataSQL, currRoom=currRoom) @manage.route('/manage/group_teach_publish', methods=['POST']) def group_teach_publish(): if (check_login_status()): return redirect('/logout') refresh_token() + data = request.form.to_dict() cclass = { - "name": db.child("Classes").child("GP_Class").child(session['category']).child( - "Class").child(session['class']).child("name").get(session['token']).val(), - "category": session['category'], - "class_id": session['class'], - "homerooms": db.child("Classes").child( - "GP_Class").child(session['category']).child("Homerooms").get(session['token']).val() + "category": data.pop('category'), + "class_id": data.pop('class_id') } - date = request.form['date'] - period = request.form['period'] - signature = request.form['signatureData'] - formData = request.form.to_dict() - notes = "" - if 'notes' in request.form: - notes = request.form['notes'] - formData.pop('notes') - signature = removeprefix(signature, 'data:image/png;base64,') - signature = bytes(signature, 'utf-8') - rand = str(date + '^' + cclass['category'] + - '^' + cclass['class_id'] + '^' + period) - rand += ".png" - with open(os.path.join('temp', rand), "wb") as fh: - fh.write(base64.decodebytes(signature)) - storage.child(os.path.join('signatures', rand) - ).put(os.path.join('temp', rand), session['token']) - formData.pop('signatureData') - formData.pop('date') - formData.pop('period') - for i in formData: - i = i.split('^') - db.child("Homerooms").child(i[1]).child(i[2]).child( - "Absent").child(date).child(period).update({i[3]: int(i[0])}, session['token']) - for h in cclass['homerooms']: - h = h.split('^') - if "confirm" in db.child("Homerooms").child(h[0]).child(h[1]).child("Absent").child(date).get(session['token']).val(): + db = refresh_db() + cursor = db.cursor() + cursor.execute("SELECT about FROM gpclasses WHERE category=%s AND subclass=%s", + (cclass['category'], cclass['class_id'])) + cclass["name"] = cursor.fetchone()[0] + cursor.execute("SELECT grade,class_,num,name,ename FROM students WHERE classes LIKE " + '\'%\"'+ cclass['category'] + '\": \"' + cclass['class_id'] +'\"%\'' + " ORDER BY grade ASC,class_ ASC,num ASC") + students = cursor.fetchall() + homerooms = [] + for x in students: + if (str(x[0]) + '^' + str(x[1])) not in homerooms: + homerooms.append(str(x[0]) + '^' + str(x[1])) + data.pop('dsnumbers') + data.pop('dsoffense') + data.pop('dsoffenseother') + date = data.pop('date') + period = data.pop('period') + signature = data.pop('signatureData') + notes = data.pop('notes') + absentData = [] + dsData = [] + for x in data: + xs = x.split('^') + if xs[0] == 'note': continue - db.child("Homerooms").child(h[0]).child(h[1]).child( - "Absent").child(date).child(period).child("signature").update({cclass['class_id']: str(storage.child(os.path.join('signatures', rand)).get_url(None))}, session['token']) - db.child("Homerooms").child(h[0]).child(h[1]).child( - "Absent").child(date).child(period).child("names").child(cclass['class_id']).set(cclass['name'], session['token']) - currPeriodData = db.child("Homerooms").child(h[0]).child(h[1]).child( - "Absent").child(date).child(period).get(session['token']).val() - if 'notes' in currPeriodData: - db.child("Homerooms").child(h[0]).child(h[1]).child( - "Absent").child(date).child(period).update({'notes': currPeriodData['notes']+'; '+notes}, session['token']) + elif xs[0] == 'ds': + dsData.append([xs[1], xs[2].split('-')[0], xs[2].split('-')[1], data[x]]) else: - db.child("Homerooms").child(h[0]).child(h[1]).child( - "Absent").child(date).child(period).update({'notes': notes}, session['token']) - os.remove(os.path.join('temp', rand)) + absentData.append([xs[1], xs[2], xs[3], 'K' if xs[0] == '1' else 'L', data['note^'+xs[1]+'^'+xs[2]+'^'+xs[3]]]) + for h in homerooms: + h = h.split('^') + cursor = db.cursor() + cursor.execute(""" + SELECT signature, notes FROM submission WHERE grade=%s AND class_=%s AND date=%s AND period=%s + """, (h[0], h[1], date, period)) + one = cursor.fetchone() + if one is None: + jSignature = json.dumps({cclass['class_id']: signature}) + cursor.execute(""" + INSERT INTO submission (grade, class_, date, period, signature, notes) + VALUES (%s, %s, %s, %s, %s, %s) + """, (h[0], h[1], date, period, jSignature, notes)) + db.commit() + else: + jSignature = json.loads(one[0]) + if cclass['class_id'] in jSignature: + continue + jSignature[cclass['class_id']] = signature + note = one[1] + '; ' + notes + cursor.execute(""" + UPDATE submission SET signature=%s, notes=%s WHERE grade=%s AND class_=%s AND date=%s AND period=%s + """, (json.dumps(jSignature), note, h[0], h[1], date, period)) + db.commit() + for d in dsData: + cursor = db.cursor() + cursor.execute(""" + INSERT INTO ds (grade, class_, num, date, period, note) + VALUES (%s, %s, %s, %s, %s, %s) + """, (d[0], d[1], d[2], date, period, d[3])) + db.commit() + for a in absentData: + cursor = db.cursor() + cursor.execute(""" + INSERT INTO absent (grade, class_, num, date, period, status, note) + VALUES (%s, %s, %s, %s, %s, %s, %s) + """, (a[0], a[1], a[2], date, period, a[3], a[4])) + db.commit() return redirect('/manage') - @manage.route('/manage/homeroom_abs', methods=['POST']) def homeroom_abs_publish(): if (check_login_status()): return redirect('/logout') refresh_token() - date = request.form['date'] - homeroom = request.form['homeroom'].split('^') - period = request.form['period'] - signature = request.form['signatureData'] - formData = request.form.to_dict() - notes = "" - if "confirm" in db.child("Homerooms").child(homeroom[0]).child(homeroom[1]).child("Absent").child(date).get(session['token']).val(): - return redirect('/manage') - if 'notes' in request.form: - notes = request.form['notes'] - formData.pop('notes') - signature = removeprefix(signature, 'data:image/png;base64,') - signature = bytes(signature, 'utf-8') - rand = str(date + '^' + homeroom[0] + '^' + homeroom[1] + '^' + period) - rand += ".png" - with open(os.path.join('temp', rand), "wb") as fh: - fh.write(base64.decodebytes(signature)) - storage.child(os.path.join('signatures', rand) - ).put(os.path.join('temp', rand), session['token']) - formData.pop('signatureData') - formData.pop('date') - formData.pop('homeroom') - formData.pop('period') - formData.pop('stype') - for i in formData: - i = i.split('^') - db.child("Homerooms").child(homeroom[0]).child( - homeroom[1]).child("Absent").child(date).child(period).update({i[1]: int(i[0])}, session['token']) - db.child("Homerooms").child(homeroom[0]).child(homeroom[1]).child( - "Absent").child(date).child(period).update({'signature': str(storage.child(os.path.join('signatures', rand)).get_url(None))}, session['token']) - db.child("Homerooms").child(homeroom[0]).child(homeroom[1]).child( - "Absent").child(date).child(period).update({'notes': notes}, session['token']) - os.remove(os.path.join('temp', rand)) + db = refresh_db() + data = request.form.to_dict() + date = data.pop('date') + period = data.pop('period') + signature = data.pop('signatureData') + notes = data.pop('notes') + homeroom = data.pop('homeroom').split('^') + ds1 = data.pop('ds^1') + ds2 = data.pop('ds^2') + ds3 = data.pop('ds^3') + ds4 = data.pop('ds^4') + ds5 = data.pop('ds^5') + ds6 = data.pop('ds^6') + ds7 = data.pop('ds^7') + # 2: L / 1: K + absentData = {} + dsidv = {} + for x in data: + xt = x.split('^') + if (xt[0] == 'note'): + if xt[2] not in absentData: + absentData[xt[2]] = {} + absentData[xt[2]]['note'] = data[x] + elif (xt[0] == 'dsidv'): + dsidv[xt[1]] = data[x] + else: + if xt[1] not in absentData: + absentData[xt[1]] = {} + absentData[xt[1]]['status'] = 'L' if x[0] == '2' else 'K' + cursor = db.cursor() + cursor.execute(""" + INSERT INTO submission + (grade, class_, date, period, signature, ds1, ds2, ds3, ds4, ds5, ds6, ds7, notes) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + """, (homeroom[0], homeroom[1], date, period, signature, ds1, ds2, ds3, ds4, ds5, ds6, ds7, notes)) + for x in absentData: + cursor.execute(""" + INSERT INTO absent + (grade, class_, date, period, num, status, note) + VALUES (%s, %s, %s, %s, %s, %s, %s) + """, (homeroom[0], homeroom[1], date, period, x, absentData[x]['status'], absentData[x]['note'])) + for x in dsidv: + cursor.execute(""" + INSERT INTO ds + (grade, class_, date, period, num, note) + VALUES (%s, %s, %s, %s, %s, %s) + """, (homeroom[0], homeroom[1], date, period, x, dsidv[x])) + db.commit() return redirect('/manage') @@ -298,69 +571,26 @@ def edit_abs(): if (check_login_status() or not check_permission()): return redirect('/logout') refresh_token() - date = request.form['date'] - homeroom = request.form['homeroom'].split('^') - period = request.form['period'] - signature = "https://firebasestorage.googleapis.com/v0/b/attendance-be176.appspot.com/o/stud_affairs.png?alt=media" - formData = request.form.to_dict() - notes = "" - oldData = list(db.child("Homerooms").child(homeroom[0]).child(homeroom[1]).child( - "Absent").child(date).child(period).shallow().get(session['token']).val()) - for k in oldData: - if k == 'name' or k == 'teacher': - continue - db.child("Homerooms").child(homeroom[0]).child(homeroom[1]).child( - "Absent").child(date).child(period).child(k).remove(session['token']) - cfrmstatus = db.child("Homerooms").child(homeroom[0]).child( - homeroom[1]).child("Absent").child(date).get(session['token']).val() - if "confirm" in cfrmstatus: - db.child("Homerooms").child(homeroom[0]).child(homeroom[1]).child( - "Absent").child(date).update({'notes': cfrmstatus['notes'] + '; (確認後學務處有更改)'}, session['token']) - if 'notes' in request.form: - notes = request.form['notes'] - formData.pop('notes') - formData.pop('date') - formData.pop('homeroom') - formData.pop('period') - for i in formData: - i = i.split('^') - db.child("Homerooms").child(homeroom[0]).child( - homeroom[1]).child("Absent").child(date).child(period).update({i[1]: int(i[0])}, session['token']) - db.child("Homerooms").child(homeroom[0]).child(homeroom[1]).child( - "Absent").child(date).child(period).update({'notes': notes}, session['token']) - if cfrmstatus[period]['name'] == 'GP': - db.child("Homerooms").child(homeroom[0]).child( - homeroom[1]).child("Absent").child(date).child(period).child("signature").set({'STUD_AFFAIR_OFFICE': signature}, session['token']) - db.child("Homerooms").child(homeroom[0]).child( - homeroom[1]).child("Absent").child(date).child(period).child("names").set({'STUD_AFFAIR_OFFICE': "學務處已編輯"}, session['token']) - else: - db.child("Homerooms").child(homeroom[0]).child( - homeroom[1]).child("Absent").child(date).child(period).child("signature").set(signature, session['token']) - return redirect('/manage/admin/'+homeroom[0]+'/'+homeroom[1]+'/'+date) - + data = request.form.to_dict() + print(data) + return "" @manage.route('/manage/homeroom_confirm', methods=['POST']) def homeroom_confirm(): if (check_login_status()): return redirect('/logout') refresh_token() - homeroom = request.form['homeroom'].split('^') - date = request.form['date'] - if 'notes' in request.form: - notes = request.form['notes'] - db.child("Homerooms").child(homeroom[0]).child(homeroom[1]).child( - "Absent").child(date).update({"notes": notes}, session['token']) - - signature = request.form['signatureData'] - signature = removeprefix(signature, 'data:image/png;base64,') - signature = bytes(signature, 'utf-8') - rand = str(date + '^' + homeroom[0] + '^' + homeroom[1] + '^' + 'hrCfrm') - rand += ".png" - with open(os.path.join('temp', rand), "wb") as fh: - fh.write(base64.decodebytes(signature)) - storage.child(os.path.join('signatures', rand) - ).put(os.path.join('temp', rand), session['token']) - db.child("Homerooms").child(homeroom[0]).child(homeroom[1]).child("Absent").child(date).update( - {"confirm": str(storage.child(os.path.join('signatures', rand)).get_url(None))}, session['token']) - os.remove(os.path.join('temp', rand)) + data = request.form.to_dict() + homeroom = data.pop('homeroom').split('^') + date = data.pop('date') + signature = data.pop('signatureData') + notes = data.pop('notes') + db = refresh_db() + cursor = db.cursor() + cursor.execute(""" + INSERT INTO submission + (grade, class_, date, period, signature, notes) + VALUES (%s, %s, %s, 'c', %s, %s) + """, (homeroom[0], homeroom[1], date, signature, notes)) + db.commit() return redirect('/manage') diff --git a/requirements.txt b/requirements.txt index 0685704..45c2810 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,19 +1,33 @@ # Automatically generated by https://github.com/damnever/pigar. -# Attendance/app.py: 2 +# Attendance/functions.py: 1,3 Flask == 2.0.1 -# Attendance/app.py: 3 -# Attendance/test.py: 1 -Pyrebase4 == 4.5.0 +# Attendance/functions.py: 19,20,21 +Flask_Admin == 1.5.8 -# Attendance/app.py: 8 +# Attendance/functions.py: 22 +Flask_BabelEx == 0.9.4 + +# Attendance/functions.py: 18 +Flask_SQLAlchemy == 2.5.1 + +# Attendance/functions.py: 15 +mysql_connector_python == 8.0.27 + +# Attendance/functions.py: 10 pandas == 1.1.3 -# Attendance/app.py: 11 -# Attendance/test.py: 3 +# Attendance/functions.py: 14 +passlib == 1.7.4 + +# Attendance/functions.py: 13 python_dotenv == 0.19.0 -# Attendance/app.py: 5 +# Attendance/functions.py: 5 pytz == 2020.1 + +# Attendance/functions.py: 16 +requests == 2.26.0 + gunicorn == 20.1.0 \ No newline at end of file diff --git a/static/allpages.css b/static/allpages.css index b563898..bc87672 100644 --- a/static/allpages.css +++ b/static/allpages.css @@ -3,6 +3,9 @@ body { font-family: "Lato", "Noto Sans TC", "Microsoft JhengHei", "sans-serif"; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + font-smooth: always; } div.col .row .col { @@ -76,10 +79,30 @@ p.highlightAbs.n-2 { p.highlightAbs.n-3 { color: rgb(15, 184, 0); } +.afterSelButton { + margin-top: 0; +} +.modal-footer>.row { + width: 70%; +} +.modal-footer>.row .col { + border: 0 !important; +} +.modal-footer>.row button { + margin-right: 10px; + margin-top: 0; +} + .margin-top { margin-top: 20px; } +.nomargin-top { + margin-top: 0; +} +.nmb { + margin-bottom: 0; +} input[type="checkbox"].absent, input[type="checkbox"].late { -webkit-appearance: initial; @@ -179,4 +202,23 @@ label { .submitButton { width: 20vw; min-width: auto; +} + +div.ds { + margin-top: 20px; +} +div.dsboard .col, .noborder { + border: 0 !important; +} + +div.container { + text-align: center; +} + +button { + width: 100%; + margin-top: 10px; +} +.text-red { + color: red; } \ No newline at end of file diff --git a/static/login.css b/static/login.css index 8ebb248..763deda 100644 --- a/static/login.css +++ b/static/login.css @@ -1,8 +1,39 @@ -div.container { - text-align: center; +html { + height: 100%; +} +body { + height: 100%; + margin: 0; + background-repeat: no-repeat; + background-attachment: fixed; + background: linear-gradient(to bottom right, #e4e4e4 0%, #afafaf 100%); } -button { - width: 100%; - margin-top: 10px; +div.loginPanel { + background: #effffc; + margin-top: 100px; + padding: 30px; + box-shadow: 0 0 10px black; + z-index: -1; +} + +div.container { + max-width: 730px; +} + +h1 .img { + height: calc(1.375rem + 1.5vw); +} +@media (min-width: 1200px) { + h1 .img { + height: 2.5rem; + } +} +h1 .img img { + vertical-align: sub; +} +#subuser_sel { + width: 80%; + margin-left: auto; + margin-right: auto; } \ No newline at end of file diff --git a/static/nav.css b/static/nav.css new file mode 100644 index 0000000..8b191d5 --- /dev/null +++ b/static/nav.css @@ -0,0 +1,66 @@ +#sidebar { + min-width: 270px; + max-width: 270px; + background: #5b5e69; + color: #fff; + -webkit-transition: all 0.3s; + -o-transition: all 0.3s; + transition: all 0.3s; + position: fixed; + top: 0; + height: 100%; } + #sidebar.active { + min-width: 80px; + max-width: 80px; + text-align: center; } + #sidebar.active ul.components li { + font-size: 14px; } + #sidebar.active ul.components li a { + padding: 10px 0; } + #sidebar.active ul.components li a span { + margin-right: 0; + display: block; + font-size: 24px; } + #sidebar.active .logo { + padding: 10px 0; } + #sidebar.active .footer { + display: none; } + #sidebar .logo { + display: block; + color: #fff; + font-weight: 900; + padding: 10px 30px; + } + #sidebar ul.components { + padding: 0; } + #sidebar ul li { + font-size: 16px; } + #sidebar ul li > ul { + margin-left: 10px; } + #sidebar ul li > ul li { + font-size: 14px; } + #sidebar ul li a { + padding: 10px 30px; + display: block; + color: white; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); } + #sidebar ul li a span { + margin-right: 15px; } + @media (max-width: 991.98px) { + #sidebar ul li a span { + display: block; } } + #sidebar ul li a:hover { + color: #fff; } + #sidebar ul li.active > a { + background: transparent; + color: #fff; } + @media (max-width: 991.98px) { + #sidebar { + min-width: 80px; + max-width: 80px; + text-align: center; + margin-left: -80px !important; } + #sidebar.active { + margin-left: 0 !important; } } +#sidebar a { + text-decoration: none; } \ No newline at end of file diff --git a/static/nav.js b/static/nav.js new file mode 100644 index 0000000..cf8504e --- /dev/null +++ b/static/nav.js @@ -0,0 +1,10 @@ +$(window).resize(function() { + var sidebar = document.getElementById('sidebar'); + var container = document.getElementsByClassName('container')[0]; + container.style.paddingLeft = 'calc(.75rem + ' + sidebar.offsetWidth + 'px)'; +}); +document.addEventListener("DOMContentLoaded", function(event){ + var sidebar = document.getElementById('sidebar'); + var container = document.getElementsByClassName('container')[0]; + container.style.paddingLeft = 'calc(.75rem + ' + sidebar.offsetWidth + 'px)'; +}) \ No newline at end of file diff --git a/static/pagejs/group_teach.js b/static/pagejs/group_teach.js index 1dd8633..b2a6251 100644 --- a/static/pagejs/group_teach.js +++ b/static/pagejs/group_teach.js @@ -1,6 +1,14 @@ var signaturePad, selPeriod, canvas, width = $(window).width(), modal; +var indDS = {}; function submitForm() { if (!signaturePad.isEmpty()) { + for (var i in indDS) { + var tmp = document.createElement('input'); + tmp.type = 'hidden'; + tmp.name = 'ds^' + i; + tmp.value = indDS[i]; + document.getElementById('attendanceData^' + selPeriod).appendChild(tmp); + } $('#' + modal).modal('hide'); loadingAnimation(); signaturePad.off(); @@ -50,12 +58,36 @@ function viewSignature(period) { }); resizeCanvas(); } -function unCheckAbs(string) { - document.getElementById('absent^' + string).checked = false; -} function unCheckLate(string) { document.getElementById('late^' + string).checked = false; + strForNote = string.substring(string.indexOf("^") + 1); + if (document.getElementById("absent^" + string).checked == true && !!!document.getElementById("note^"+strForNote)) { + var tmp = document.createElement('input') + tmp.type = 'text' + tmp.id = 'note^'+strForNote + tmp.name = 'note^'+strForNote + tmp.className = 'form-control' + document.getElementById('input^' + string).appendChild(tmp) + } else if (document.getElementById("absent^" + string).checked == false) { + document.getElementById('note^' + strForNote).remove(); + } + } +function unCheckAbs(string) { + document.getElementById('absent^' + string).checked = false; + strForNote = string.substring(string.indexOf("^") + 1); + if (document.getElementById("late^" + string).checked == true && !!!document.getElementById("note^"+strForNote)) { + var tmp = document.createElement('input') + tmp.type = 'text' + tmp.id = 'note^'+strForNote + tmp.name = 'note^'+strForNote + tmp.className = 'form-control' + document.getElementById('input^' + string).appendChild(tmp) + } else if (document.getElementById("late^" + string).checked == false) { + document.getElementById('note^' + strForNote).remove(); + } +} + function loadingAnimation() { $("div.container").hide(); @@ -70,3 +102,18 @@ function chgDate(sel) { document.body.appendChild(new_form); new_form.submit(); } +function addDS() { + if ($('#dsnumbersel').val() == "" || $('#dsoffensesel').val() == "") { + return; + } + var text = $('#dsoffensesel').val() + if ($('#dsoffenseother').val() != "") { + text = text.concat(": ", $('#dsoffenseother').val()); + } + indDS[$("#dsnumbersel").val()] = text; + $('#inddsview>.col').append('
'+$("#dsnumbersel").val()+'
'+text+'
') + $('#dsnumbersel option[value="'+$("#dsnumbersel").val()+'"]').remove(); + $('#dsoffenseother').val(""); + $('#dsoffensesel').val(""); + $('#dsnumbersel').val(""); +} \ No newline at end of file diff --git a/static/pagejs/homeroom.js b/static/pagejs/homeroom.js index 5a8de85..1ae3544 100644 --- a/static/pagejs/homeroom.js +++ b/static/pagejs/homeroom.js @@ -1,5 +1,6 @@ var signaturePad, hrCfrm = false, canvas = document.getElementById("signature_pad"); var width = $(window).width(); +var indDS = {}; function loadingAnimation() { $('.container').hide(); $('#loading').show(); @@ -25,6 +26,11 @@ function submitForm() { document.getElementById('homeroom_confirm').submit() } else { var notes = $('#subjectNotes').val(); + for (var i = 0; i < 7; i++) + document.getElementById('HR-ds'+(i+1)).value = $('.dsboard input[name="ds'+(i+1)+'"]:checked').val(); + for (var i in indDS) { + $('#postHomeroomAbs').append('') + } document.getElementById('HR-signatureData').value = data; document.getElementById('HR-notes').value = notes; document.getElementById('postHomeroomAbs').submit(); @@ -70,6 +76,8 @@ function afterSelAbs(period) { $('#postHomeroomAbs').append(''); + $('#postHomeroomAbs').append('') } }); if (cnt == 0) { @@ -80,6 +88,7 @@ function afterSelAbs(period) { } function homeroomCfrm() { hrCfrm = true; + $('.ds').attr('hidden', 'hidden'); $('#showSignPeriod').text("HOMEROOM CONFIRM"); $('#showSignSubjectName').text("班導確認"); $('.tobeform').attr('disabled', 'disabled'); @@ -88,7 +97,32 @@ function homeroomCfrm() { } function unCheckLate(string) { document.getElementById('late^' + string).checked = false; + if (document.getElementById("absent^" + string).checked == true && $('#note-'+string.split('^')[0]+'-'+string.split('^')[1]).length == 0) { + $('.input-'+string.split('^')[0]+'-'+string.split('^')[1]).append('') + } else if (document.getElementById("absent^" + string).checked == false) { + $('#note-'+string.split('^')[0]+'-'+string.split('^')[1]).remove(); + } } function unCheckAbs(string) { document.getElementById('absent^' + string).checked = false; + if (document.getElementById("late^" + string).checked == true && $('#note-'+string.split('^')[0]+'-'+string.split('^')[1]).length == 0) { + $('.input-'+string.split('^')[0]+'-'+string.split('^')[1]).append('') + } else if (document.getElementById("late^" + string).checked == false) { + $('#note-'+string.split('^')[0]+'-'+string.split('^')[1]).remove(); + } } +function addDS() { + if ($('#dsnumbersel').val() == "" || $('#dsoffensesel').val() == "") { + return; + } + var text = $('#dsoffensesel').val() + if ($('#dsoffenseother').val() != "") { + text = text.concat(": ", $('#dsoffenseother').val()); + } + indDS[$("#dsnumbersel").val()] = text; + $('#inddsview>.col').append('
'+$("#dsnumbersel").val()+'
'+text+'
') + $('#dsnumbersel option[value="'+$("#dsnumbersel").val()+'"]').remove(); + $('#dsoffenseother').val(""); + $('#dsoffensesel').val(""); + $('#dsnumbersel').val(""); +} \ No newline at end of file diff --git a/static/time.js b/static/time.js index a4899bf..6a48170 100644 --- a/static/time.js +++ b/static/time.js @@ -17,4 +17,6 @@ }, 500); } startTime(); -})(); \ No newline at end of file +})(); +$(window).resize(); +$(window).scroll(); \ No newline at end of file diff --git a/templates/admin.html b/templates/admin.html index c5f3314..d754215 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -8,6 +8,7 @@ Admin 管理員 - Attendance 點名 + @@ -18,48 +19,17 @@
+ {% include 'sidebar.html' %}

Admin View | 管理頁面

-

{{homeroomCode[0]}} {{homeroomCode[1]}}

+

{{currRoom[0]}} {{currRoom[1]}}

[{{currDate}}]

- {% if 'confirm' in absData[currDate] %} + {% if 'c' in submission %}

Homeroom Confirmed 班導已確認

{% else %}

Homeroom NOT Confirmed 班導尚未確認

{% endif %} - -
-
-
- -
-
- -
-
- -
-
- -
-
-
-
+
班級
@@ -77,7 +47,7 @@
{% for i in periods %}
- {{absData[currDate][i]['name']}}
+ {{schedule[i]['subject']}} {% if schedule[i]['special'] == True %} (換) {% endif %}
{% endfor %}
@@ -86,73 +56,101 @@
{% for i in periods %} -
{{absData[currDate][i]['teacher']}}
+
{{schedule[i]['teacher']}}
{% endfor %}
- {% for i in homeroomData %} + {% for i in students %}
-
{{homeroomCode[0]}}{{homeroomCode[1]}}
-
{{i}}
-
{{ homeroomData[i]['name'] }}
-
{{ homeroomData[i]['eng_name'] }}
+
{{currRoom[0]}}{{currRoom[1]}}
+
{{ i[0] }}
+
{{ i[1] }}
+
{{ i[2] }}
{% for j in periods %}
- {% if 'signature' in absData[currDate][j] %} - {% if i in absData[currDate][j] %} - {% if absData[currDate][j][i] == 1 %} -

X

+ {% if schedule[j]['subject'] == 'GP' %} + {% for k in submission[j] %} + {% if studGP[i[0]][schedule[j]['teacher']] == k %} + {% if i[0] in absentData[j] %} + {% if absentData[j][i[0]]['status'] == 'L' %} +

𝜑

+ {% elif absentData[j][i[0]]['status'] == 'K' %} +

+ {% elif absentData[j][i[0]]['status'] == 'G' %} +

+ {% elif absentData[j][i[0]]['status'] == 'S' %} +

+ {% elif absentData[j][i[0]]['status'] == 'F' %} +

+ {% elif absentData[j][i[0]]['status'] == 'P' %} +

+ {% elif absentData[j][i[0]]['status'] == 'O' %} +

{% else %} -

𝜑

+

{{absentData[j][i[0]]['status']}}

{% endif %} +

{{absentData[j][i[0]]['note']}}

{% else %} - {% if absData[currDate][j]['name'] != 'GP' %} -

V

- {% else %} - {% if (homeroomData[i]['GP_Class'][absData[currDate][j]['teacher']] in - absData[currDate][j]['signature'] or 'STUD_AFFAIR_OFFICE' in absData[currDate][j]['signature'])%} -

V

- {% else %} -

+

V

+ {% if j in idvDS and i[0] in idvDS[j] %} +

{{idvDS[j][i[0]]}}

{% endif %} {% endif %} - {% endif %} - {% elif absData[currDate][j]['name'] == 'GP' %} -

{% else %} - {% if 'confirm' in absData[currDate] %} -

- {% else %} -

+

+ {% endif %} + {% endfor %} + {% else %} + {% if j in submission %} + {% if i[0] in absentData[j] %} + {% if absentData[j][i[0]]['status'] == 'L' %} +

𝜑

+ {% elif absentData[j][i[0]]['status'] == 'K' %} +

+ {% elif absentData[j][i[0]]['status'] == 'G' %} +

+ {% elif absentData[j][i[0]]['status'] == 'S' %} +

+ {% elif absentData[j][i[0]]['status'] == 'F' %} +

+ {% elif absentData[j][i[0]]['status'] == 'P' %} +

+ {% elif absentData[j][i[0]]['status'] == 'O' %} +

+ {% else %} +

{{absentData[j][i[0]]['status']}}

+ {% endif %} +

{{absentData[j][i[0]]['note']}}

+ {% else %} +

V

+ {% if j in idvDS and i[0] in idvDS[j] %} +

{{idvDS[j][i[0]]}}

+ {% endif %} + {% endif %} + {% else %} +

{% endif %} - {% endif %}
{% endfor %}
{% endfor %} - {% if showUpload == '1' %} -
+ {% for i in range(7) %} +
+
{{dstext[i]}}
+
{{dsboard[i]}}
+ {% for j in periods %} + {% if j in submission and schedule[j] != 'GP' %} +
+ {{submission[j]['ds' + (i+1)|string]}}
+ {% else %}
-
- {% for i in periods %} -
- {% if ('signature' in absData[currDate][i] or 'confirm' in absData[currDate][i]) %} - - {% endif %} -
+ {% endif %} {% endfor %}
- {% endif %} - + {% endfor %} {% for c in range(periods|length + 1) %} {% if c % 4 == 0 %}
@@ -160,38 +158,41 @@
{% if c == 0 %}
Homeroom Teacher 導師
- {% if 'confirm' in absData[currDate] %} -
-
備註: {{absData[currDate]['notes']}}
+ {% if 'c' in submission %} +
+
備註: {{submission['c']['notes']}}
{% else %}
No Signature 導師尚未簽名
{% endif %} {% else %} - {% if absData[currDate][periods[c-1]]['name'] == 'GP' %} - {% if 'signature' in absData[currDate][periods[c-1]] %} - {% for i in absData[currDate][periods[c-1]]['signature'] %} + {% if schedule[periods[c-1]]['subject'] == 'GP' %} + {% if periods[c-1] in submission %} + {% for i in submission[periods[c-1]] %} + {% if i != 'notes' %}
{{periods[c-1]}}: - {{absData[currDate][periods[c-1]]['teacher']}}: {{i}}: - {{absData[currDate][periods[c-1]]['names'][i]}}
-
- {% if loop.index == loop.length %} -
備註: {{absData[currDate][periods[c-1]]['notes']}} + {{schedule[periods[c-1]]['teacher']}}: {{i}}
+
+ {% if loop.index == loop.length-1 %} +
備註: {{submission[periods[c-1]]['notes']}} {% endif %}
+ {% endif %} {% endfor %} {% else %}
{{periods[c-1]}}: - {{absData[currDate][periods[c-1]]['teacher']}}: No + {{schedule[periods[c-1]]['subject']}}: {{i}}: No Signature
{% endif %} {% else %} -
{{periods[c-1]}}: {{absData[currDate][periods[c-1]]['name']}}: - {{absData[currDate][periods[c-1]]['teacher']}} +
{{periods[c-1]}}: {{schedule[periods[c-1]]['subject']}}: + {{schedule[periods[c-1]]['teacher']}}
-

備註: - {{absData[currDate][periods[c-1]]['notes']}}
+ {% if periods[c-1] in submission %} +

備註: + {{submission[periods[c-1]]['notes']}}
+ {% endif %} {% endif %} {% endif %}
@@ -200,29 +201,6 @@ {% endif %} {% endfor %}
- {% if showUpload == '1' %}
diff --git a/templates/admin/index.html b/templates/admin/index.html new file mode 100644 index 0000000..7ca2307 --- /dev/null +++ b/templates/admin/index.html @@ -0,0 +1,82 @@ +{% extends 'admin/master.html' %} + +{% block body %} + +

歡迎 後台管理

+

請利用上方選單選取功能

+

特殊功能

+
    +
  • 為保障用戶安全,Users 的密碼欄為 sha256_crypt.hash,請使用 Python 產生出後輸入,或先改 Email,寄忘記密碼信,最後再改回來。
  • +
      +
    • password 的 hash 為 $5$rounds=535000$VjoY30Bt535dL/7x$xXmg8BZ3VJe/odbQQ5hd0uPmb1o9EhHkJmwWWTc8K21
    • +
    +
  • 為簡化程式,Students->Classes, GPClasses->Accs, Homerooms->Accs, 分組課的 Submission->Notes 都使用標準 JSON 格式輸入,請留意
  • +
  • 請假代碼
    + + + + + + + + + + + + + + + + + + + + +
    遲到曠課事假病假喪假疫情假公假
    LKGSFPO
    +
  • +
  • 使用者代碼
    + + + + + + + + + + + + + +
    超級管理員管理員一般使用者
    SAR
    +
  • +
+{% if session['showUpload'] %} +
+
+

大量匯入

+
+ + + + +
+ +
+
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/chgPassword.html b/templates/chgPassword.html index fb52536..570cd2c 100644 --- a/templates/chgPassword.html +++ b/templates/chgPassword.html @@ -4,7 +4,7 @@ - Attendance 點名系統 (β) + Attendance 點名系統 2.0 @@ -24,69 +24,62 @@
-
-

Attendance 點名系統 (β) | Change Password 更改密碼

-
-
-
-
-
-
-
- -
- - - - -
+
+
+

+
Change Password 更改密碼

+ +
+
+
+ +
+ + + +
-
-
-
- -
-
-
-
-
- -
- - - - -
-
- 6 characters - minimum. -
- - - - - {% with messages = get_flashed_messages() %} - {% if messages %} -
- {% include 'footer.html' %} diff --git a/templates/footer.html b/templates/footer.html index 939204a..5d8b5a5 100644 --- a/templates/footer.html +++ b/templates/footer.html @@ -5,8 +5,8 @@
\ No newline at end of file diff --git a/templates/forgotPassword.html b/templates/forgotPassword.html index d49cc5c..9a94926 100644 --- a/templates/forgotPassword.html +++ b/templates/forgotPassword.html @@ -5,7 +5,7 @@ - Attendance 點名系統 (β) + Attendance 點名系統 2.0 @@ -20,38 +20,48 @@
-
-

Attendance 點名系統 (β) | Reset Password 忘記密碼

-
-
-
-
-
-
- -
- -
- -
-

This will send an email to the email address to verify your identity.
- 這會傳送一個郵件到指定的信箱,以驗證您的身份 -

- {% with messages = get_flashed_messages() %} - {% if messages %} -