564 lines
23 KiB
Python
564 lines
23 KiB
Python
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
|
||
from flask_login import login_required, current_user
|
||
from app.models import db, Student, WeeklyAttendance, DailyAttendanceDetail, LeaveRecord
|
||
from app.utils.auth_helpers import student_required
|
||
from app.utils.database import safe_add_and_commit
|
||
from datetime import datetime, timedelta
|
||
from sqlalchemy import and_, or_, desc
|
||
|
||
# 这个文件包含需要在student.py中添加的缺勤次数计算函数
|
||
|
||
def calculate_absent_count_for_record(record):
|
||
"""为单个考勤记录计算真实的缺勤次数"""
|
||
import json
|
||
from app.models import DailyAttendanceDetail
|
||
|
||
# 获取该记录的每日明细
|
||
daily_details = DailyAttendanceDetail.query.filter_by(
|
||
weekly_record_id=record.record_id
|
||
).all()
|
||
|
||
absent_count = 0
|
||
|
||
for detail in daily_details:
|
||
# 处理完全缺勤的情况
|
||
if detail.status == '缺勤' and (not detail.remarks or not detail.remarks.startswith('{')):
|
||
# 完全缺勤的天数,早上+下午都缺勤
|
||
absent_count += 2 # 早上缺勤1次 + 下午缺勤1次
|
||
continue
|
||
|
||
if detail.remarks and detail.remarks.startswith('{'):
|
||
try:
|
||
remarks_data = json.loads(detail.remarks)
|
||
details_info = remarks_data.get('details', {})
|
||
|
||
# 统计缺勤次数
|
||
# 检查早上缺勤:morning_in AND morning_out 都missing
|
||
morning_data = details_info.get('morning', {})
|
||
morning_in_time = morning_data.get('in')
|
||
morning_out_time = morning_data.get('out')
|
||
morning_in_status = morning_data.get('status', 'missing')
|
||
|
||
if ((not morning_in_time or morning_in_status == 'missing') and
|
||
(not morning_out_time)):
|
||
absent_count += 1
|
||
|
||
# 检查下午缺勤:afternoon_in AND afternoon_out 都missing
|
||
afternoon_data = details_info.get('afternoon', {})
|
||
afternoon_in_time = afternoon_data.get('in')
|
||
afternoon_out_time = afternoon_data.get('out')
|
||
afternoon_in_status = afternoon_data.get('status', 'missing')
|
||
|
||
if ((not afternoon_in_time or afternoon_in_status == 'missing') and
|
||
(not afternoon_out_time)):
|
||
absent_count += 1
|
||
|
||
except (json.JSONDecodeError, KeyError, AttributeError):
|
||
continue
|
||
|
||
return absent_count
|
||
|
||
def add_absent_count_to_records(records):
|
||
"""为考勤记录列表添加缺勤次数计算属性"""
|
||
total_absent_count = 0
|
||
for record in records:
|
||
absent_count = calculate_absent_count_for_record(record)
|
||
record.absent_count = absent_count
|
||
total_absent_count += absent_count
|
||
return total_absent_count
|
||
|
||
|
||
student_bp = Blueprint('student', __name__)
|
||
|
||
|
||
@student_bp.route('/dashboard')
|
||
@student_required
|
||
def dashboard():
|
||
"""学生主页"""
|
||
if current_user.is_admin():
|
||
return redirect(url_for('admin.dashboard'))
|
||
|
||
student = Student.query.filter_by(student_number=current_user.student_number).first()
|
||
if not student:
|
||
flash('学生信息不存在,请联系管理员', 'error')
|
||
return redirect(url_for('auth.logout'))
|
||
|
||
# 获取最近的考勤记录
|
||
recent_attendance = WeeklyAttendance.query.filter_by(
|
||
student_number=current_user.student_number
|
||
).order_by(desc(WeeklyAttendance.week_start_date)).limit(5).all()
|
||
|
||
# 统计数据
|
||
total_records = WeeklyAttendance.query.filter_by(
|
||
student_number=current_user.student_number
|
||
).count()
|
||
|
||
total_work_hours = db.session.query(
|
||
db.func.sum(WeeklyAttendance.actual_work_hours)
|
||
).filter_by(student_number=current_user.student_number).scalar() or 0
|
||
|
||
# 获取所有考勤记录并计算真实缺勤次数
|
||
all_records = WeeklyAttendance.query.filter_by(student_number=current_user.student_number).all()
|
||
total_absent_count = add_absent_count_to_records(all_records)
|
||
|
||
# 获取未审批的请假记录
|
||
pending_leaves = LeaveRecord.query.filter_by(
|
||
student_number=current_user.student_number,
|
||
status='待审批'
|
||
).order_by(desc(LeaveRecord.created_at)).all()
|
||
|
||
return render_template('student/dashboard.html',
|
||
student=student,
|
||
recent_attendance=recent_attendance,
|
||
total_records=total_records,
|
||
total_work_hours=float(total_work_hours),
|
||
total_absent_count=int(total_absent_count),
|
||
pending_leaves=pending_leaves)
|
||
|
||
|
||
@student_bp.route('/attendance')
|
||
@student_required
|
||
def attendance():
|
||
"""考勤记录页面"""
|
||
from sqlalchemy import desc, func, case, or_
|
||
|
||
page = request.args.get('page', 1, type=int)
|
||
per_page = 20
|
||
|
||
# 日期筛选
|
||
start_date = request.args.get('start_date')
|
||
end_date = request.args.get('end_date')
|
||
|
||
# 构建基础查询,同时计算迟到次数
|
||
query = db.session.query(
|
||
WeeklyAttendance,
|
||
func.coalesce(
|
||
func.sum(
|
||
case(
|
||
(DailyAttendanceDetail.status.like('%迟到%'), 1),
|
||
else_=0
|
||
)
|
||
), 0
|
||
).label('late_count')
|
||
).outerjoin(
|
||
DailyAttendanceDetail,
|
||
WeeklyAttendance.record_id == DailyAttendanceDetail.weekly_record_id
|
||
).filter(
|
||
WeeklyAttendance.student_number == current_user.student_number
|
||
).group_by(WeeklyAttendance.record_id)
|
||
|
||
# 应用筛选条件
|
||
if start_date:
|
||
try:
|
||
start_date_obj = datetime.strptime(start_date, '%Y-%m-%d').date()
|
||
query = query.filter(WeeklyAttendance.week_start_date >= start_date_obj)
|
||
except ValueError:
|
||
flash('开始日期格式错误', 'error')
|
||
|
||
if end_date:
|
||
try:
|
||
end_date_obj = datetime.strptime(end_date, '%Y-%m-%d').date()
|
||
query = query.filter(WeeklyAttendance.week_end_date <= end_date_obj)
|
||
except ValueError:
|
||
flash('结束日期格式错误', 'error')
|
||
|
||
# 执行分页查询
|
||
pagination = query.order_by(desc(WeeklyAttendance.week_start_date)).paginate(
|
||
page=page, per_page=per_page, error_out=False
|
||
)
|
||
|
||
# 处理结果,将迟到次数添加到记录对象中
|
||
attendance_records = []
|
||
for record, late_count in pagination.items:
|
||
record.late_count = int(late_count) if late_count else 0
|
||
attendance_records.append(record)
|
||
|
||
# 更新pagination对象的items
|
||
pagination.items = attendance_records
|
||
|
||
# 计算总体统计
|
||
total_stats = None
|
||
if attendance_records:
|
||
# 计算所有记录的统计信息
|
||
all_records_query = db.session.query(
|
||
WeeklyAttendance,
|
||
func.coalesce(
|
||
func.sum(
|
||
case(
|
||
(DailyAttendanceDetail.status.like('%迟到%'), 1),
|
||
else_=0
|
||
)
|
||
), 0
|
||
).label('late_count')
|
||
).outerjoin(
|
||
DailyAttendanceDetail,
|
||
WeeklyAttendance.record_id == DailyAttendanceDetail.weekly_record_id
|
||
).filter(
|
||
WeeklyAttendance.student_number == current_user.student_number
|
||
).group_by(WeeklyAttendance.record_id)
|
||
|
||
# 应用相同的筛选条件
|
||
if start_date:
|
||
start_date_obj = datetime.strptime(start_date, '%Y-%m-%d').date()
|
||
all_records_query = all_records_query.filter(WeeklyAttendance.week_start_date >= start_date_obj)
|
||
if end_date:
|
||
end_date_obj = datetime.strptime(end_date, '%Y-%m-%d').date()
|
||
all_records_query = all_records_query.filter(WeeklyAttendance.week_end_date <= end_date_obj)
|
||
|
||
all_records = all_records_query.all()
|
||
|
||
total_actual_hours = sum(record.actual_work_hours for record, _ in all_records)
|
||
total_class_hours = sum(record.class_work_hours for record, _ in all_records)
|
||
# 为记录添加缺勤次数计算
|
||
record_list = [record for record, _ in all_records]
|
||
total_absent_count = add_absent_count_to_records(record_list)
|
||
total_overtime_hours = sum(record.overtime_hours for record, _ in all_records)
|
||
total_late_count = sum(late_count for _, late_count in all_records)
|
||
|
||
# 计算请假天数
|
||
record_ids = [record.record_id for record, _ in all_records]
|
||
total_leave_days = 0
|
||
if record_ids:
|
||
total_leave_days = DailyAttendanceDetail.query.filter(
|
||
DailyAttendanceDetail.weekly_record_id.in_(record_ids),
|
||
DailyAttendanceDetail.status == '请假'
|
||
).count()
|
||
|
||
total_stats = {
|
||
'total_weeks': len(all_records),
|
||
'total_actual_hours': total_actual_hours,
|
||
'total_class_hours': total_class_hours,
|
||
'total_absent_count': total_absent_count,
|
||
'total_overtime_hours': total_overtime_hours,
|
||
'total_late_count': total_late_count,
|
||
'total_leave_days': total_leave_days,
|
||
'avg_weekly_hours': total_actual_hours / max(len(all_records), 1)
|
||
}
|
||
|
||
return render_template('student/attendance.html',
|
||
attendance_records=attendance_records,
|
||
pagination=pagination,
|
||
start_date=start_date,
|
||
end_date=end_date,
|
||
total_stats=total_stats)
|
||
|
||
|
||
@student_bp.route('/attendance/<int:record_id>/details')
|
||
@student_required
|
||
def attendance_details(record_id):
|
||
"""考勤详细信息"""
|
||
from datetime import datetime, timedelta
|
||
import json
|
||
|
||
# 获取周考勤汇总记录(确保只能查看自己的记录)
|
||
record = WeeklyAttendance.query.filter_by(
|
||
record_id=record_id,
|
||
student_number=current_user.student_number
|
||
).first_or_404()
|
||
|
||
# 获取学生信息
|
||
student = Student.query.filter_by(student_number=current_user.student_number).first()
|
||
|
||
# 获取该周的每日考勤明细
|
||
daily_details = DailyAttendanceDetail.query.filter_by(
|
||
weekly_record_id=record_id
|
||
).order_by(DailyAttendanceDetail.attendance_date).all()
|
||
|
||
# 🔥 新增:计算真实的缺勤次数和迟到次数
|
||
def calculate_real_counts(daily_details_list):
|
||
"""计算真实的缺勤次数和迟到次数"""
|
||
late_count = 0
|
||
absent_count = 0
|
||
|
||
for detail in daily_details_list:
|
||
# 🔥 处理完全缺勤的情况
|
||
if detail.status == '缺勤' and (not detail.remarks or not detail.remarks.startswith('{')):
|
||
# 完全缺勤的天数,早上+下午都缺勤
|
||
absent_count += 2 # 早上缺勤1次 + 下午缺勤1次
|
||
continue
|
||
|
||
if detail.remarks and detail.remarks.startswith('{'):
|
||
try:
|
||
remarks_data = json.loads(detail.remarks)
|
||
details_info = remarks_data.get('details', {})
|
||
|
||
# 统计迟到次数
|
||
for period in ['morning', 'afternoon', 'evening']:
|
||
period_data = details_info.get(period, {})
|
||
if period_data.get('status') == 'late':
|
||
late_count += 1
|
||
|
||
# 统计缺勤次数
|
||
# 检查早上缺勤:morning_in AND morning_out 都missing
|
||
morning_data = details_info.get('morning', {})
|
||
morning_in_time = morning_data.get('in')
|
||
morning_out_time = morning_data.get('out')
|
||
morning_in_status = morning_data.get('status', 'missing')
|
||
|
||
if ((not morning_in_time or morning_in_status == 'missing') and
|
||
(not morning_out_time)):
|
||
absent_count += 1
|
||
|
||
# 检查下午缺勤:afternoon_in AND afternoon_out 都missing
|
||
afternoon_data = details_info.get('afternoon', {})
|
||
afternoon_in_time = afternoon_data.get('in')
|
||
afternoon_out_time = afternoon_data.get('out')
|
||
afternoon_in_status = afternoon_data.get('status', 'missing')
|
||
|
||
if ((not afternoon_in_time or afternoon_in_status == 'missing') and
|
||
(not afternoon_out_time)):
|
||
absent_count += 1
|
||
|
||
except (json.JSONDecodeError, KeyError, AttributeError):
|
||
continue
|
||
|
||
return late_count, absent_count
|
||
|
||
# 🔥 计算真实的次数并添加到record对象中
|
||
real_late_count, real_absent_count = calculate_real_counts(daily_details)
|
||
record.late_count = real_late_count
|
||
record.absent_count = real_absent_count
|
||
|
||
# 处理每日详情,计算工作时长和解析详细信息
|
||
processed_daily_details = []
|
||
for detail in daily_details:
|
||
processed_detail = {
|
||
'detail_id': detail.detail_id,
|
||
'attendance_date': detail.attendance_date,
|
||
'status': detail.status,
|
||
'check_in_time': detail.check_in_time,
|
||
'check_out_time': detail.check_out_time,
|
||
'remarks': detail.remarks,
|
||
'duration_hours': None,
|
||
'detailed_info': None
|
||
}
|
||
|
||
# 计算工作时长
|
||
if detail.check_in_time and detail.check_out_time:
|
||
try:
|
||
# 创建完整的datetime对象
|
||
start_datetime = datetime.combine(detail.attendance_date, detail.check_in_time)
|
||
end_datetime = datetime.combine(detail.attendance_date, detail.check_out_time)
|
||
|
||
# 如果结束时间小于开始时间,说明跨天了
|
||
if end_datetime < start_datetime:
|
||
end_datetime += timedelta(days=1)
|
||
|
||
duration = (end_datetime - start_datetime).total_seconds() / 3600
|
||
processed_detail['duration_hours'] = round(duration, 1)
|
||
except Exception as e:
|
||
print(f"计算工作时长失败: {e}")
|
||
processed_detail['duration_hours'] = None
|
||
|
||
# 解析详细信息
|
||
if detail.remarks:
|
||
try:
|
||
if detail.remarks.startswith('{'):
|
||
remarks_data = json.loads(detail.remarks)
|
||
processed_detail['detailed_info'] = remarks_data.get('details')
|
||
processed_detail['summary_remarks'] = remarks_data.get('summary', detail.remarks)
|
||
else:
|
||
processed_detail['summary_remarks'] = detail.remarks
|
||
except:
|
||
processed_detail['summary_remarks'] = detail.remarks
|
||
|
||
processed_daily_details.append(processed_detail)
|
||
|
||
# 计算统计数据
|
||
total_days = len(processed_daily_details)
|
||
present_days = len([d for d in processed_daily_details if d['status'] == '正常'])
|
||
late_days = len([d for d in processed_daily_details if '迟到' in d['status']])
|
||
absent_days = len([d for d in processed_daily_details if d['status'] == '缺勤'])
|
||
|
||
# 计算平均每日工作时长
|
||
if processed_daily_details:
|
||
avg_daily_hours = record.actual_work_hours / max(present_days, 1)
|
||
else:
|
||
avg_daily_hours = 0
|
||
|
||
# 获取该学生最近的其他考勤记录(用于对比)
|
||
recent_records = WeeklyAttendance.query.filter_by(
|
||
student_number=current_user.student_number
|
||
).filter(WeeklyAttendance.record_id != record_id).order_by(
|
||
desc(WeeklyAttendance.week_start_date)
|
||
).limit(5).all()
|
||
|
||
# 🔥 修改:传递 weekly_record 而不是 record,以匹配模板
|
||
return render_template('student/attendance_details.html',
|
||
weekly_record=record, # 🔥 关键修改
|
||
student=student,
|
||
daily_details=processed_daily_details,
|
||
total_days=total_days,
|
||
present_days=present_days,
|
||
late_days=late_days,
|
||
absent_days=absent_days,
|
||
avg_daily_hours=avg_daily_hours,
|
||
recent_records=recent_records)
|
||
|
||
@student_bp.route('/statistics')
|
||
@login_required
|
||
def statistics():
|
||
"""学生个人统计"""
|
||
from sqlalchemy import desc, func, case
|
||
from datetime import datetime, timedelta
|
||
|
||
# 获取当前学生信息
|
||
student = Student.query.filter_by(student_number=current_user.student_number).first_or_404()
|
||
|
||
# 获取筛选参数
|
||
start_date = request.args.get('start_date', '')
|
||
end_date = request.args.get('end_date', '')
|
||
|
||
# 构建考勤记录查询
|
||
attendance_query = WeeklyAttendance.query.filter_by(
|
||
student_number=current_user.student_number
|
||
)
|
||
|
||
# 应用日期筛选
|
||
if start_date:
|
||
try:
|
||
start_date_obj = datetime.strptime(start_date, '%Y-%m-%d').date()
|
||
attendance_query = attendance_query.filter(WeeklyAttendance.week_start_date >= start_date_obj)
|
||
except ValueError:
|
||
flash('开始日期格式错误', 'error')
|
||
|
||
if end_date:
|
||
try:
|
||
end_date_obj = datetime.strptime(end_date, '%Y-%m-%d').date()
|
||
attendance_query = attendance_query.filter(WeeklyAttendance.week_end_date <= end_date_obj)
|
||
except ValueError:
|
||
flash('结束日期格式错误', 'error')
|
||
|
||
# 获取考勤记录,按周排序
|
||
attendance_records = attendance_query.order_by(desc(WeeklyAttendance.week_start_date)).all()
|
||
|
||
# 计算真实缺勤次数
|
||
add_absent_count_to_records(attendance_records)
|
||
|
||
# 计算统计数据
|
||
total_stats = {
|
||
'total_work_hours': sum(record.actual_work_hours for record in attendance_records),
|
||
'total_class_hours': sum(record.class_work_hours for record in attendance_records),
|
||
'total_overtime_hours': sum(record.overtime_hours for record in attendance_records),
|
||
'total_absent_count': sum(getattr(record, 'absent_count', 0) for record in attendance_records),
|
||
'attendance_weeks': len(attendance_records)
|
||
}
|
||
|
||
# 计算迟到次数
|
||
total_late_count = db.session.query(
|
||
func.sum(
|
||
case(
|
||
(DailyAttendanceDetail.status.like('%迟到%'), 1),
|
||
else_=0
|
||
)
|
||
)
|
||
).join(
|
||
WeeklyAttendance, DailyAttendanceDetail.weekly_record_id == WeeklyAttendance.record_id
|
||
).filter(WeeklyAttendance.student_number == current_user.student_number)
|
||
|
||
if start_date:
|
||
try:
|
||
start_date_obj = datetime.strptime(start_date, '%Y-%m-%d').date()
|
||
total_late_count = total_late_count.filter(WeeklyAttendance.week_start_date >= start_date_obj)
|
||
except ValueError:
|
||
pass
|
||
|
||
if end_date:
|
||
try:
|
||
end_date_obj = datetime.strptime(end_date, '%Y-%m-%d').date()
|
||
total_late_count = total_late_count.filter(WeeklyAttendance.week_end_date <= end_date_obj)
|
||
except ValueError:
|
||
pass
|
||
|
||
total_stats['total_late_count'] = int(total_late_count.scalar() or 0)
|
||
|
||
# 计算平均值
|
||
if total_stats['attendance_weeks'] > 0:
|
||
total_stats['avg_weekly_hours'] = round(total_stats['total_work_hours'] / total_stats['attendance_weeks'], 1)
|
||
total_stats['avg_weekly_class_hours'] = round(
|
||
total_stats['total_class_hours'] / total_stats['attendance_weeks'], 1)
|
||
else:
|
||
total_stats['avg_weekly_hours'] = 0
|
||
total_stats['avg_weekly_class_hours'] = 0
|
||
|
||
# 按月统计
|
||
monthly_stats = db.session.query(
|
||
func.date_format(WeeklyAttendance.week_start_date, '%Y-%m').label('month'),
|
||
func.count(WeeklyAttendance.record_id).label('record_count'),
|
||
func.sum(WeeklyAttendance.actual_work_hours).label('total_hours'),
|
||
func.sum(WeeklyAttendance.class_work_hours).label('class_hours'),
|
||
func.sum(WeeklyAttendance.overtime_hours).label('overtime_hours'),
|
||
func.sum(WeeklyAttendance.absent_days).label('absent_days')
|
||
).filter_by(student_number=current_user.student_number)
|
||
|
||
if start_date:
|
||
try:
|
||
start_date_obj = datetime.strptime(start_date, '%Y-%m-%d').date()
|
||
monthly_stats = monthly_stats.filter(WeeklyAttendance.week_start_date >= start_date_obj)
|
||
except ValueError:
|
||
pass
|
||
|
||
if end_date:
|
||
try:
|
||
end_date_obj = datetime.strptime(end_date, '%Y-%m-%d').date()
|
||
monthly_stats = monthly_stats.filter(WeeklyAttendance.week_end_date <= end_date_obj)
|
||
except ValueError:
|
||
pass
|
||
|
||
monthly_stats = monthly_stats.group_by('month').order_by('month').all()
|
||
|
||
# 最近几周的趋势数据
|
||
recent_weeks = attendance_query.order_by(desc(WeeklyAttendance.week_start_date)).limit(12).all()
|
||
recent_weeks.reverse() # 按时间正序排列
|
||
|
||
# 计算入学以来的总体表现
|
||
all_time_stats = None
|
||
if student.enrollment_date:
|
||
all_time_query = WeeklyAttendance.query.filter_by(
|
||
student_number=current_user.student_number
|
||
)
|
||
|
||
all_records = all_time_query.all()
|
||
if all_records:
|
||
# 计算入学以来的总统计
|
||
enrollment_weeks = (datetime.now().date() - student.enrollment_date).days // 7
|
||
|
||
# 🔥 修复:先计算缺勤次数,再定义字典
|
||
add_absent_count_to_records(all_records)
|
||
|
||
all_time_stats = {
|
||
'total_work_hours': sum(record.actual_work_hours for record in all_records),
|
||
'total_class_hours': sum(record.class_work_hours for record in all_records),
|
||
'total_overtime_hours': sum(record.overtime_hours for record in all_records),
|
||
'total_absent_count': sum(getattr(record, 'absent_count', 0) for record in all_records),
|
||
'attendance_weeks': len(all_records),
|
||
'enrollment_weeks': enrollment_weeks,
|
||
'attendance_rate': round(len(all_records) / max(enrollment_weeks, 1) * 100,
|
||
1) if enrollment_weeks > 0 else 0
|
||
}
|
||
|
||
# 计算入学以来的迟到次数
|
||
all_time_late_count = db.session.query(
|
||
func.sum(
|
||
case(
|
||
(DailyAttendanceDetail.status.like('%迟到%'), 1),
|
||
else_=0
|
||
)
|
||
)
|
||
).join(
|
||
WeeklyAttendance, DailyAttendanceDetail.weekly_record_id == WeeklyAttendance.record_id
|
||
).filter(WeeklyAttendance.student_number == current_user.student_number).scalar()
|
||
|
||
all_time_stats['total_late_count'] = int(all_time_late_count or 0)
|
||
|
||
return render_template('student/statistics.html',
|
||
student=student,
|
||
attendance_records=attendance_records,
|
||
total_stats=total_stats,
|
||
monthly_stats=monthly_stats,
|
||
recent_weeks=recent_weeks,
|
||
all_time_stats=all_time_stats,
|
||
start_date=start_date,
|
||
end_date=end_date)
|
||
|