app.course_views 源代码

"""
course_views.py

选课页面: selectCourse
课程详情页面: viewCourse
"""
from app.views_dependency import *
from app.models import (
    NaturalPerson,
    Semester,
    Activity,
    Course,
    CourseRecord,
)
from app.course_utils import (
    cancel_course_activity,
    create_single_course_activity,
    modify_course_activity,
    registration_status_change,
    course_to_display,
    create_course,
    cal_participate_num,
    check_post_and_modify,
    finish_course,
    download_course_record,
    download_select_info,
)
from app.utils import get_person_or_org

from datetime import datetime

from django.db import transaction

from utils.config.cast import str_to_time


__all__ = [
    'editCourseActivity',
    'addSingleCourseActivity',
    'showCourseActivity',
    'showCourseRecord',
    'selectCourse',
    'viewCourse',
    'outputRecord',
    'outputSelectInfo',
]

APP_CONFIG = CONFIG.course


[文档] @login_required(redirect_field_name="origin") @utils.check_user_access(redirect_url="/logout/") @logger.secure_view() def editCourseActivity(request: HttpRequest, aid: int): """ 编辑单次书院课程活动,addActivity的简化版 :param request: 修改单次课程活动的请求 :type request: HttpRequest :param aid: 待修改的课程活动id :type aid: int :return: 返回"修改课程活动"页面 :rtype: HttpResponse """ try: aid = int(aid) activity = Activity.objects.get(id=aid) except: return redirect(message_url(wrong("活动不存在!"))) # 检查用户身份 html_display = {} if request.user.is_person(): my_messages.transfer_message_context( utils.user_login_org(request, activity.organization_id), html_display, ) if html_display['warn_code'] == 1: return redirect(message_url(html_display)) else: # 登陆成功,重新加载 return redirect(message_url(html_display, request.get_full_path())) me = utils.get_person_or_org(request.user) if activity.organization_id != me: return redirect(message_url(wrong("无法修改其他课程小组的活动!"))) # 这个页面只能修改书院课程活动(category=1) if activity.category != Activity.ActivityCategory.COURSE: return redirect(message_url(wrong('当前活动不是书院课程活动!'), f'/viewActivity/{activity.id}')) # 课程活动只能在发布前进行修改 if activity.status != Activity.Status.UNPUBLISHED: return redirect(message_url(wrong('当前活动状态不允许修改!'), f'/viewActivity/{activity.id}')) my_messages.transfer_message_context(request.GET, html_display) if request.method == "POST" and request.POST: # 修改活动 try: # 只能修改自己的活动 with transaction.atomic(): activity = Activity.objects.select_for_update().get(id=aid) assert activity.organization_id == me, "无法修改其他课程小组的活动!" modify_course_activity(request, activity) succeed("修改成功。", html_display) except AssertionError as err_info: return redirect(message_url(wrong(str(err_info)), request.get_full_path())) except Exception as e: print(e) return redirect(message_url(wrong("修改课程活动失败!"), request.get_full_path())) # 前端使用量 html_display["applicant_name"] = me.oname html_display["app_avatar_path"] = me.get_user_ava() bar_display = utils.get_sidebar_and_navbar(request.user, "修改课程活动") # 前端使用量,均可编辑 title = utils.escape_for_templates(activity.title) location = utils.escape_for_templates(activity.location) start = activity.start.strftime("%Y-%m-%d %H:%M") end = activity.end.strftime("%Y-%m-%d %H:%M") # introduction = escape_for_templates(activity.introduction) # 暂定不需要简介 edit = True # 前端据此区分是编辑还是创建 publish_day = activity.publish_day need_apply = activity.need_apply # 判断本活动是否为长期定时活动 course_time_tag = (activity.course_time is not None) return render(request, "course/lesson_add.html", locals())
[文档] @login_required(redirect_field_name="origin") @utils.check_user_access(redirect_url="/logout/") @logger.secure_view() def addSingleCourseActivity(request: HttpRequest): """ 创建单次书院课程活动,addActivity的简化版 :param request: 创建单次课程活动的请求 :type request: HttpRequest :return: 返回"发起单次课程活动"页面 :rtype: HttpResponse """ # 检查用户身份 html_display = {} me = utils.get_person_or_org(request.user) # 这里的me应该为小组账户 if not request.user.is_org() or me.otype.otype_name != APP_CONFIG.type_name: return redirect(message_url(wrong('书院课程小组账号才能开设课程活动!'))) if me.oname == CONFIG.yqpoint.org_name: return redirect("/showActivity/") # TODO: 可以重定向到书院课程聚合页面 # 检查是否已经开课 try: course = Course.objects.activated().get(organization=me) except: return redirect(message_url(wrong('本学期尚未开设书院课程,请先发起选课!'), '/showCourseActivity/')) if course.status != Course.Status.STAGE2 and course.status != Course.Status.SELECT_END: return redirect(message_url(wrong('只有补退选开始或选课结束以后才能增加课时!'), '/showCourseActivity/')) my_messages.transfer_message_context(request.GET, html_display) if request.method == "POST" and request.POST: # 创建活动 try: with transaction.atomic(): aid, created = create_single_course_activity(request) if not created: return redirect(message_url( succeed('存在信息相同的课程活动,已为您自动跳转!'), f'/viewActivity/{aid}')) return redirect(message_url(succeed('活动创建成功!'), f'/showCourseActivity/')) except AssertionError as err_info: return redirect(message_url(wrong(str(err_info)), request.path)) except Exception as e: return redirect(message_url(wrong("课程活动创建失败!"), request.path)) # 前端使用量 html_display["applicant_name"] = me.oname html_display["app_avatar_path"] = me.get_user_ava() bar_display = utils.get_sidebar_and_navbar(request.user, "发起单次课程活动") edit = False # 前端据此区分是编辑还是创建 course_time_tag = False return render(request, "course/lesson_add.html", locals())
[文档] @login_required(redirect_field_name='origin') @utils.check_user_access(redirect_url="/logout/") @logger.secure_view() def showCourseActivity(request: HttpRequest): """ 筛选本学期已结束的课程活动、未开始的课程活动,在课程活动聚合页面进行显示。 """ # Sanity check and start a html_display. html_display = {} me = get_person_or_org(request.user) # 获取自身 if not request.user.is_org() or me.otype.otype_name != APP_CONFIG.type_name: return redirect(message_url(wrong('只有书院课程组织才能查看此页面!'))) my_messages.transfer_message_context(request.GET, html_display) all_activity_list = ( Activity.objects .activated() .filter(organization_id=me) .filter(category=Activity.ActivityCategory.COURSE) .order_by("-start") ) future_activity_list = ( all_activity_list.filter( status__in=[ Activity.Status.UNPUBLISHED, Activity.Status.REVIEWING, Activity.Status.APPLYING, Activity.Status.WAITING, Activity.Status.PROGRESSING, ] ) ) finished_activity_list = ( all_activity_list .filter( status__in=[ Activity.Status.END, Activity.Status.CANCELED, ] ) .order_by("-end") ) # 本学期的已结束活动(包括已取消的) bar_display = utils.get_sidebar_and_navbar( request.user, navbar_name="我的活动") if request.method == "GET": html_display["warn_code"], html_display["warn_message"] = my_messages.get_request_message(request) # 取消单次活动 if request.method == "POST" and request.POST: cancel_all = False # 获取待取消的活动 try: aid = int(request.POST.get("cancel-action")) post_type = str(request.POST.get("post_type")) if post_type == "cancel_all": cancel_all = True activity = Activity.objects.get(id=aid) except: return redirect(message_url(wrong('遇到不可预料的错误。如有需要,请联系管理员解决!'), request.path)) if activity.organization_id != me: return redirect(message_url(wrong('您没有取消该课程活动的权限!'), request.path)) if activity.status in [ Activity.Status.REJECT, Activity.Status.ABORT, Activity.Status.END, Activity.Status.CANCELED, ]: return redirect(message_url(wrong('该课程活动已结束,不可取消!'), request.path)) assert activity.status not in [ Activity.Status.REVIEWING, # Activity.Status.APPLYING, ], "课程活动状态非法" # 课程活动不应出现审核状态 # 取消活动 with transaction.atomic(): activity = Activity.objects.select_for_update().get(id=aid) error = cancel_course_activity(request, activity, cancel_all) # 无返回值表示取消成功,有则失败 if error is None: html_display["warn_code"] = 2 html_display["warn_message"] = "成功取消活动。" else: return redirect(message_url(wrong(error)), request.path) return render(request, "course/show_course_activity.html", locals())
[文档] @login_required(redirect_field_name="origin") @utils.check_user_access(redirect_url="/logout/") @logger.secure_view() def showCourseRecord(request: UserRequest) -> HttpResponse: """ 展示及修改学时数据 在开启修改功能前,显示本学期已完成的所有课程活动的学生的参与次数 开启修改功能后,自动创建学时表,并且允许修改学时 :param request: 请求 :type request: HttpRequest :return: 下载导出的学时文件或者返回前端展示的数据 :rtype: HttpResponse """ # ----身份检查---- me = utils.get_person_or_org(request.user) # 获取自身 if request.user.is_person(): return redirect(message_url(wrong('学生账号不能访问此界面!'))) if me.otype.otype_name != APP_CONFIG.type_name: return redirect(message_url(wrong('非书院课程组织账号不能访问此界面!'))) # 提取课程,后端保证每个组织只有一个Course字段 # 获取课程开设筛选信息 year = GLOBAL_CONFIG.acadamic_year semester = GLOBAL_CONFIG.semester course = Course.objects.activated(noncurrent=False).filter(organization=me) if len(course) == 0: # 尚未开课的情况 return redirect(message_url(wrong('没有检测到该组织本学期开设的课程。'))) # TODO: 报错 这是代码不应该出现的bug assert len(course) == 1, "检测到该组织的课程超过一门,属于不可预料的错误,请及时处理!" course = course.first() # 是否可以编辑 editable = course.status == Course.Status.END # 获取前端可能的提示 messages = my_messages.transfer_message_context(request.GET) # -------- POST 表单处理 -------- # 默认状态为正常 if request.method == "POST" and request.POST: post_type = str(request.POST.get("post_type", "")) if not editable: # 由于未开放修改功能时前端无法通过表格和按钮修改和提交, # 所以如果出现POST请求,则为非法情况 if post_type == "end": with transaction.atomic(): course = Course.objects.select_for_update().get(id=course.id) messages = finish_course(course) return redirect(message_url(messages, request.path)) elif post_type == "download": return redirect(message_url( wrong('请先结课再下载学时数据!'), request.path)) else: return redirect(message_url( wrong('学时修改尚未开放。如有疑问,请联系管理员!'), request.path)) # 获取记录的QuerySet record_search = CourseRecord.objects.current().filter(course=course) # 导出学时为表格 if post_type == "download": if not record_search.exists(): return redirect(message_url( wrong('未查询到相应课程记录,请联系管理员。'), request.path)) return download_course_record(course, year, semester) # 不是其他post类型时的默认行为 with transaction.atomic(): # 检查信息并进行修改 record_search = record_search.select_for_update() messages = check_post_and_modify(record_search, request.POST) # TODO: 发送微信消息?不一定需要 # -------- GET 部分 -------- # 如果进入这个页面时课程的状态(Course.Status)为未结束,那么只能查看不能修改,此时从函数读取 # 每次进入都获取形如{id: times}的字典,这里id是naturalperson的主键id而不是userid participate_raw = cal_participate_num(course) if not editable: convert_dict = participate_raw # 转换为字典方便查询, 这里已经是字典了 # 选取人选 participant_list = NaturalPerson.objects.activated().filter( id__in=convert_dict.keys() ) # 转换为前端使用的list records_list = [ { "pk": person.id, "name": person.name, "grade": person.stu_grade, "avatar": person.get_user_ava(), "times": convert_dict[person.id], # 参与次数 } for person in participant_list ] # 否则可以修改表单,从CourseRecord读取 else: records_list = [] with transaction.atomic(): # 查找此课程本学期所有成员的学时表 record_search = CourseRecord.objects.current().filter( course=course, ).select_for_update().select_related( "person" ) # Prefetch person to use its name, stu_grade and avatar. Help speed up. # 前端循环list for record in record_search: # 每次都需要更新一下参与次数,避免出现手动调整签到但是未能记录在学时表的情况 record.attend_times = participate_raw[record.person.id] if int(record.total_hours) != record.total_hours: record.total_hours = int(record.total_hours) records_list.append({ "pk": record.person.id, "name": record.person.name, "grade": record.person.stu_grade, "avatar": record.person.get_user_ava(), "times": record.attend_times, "hours": record.total_hours }) CourseRecord.objects.bulk_update(record_search, ["attend_times"]) # 如果点击提交学时按钮,修改数据库之后,跳转至已结束的活动界面 if request.method == "POST": return(redirect("/showCourseActivity")) # 前端呈现信息,用于展示 course_info = { 'course': course.name, 'year': year, 'semester': "春季" if semester == Semester.SPRING else "秋季", } bar_display = utils.get_sidebar_and_navbar(request.user, "课程学时") render_context = dict( course_info=course_info, records_list=records_list, editable=editable, bar_display=bar_display, messages=messages, ) return render(request, "course/course_record.html", render_context)
[文档] @login_required(redirect_field_name="origin") @utils.check_user_access(redirect_url="/logout/") @logger.secure_view() def selectCourse(request: HttpRequest): """ 学生选课的聚合页面,包括: 1. 所有开放课程的选课信息 2. 在预选和补退选阶段,学生可以通过点击课程对应的按钮实现选课或者退选, 且点击后页面显示发生相应的变化 3. 显示选课结果 用户权限:学生和老师可以进入,组织不能进入;只有学生可以进行选课 :param request: POST courseid=<int> & action= "select" or "cancel" :type request: HttpRequest """ html_display = {} me = get_person_or_org(request.user) if request.user.is_org(): return redirect(message_url(wrong("组织账号无法访问书院选课页面。如需选课,请切换至个人账号;如需查看您发起的书院课程,请点击【我的课程】。"))) is_student = (me.identity == NaturalPerson.Identity.STUDENT) # 暂时不启用意愿点机制 # if not is_staff: # html_display["willing_point"] = remaining_willingness_point(me) my_messages.transfer_message_context(request.GET, html_display) # 学生选课或者取消选课 if request.method == 'POST': if not is_student: wrong("非学生账号不能进行选课!", html_display) return redirect(message_url(html_display, request.path)) if not request.user.active: wrong("您已毕业,不能进行选课或退课操作!", html_display) return redirect(message_url(html_display, request.path)) # 参数: 课程id,操作action: select/cancel try: course_id = request.POST.get('courseid') action = request.POST.get('action') # 合法性检查 assert action == "select" or action == "cancel" assert Course.objects.activated().filter(id=course_id).exists() except: wrong("出现预料之外的错误!如有需要,请联系管理员。", html_display) try: # 对学生的选课状态进行变更 context = registration_status_change(course_id, me, action) return redirect(message_url(context, request.path)) except: wrong("选课过程出现错误!请联系管理员。", html_display) html_display["current_year"] = GLOBAL_CONFIG.acadamic_year html_display["semester"] = ("春" if GLOBAL_CONFIG.semester == Semester.SPRING else "秋") html_display["yx_election_start"] = APP_CONFIG.yx_election_start html_display["yx_election_end"] = APP_CONFIG.yx_election_end html_display["btx_election_start"] = APP_CONFIG.btx_election_start html_display["btx_election_end"] = APP_CONFIG.btx_election_end html_display["publish_time"] = APP_CONFIG.publish_time html_display["status"] = None is_drawing = False # 是否正在进行抽签 if str_to_time(html_display["yx_election_start"]) > datetime.now(): html_display["status"] = "未开始" elif (str_to_time(html_display["yx_election_start"])) <= datetime.now() < ( str_to_time(html_display["yx_election_end"])): html_display["status"] = "预选" elif (str_to_time( html_display["yx_election_end"])) <= datetime.now() < (str_to_time( html_display["publish_time"])): html_display["status"] = "抽签中" is_drawing = True elif (str_to_time( html_display["btx_election_start"])) <= datetime.now() < ( str_to_time(html_display["btx_election_end"])): html_display["status"] = "补退选" # 选课是否已经全部结束 # is_end = (datetime.now() > str_to_time(html_display["btx_election_end"])) unselected_courses = Course.objects.unselected(me) selected_courses = Course.objects.selected(me) # 未选的课程需要按照课程类型排序 courses = {} for type, label in Course.CourseType.choices: # 前端使用键呈现 courses[label] = course_to_display(unselected_courses.filter(type=type), me) unselected_display = course_to_display(unselected_courses, me) selected_display = course_to_display(selected_courses, me) bar_display = utils.get_sidebar_and_navbar(request.user, "书院课程") return render(request, "course/select_course.html", locals())
[文档] @login_required(redirect_field_name="origin") @utils.check_user_access(redirect_url="/logout/") @logger.secure_view() def viewCourse(request: HttpRequest): """ 展示一门课程的详细信息,所有用户类型均可访问 :param request: GET courseid=<int> :type request: HttpRequest """ try: course_id = int(request.GET.get("courseid", None)) course = Course.objects.filter(id=course_id) assert course.exists() except: return redirect(message_url(wrong("该课程不存在!"))) me = utils.get_person_or_org(request.user) course_display = course_to_display(course, me, detail=True) bar_display = utils.get_sidebar_and_navbar(request.user, course_display[0]["name"]) return render(request, "course/course_info.html", locals())
@login_required(redirect_field_name="origin") @utils.check_user_access(redirect_url="/logout/") @logger.secure_view() def addCourse(request: HttpRequest, cid=None): """ 发起课程页 --------------- 页面逻辑: 该函数处理 GET, POST 两种请求,发起和修改两类操作 1. 访问 /addCourse/ 时,为创建操作,要求用户是小组; 2. 访问 /editCourse/aid 时,为编辑操作,要求用户是该活动的发起者 3. GET 请求创建课程的界面,placeholder 为 prompt 4. GET 请求编辑课程的界面,表单的 placeholder 会被修改为课程的旧值。 """ # 检查:不是超级用户,必须是小组,修改是必须是自己 html_display = {} # assert valid 已经在check_user_access检查过了 me = utils.get_person_or_org(request.user) # 这里的me应该为小组账户 if cid is None: if not request.user.is_org() or me.otype.otype_name != APP_CONFIG.type_name: return redirect(message_url(wrong('书院课程账号才能发起课程!'))) #暂时仅支持一个课程账号一学期只能开一门课 courses = Course.objects.activated().filter(organization=me) if courses.exists(): cid = courses[0].id return redirect(message_url( succeed('您已在本学期创建过课程,已为您自动跳转!'), f'/editCourse/{cid}')) edit = False else: try: cid = int(cid) course = Course.objects.get(id=cid) except: return redirect(message_url(wrong("课程不存在!"))) if course.organization != me: return redirect(message_url(wrong("无法修改其他小组的课程!"))) edit = True my_messages.transfer_message_context(request.GET, html_display) editable = False time_limit = False if edit: # 选课结束前才能修改课程信息 if course.status not in [Course.Status.SELECT_END, Course.Status.END]: editable = True # 上课时间只有在选课未开始才能修改 if course.status != Course.Status.WAITING: time_limit = True # 处理 POST 请求 # 在这个界面,不会返回render,而是直接跳转到viewCourse,可以不设计bar_display if request.method == "POST" and request.POST: if not edit: # 发起选课 course_DDL = str_to_time(APP_CONFIG.btx_election_end) if datetime.now() > course_DDL: return redirect(message_url(succeed("已超过选课时间节点,无法发起课程!"), f'/showCourseActivity/')) context = create_course(request) html_display["warn_code"] = context["warn_code"] if html_display["warn_code"] == 2: return redirect(message_url(succeed("创建课程成功!为您自动跳转到编辑界面。您也可切换到个人账号在书院课程页面查看这门课程!"), f'/editCourse/{context["cid"]}')) else: if not editable: return redirect(message_url(wrong('当前课程状态不允许修改!'), f'/editCourse/{course.id}')) context = create_course(request, course.id) html_display["warn_code"] = context["warn_code"] html_display["warn_message"] = context["warn_message"] # 下面的操作基本如无特殊说明,都是准备前端使用量 html_display["applicant_name"] = me.oname html_display["app_avatar_path"] = me.get_user_ava() html_display["today"] = datetime.now().strftime("%Y-%m-%d") course_type_all = [ ["德" , Course.CourseType.MORAL] , ["智" , Course.CourseType.INTELLECTUAL] , ["体" , Course.CourseType.PHYSICAL] , ["美" , Course.CourseType.AESTHETICS], ["劳" , Course.CourseType.LABOUR], ] defaultpics = [{"src": f"/static/assets/img/announcepics/{i+1}.JPG", "id": f"picture{i+1}"} for i in range(5)] if edit: course = Course.objects.get(id=cid) name = utils.escape_for_templates(course.name) organization = course.organization year = course.year semester = utils.escape_for_templates(course.semester) classroom = utils.escape_for_templates(course.classroom) teacher = utils.escape_for_templates(course.teacher) course_time = course.time_set.all() introduction = utils.escape_for_templates(course.introduction) teaching_plan=utils.escape_for_templates(course.teaching_plan) record_cal_method=utils.escape_for_templates(course.record_cal_method) status = course.status need_apply = course.need_apply publish_day = course.publish_day capacity = course.capacity type = course.type current_participants = course.current_participants QRcode=course.QRcode if not edit: bar_display = utils.get_sidebar_and_navbar(request.user, "发起课程") else: bar_display = utils.get_sidebar_and_navbar(request.user, "修改课程") return render(request, "course/register_course.html", locals())
[文档] @login_required(redirect_field_name="origin") @utils.check_user_access(redirect_url="/logout/") @logger.secure_view() def outputRecord(request: UserRequest): """ 导出所有学时信息 导出文件格式为excel,包括汇总和详情两个sheet。 汇总包括每位同学的学号、姓名和总有效学时 详情包括每位同学所有学时(有效或无效)的详细获得情况:课程、学年等 """ me = utils.get_person_or_org(request.user) # 获取默认审核老师,不应该出错 examine_teachers = NaturalPerson.objects.get_teachers(APP_CONFIG.audit_teachers) if me not in examine_teachers: return redirect(message_url(wrong("只有书院课审核老师账号可以访问该链接!"))) return download_course_record()
[文档] @login_required(redirect_field_name="origin") @utils.check_user_access(redirect_url="/logout/") @logger.secure_view() def outputSelectInfo(request: UserRequest): """ 导出该课程的选课名单 """ # 检查:不是超级用户,必须是小组,修改是必须是自己 me = utils.get_person_or_org(request.user) try: assert (request.user.is_org() and me.otype.otype_name == APP_CONFIG.type_name), '只有书院课程账号才能下载选课名单!' # 暂时仅支持一个课程账号一学期只能开一门课 courses = Course.objects.activated().filter(organization=me) assert courses.exists(), '只有在开课以后才能下载选课名单!' course = courses[0] assert course.status in [Course.Status.STAGE2, Course.Status.SELECT_END], '补退选以后才能下载选课名单!' except Exception as e: return redirect(message_url(wrong(str(e)), '/showCourseActivity/')) return download_select_info(course)
@login_required(redirect_field_name="origin") @utils.check_user_access(redirect_url="/logout/") @logger.secure_view() def outputAllSelectInfo(request: UserRequest): """ 导出所有课程的选课名单 """ me = utils.get_person_or_org(request.user) # 获取默认审核老师,不应该出错 examine_teachers = NaturalPerson.objects.get_teachers(APP_CONFIG.audit_teachers) if me not in examine_teachers: return redirect(message_url(wrong("只有书院课审核老师账号可以访问该链接!"))) return download_select_info()