app.activity_views 源代码

import os
import io
import urllib.parse
from datetime import datetime, timedelta
from typing import Literal

from django.db import transaction
from django.db.models import F
import csv
import qrcode

from generic.models import User
from generic.utils import to_search_indices
from app.views_dependency import *
from app.view.base import ProfileTemplateView
from app.models import (
    NaturalPerson,
    OrganizationType,
    Position,
    Activity,
    ActivityPhoto,
    Participation,
    ActivitySummary,
)
from app.activity_utils import (
    ActivityException,
    create_activity,
    modify_activity,
    accept_activity,
    reject_activity,
    apply_activity,
    cancel_activity,
    withdraw_activity,
    get_activity_QRcode,
    create_participate_infos,
    modify_participants,
    weekly_summary_orgs,
    available_participants,
)
from app.comment_utils import addComment, showComment
from app.utils import (
    get_person_or_org,
    escape_for_templates,
)

__all__ = [
    'viewActivity', 'getActivityInfo', 'checkinActivity',
    'addActivity', 'activityCenter', 'examineActivity',
    'offlineCheckinActivity', 'finishedActivityCenter', 'activitySummary',
    'WeeklyActivitySummary',
]


[文档] @login_required(redirect_field_name="origin") @utils.check_user_access(redirect_url="/logout/") @logger.secure_view() def viewActivity(request: HttpRequest, aid=None): """ 页面逻辑: 1. 方法为 GET 时,展示一个活动的详情。 a. 如果当前用户是个人,有立即报名/已报名的 button b. 如果当前用户是小组,并且是该活动的所有者,有修改和取消活动的 button 2. 方法为 POST 时,通过 option 确定操作 a. 如果修改活动,跳转到 addActivity b. 如果取消活动,本函数处理 c. 如果报名活动,本函数处理 ( 还未实现 ) # TODO 个人操作,包括报名与取消 ---------------------------- 活动逻辑 1. 活动开始前一小时,不能修改活动 2. 活动开始当天晚上之前,不能再取消活动 ( 目前用的 12 小时,感觉基本差不多 ) """ """ aname = str(request.POST["aname"]) # 活动名称 organization_id = request.POST["organization_id"] # 小组id astart = request.POST["astart"] # 默认传入的格式为 2021-07-21 21:00:00 afinish = request.POST["afinish"] content = str(request.POST["content"]) URL = str(request.POST["URL"]) # 活动推送链接 QRcode = request.POST["QRcode"] # 收取元气值的二维码 capacity = request.POST["capacity"] # 活动举办的容量 """ aid = int(aid) activity: Activity = Activity.objects.get(id=aid) org = activity.organization_id me = get_person_or_org(request.user) ownership = False if request.user.is_org() and org == me: ownership = True examine = False if request.user.is_person() and activity.examine_teacher == me: examine = True if not (ownership or examine) and activity.status in [ Activity.Status.REVIEWING, Activity.Status.ABORT, Activity.Status.REJECT, ]: return redirect(message_url(wrong('该活动暂不可见!'))) html_display = dict() inform_share, alert_message = utils.get_inform_share(me) if request.method == "POST" and request.POST: option = request.POST.get("option") if option == "cancel": try: if activity.status in [ Activity.Status.REJECT, Activity.Status.ABORT, Activity.Status.END, Activity.Status.CANCELED, ]: return redirect(message_url(wrong('该活动已结束,不可取消!'), request.path)) if not ownership: return redirect(message_url(wrong('您没有修改该活动的权限!'), request.path)) with transaction.atomic(): activity = Activity.objects.select_for_update().get(id=aid) cancel_activity(request, activity) succeed("成功取消活动。", html_display) except ActivityException as e: wrong(str(e), html_display) except Exception as e: raise elif option == "edit": if ( activity.status == Activity.Status.APPLYING or activity.status == Activity.Status.REVIEWING ): return redirect(f"/editActivity/{aid}") if activity.status == Activity.Status.WAITING: if activity.start + timedelta(hours=1) < datetime.now(): return redirect(f"/editActivity/{aid}") wrong(f"距离活动开始前1小时内将不再允许修改活动。如确有雨天等意外情况,请及时取消活动。", html_display) else: wrong(f"活动状态为{activity.status}, 不能修改。", html_display) elif option == "apply": try: with transaction.atomic(): activity = Activity.objects.select_for_update().get(id=int(aid)) if activity.status != Activity.Status.APPLYING: return redirect(message_url(wrong('活动不在报名状态!'), request.path)) apply_activity(request, activity) if activity.bidding: succeed(f"活动申请中,请等待报名结果。", html_display) else: succeed(f"报名成功。", html_display) except ActivityException as e: wrong(str(e), html_display) except Exception as e: raise elif option == "quit": try: with transaction.atomic(): activity = Activity.objects.select_for_update().get(id=aid) if activity.status not in [ Activity.Status.APPLYING, Activity.Status.WAITING, ]: return redirect(message_url(wrong('当前状态不允许取消报名!'), request.path)) withdraw_activity(request, activity) if activity.bidding: html_display["warn_message"] = f"已取消申请。" else: html_display["warn_message"] = f"已取消报名。" html_display["warn_code"] = 2 except ActivityException as e: html_display["warn_code"] = 1 html_display["warn_message"] = str(e) except Exception as e: raise elif option == "checkinoffline": # 进入调整签到界面 if activity.status != Activity.Status.END: return redirect(message_url(wrong('活动尚未结束!'), request.path)) if not ownership: return redirect(message_url(wrong('您没有调整签到信息的权限!'), request.path)) return redirect(f"/offlineCheckinActivity/{aid}") elif option == "sign" or option == "enroll": # 下载活动签到信息或者报名信息 if not ownership: return redirect(message_url(wrong('没有下载权限!'), request.path)) return utils.export_activity(activity, option) elif option == "cancelInformShare": me.inform_share = False me.save() return redirect("/welcome/") elif option == "ActivitySummary": try: re = ActivitySummary.objects.get(activity=activity, status__in=[ActivitySummary.Status.WAITING, ActivitySummary.Status.CONFIRMED]) return redirect(f"/modifyEndActivity/?apply_id={re.id}") except: return redirect(f"/modifyEndActivity/") else: return redirect(message_url(wrong('无效的请求!'), request.path)) elif request.method == "GET": my_messages.transfer_message_context(request.GET, html_display) # 下面这些都是展示前端页面要用的 title = activity.title org_name = org.oname org_avatar_path = org.get_user_ava() org_type = OrganizationType.objects.get(otype_id=org.otype_id).otype_name start_month = activity.start.month start_date = activity.start.day duration = activity.end - activity.start duration = duration - timedelta(microseconds=duration.microseconds) prepare_times = Activity.EndBeforeHours.prepare_times apply_deadline = activity.apply_end.strftime("%Y-%m-%d %H:%M") introduction = activity.introduction show_url = True # 前端使用量 aURL = activity.URL if (aURL is None) or (aURL == ""): show_url = False bidding = activity.bidding current_participants = activity.current_participants status = activity.status capacity = activity.capacity if capacity == -1 or capacity == 10000: capacity = "INF" if activity.bidding: apply_manner = "抽签模式" else: apply_manner = "先到先得" # person 表示是否是个人而非小组 person = False if request.user.is_person(): """ 老师能否报名活动? if me.identity == NaturalPerson.Identity.STUDENT: person = True """ person = True try: participant = Participation.objects.get( SQ.sq(Participation.activity, activity), SQ.sq(Participation.person, me)) # pStatus 是参与状态 pStatus = participant.status except: pStatus = "未参与" if pStatus == "放弃": pStatus = "未参与" # 签到 need_checkin = activity.need_checkin show_QRcode = activity.need_checkin and activity.status in [ Activity.Status.APPLYING, Activity.Status.WAITING, Activity.Status.PROGRESSING ] if activity.inner and request.user.is_person(): position = Position.objects.activated().filter( person=me, org=activity.organization_id) if len(position) == 0: not_inner = True if ownership and need_checkin: aQRcode = get_activity_QRcode(activity) # 活动宣传图片 ( 一定存在 ) photo: ActivityPhoto = activity.photos.get( type=ActivityPhoto.PhotoType.ANNOUNCE) # 不是static静态文件夹里的文件,而是上传到media/activity的图片 firstpic = photo.get_image_path() # 总结图片,不一定存在 summary_photo_exists = False if activity.status == Activity.Status.END: try: summary_photos = activity.photos.filter( type=ActivityPhoto.PhotoType.SUMMARY) summary_photo_exists = True except Exception as e: pass # 参与者, 无论报名是否通过 participants = Participation.objects.filter( SQ.sq(Participation.activity, activity), status__in=[ Participation.AttendStatus.APPLYING, Participation.AttendStatus.APPLYSUCCESS, Participation.AttendStatus.ATTENDED, Participation.AttendStatus.UNATTENDED, ]) people_list = NaturalPerson.objects.activated().filter( id__in=SQ.qsvlist(participants, Participation.person)) # 新版侧边栏,顶栏等的呈现,采用bar_display,必须放在render前最后一步,但这里render太多了 # TODO: 整理好代码结构,在最后统一返回 bar_display = utils.get_sidebar_and_navbar( request.user, navbar_name="活动信息", title_name=title) # 补充一些呈现信息 # bar_display["title_name"] = "活动信息" # bar_display["navbar_name"] = "活动信息" # 浏览次数,必须在render之前 # 为了防止发生错误的存储,让数据库直接更新浏览次数,并且不再显示包含本次浏览的数据 Activity.objects.filter(id=activity.id).update( visit_times=F('visit_times')+1) # activity.visit_times += 1 # activity.save() return render(request, "activity/info.html", locals())
[文档] @login_required(redirect_field_name="origin") @utils.check_user_access(redirect_url="/logout/") @logger.secure_view() def getActivityInfo(request: HttpRequest): ''' 通过GET获得活动信息表下载链接 GET参数?activityid=id&infotype=sign[&output=id,name,gender,telephone][&format=csv|excel] GET参数?activityid=id&infotype=qrcode activity_id : 活动id infotype : sign or qrcode or 其他(以后可以拓展) sign报名信息: output : [可选]','分隔的需要返回的的field名 [默认]id,name,gender,telephone format : [可选]csv or excel [默认]csv qrcode签到二维码 example: http://127.0.0.1:8000/getActivityInfo?activityid=1&infotype=sign example: http://127.0.0.1:8000/getActivityInfo?activityid=1&infotype=sign&output=id,wtf example: http://127.0.0.1:8000/getActivityInfo?activityid=1&infotype=sign&format=excel example: http://127.0.0.1:8000/getActivityInfo?activityid=1&infotype=qrcode TODO: 前端页面待对接 ''' # check activity existence activity_id = request.GET.get("activityid", None) activity = Activity.objects.get(id=activity_id) # check organization existance and ownership to activity organization = get_person_or_org(request.user, UTYPE_ORG) assert activity.organization_id == organization, f"{organization}不是活动的组织者" info_type = request.GET.get("infotype", None) assert info_type in ["sign", "qrcode"], "不支持的infotype" info_type: Literal["sign", "qrcode"] if info_type == "sign": # get registration information # make sure registration is over assert activity.status != Activity.Status.REVIEWING, "活动正在审核" assert activity.status != Activity.Status.CANCELED, "活动已取消" assert activity.status != Activity.Status.APPLYING, "报名尚未截止" # get participants participants = SQ.sfilter(Participation.activity, activity).filter( status=Participation.AttendStatus.APPLYSUCCESS ) # get required fields output = request.GET.get("output", "id,name,gender,telephone") fields = output.split(",") # check field existence allowed_fields = ["id", "name", "gender", "telephone"] for field in fields: assert field in allowed_fields, f"不允许的字段名{field}" filename = f"{activity_id}-{info_type}-{output}" content = map( lambda paticipant: map(lambda key: paticipant[key], fields), participants, ) format = request.GET.get("format", "csv") assert format in ["csv"], f"不支持的格式{format}" format: Literal["csv"] if format == "csv": buffer = io.StringIO() csv.writer(buffer).writerows(content), buffer.seek(0) response = HttpResponse(buffer, content_type="text/csv") response["Content-Disposition"] = f"attachment; filename={filename}.csv" return response # downloadable elif info_type == "qrcode": # checkin begins 1 hour ahead assert datetime.now() > activity.start - timedelta(hours=1), "签到未开始" checkin_url = f"/checkinActivity?activityid={activity.id}" origin_url = request.scheme + "://" + request.META["HTTP_HOST"] checkin_url = urllib.parse.urljoin( origin_url, checkin_url) # require full path buffer = io.BytesIO() qr = qrcode.QRCode(version=1, box_size=10, border=5) qr.add_data(checkin_url), qr.make(fit=True) img = qr.make_image(fill_color="black", back_color="white") img.save(buffer, "jpeg"), buffer.seek(0) return HttpResponse(buffer, content_type="img/jpeg")
[文档] @login_required(redirect_field_name="origin") @utils.check_user_access(redirect_url="/logout/") @logger.secure_view() def checkinActivity(request: UserRequest, aid=None): if not request.user.is_person(): return redirect(message_url(wrong('签到失败:请使用个人账号签到'))) try: np = get_person_or_org(request.user) aid = int(aid) activity = Activity.objects.get(id=aid) varifier = request.GET["auth"] except: return redirect(message_url(wrong('签到失败!'))) if varifier != GLOBAL_CONFIG.hasher.encode(str(aid)): return redirect(message_url(wrong('签到失败:活动校验码不匹配'))) # context = wrong('发生意外错误') # 理应在任何情况都生成context, 如果没有就让包装器捕获吧 if activity.status == Activity.Status.END: context = wrong("活动已结束,不再开放签到。") elif ( activity.status == Activity.Status.PROGRESSING or (activity.status == Activity.Status.WAITING and datetime.now() + timedelta(hours=1) >= activity.start) ): try: with transaction.atomic(): participant = Participation.objects.select_for_update().get( SQ.sq(Participation.activity, activity), SQ.sq(Participation.person, np), status__in=[ Participation.AttendStatus.UNATTENDED, Participation.AttendStatus.APPLYSUCCESS, Participation.AttendStatus.ATTENDED, ] ) if participant.status == Participation.AttendStatus.ATTENDED: context = succeed("您已签到,无需重复签到!") else: participant.status = Participation.AttendStatus.ATTENDED participant.save() context = succeed("签到成功!") except: context = wrong("您尚未报名该活动!") else: context = wrong("活动开始前一小时开放签到,请耐心等待!") # TODO 在 activity_info 里加更多信息 return redirect(message_url(context, f"/viewActivity/{aid}"))
[文档] @login_required(redirect_field_name="origin") @utils.check_user_access(redirect_url="/logout/") @logger.secure_view() def addActivity(request: UserRequest, aid=None): """ 发起活动与修改活动页 --------------- 页面逻辑: 该函数处理 GET, POST 两种请求,发起和修改两类操作 1. 访问 /addActivity/ 时,为创建操作,要求用户是小组; 2. 访问 /editActivity/aid 时,为编辑操作,要求用户是该活动的发起者 3. GET 请求创建活动的界面,placeholder 为 prompt 4. GET 请求编辑活动的界面,表单的 placeholder 会被修改为活动的旧值。 """ # TODO 定时任务 # 检查:不是超级用户,必须是小组,修改是必须是自己 html_display = {} # assert valid 已经在check_user_access检查过了 me = get_person_or_org(request.user) # 这里的me应该为小组账户 if aid is None: if not request.user.is_org(): return redirect(message_url(wrong('小组账号才能添加活动!'))) if me.oname == CONFIG.yqpoint.org_name: return redirect("/showActivity") edit = False else: aid = int(aid) activity = Activity.objects.get(id=aid) if request.user.is_person(): # 自动更新request.user html_display = utils.user_login_org( request, activity.organization_id) if html_display['warn_code'] == 1: return redirect(message_url(html_display)) else: # 成功以小组账号登陆 me = activity.organization_id if activity.organization_id != me: return redirect(message_url(wrong("无法修改其他小组的活动!"))) edit = True # 处理 POST 请求 # 在这个界面,不会返回render,而是直接跳转到viewactivity,可以不设计bar_display if request.method == "POST" and request.POST: if not edit: with transaction.atomic(): aid, created = create_activity(request) if not created: return redirect(message_url( succeed('存在信息相同的活动,已为您自动跳转!'), f'/viewActivity/{aid}')) return redirect(f"/editActivity/{aid}") # 仅这几个阶段可以修改 if ( activity.status != Activity.Status.REVIEWING and activity.status != Activity.Status.APPLYING and activity.status != Activity.Status.WAITING ): return redirect(message_url(wrong('当前活动状态不允许修改!'), f'/viewActivity/{activity.id}')) # 处理 comment if request.POST.get("comment_submit"): # 创建活动只能在审核时添加评论 assert not activity.valid context = addComment( request, activity, activity.examine_teacher.person_id) # 评论内容不为空,上传文件类型为图片会在前端检查,这里有错直接跳转 assert context["warn_code"] == 2, context["warn_message"] # 成功后重新加载界面 succeed("评论成功。", html_display) # return redirect(f"/editActivity/{aid}") else: try: # 只能修改自己的活动 with transaction.atomic(): activity = Activity.objects.select_for_update().get(id=aid) org = get_person_or_org(request.user) assert activity.organization_id == org modify_activity(request, activity) succeed("修改成功。", html_display) except ActivityException as e: wrong(str(e), html_display) # 下面的操作基本如无特殊说明,都是准备前端使用量 defaultpics = [{"src": f"/static/assets/img/announcepics/{i+1}.JPG", "id": f"picture{i+1}"} for i in range(5)] html_display["applicant_name"] = me.oname html_display["app_avatar_path"] = me.get_user_ava() use_template = False if request.method == "GET" and request.GET.get("template"): use_template = True template_id = int(request.GET["template"]) activity = Activity.objects.get(id=template_id) if not edit and not use_template: available_teachers = NaturalPerson.objects.teachers() else: org = get_person_or_org(request.user) # 没过审,可以编辑评论区 if not activity.valid: commentable = True front_check = True if use_template: commentable = False # 全可编辑 full_editable = False accepted = False if activity.status == Activity.Status.REVIEWING: full_editable = True accepted = True # 部分可编辑 # 活动只能在开始 1 小时前修改 elif ( activity.status == Activity.Status.APPLYING or activity.status == Activity.Status.WAITING ) and datetime.now() + timedelta(hours=1) < activity.start: accepted = True else: # 不是三个可以评论的状态 commentable = front_check = False # 决定状态的变量 # None/edit/examine ( 小组申请活动/小组编辑/老师审查 ) # full_editable/accepted/None ( 小组编辑活动:除审查老师外全可修改/部分可修改/全部不可改 ) # full_editable 为 true 时,accepted 也为 true # commentable ( 是否可以评论 ) # 下面是前端展示的变量 title = utils.escape_for_templates(activity.title) location = utils.escape_for_templates(activity.location) apply_end = activity.apply_end.strftime("%Y-%m-%d %H:%M") # apply_end_for_js = activity.apply_end.strftime("%Y-%m-%d %H:%M") start = activity.start.strftime("%Y-%m-%d %H:%M") end = activity.end.strftime("%Y-%m-%d %H:%M") introduction = escape_for_templates(activity.introduction) url = utils.escape_for_templates(activity.URL) endbefore = activity.endbefore bidding = activity.bidding signscheme = "先到先得" if bidding: signscheme = "抽签模式" capacity = activity.capacity no_limit = False if capacity == 10000: no_limit = True examine_teacher = activity.examine_teacher.name status = activity.status available_teachers = NaturalPerson.objects.teachers() need_checkin = activity.need_checkin inner = activity.inner if not use_template: comments = showComment(activity) photo = str(activity.photos.get( type=ActivityPhoto.PhotoType.ANNOUNCE).image) uploaded_photo = False if str(photo).startswith("activity"): uploaded_photo = True photo_path = photo photo = os.path.basename(photo) else: photo_id = "picture" + os.path.basename(photo).split(".")[0] html_display["today"] = datetime.now().strftime("%Y-%m-%d") 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, "activity/application.html", locals())
[文档] @login_required(redirect_field_name="origin") @utils.check_user_access(redirect_url="/logout/") @logger.secure_view() def activityCenter(request: UserRequest): """ 活动信息的聚合界面 只有老师和小组才能看到,老师看到检查者是自己的,小组看到发起方是自己的 """ me = get_person_or_org(request.user) # 获取自身 is_teacher = False # 该变量同时用于前端 if request.user.is_person(): is_teacher = me.is_teacher() if not is_teacher: return redirect(message_url(wrong('学生账号不能进入活动立项页面!'))) if is_teacher: all_instances = { "undone": Activity.objects.activated( only_displayable=False).filter(examine_teacher=me.id, valid=False), "done": Activity.objects.activated( only_displayable=False).filter(examine_teacher=me.id, valid=True) } else: all_instances = { "undone": Activity.objects.activated( only_displayable=False).filter(organization_id=me.id, valid=False), "done": Activity.objects.activated( only_displayable=False).filter(organization_id=me.id, valid=True) } all_instances = {key: value.order_by( "-modify_time", "-time") for key, value in all_instances.items()} bar_display = utils.get_sidebar_and_navbar(request.user, "活动立项") # 前端不允许元气值中心创建活动 if request.user.is_org() and me.oname == CONFIG.yqpoint.org_name: YQPoint_Source_Org = True return render(request, "activity/center.html", locals() | dict(user=request.user))
[文档] @login_required(redirect_field_name="origin") @logger.secure_view() def examineActivity(request: UserRequest, aid: int | str): try: assert request.user.is_valid() assert request.user.is_person() me = get_person_or_org(request.user) activity = Activity.objects.get(id=int(aid)) assert activity.examine_teacher == me except: return redirect(message_url(wrong('没有审核权限!'))) html_display = {} if request.method == "POST" and request.POST: if ( activity.status != Activity.Status.REVIEWING and activity.status != Activity.Status.APPLYING and activity.status != Activity.Status.WAITING ): return redirect(message_url(wrong('当前活动状态不可审核!'))) if activity.valid: return redirect(message_url(succeed('活动已审核!'))) if request.POST.get("comment_submit"): context = addComment( request, activity, activity.organization_id.get_user()) # 评论内容不为空,上传文件类型为图片会在前端检查,这里有错直接跳转 assert context["warn_code"] == 2 succeed("评论成功。", html_display) elif request.POST.get("review_accepted"): with transaction.atomic(): activity = Activity.objects.select_for_update().get( id=int(aid) ) accept_activity(request, activity) succeed("活动已通过审核。", html_display) else: with transaction.atomic(): activity = Activity.objects.select_for_update().get( id=int(aid) ) reject_activity(request, activity) succeed("活动已被拒绝。", html_display) # 状态量,无可编辑量 examine = True commentable = not activity.valid if ( activity.status != Activity.Status.REVIEWING and activity.status != Activity.Status.APPLYING and activity.status != Activity.Status.WAITING ): commentable = False # 展示变量 title = utils.escape_for_templates(activity.title) location = utils.escape_for_templates(activity.location) apply_end = activity.apply_end.strftime("%Y-%m-%d %H:%M") start = activity.start.strftime("%Y-%m-%d %H:%M") end = activity.end.strftime("%Y-%m-%d %H:%M") introduction = escape_for_templates(activity.introduction) url = utils.escape_for_templates(activity.URL) endbefore = activity.endbefore bidding = activity.bidding signscheme = "先到先得" if bidding: signscheme = "投点参与" capacity = activity.capacity no_limit = False if capacity == 10000: no_limit = True examine_teacher = activity.examine_teacher.name html_display["today"] = datetime.now().strftime("%Y-%m-%d") html_display["app_avatar_path"] = activity.organization_id.get_user_ava() html_display["applicant_name"] = activity.organization_id.oname bar_display = utils.get_sidebar_and_navbar(request.user) status = activity.status comments = showComment(activity) examine_pic = activity.photos.get(type=ActivityPhoto.PhotoType.ANNOUNCE) # 不是static静态文件夹里的文件,而是上传到media/activity的图片 if str(examine_pic.image)[0] == 'a': examine_pic.image = MEDIA_URL + str(examine_pic.image) intro_pic = examine_pic.image need_checkin = activity.need_checkin bar_display = utils.get_sidebar_and_navbar(request.user, "活动审核") # bar_display["title_name"] = "审查活动" # bar_display["narbar_name"] = "审查活动" return render(request, "activity/application.html", locals())
[文档] @login_required(redirect_field_name="origin") @utils.check_user_access(redirect_url="/logout/") @logger.secure_view() def offlineCheckinActivity(request: HttpRequest, aid): ''' 修改签到记录,只有举办活动的组织账号可查看和修改 :param request: 修改请求 :type request: HttpRequest :param aid: 活动id :type aid: int :return: 修改签到页面 :rtype: HttpResponse ''' try: me = get_person_or_org(request.user) aid = int(aid) src = request.GET.get('src') activity = Activity.objects.get(id=aid) assert me == activity.organization_id and request.user.is_org() except: return redirect(message_url(wrong('请不要随意访问其他网页!'))) member_participation = Participation.objects.filter( SQ.sq(Participation.activity, activity), status__in=[ Participation.AttendStatus.UNATTENDED, Participation.AttendStatus.ATTENDED, ]) if request.method == "POST" and request.POST: option = request.POST.get("option") if option == "saveSituation": # 修改签到状态 member_userids = SQ.qsvlist(member_participation, Participation.person) attend_pids, unattend_pids = [], [] for pid in member_userids: checkin = request.POST.get(f"checkin_{pid}") if checkin == "yes": attend_pids.append(pid) elif checkin == "no": unattend_pids.append(pid) with transaction.atomic(): member_participation.select_for_update().filter( SQ.mq(Participation.person, IN=attend_pids)).update( status=Participation.AttendStatus.ATTENDED) member_participation.select_for_update().filter( SQ.mq(Participation.person, IN=unattend_pids)).update( status=Participation.AttendStatus.UNATTENDED) # 修改成功之后根据src的不同返回不同的界面,1代表聚合页面,2代表活动主页 if src == "course_center": return redirect(message_url( succeed("修改签到信息成功。"), f"/showCourseActivity/")) else: return redirect(message_url( succeed("修改签到信息成功。"), f"/viewActivity/{aid}")) bar_display = utils.get_sidebar_and_navbar(request.user, navbar_name="调整签到信息") member_participation = member_participation.select_related( SQ.f(Participation.person)) render_context = dict(bar_display=bar_display, member_list=member_participation) return render(request, "activity/modify_checkin.html", render_context)
[文档] @login_required(redirect_field_name="origin") @utils.check_user_access(redirect_url="/logout/") @logger.secure_view() def finishedActivityCenter(request: HttpRequest): """ 之前被用为报销信息的聚合界面,现已将报销删去,留下总结图片的功能 对审核老师进行了特判 """ is_auditor = False if request.user.is_person(): try: person = get_person_or_org(request.user) is_auditor = person.is_teacher() assert is_auditor except: return redirect(message_url(wrong("请不要使用个人账号申请活动结项!"))) if is_auditor: all_instances = { "undone": ActivitySummary.objects.filter( activity__examine_teacher=person, status=ActivitySummary.Status.WAITING).order_by("-time"), "done": ActivitySummary.objects.filter( activity__examine_teacher=person).exclude( status=ActivitySummary.Status.WAITING).order_by("-time") } else: all_instances = { "undone": ActivitySummary.objects.filter( activity__organization_id__organization_id=request.user, status=ActivitySummary.Status.WAITING).order_by("-time"), "done": ActivitySummary.objects.filter( activity__organization_id__organization_id=request.user ).exclude(status=ActivitySummary.Status.WAITING).order_by("-time") } # 判断是否有权限进行每周活动总结 weekly_summary_active = (request.user.is_org() and get_person_or_org(request.user) in weekly_summary_orgs()) # 前端使用 context = dict( bar_display=utils.get_sidebar_and_navbar(request.user, "活动结项"), all_instances=all_instances, user=request.user, weekly_summary_active=weekly_summary_active, ) return render(request, "activity/finished_center.html", context)
# 新建+修改+取消+审核 报销信息
[文档] @login_required(redirect_field_name="origin") @utils.check_user_access(redirect_url="/logout/") @logger.secure_view() def activitySummary(request: UserRequest): html_display = {} # ———————————————— 读取可能存在的申请 为POST和GET做准备 ———————————————— # 设置application为None, 如果非None则自动覆盖 application = None # 根据是否有newid来判断是否是第一次 apply_id = request.GET.get("apply_id", None) # 获取前端页面中可能存在的提示 my_messages.transfer_message_context(request.GET, html_display) if apply_id is not None: # 如果存在对应申请 try: # 尝试获取已经新建的apply application: ActivitySummary = ActivitySummary.objects.get( id=apply_id) auditor = application.activity.examine_teacher.get_user() # 审核老师 if request.user.is_person() and auditor != request.user: html_display = utils.user_login_org( request, application.get_org()) if html_display['warn_code'] == 1: return redirect(message_url(html_display)) # 接下来检查是否有权限check这个条目 # 至少应该是申请人或者被审核老师之一 assert (application.get_org().get_user() == request.user) or (auditor == request.user) except: # 恶意跳转 return redirect(message_url(wrong("您没有权限访问该网址!"))) is_new_application = False # 前端使用量, 表示是老申请还是新的 else: # 如果不存在id, 默认应该传入活动信息 # 只有小组才有可能申请 if not request.user.is_org(): return redirect(message_url(wrong("您没有权限访问该网址!"))) is_new_application = True # 新的申请 me = get_person_or_org(request.user) # 获取自身,便于之后查询 # 这种写法是为了方便随时取消某个条件 ''' 至此,如果是新申请那么application为None,否则为对应申请 application = None只有在小组新建申请的时候才可能出现,对应位is_new_application为True 接下来POST ''' if request.user.is_org(): # 未总结活动 summary_act_ids = ( ActivitySummary.objects.all().exclude( status=ActivitySummary.Status.CANCELED) # 未被取消的 .exclude(status=ActivitySummary.Status.REFUSED) # 未被拒绝的 .values_list("activity__id", flat=True)) # 可以新建申请的活动 activities = ( Activity.objects.activated() # 本学期的 .filter(organization_id=me) # 本部门小组的 .filter(status=Activity.Status.END) # 已结束的 .exclude(id__in=summary_act_ids)) # 还没有报销的 else: activities = None # ———————— Post操作,分为申请变更以及添加评论 ———————— if request.method == "POST" and request.POST.get("post_type") is not None: # 首先确定申请状态 post_type = request.POST.get("post_type") feasible_post = [ "new_submit", "modify_submit", "cancel_submit", "accept_submit", "refuse_submit" ] if post_type not in feasible_post: return redirect(message_url(wrong('申请状态异常!'))) # 接下来确定访问的个人/小组是不是在做分内的事情 if (request.user.is_person() and feasible_post.index(post_type) <= 2 ) or (request.user.is_org() and feasible_post.index(post_type) >= 3): return redirect(message_url(wrong('您无权进行此操作,如有疑惑, 请联系管理员'))) if (post_type != "new_submit") and not application.is_pending(): return redirect(message_url(wrong("不可以修改状态不为申请中的申请"))) full_path = request.get_full_path() if post_type == "new_submit": # 检查活动 try: act_id = int(request.POST.get('activity_id')) activity = Activity.objects.get(id=act_id) assert activity in activities # 防止篡改POST导致伪造 except: return redirect(message_url(wrong('找不到该活动,请检查活动总结的合法性!'))) # 活动总结图片合法性检查 summary_photos = request.FILES.getlist('summaryimages') photo_num = len(summary_photos) if photo_num != 1: return redirect(message_url(wrong('图片内容为空或有多张图片!'), full_path)) for image in summary_photos: if utils.if_image(image) != 2: return redirect(message_url(wrong("上传的总结图片只支持图片格式!"), full_path)) # 新建activity summary application: ActivitySummary = ActivitySummary.objects.create( status=ActivitySummary.Status.WAITING, activity=activity, image=summary_photos[0] ) context = succeed( f'活动“{application.activity.title}”的申请已成功发送,' + f'请耐心等待{application.activity.examine_teacher.name}老师审批!' ) elif post_type == "modify_submit": summary_photos = request.FILES.getlist('summaryimages') now_participant_uids = request.POST.getlist('students') photo_num = len(summary_photos) if photo_num > 1: return redirect(message_url(wrong('有多张图片!'), full_path)) for image in summary_photos: if utils.if_image(image) != 2: return redirect(message_url(wrong("上传的总结图片只支持图片格式!"), full_path)) if len(now_participant_uids) == 0: return redirect(message_url(wrong('参与人员不能为空'), full_path)) available_uids = SQ.qsvlist(available_participants(), User.username) if set(now_participant_uids) - set(available_uids): return redirect(message_url(wrong('参与人员不合法'), full_path)) with transaction.atomic(): # 修改活动总结图片 if photo_num > 0: application.image = summary_photos[0] application.save() # 修改参与人员 modify_participants(application.activity, now_participant_uids) context = succeed( f'活动“{application.activity.title}”的申请已成功修改,' + f'请耐心等待{application.activity.examine_teacher.name}老师审批!' ) elif post_type == "cancel_submit": if not application.is_pending(): # 如果不在pending状态, 可能是重复点击 return redirect(message_url(wrong("该申请已经完成或被取消"))) application.status = ActivitySummary.Status.CANCELED application.save() context = succeed(f"成功取消“{application.activity.title}”的活动总结申请!") else: if not application.is_pending(): return redirect(message_url(wrong("无法操作, 该申请已经完成或被取消!"))) if post_type == "refuse_submit": # 修改申请状态 application.status = ActivitySummary.Status.REFUSED application.save() context = succeed( f'已成功拒绝活动“{application.activity.title}”的活动总结申请!') elif post_type == "accept_submit": # 修改申请的状态 application.status = ActivitySummary.Status.CONFIRMED old_image = application.image if not old_image is None: ActivityPhoto.objects.create( image=old_image, activity=application.activity, time=datetime.now(), type=ActivityPhoto.PhotoType.SUMMARY) application.save() context = succeed(f'活动“{application.activity.title}”的总结申请已通过!') # 为了保证稳定性,完成POST操作后同意全体回调函数,进入GET状态 if application is None: return redirect(message_url(context, '/modifyEndActivity/')) else: return redirect(message_url(context, f'/modifyEndActivity/?apply_id={application.id}')) # ———————— 完成Post操作, 接下来开始准备前端呈现 ———————— ''' 小组:可能是新建、修改申请 老师:可能是审核申请 ''' render_context = dict() render_context.update(application=application, activities=activities, is_new_application=is_new_application) # (1) 是否允许修改表单 # 小组写表格? allow_form_edit = (request.user.is_org() and (is_new_application or application.is_pending())) # 老师审核? allow_audit_submit = (request.user.is_person() and not is_new_application and application.is_pending()) # 用于前端展示:如果是新申请,申请人即“me”,否则从application获取。 render_context.update( allow_form_edit=allow_form_edit, allow_audit_submit=allow_audit_submit, applicant=me if is_new_application else application.get_org(), ) # 活动总结图片 if application is not None: render_context.update(summary_photo=application.image) # 所有人员和参与人员 # 用于前端展示,把js数据都放在这里 json_context = dict(user_infos=to_search_indices(available_participants())) if application is not None: participation = SQ.sfilter(Participation.activity, application.activity) json_context.update(participant_ids=SQ.qsvlist( participation.filter(status=Participation.AttendStatus.ATTENDED), Participation.person, NaturalPerson.person_id, User.username )) render_context.update(json_context=json_context) bar_display = utils.get_sidebar_and_navbar(request.user, '活动总结详情') render_context.update(bar_display=bar_display, html_display=html_display) return render(request, "activity/summary_application.html", render_context)
[文档] class WeeklyActivitySummary(ProfileTemplateView): template_name = "activity/weekly_summary.html" page_name = "每周活动总结"
[文档] def prepare_get(self): return self.get
[文档] def prepare_post(self): self.context = { "bidding": False, "inner": False, "need_checkin": False, "recorded": True, "valid": True, "unlimited_capacity": True, "signscheme": 0, "maxpeople": 10000, "prepare_scheme": 1, "URL": "", "announce_pic_src": "/static/assets/img/announcepics/1.JPG", # Summary do not need an auditor, so we set it to arbitrary value "examine_teacher": NaturalPerson.objects.teachers().first() } summary_photos = self.request.FILES.getlist('summaryimages') # 检查总结图片合法性 photo_num = len(summary_photos) if photo_num == 1: for image in summary_photos: if utils.if_image(image) != 2: return redirect( message_url(wrong("上传的总结图片只支持图片格式!"))) else: return redirect(message_url(wrong('图片内容为空或有多张图片!'), self.request.path)) self.context['summary_pic'] = summary_photos[0] return self.post
[文档] def get(self): html_display = {} me = utils.get_person_or_org(self.request.user) if not self.request.user.is_org(): return redirect(message_url(wrong('小组账号才能发起每周活动总结'))) valid_orgs = weekly_summary_orgs() if not me in valid_orgs: return redirect(message_url(wrong('您没有权限发起每周活动总结'))) # 准备前端展示量 html_display["applicant_name"] = me.oname html_display["app_avatar_path"] = me.get_user_ava() html_display["today"] = datetime.now().strftime("%Y-%m-%d") bar_display = utils.get_sidebar_and_navbar(self.request.user, "活动发起") person_list = NaturalPerson.objects.activated() user_id_list = [person.person_id.id for person in person_list] user_queryset = User.objects.filter(id__in=user_id_list) js_stu_list = to_search_indices(user_queryset) self.extra_context.update({ 'html_display': html_display, 'bar_display': bar_display, 'js_stu_list': js_stu_list, }) return self.render()
[文档] def post(self): self.weekly_summary_base_check() aid, created = self.create_weekly_summary() if not created: return redirect(message_url( succeed('存在信息相同的活动,已为您自动跳转!'), f'/viewActivity/{aid}')) return redirect(f"/editActivity/{aid}")
[文档] def check_summary_time(self, start_time: datetime, end_time: datetime) -> bool: '''由每周活动总结新建的活动,检查开始时间早于结束时间''' now_time = datetime.now() if start_time < end_time <= now_time: return True return False
[文档] def weekly_summary_base_check(self): ''' 从request.POST中获取活动信息并检查合法性 正常情况下检查出错误会抛出不含错误信息的AssertionError,不抛出ActivityException ''' for k in ['title', 'introduction', 'location']: v = self.request.POST.get(k) assert v is not None and v != "" self.context[k] = v # 时间 act_start = datetime.strptime( self.request.POST["actstart"], "%Y-%m-%d %H:%M") # 活动报名时间 act_end = datetime.strptime( self.request.POST["actend"], "%Y-%m-%d %H:%M") # 活动报名结束时间 self.context["start"] = act_start self.context["end"] = act_end assert self.check_summary_time(act_start, act_end) prepare_scheme = int(self.context["prepare_scheme"]) prepare_times = Activity.EndBeforeHours.prepare_times prepare_time = prepare_times[prepare_scheme] self.context["endbefore"] = prepare_scheme self.context["apply_end"] = act_start - timedelta(hours=prepare_time)
[文档] def create_weekly_summary(self) -> tuple[int, bool]: ''' 检查是否存在一致的活动及活动合法性,若通过检查则创建活动及活动总结; 返回(activity.id, created); 若查询到一致的活动或检查不合格时抛出AssertionError ''' # 查找是否有类似活动存在 old_ones = Activity.objects.activated().filter( title=self.context["title"], start=self.context["start"], introduction=self.context["introduction"], location=self.context["location"] ) if len(old_ones): assert len(old_ones) == 1, "创建活动时,已存在的相似活动不唯一" return old_ones[0].id, False # 检查完毕,创建活动、活动总结 org = get_person_or_org(self.request.user, UTYPE_ORG) participants_ids = self.request.POST.getlist("students") with transaction.atomic(): # 创建活动、活动宣传图片 activity = Activity.objects.create( title=self.context["title"], organization_id=org, examine_teacher=self.context["examine_teacher"], introduction=self.context["introduction"], location=self.context["location"], capacity=self.context["maxpeople"], URL=self.context["URL"], start=self.context["start"], end=self.context["end"], bidding=self.context["bidding"], apply_end=self.context["apply_end"], inner=self.context["inner"], endbefore=self.context["endbefore"], need_checkin=self.context["need_checkin"], recorded=self.context["recorded"], valid=self.context["valid"], # 默认已审核 status=Activity.Status.END, ) ActivityPhoto.objects.create( image=self.context["announce_pic_src"], type=ActivityPhoto.PhotoType.ANNOUNCE, activity=activity) # 创建参与人 nps = SQ.mfilter(NaturalPerson.person_id, User.username, IN=participants_ids) status = Participation.AttendStatus.ATTENDED create_participate_infos(activity, nps, status=status) activity.current_participants = len(participants_ids) activity.settle_yqpoint() activity.save() # 创建活动总结 application: ActivitySummary = ActivitySummary.objects.create( activity=activity, status=ActivitySummary.Status.WAITING, image=self.context["summary_pic"] ) application.save() return activity.id, True