diff --git a/Appointment/appoint/manage.py b/Appointment/appoint/manage.py index c96bfc728..99f8a56d2 100644 --- a/Appointment/appoint/manage.py +++ b/Appointment/appoint/manage.py @@ -11,7 +11,6 @@ from Appointment.extern.wechat import MessageType, notify_appoint from Appointment.extern.jobs import set_appoint_reminder from utils.wrap import return_on_except, stringify_to -from achievement.api import unlock_achievement __all__ = [ @@ -186,8 +185,6 @@ def create_appoint( # 如果预约者是个人,解锁成就-完成地下室预约 该部分尚未测试 user = appointer.Sid - if user.is_person(): - unlock_achievement(user, '完成地下室预约') return _success(appoint) diff --git a/Appointment/summary.py b/Appointment/summary.py deleted file mode 100644 index 7df4bded9..000000000 --- a/Appointment/summary.py +++ /dev/null @@ -1,374 +0,0 @@ -import os -import json -from datetime import datetime - -from django.http import HttpRequest -from django.shortcuts import render, redirect -from django.urls import reverse - -from Appointment.models import Room -from Appointment.utils.identity import identity_check - - -@identity_check(redirect_field_name='origin') -def summary(request): # 主页 - Pid = "" - - try: - if not Pid: - Pid = request.user.username - with open(f'Appointment/summary_info/{Pid}.txt', 'r', encoding='utf-8') as fp: - myinfo = json.load(fp) - except: - return redirect(reverse("Appointment:logout")) - - Rid_list = {room.Rid: room.Rtitle.split( - '(')[0] for room in Room.objects.all()} - - # page 0 - Sname = myinfo['Sname'] - - # page 1 - all_appoint_num = 12649 - all_appoint_len = 19268.17 - all_appoint_len_day = round(all_appoint_len/24) - - # page 2 - appoint_make_num = int(myinfo['appoint_make_num']) - appoint_make_num_pct = myinfo['rank_num'] - appoint_make_hour = round(myinfo['appoint_make_hour'], 2) - appoint_make_hour_pct = myinfo['rank_hour'] - appoint_attend_num = int(myinfo['appoint_attend_num']) - appoint_attend_hour = round(myinfo['appoint_attend_hour'], 2) - - # page 3 - hottest_room_1 = ['B214', Rid_list['B214'], 1952] - hottest_room_2 = ['B220', Rid_list['B220'], 1715] - hottest_room_3 = ['B221', Rid_list['B221'], 1661] - - # page 4 - Sfav_room_id = myinfo['favourite_room_id'] - if Sfav_room_id: - Sfav_room_name = Rid_list[Sfav_room_id] - Sfav_room_freq = int(myinfo['favourite_room_freq']) - - # page 5 - Smake_time_most = myinfo['make_time_most'] - if Smake_time_most: - Smake_time_most = int(Smake_time_most) - - try: - Suse_time_list = myinfo['use_time_list'].split(';') - except: - Suse_time_list = [0]*24 - Suse_time_list = list(map(lambda x: int(x), Suse_time_list)) - try: - Suse_time_most = Suse_time_list.index(max(Suse_time_list)) - except: - Suse_time_most = -1 - Suse_time_list_js = json.dumps(Suse_time_list[6:]) - Suse_time_list_label = [str(i) for i in range(6, 24)] - Suse_time_list_label_js = json.dumps(Suse_time_list_label) - - # page 6 - Sfirst_appoint = myinfo['first_appoint'] - if Sfirst_appoint: - Sfirst_appoint = Sfirst_appoint.split('|') - Sfirst_appoint.append(Rid_list[Sfirst_appoint[4]]) - - # page 7 - Skeywords = myinfo['usage'] - if Skeywords: - Skeywords = Skeywords.split('|') - Skeywords_for_len = Skeywords.copy() - if '' in Skeywords_for_len: - Skeywords_for_len.remove('') - Skeywords_len = len(Skeywords_for_len) - else: - Skeywords_len = 0 - - # page 8 - Sfriend = myinfo['friend'] - if Sfriend == '': - Sfriend = None - if Sfriend: - Sfriend = Sfriend.split(';') - - # page 9 熬夜冠军 - aygj = myinfo['aygj'] - if aygj: - aygj = aygj.split('|') - aygj_num = 80 - - # page 10 早起冠军 - zqgj = myinfo['zqgj'] - if zqgj: - zqgj = zqgj.split('|') - # print(zqgj) - zqgj.insert(6, Rid_list[zqgj[5]]) - zqgj_num = 109 - - # page 11 未雨绸缪 - wycm = myinfo['wycm'] - wycm_num = 44 - - # page 12 极限操作 - jxcz = myinfo['jxcz'] - if jxcz: - jxcz = jxcz.split('|') - jxcz.insert(6, Rid_list[jxcz[5]]) - jxcz_num = 102 - - # page 13 元培鸽王 - ypgw = myinfo['ypgw'] - ypgw_num = 22 - - # page 14 新功能预告 - return render(request, 'Appointment/summary.html', locals()) - - -def summary2021(request: HttpRequest): - # 年度总结 - from dm.summary import generic_info, person_info - - base_dir = 'test_data' - - logged_in = request.user.is_authenticated - if logged_in: - username = request.session.get("NP", "") - if username: - from app.utils import update_related_account_in_session - update_related_account_in_session(request, username, shift=True) - - is_freshman = request.user.username.startswith('22') - user_accept = request.GET.get('accept') == 'true' - infos = generic_info() - infos.update( - logged_in=logged_in, - is_freshman=is_freshman, - user_accept=user_accept, - ) - - if user_accept and logged_in and not is_freshman: - try: - infos.update(person_info(request.user)) - with open(os.path.join(base_dir, 'rank_info.json')) as f: - rank_info = json.load(f) - sid = request.user.username - for k in ['co_pct', 'func_appoint_pct', 'discuss_appoint_pct']: - infos[k] = rank_info[k].index( - sid) * 100 // len(rank_info[k]) - except: - pass - else: - try: - example_file = os.path.join(base_dir, 'example.json') - with open(example_file) as f: - infos.update(json.load(f)) - except: - pass - - return render(request, 'Appointment/summary2021.html', infos) - -def summary2023(request: HttpRequest): - # 2023年度总结 - base_dir = 'static/Appointment/assets/summary_data/summary2023' - - logged_in = request.user.is_authenticated - if logged_in: - username = request.session.get("NP", "") - if username: - from app.utils import update_related_account_in_session - update_related_account_in_session(request, username, shift=True) - - user_accept = request.GET.get('accept') == 'true' - user_cancel = request.GET.get('cancel') == 'true' - infos = {} - - infos.update(logged_in=logged_in, user_accept=user_accept, user_cancel=user_cancel) - - if not user_accept or not logged_in or user_cancel: - # 新生/不接受协议/未登录 展示样例 - example_file = os.path.join(base_dir, 'template.json') - with open(example_file) as f: - infos.update(json.load(f)) - if logged_in: - with open(os.path.join(base_dir, 'summary2023.json'), 'r') as f: - infos.update(home_Sname=json.load(f)[request.user.username].get('Sname', '')) - else: - # 读取年度总结中该用户的个人数据 - with open(os.path.join(base_dir, 'summary2023.json'), 'r') as f: - infos.update(json.load(f)[request.user.username]) - - infos.update(home_Sname = infos['Sname']) - - # 读取年度总结中该用户的排名数据 - with open(os.path.join(base_dir, 'rank2023.json'), 'r') as f: - infos.update(json.load(f)[request.user.username]) - - # 读取年度总结中所有用户的总体数据 - with open(os.path.join(base_dir, 'summary_overall_2023.json'), 'r') as f: - infos.update(json.load(f)) - - # 将数据中缺少的项利用white-template中的默认值补齐 - with open(os.path.join(base_dir, 'white-template.json'), 'r') as f: - white_template = json.load(f) - for key, value in white_template.items(): - if key not in infos.keys(): - infos[key] = value - - # 计算用户自注册起至今过去的天数 - _date_joint = datetime.fromisoformat(infos['date_joined']) - _date_now = datetime.now() - days_passed = (_date_now - _date_joint).days - infos.update(days_passed=days_passed) - - # 处理导出的最常预约研讨室/功能室的数据格式是单元素list的情况 - Function_appoint_most_room = infos.get('Function_appoint_most_room') - if Function_appoint_most_room is not None: - if isinstance(Function_appoint_most_room, list): - if Function_appoint_most_room: - infos['Function_appoint_most_room'] = Function_appoint_most_room[0] - else: - infos['Function_appoint_most_room'] = '' - - Discuss_appoint_most_room = infos.get('Discuss_appoint_most_room') - if Discuss_appoint_most_room is not None: - if isinstance(Discuss_appoint_most_room, list): - if Discuss_appoint_most_room: - infos['Discuss_appoint_most_room'] = Discuss_appoint_most_room[0] - else: - infos['Discuss_appoint_most_room'] = '' - - # 将导出数据中iosformat的日期转化为只包含年、月、日的文字 - if infos.get('Discuss_appoint_longest_day'): # None or '' - Discuss_appoint_longest_day = datetime.fromisoformat(infos['Discuss_appoint_longest_day']) - infos['Discuss_appoint_longest_day'] = Discuss_appoint_longest_day.strftime("%Y年%m月%d日") - if infos.get('Function_appoint_longest_day'): - Function_appoint_longest_day = datetime.fromisoformat(infos['Function_appoint_longest_day']) - infos['Function_appoint_longest_day'] = Function_appoint_longest_day.strftime("%Y年%m月%d日") - - # 对最长研讨室/功能室预约的小时数向下取整 - if infos.get('Discuss_appoint_longest_duration'): - Discuss_appoint_longest_day_hours = infos['Discuss_appoint_longest_duration'].split('小时')[0] - infos.update(Discuss_appoint_longest_day_hours = Discuss_appoint_longest_day_hours) - else: - infos.update(Discuss_appoint_longest_day_hours = 0) - - if infos.get('Function_appoint_longest_duration'): - Function_appoint_longest_day_hours = infos['Function_appoint_longest_duration'].split('小时')[0] - infos.update(Function_appoint_longest_day_hours = Function_appoint_longest_day_hours) - else: - infos.update(Function_appoint_longest_day_hours = 0) - - # 处理导出共同预约关键词数据格式为[co_keyword, appear_num]的情况 - if infos.get('co_keyword'): - if isinstance(infos.get('co_keyword'), list): - if infos['co_keyword']: - co_keyword, num = infos['co_keyword'] - infos['co_keyword'] = co_keyword - else: - infos['co_keyword'] = '' - - # 将list格式的top3最热门课程转化为一个字符串 - hottest_courses_23_fall_dict = infos['hottest_courses_23_Fall'] - hottest_course_names_23_fall = '\n'.join([list(dic.keys())[0] for dic in hottest_courses_23_fall_dict]) - infos.update(hottest_course_names_23_fall=hottest_course_names_23_fall) - hottest_courses_23_spring_dict = infos['hottest_courses_23_Spring'] - hottest_course_names_23_spring = '\n'.join([list(dic.keys())[0] for dic in hottest_courses_23_spring_dict]) - infos.update(hottest_course_names_23_spring=hottest_course_names_23_spring) - - # 根据最长连续签到天数授予用户称号 - max_consecutive_days = infos.get('max_consecutive_days') - if max_consecutive_days is not None: - if max_consecutive_days <= 3: - infos.update(consecutive_days_name='初探新世界') - elif max_consecutive_days <= 7: - infos.update(consecutive_days_name='到此一游') - elif max_consecutive_days <= 15: - infos.update(consecutive_days_name='常住居民') - else: - infos.update(consecutive_days_name='永恒真爱粉') - else: - infos.update(consecutive_days_name='') - - # 处理用户创建学生小组过多的情况 - if infos.get('myclub_name'): - myclub_name_list = infos['myclub_name'].split(',') - if len(myclub_name_list) > 3: - myclub_name_list = myclub_name_list[:3] - infos.update(myclub_name=','.join(myclub_name_list) + '等') - - # 处理用户担任admin职务的小组数过多的情况 - if infos.get('admin_org_names'): - admin_org_names = infos['admin_org_names'] - if len(admin_org_names) > 3: - admin_org_names = admin_org_names[:3] - infos.update(admin_org_names_str=','.join(admin_org_names) + '等') - else: - infos.update(admin_org_names_str=','.join(admin_org_names)) - else: - infos.update(admin_org_names_str='') - - # 将小组活动预约top3关键词由list转为一个string - if infos.get('act_top_three_keywords'): - act_top_three_keywords = infos['act_top_three_keywords'] - infos.update(act_top_three_keywords_str=','.join(act_top_three_keywords)) - else: - infos.update(act_top_three_keywords_str='') - - # 根据参加小组活动最频繁时间段授予用户称号 - most_act_common_hour = infos.get('most_act_common_hour') - if most_act_common_hour is not None: - if most_act_common_hour <= 10: - infos.update(most_act_common_hour_name='用相聚开启元气满满的一天') - elif most_act_common_hour <= 13: - infos.update(most_act_common_hour_name='不如再用一顿美食为这次相聚做个注脚') - elif most_act_common_hour <= 16: - infos.update(most_act_common_hour_name='突击检查,瞌睡虫有没有出现?') - elif most_act_common_hour <= 18: - infos.update(most_act_common_hour_name='此刻的欢畅还有落霞余晖作伴') - elif most_act_common_hour <= 23: - infos.update(most_act_common_hour_name='夜色深沉时,每一个细胞都在期待着相约相聚') - else: - infos.update(most_act_common_hour_name='让星月陪我们狂歌竞夜') - else: - infos.update(most_act_common_hour_name='') - - # 计算参与的学生小组+书院课程小组数 - infos.update(club_course_num=infos.get('club_num', 0)+infos.get('course_org_num', 0)) - - # 根据已选修书院课程种类数授予成就 - type_count = infos.get('type_count', 0) - if type_count == 5: - infos.update(type_count_name='五边形战士') - elif type_count >= 2: - infos.update(type_count_name='广泛涉猎') - elif type_count == 1: - infos.update(type_count_name='垂直深耕') - else: - infos.update(type_count_name='你先别急') - - # 计算2023年两学期平均书院课程预选数和选中数 - avg_preelect_num = (infos['preelect_course_23fall_num'] + infos['preelect_course_23spring_num']) / 2 - avg_elected_num = (infos['elected_course_23fall_num'] + infos['elected_course_23spring_num']) / 2 - infos.update(avg_preelect_num=avg_preelect_num, avg_elected_num=avg_elected_num) - - # 根据盲盒中奖率授予成就 - mystery_boxes_num = infos['mystery_boxes_num'] - # 处理导出数据中的typo - if 'lukcy_mystery_boxes_num' in infos.keys(): - lucky_mystery_boxes_num = infos.pop('lukcy_mystery_boxes_num') - infos.update(lucky_mystery_boxes_num=lucky_mystery_boxes_num) - lucky_mystery_boxes_num = infos['lucky_mystery_boxes_num'] - # 防止除零错误 - if (lucky_mystery_boxes_num != 0): - lucky_rate = mystery_boxes_num / lucky_mystery_boxes_num - if lucky_rate >= 0.5: - infos.update(mystery_boxes_name='恭迎欧皇加冕') - else: - infos.update(mystery_boxes_name='发出尖锐爆鸣的非酋') - else: - infos.update(mystery_boxes_name='') - - return render(request, 'Appointment/summary2023.html', infos) diff --git a/Appointment/urls.py b/Appointment/urls.py index ca35d35c6..d35fedc26 100644 --- a/Appointment/urls.py +++ b/Appointment/urls.py @@ -14,7 +14,7 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) ''' from django.urls import path -from Appointment import views, summary, hardware_api +from Appointment import views, hardware_api # 参考:https://stackoverflow.com/questions/61254816/what-is-the-purpose-of-app-name-in-urls-py-in-django # 一般情况下不需要使用 app_name @@ -49,7 +49,4 @@ path('camera-check', hardware_api.cameracheck, name='cameracheck'), path('display_getappoint', hardware_api.display_getappoint, name='display_getappoint'), - path('summary', summary.summary, name='summary'), - path('summary/2021', summary.summary2021, name='summary2021'), - path('summary/2023', summary.summary2023, name='summary2023'), ] diff --git a/achievement/__init__.py b/achievement/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/achievement/admin.py b/achievement/admin.py deleted file mode 100644 index 6b253a8e5..000000000 --- a/achievement/admin.py +++ /dev/null @@ -1,24 +0,0 @@ -from django.contrib import admin - -from achievement.models import * -from generic.admin import UserAdmin - - -@admin.register(AchievementType) -class AchievementTypeAdmin(admin.ModelAdmin): - list_display = ['title', 'description'] - search_fields = ['title'] - - -@admin.register(Achievement) -class AchievementAdmin(admin.ModelAdmin): - list_display = ['name', 'description', - 'achievement_type', 'hidden', 'auto_trigger'] - search_fields = ['name'] - - -@admin.register(AchievementUnlock) -class AchievementUnlockAdmin(admin.ModelAdmin): - list_display = ['user', 'achievement', - 'time', 'private'] - search_fields = [*UserAdmin.suggest_search_fields('user'), 'achievement__name'] diff --git a/achievement/api.py b/achievement/api.py deleted file mode 100644 index ae16b76ad..000000000 --- a/achievement/api.py +++ /dev/null @@ -1,173 +0,0 @@ -''' -本部分包含所有解锁成就相关的API -''' -from datetime import datetime, date - -from django.db.models import Sum - -import utils.models.query as SQ -from generic.models import User -from app.models import CourseRecord, Course, NaturalPerson as Person -from achievement.models import Achievement -from achievement.utils import trigger_achievement, bulk_add_achievement_record, get_students_without_credit_record - - -__all__ = [ - 'unlock_achievement', - 'unlock_course_achievements', - 'unlock_YQPoint_achievements', - 'unlock_signin_achievements', - 'unlock_credit_achievements', -] - - -def unlock_achievement(user: User, achievement_name: str) -> bool: - ''' - 解锁成就 - - :param user: 要解锁的用户 - :type user: User - :param achievement_name: 要解锁的成就名 - :type achievement_name: str - :return: 是否成功解锁 - :rtype: bool - ''' - try: - achivement = Achievement.objects.get(name=achievement_name) - except: - return False - return trigger_achievement(user, achivement) - - -def _unlock_by_value(user: User, acquired_value: float, - sorted_achievements: list[tuple[float, str]]) -> bool: - '''将所有超过了解锁阈值的成就解锁''' - created = False - for bound, achievement_name in sorted_achievements: - if acquired_value < bound: - break - created |= unlock_achievement(user, achievement_name) - return created - - -''' 元气人生 ''' - -''' 洁身自好 : 暂时不做 ''' - -''' 严于律己 : 定时任务 ''' - -''' 五育并举 ''' - - -def unlock_course_achievements(user: User) -> None: - ''' - 解锁成就 包含五育并举与学时相关的成就的判断 - 这个不太清楚怎么调用合适 是专门写一个command(个人倾向) 还是写在view的homepage里面? - - :param user: 要查询的用户 - :type user: User - ''' - records = SQ.sfilter([CourseRecord.person, Person.person_id], user - ).filter(invalid=False) - if not records: - return - - # 统计有效学时 - total_hours = records.aggregate( - total_hours=Sum('total_hours'))['total_hours'] - # 解锁成就 - _unlock_by_value(user, total_hours, [ - (32, '完成一半书院学分要求'), - (64, '完成全部书院学分要求'), - (96, '超额完成一半书院学分要求'), - (128, '超额完成一倍书院学分要求'), - ]) - - # 德智体美劳检验 - course_types = set(SQ.qsvlist(records, CourseRecord.course, Course.type)) - COURSE_DICT = { - Course.CourseType.MORAL: '德育', - Course.CourseType.INTELLECTUAL: '智育', - Course.CourseType.PHYSICAL: '体育', - Course.CourseType.AESTHETICS: '美育', - Course.CourseType.LABOUR: '劳动教育', - } - for course_type in course_types: - unlock_achievement(user, '首次修习' + COURSE_DICT[course_type] + '课程') - - -''' 志同道合 ''' - -''' 元气满满 ''' - - -def unlock_YQPoint_achievements(user: User, start_time: datetime, end_time: datetime) -> None: - ''' - 解锁成就 包含元气满满所有成就的判断 - - :param user: 要查询的用户 - :type user: User - :param start_time: 开始时间 - :type start_time: datetime - :param end_time: 结束时间 - :type end_time: datetime - ''' - # 计算收支情况 - # TODO: 存在循环引用,暂时放在这里,后续改为信号控制的方式后可改回 - from app.YQPoint_utils import get_income_expenditure - income, expenditure = get_income_expenditure(user, start_time, end_time) - _unlock_by_value(user, income, [ - (1, '首次获得元气值'), - (10, '学期内获得10元气值'), - (30, '学期内获得30元气值'), - (50, '学期内获得50元气值'), - (100, '学期内获得100元气值'), - ]) - _unlock_by_value(user, expenditure, [ - (1, '首次消费元气值'), - (10, '学期内消费10元气值'), - (30, '学期内消费30元气值'), - (50, '学期内消费50元气值'), - (100, '学期内消费100元气值'), - ]) - - -''' 三五成群 : 全部外部录入 ''' - -''' 智慧生活 ''' - -# 连续登录系列 - - -def unlock_signin_achievements(user: User, continuous_days: int) -> bool: - ''' - 解锁成就 - 智慧生活-连续登录一周/一学期/一整年 - - :param user: 要解锁的用户 - :type user: User - :return: 是否成功解锁 - :rtype: bool - ''' - created = _unlock_by_value(user, continuous_days, [ - (7, '连续登录一周'), - (7 * 16, '连续登录一学期'), - (365, '连续登录一整年'), - ]) - return created - -def unlock_credit_achievements(start_date: date, end_date: date, achievement_name: str) -> None: - ''' - 解锁成就 - 信用分相关成就激活判断 - - :param start_date: 开始日期 - :type start_date: date - :param end_date: 结束日期 - :type end_date: date - :param achievement_name: 要解锁的成就名 - :type achievement_name: str - ''' - students = get_students_without_credit_record(start_date, end_date) - achievement = Achievement.objects.get(name=achievement_name) - bulk_add_achievement_record(students, achievement) diff --git a/achievement/apps.py b/achievement/apps.py deleted file mode 100644 index 3c6023f5f..000000000 --- a/achievement/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class AchievementConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "achievement" diff --git a/achievement/jobs.py b/achievement/jobs.py deleted file mode 100644 index a249cb4ec..000000000 --- a/achievement/jobs.py +++ /dev/null @@ -1,51 +0,0 @@ -from datetime import date, timedelta - -from scheduler.periodic import periodical -from achievement.models import Achievement -from achievement.utils import bulk_add_achievement_record, get_students_by_grade -from achievement.api import unlock_credit_achievements -from semester.api import current_semester - - -__all__ = [ - 'unlock_credit_achievements', - 'new_school_year_achievements', -] - - -# 信用分相关成就激活判断 每月1日6点运行 -@periodical('cron', job_id='解锁信用分成就', day=1, hour=6, minute=0) -def unlock_credit_achievements(): - semester = current_semester() - today = date.today() - last_month_lastday = today - timedelta(days=today.day) - last_month_firstday = last_month_lastday.replace(day=1) - DAYS_LIMIT = 21 - # 如果当前日期位于学期中间,进行'当月没有扣除信用分'成就的触发 - if semester.start_date <= today < semester.end_date: # 注意end_date是指放假开始的当天 - # 上月在学期内的天数超过阈值 - if (today - semester.start_date).days >= DAYS_LIMIT: - unlock_credit_achievements( - last_month_firstday, last_month_lastday, '当月没有扣除信用分') - # 如果当前日期位于学期结束后 - elif today >= semester.end_date: - # 进行'一学期没有扣除信用分'成就的触发 - unlock_credit_achievements( - semester.start_date, semester.end_date-timedelta(days=1), '一学期没有扣除信用分') - # 进行'当月没有扣除信用分'成就的触发 - if last_month_firstday < semester.end_date: - # 上月在学期内的天数超过阈值 - if (semester.end_date - last_month_firstday).days >= DAYS_LIMIT: - unlock_credit_achievements( - last_month_firstday, last_month_lastday, '当月没有扣除信用分') - # 简单起见,不妨每年7月1日进行'一学年没有扣除信用分'成就触发 - if today.month == 7: - unlock_credit_achievements( - today-timedelta(days=365), today, '一学年没有扣除信用分') - - -def new_school_year_achievements(): - '''触发 元气人生-开启大学第二、三、四年''' - for i, name in zip(range(2, 7), ['二', '三', '四', '五', '六']): - achievement = Achievement.objects.get(name=f'开启大学生活第{name}年') - bulk_add_achievement_record(get_students_by_grade(i), achievement) diff --git a/achievement/management/__init__.py b/achievement/management/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/achievement/management/commands/__init__.py b/achievement/management/commands/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/achievement/management/commands/init_achievements.py b/achievement/management/commands/init_achievements.py deleted file mode 100644 index e728627f1..000000000 --- a/achievement/management/commands/init_achievements.py +++ /dev/null @@ -1,102 +0,0 @@ -""" -初始化 AchievementType Achievement -假定achievement_type都存在 -""" -from django.core.management.base import BaseCommand -from achievement.models import AchievementType, Achievement - - -class Command(BaseCommand): - help = "init AchievementType Achievement" - - def handle(self, *args, **options): - DISPLAYED = True - HIDDEN = False - AUTO = True - MANUAL = False - achievement_info = [('元气人生', - [('完成游园会印章收集任务', 3, DISPLAYED, MANUAL), - ('集齐游园会全部印章', 10, HIDDEN, MANUAL), - ('开始大学第二年', 5, DISPLAYED, AUTO), - ('开始大学第三年', 5, DISPLAYED, AUTO), - ('开始大学第四年', 5, DISPLAYED, AUTO), - ('开始大学第五年', 5, HIDDEN, AUTO), - ('开始大学第六年', 5, HIDDEN, AUTO), - ('完成军训', 1, DISPLAYED, AUTO), - ('完成书院实践育人活动', 3, DISPLAYED, MANUAL), - ('参与书院嘉年华', 2, DISPLAYED, MANUAL), - ('本科顺利毕业', 35, DISPLAYED, AUTO)]), - ('洁身自好', - [('月度卫生检查通过', 1, DISPLAYED, MANUAL), - ('一学期月度卫生检查均获得“优秀”评价', 10, HIDDEN, AUTO), - ('一学年月度卫生检查均获得“优秀”评价', 15, HIDDEN, AUTO), - ('本科月度卫生检查均获得“优秀”评价', 50, HIDDEN, AUTO)]), - ('五育并举', - [('首次报名书院课程', 1, DISPLAYED, AUTO), - ('完成德育学分要求', 2, DISPLAYED, AUTO), - ('完成智育学分要求', 2, DISPLAYED, AUTO), - ('完成体育学分要求', 2, DISPLAYED, AUTO), - ('完成美育学分要求', 2, DISPLAYED, AUTO), - ('完成劳动教育学分要求', 2, DISPLAYED, AUTO), - ('完成一半书院学分要求', 4, DISPLAYED, AUTO), - ('完成全部书院学分要求', 5, DISPLAYED, AUTO), - ('超额完成一半书院学分要求', 7, HIDDEN, AUTO), - ('超额完成一倍书院学分要求', 10, HIDDEN, AUTO)]), - ('志同道合', - [('加入书院组织', 1, DISPLAYED, AUTO), - ('参与书院俱乐部一半活动', 2, HIDDEN, AUTO), - ('参与书院俱乐部全部活动', 5, HIDDEN, AUTO), - ('成为书院小组负责人', 10, HIDDEN, AUTO), - ('成为书院俱乐部负责人', 10, HIDDEN, AUTO), - ('成为书院星级俱乐部负责人', 20, HIDDEN, AUTO), - ('发起成立书院小组', 5, HIDDEN, AUTO)]), - ('严于律己', - [('当月没有扣除信用分', 0, DISPLAYED, AUTO), - ('一学期没有扣除信用分', 2, HIDDEN, AUTO), - ('一学年没有扣除信用分', 10, HIDDEN, AUTO), - ('本科均没有扣除信用分', 20, HIDDEN, AUTO)]), - ('元气满满', - [('首次获得元气值', 0, DISPLAYED, AUTO), - ('学期内获得10元气值', 0, DISPLAYED, AUTO), - ('学期内获得30元气值', 0, HIDDEN, AUTO), - ('学期内获得50元气值', 0, HIDDEN, AUTO), - ('学期内获得100元气值', 0, HIDDEN, AUTO), - ('首次消费元气值', 1, DISPLAYED, AUTO), - ('学期内消费10元气值', 1, DISPLAYED, AUTO), - ('学期内消费30元气值', 2, HIDDEN, AUTO), - ('学期内消费50元气值', 5, HIDDEN, AUTO), - ('学期内消费100元气值', 10, HIDDEN, AUTO)]), - ('三五成群', - [('加入宿舍群', 1, DISPLAYED, MANUAL), - ('参与宿舍片区管理', 0, DISPLAYED, MANUAL), - ('参与宿舍片区活动', 0, DISPLAYED, MANUAL)]), - ('智慧生活', - [('注册智慧书院', 2, DISPLAYED, AUTO), - ('连续登录一周', 0, HIDDEN, AUTO), - ('连续登录一学期', 20, HIDDEN, AUTO), - ('连续登录一整年', 50, HIDDEN, AUTO), - ('完成地下室预约', 1, DISPLAYED, AUTO), - ('更新一次个人档案', 2, DISPLAYED, AUTO), - ('编辑自己的学术地图', 10, DISPLAYED, AUTO), - ('参与学术问答', 5, DISPLAYED, AUTO), - ('使用一次反馈中心', 2, DISPLAYED, AUTO), - ('使用一次元培书房查询', 2, DISPLAYED, AUTO)]), - ('纪念成就', - [('参与9月5日的团学联宣讲会', 0, DISPLAYED, MANUAL)]),] - for achievement_type_name, achievement_list in achievement_info: - try: - achievement_type = AchievementType.objects.get( - title=achievement_type_name) - for achievement_name, reward_points, if_displayed, if_auto_trigger in achievement_list: - Achievement.objects.update_or_create( - name=achievement_name, - description=achievement_name, # 默认重复一遍name - achievement_type=achievement_type, - hidden=not if_displayed, - auto_trigger=if_auto_trigger, - reward_points=reward_points - ) - except AchievementType.DoesNotExist: - print('AchievementType %s does not exist' % - achievement_type_name) - continue diff --git a/achievement/management/commands/unlock_graduates_credit_achievements.py b/achievement/management/commands/unlock_graduates_credit_achievements.py deleted file mode 100644 index 7e06b6a8a..000000000 --- a/achievement/management/commands/unlock_graduates_credit_achievements.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -解锁毕业生 '本科均没有扣除信用分' 成就 -""" -from datetime import date, timedelta -import pandas as pd - -from django.core.management.base import BaseCommand -from achievement.models import Achievement -from achievement.utils import bulk_add_achievement_record, get_students_without_credit_record - - -class Command(BaseCommand): - ''' - 从excel中导入成就 格式:学号 - - filepath: excel文件路径 - - 会自动判断是否有扣分记录 - ''' - help = "解锁毕业生 '本科均没有扣除信用分' 成就" - - def add_arguments(self, parser): - parser.add_argument('filepath', type=str) - - def handle(self, *args, **options): - # 读取 excel - filepath = options['filepath'] - full_path = filepath - data = None - if filepath.endswith('xlsx') or filepath.endswith('xls'): - data = pd.read_excel(f'{full_path}', sheet_name=None) - elif filepath.endswith('csv'): - data = pd.read_csv(f'{full_path}', dtype=object, encoding='utf-8') - else: - data = pd.read_table(f'{full_path}', dtype=object, encoding='utf-8') - - if data == None: - print('文件格式不正确') - return - - # 默认选取第一个sheet - if type(data) == dict: - data = data[list(data.keys())[0]] - - graduate_number = data['学号'].astype(str).tolist() - - # 读取学号 - today = date.today() - students = get_students_without_credit_record( - today-timedelta(days=365*6), today).filter(username__in=graduate_number) - achievement = Achievement.objects.get(name='本科均没有扣除信用分') - bulk_add_achievement_record(students, achievement) diff --git a/achievement/management/commands/upload_achievements.py b/achievement/management/commands/upload_achievements.py deleted file mode 100644 index ea81e2788..000000000 --- a/achievement/management/commands/upload_achievements.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -excel 上传成就 -""" - -from django.core.management.base import BaseCommand -import pandas as pd - -from achievement.models import Achievement -from achievement.utils import bulk_add_achievement_record -from generic.models import User - -class Command(BaseCommand): - """ - 从excel中导入成就 格式:学号 成就 - - filepath: excel文件路径 - """ - - help = "upload achievements from excel" - - def add_arguments(self, parser): - parser.add_argument('filepath', type=str) - - def handle(self, *args, **options): - - # 读取 excel - filepath = options['filepath'] - full_path = filepath - data = None - if filepath.endswith('xlsx') or filepath.endswith('xls'): - data = pd.read_excel(f'{full_path}', sheet_name=None) - elif filepath.endswith('csv'): - data = pd.read_csv(f'{full_path}', dtype=object, encoding='utf-8') - else: - data = pd.read_table(f'{full_path}', dtype=object, encoding='utf-8') - - if data == None: - print('文件格式不正确') - return - - # 默认选取第一个sheet - if type(data) == dict: - data = data[list(data.keys())[0]] - - data['学号'] = data['学号'].astype(str) - - # 应用api接口分类批量上传成就 - grouped = data.groupby('成就') - for achievement_name, achievement_data in grouped: - user_number_list = list(set(achievement_data['学号'].values)) - user_list = User.objects.filter(username__in=user_number_list) - bulk_add_achievement_record(user_list, Achievement.objects.get(name=achievement_name)) diff --git a/achievement/migrations/0001_initial.py b/achievement/migrations/0001_initial.py deleted file mode 100644 index 3c21d832a..000000000 --- a/achievement/migrations/0001_initial.py +++ /dev/null @@ -1,57 +0,0 @@ -# Generated by Django 4.2.3 on 2023-10-18 18:25 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='Achievement', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100, verbose_name='名称')), - ('description', models.TextField(verbose_name='描述')), - ('hidden', models.BooleanField(default=False, verbose_name='隐藏')), - ('auto_trigger', models.BooleanField(default=False, verbose_name='自动触发')), - ('reward_points', models.PositiveIntegerField(default=0, verbose_name='奖励积分')), - ], - options={ - 'verbose_name': '成就', - 'verbose_name_plural': '成就', - }, - ), - migrations.CreateModel( - name='AchievementType', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=100, verbose_name='名称')), - ('description', models.TextField(blank=True, verbose_name='描述')), - ('badge', models.ImageField(upload_to='achievement/badges/', verbose_name='徽章')), - ('avatar', models.ImageField(upload_to='achievement/avatars/', verbose_name='图标')), - ], - options={ - 'verbose_name': '成就类型', - 'verbose_name_plural': '成就类型', - }, - ), - migrations.CreateModel( - name='AchievementUnlock', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('time', models.DateTimeField(auto_now_add=True, verbose_name='解锁时间')), - ('private', models.BooleanField(default=False, verbose_name='不公开')), - ('achievement', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='achievement.achievement', verbose_name='解锁成就')), - ], - options={ - 'verbose_name': '成就解锁记录', - 'verbose_name_plural': '成就解锁记录', - }, - ), - ] diff --git a/achievement/migrations/0002_initial.py b/achievement/migrations/0002_initial.py deleted file mode 100644 index 1c0ceec60..000000000 --- a/achievement/migrations/0002_initial.py +++ /dev/null @@ -1,32 +0,0 @@ -# Generated by Django 4.2.3 on 2023-10-18 18:25 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('achievement', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AddField( - model_name='achievementunlock', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='用户'), - ), - migrations.AddField( - model_name='achievement', - name='achievement_type', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='achievement.achievementtype', verbose_name='类型'), - ), - migrations.AlterUniqueTogether( - name='achievementunlock', - unique_together={('user', 'achievement')}, - ), - ] diff --git a/achievement/migrations/__init__.py b/achievement/migrations/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/achievement/models.py b/achievement/models.py deleted file mode 100644 index 707bd1855..000000000 --- a/achievement/models.py +++ /dev/null @@ -1,67 +0,0 @@ -from django.db import models - -from utils.models.descriptor import admin_only -from generic.models import User - -__all__ = ['AchievementType', 'Achievement', 'AchievementUnlock'] - - -class AchievementType(models.Model): - class Meta: - verbose_name = '成就类型' - verbose_name_plural = verbose_name - - title = models.CharField('名称', max_length=100) - description = models.TextField('描述', blank=True) - badge = models.ImageField('徽章', upload_to='achievement/badges/') - avatar = models.ImageField('图标', upload_to='achievement/avatars/') - - @admin_only - def __str__(self): - return self.title - - # Actual types in use (remove later) - # UNDEFINED = (0, "未定义") - # YUANQIRENSHENG = (1, "元气人生") - # JIESHENZIHAO = (2, "洁身自好") - # WUYUBINGJU = (3, "五育并举") - # ZHITONGDAOHE = (4, "志同道合") - # YANYULVJI = (5, "严于律己") - # YUANQIMANMAN = (6, "元气满满") - # SANWUCHENGQUN = (7, "三五成群") - # ZHIHUISHWNGHUO = (8, "智慧生活") - - -class Achievement(models.Model): - class Meta: - verbose_name = '成就' - verbose_name_plural = verbose_name - - name = models.CharField('名称', max_length=100) - description = models.TextField('描述') - achievement_type = models.ForeignKey( - AchievementType, on_delete=models.CASCADE, verbose_name='类型') - hidden = models.BooleanField('隐藏', default=False) - # Only used for filtering. Whether an achievement is auto-triggered is - # not stored in the database. - auto_trigger = models.BooleanField('自动触发', default=False) - - reward_points = models.PositiveIntegerField('奖励积分', default=0) - - @admin_only - def __str__(self): - return self.name - - -class AchievementUnlock(models.Model): - class Meta: - verbose_name = '成就解锁记录' - verbose_name_plural = verbose_name - # XXX: 工具函数的并行安全性完全依赖于此约束 - unique_together = ['user', 'achievement'] - - user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='用户') - achievement = models.ForeignKey(Achievement, on_delete=models.CASCADE, - verbose_name='解锁成就') - time = models.DateTimeField('解锁时间', auto_now_add=True) - private = models.BooleanField('不公开', default=False) diff --git a/achievement/tests.py b/achievement/tests.py deleted file mode 100644 index 7ce503c2d..000000000 --- a/achievement/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/achievement/utils.py b/achievement/utils.py deleted file mode 100644 index 41e2d58cb..000000000 --- a/achievement/utils.py +++ /dev/null @@ -1,184 +0,0 @@ -'''成就系统 API -- 处理用户触发成就 -- 后台批量添加成就 -''' -from datetime import date - -from django.db import transaction -from django.db.models import QuerySet - -from generic.models import User, YQPointRecord, CreditRecord -from app.models import Notification -from achievement.models import Achievement, AchievementType, AchievementUnlock -from utils.wrap import return_on_except -from app.notification_utils import notification_create, bulk_notification_create -from semester.api import current_semester -from utils.marker import need_refactor - - -__all__ = [ - 'personal_achievements', - 'trigger_achievement', - 'bulk_add_achievement_record', - 'get_students_by_grade', - 'get_students_without_credit_record', -] - - -def personal_achievements(user: User): - unlocked_achievements = AchievementUnlock.objects.filter(user=user) - invisible_achievements = list(unlocked_achievements.filter(private=True)) - visible_achievements = list(unlocked_achievements.filter(private=False)) - - unlocked_ids = list(unlocked_achievements.values_list('achievement', flat=True)) - achievement_types = AchievementType.objects.all().order_by('id') - # (类型,总数,已解锁成就,未解锁成就,隐藏成就) - display_by_types: list[tuple[AchievementType, int, - list[Achievement], list[Achievement], list[Achievement]]] = [] - for achievement_type in achievement_types: - achievements = Achievement.objects.filter(achievement_type=achievement_type) - unlocked = list(achievements.filter(pk__in=unlocked_ids)) - all_locked = achievements.exclude(pk__in=unlocked_ids) - locked = list(all_locked.filter(hidden=False)) - hidden = list(all_locked.filter(hidden=True)) - display = (achievement_type, achievements.count(), unlocked, locked, hidden) - display_by_types.append(display) - return invisible_achievements, visible_achievements, display_by_types - - -@return_on_except(False, Exception) -@transaction.atomic -def trigger_achievement(user: User, achievement: Achievement): - ''' - 处理用户触发成就,添加单个解锁记录 - 若已解锁则不添加 - 按需发布通知与发放元气值奖励 - - Args: - - user (User): 触发该成就的用户 - - achievement (Achievement): 该成就 - - Returns: - - bool: 是否成功解锁 - - Warning: - 本函数保证原子化,且保证并行安全性,但后者实现存在风险 - ''' - - # XXX: 并行安全性依赖于AchievementUnlock在数据库中的唯一性约束unique_together - # 如果该约束被破坏,本函数将不再是安全的,但不易发现 - - assert user.is_person(), '暂时只允许个人解锁成就' - - _, created = AchievementUnlock.objects.get_or_create( - user=user, - achievement=achievement - ) - - # 是否成功解锁 - assert created, '成就已解锁' - - content = f'恭喜您解锁新成就:{achievement.name}!' - # 如果有奖励元气值 - if achievement.reward_points > 0: - User.objects.modify_YQPoint( - user, - achievement.reward_points, - source=achievement.name, - source_type=YQPointRecord.SourceType.ACHIEVE - ) - content += f'获得{achievement.reward_points}元气值奖励!' - notification_create( - receiver=user, - sender=None, - typename=Notification.Type.NEEDREAD, - title=Notification.Title.ACHIEVE_INFORM, - content=content - ) - - return True - - -@return_on_except(False, Exception) -@transaction.atomic -def bulk_add_achievement_record(users: QuerySet[User], achievement: Achievement): - ''' - 批量添加成就解锁记录 - 若已解锁则不添加 - 按需发布通知与发放元气值奖励 - - Args: - - users (QuerySet[User]): 待更改User的QuerySet - - achievement (Achievement): 需添加的成就 - - Returns: - - bool: 是否成功添加 - - Warning: - 本函数保证原子化,且保证并行安全性,但后者实现存在风险 - ''' - - # XXX: 并行安全性依赖于AchievementUnlock在数据库中的唯一性约束unique_together - # 如果该约束被破坏,本函数将**重复解锁**成就。 - users_with_achievement = AchievementUnlock.objects.filter( - achievement=achievement).values_list('user', flat=True) - - # 排除已经解锁的用户 - users_to_add = users.exclude(pk__in=users_with_achievement) - # 批量添加成就解锁记录 - AchievementUnlock.objects.bulk_create([ - AchievementUnlock(user=user, achievement=achievement) - for user in users_to_add - ]) - - content = f'恭喜您解锁新成就:{achievement.name}!' - - if achievement.reward_points > 0: - User.objects.bulk_increase_YQPoint( - users_to_add, - achievement.reward_points, - source=achievement.name, - source_type=YQPointRecord.SourceType.ACHIEVE - ) - content += f'获得{achievement.reward_points}元气值奖励!' - bulk_notification_create( - users_to_add, - sender=None, - typename=Notification.Type.NEEDREAD, - title=Notification.Title.ACHIEVE_INFORM, - content=content - ) - - return True - - -@need_refactor -def get_students_by_grade(grade: int) -> QuerySet[User]: - ''' - 传入目标入学年份数,返回满足的、未毕业的学生User列表。 - 示例:今年是2023年,我希望返回入学第二年的user,即查找username前两位为22的user - 仅限秋季学期开始后使用。 - ''' - semester_year = current_semester().year - goal_year = semester_year - 2000 - grade + 1 - students = User.objects.filter_type(User.Type.STUDENT).filter( - active=True, username__startswith=str(goal_year)) - return students - - -def get_students_without_credit_record(start_date: date, end_date: date) -> QuerySet[User]: - ''' - 获取一段时间内没有扣分记录的在读同学 - - Args: - - start_date: 查询起始时间 - - end_date: 查询结束时间 - - Returns: - - QuerySet[User]: 没有扣分记录的在读同学 - ''' - records = CreditRecord.objects.filter( - time__date__gte=start_date, time__date__lte=end_date) - students = User.objects.filter_type(User.Type.STUDENT).filter( - active=True).exclude(pk__in=records.values_list('user', flat=True)) - return students diff --git a/app/YQPoint_utils.py b/app/YQPoint_utils.py deleted file mode 100644 index b6fc1fdd6..000000000 --- a/app/YQPoint_utils.py +++ /dev/null @@ -1,692 +0,0 @@ -import random -from typing import List, Dict, Optional, Tuple -from datetime import datetime, timedelta, date - -from django.db.models import QuerySet, Q, Exists, OuterRef -from django.forms.models import model_to_dict - -from generic.models import User, YQPointRecord -from app.config import CONFIG -from app.utils_dependency import * -from app.models import ( - Pool, - PoolItem, - PoolRecord, - Notification, - Organization, - Participation, -) -from app.extern.wechat import WechatApp, WechatMessageLevel -from app.notification_utils import bulk_notification_create, notification_create -from achievement.api import unlock_signin_achievements - - -__all__ = [ - 'add_signin_point', - 'get_pools_and_items', - 'buy_exchange_item', - 'buy_lottery_pool', - 'buy_random_pool', - 'run_lottery', - 'get_income_expenditure', -] - - -DAY2POINT = CONFIG.yqpoint.signin_points -MAX_CHECK_DAYS = len(DAY2POINT) - - -def get_signin_infos(user: User, detailed_days: int = MAX_CHECK_DAYS, - check_days: int = None, today: date = None, - signin_today: bool = True): - ''' - 获取一定日期内每天的签到信息 - - :param user: 要查询的用户 - :type user: User - :param detailed_days: 显示详细签到信息的天数, defaults to None - :type detailed_days: int, optional - :param check_days: 查询天数(包括今天), defaults to None - :type check_days: int, optional - :param today: 查询的当天, defaults to None - :type today: date, optional - :param signin_today: 计算连续签到天数时认为今天已签到, defaults to True - :type signin_today: bool, optional - :return: 已连续签到天数,和今天起共detailed_days天的签到信息 - :rtype: tuple[int, list[bool] | None] - ''' - if today is None: - today = datetime.now().date() - day_check_kws = {} - if check_days is not None: - day_check_kws.update(time__date__gt=today - timedelta(days=check_days)) - signin_days = set(YQPointRecord.objects.filter( - user=user, - source_type=YQPointRecord.SourceType.CHECK_IN, - **day_check_kws, - ).order_by('time').values_list('time__date', flat=True).distinct()) - # 获取连续签到天数 - last_day = today - if signin_today: - last_day -= timedelta(days=1) - while last_day in signin_days: - last_day -= timedelta(days=1) - continuous_days = (today - last_day).days - 1 - if signin_today: - continuous_days += 1 - if detailed_days is not None: - # 从今天开始,第前n天是否签到(今天不计入本次签到) - # 可用来提供提示信息 - detailed_infos = [ - (today - timedelta(days=day)) in signin_days - for day in range(detailed_days) - ] - else: - detailed_infos = None - return continuous_days, detailed_infos - - -def distribution2point(distribution: list, day_type: int) -> int: - '''根据获取积分分布和当日类别,获取应获得的实际元气值''' - result = distribution[day_type] - if isinstance(result, (tuple, list)) and len(result) == 2: - result = random.randint(*result) - return result - - -def add_signin_point(user: User): - ''' - 用户获得今日签到的积分,并返回用户提示信息 - - :param user: 签到的用户 - :type user: User - :return: 本次签到获得的积分,以及应看到的提示(若为空则显示默认提示) - :rtype: tuple[int, str] - ''' - # 获取已连续签到的日期和近几天签到信息 - continuous_days, signed_in = get_signin_infos( - user, MAX_CHECK_DAYS, signin_today=True) - day_type = (continuous_days - 1) % MAX_CHECK_DAYS - # 连续签到的基础元气值,可以从文件中读取,此类写法便于分析 - add_point = distribution2point(DAY2POINT, day_type) - User.objects.modify_YQPoint(user, add_point, "每日登录", - YQPointRecord.SourceType.CHECK_IN) - # 元气值活动等获得的额外元气值 - bonus_point = 0 - if bonus_point: - User.objects.modify_YQPoint(user, bonus_point, "登录额外奖励", - YQPointRecord.SourceType.CHECK_IN) - # 顺便进行解锁成就检验 - unlock_signin_achievements(user, continuous_days) - # 用户应看到的信息 - user_display = [ - f'今日首次签到,获得{add_point}元气值!', - f'连续签到{continuous_days}天,获得{add_point}元气值!', - f'连续签到{continuous_days}天,获得{add_point}元气值,连续签到{7}天有惊喜!', - f'连续签到{continuous_days}天,获得{add_point}元气值!', - f'连续签到{continuous_days}天,再签到{2}天即可获得大量元气值!', - f'连续签到{continuous_days}天,获得{add_point}元气值,明日可获得大量元气值!', - f'第7日签到,获得{add_point}元气值!', - ][day_type] - # 获取的额外元气值可能需要提示 - if bonus_point: - pass - total_point = add_point + bonus_point - return total_point, user_display - - -def get_pools_and_items(pool_type: Pool.Type, user: User, frontend_dict: Dict[str, any]): - """ - 获取某一种类的所有当前开放的pool的前端所需信息。如果用户未参加奖池关联的活动,这个奖池的信息不会被返回。 - - :param pool_type: pool种类 - :type pool_type: Pool.Type - :param user: 当前用户 - :type user: User - :param frontend_dict: 前端字典 - :type frontend_dict: Dict[str, any] - """ - assert hasattr(user, 'naturalperson'), "非个人用户发起了奖池兑换请求" - pools = Pool.objects.filter( - Q(type=pool_type) & Q(start__lte=datetime.now()) - & (Q(end__isnull=True) | Q(end__gte=datetime.now() - timedelta(days=1))) - & (Q(activity__isnull=True) | Exists( - Participation.objects.filter( - activity = OuterRef('activity'), - person = user.naturalperson, - status = Participation.AttendStatus.ATTENDED, - ) - )) - ) - - pools_info = [] - # 此列表中含有若干dict,每个dict对应一个待展示的pool,例如: - # { - # "title": "xxx", "type": "兑换/抽奖/盲盒", - # "entry_time": 1, # 对于盲盒/抽奖奖池,一个用户最多能买几次 - # "ticket_price": 1, # 盲盒/抽奖奖池价格 - # "start": "2022-9-4", "end": "2022-9-5", # end可能为空 - # "redeem_start": "2022-9-10", "redeem_end": "2022-9-20", # 指线下获取奖品实物的时间,均可能为空 - # - # "status": 0/1, # 0表示进行中的奖池,1表示结束一天内的抽奖奖池 - # "items": [], # 含有若干dict,每个dict代表该奖池中的一个poolitem - # # key包括"id", "origin_num", "consumed_num", "exchange_price", - # # "exchange_limit", "is_big_prize", "is_empty", - # # "prize__name", "prize__more_info", "prize__stock", - # # "prize__reference_price", "prize__image", "prize__id", - # # 以及origin_num-consumed_num得到的remain_num - # # 如果是兑换类奖池,还有my_exchange_time,即当前用户兑换过该item多少次 - # "my_entry_time": 0, # 当前用户进过抽奖/盲盒奖池多少次 - # "records_num": 0, # 抽奖/盲盒奖池总共被买了多少次 - # "capacity": 0, # 盲盒奖池最多能被买多少次(即包括谢谢参与在内的所有poolitem的数量和) - # "results": { # 已结束的抽奖奖池有这一项,表示抽奖结果, - # # 其中包含"big_prize_results"和"normal_prize_results"两个列表 - # # 每个列表中又是若干词典,每个词典表示一种奖品的获奖情况(这些词典按奖品参考价格的降序排列), - # # 其key包括prize_name、prize_image和winners,其中winners是NaturalPerson.name的list,即这种奖品的获奖者列表 - # "big_prize_results": [ - # {"prize_name": "大奖1", "prize_image": "imageurl", "winners": ["张三", "李四"]}, - # {"prize_name": "大奖2", "prize_image": "imageurl", "winners": ["Alice"]}, - # ], - # "normal_prize_results": [ - # {"prize_name": "奖品1", "prize_image": "imageurl", "winners": ["王五"]}, - # {"prize_name": "奖品2", "prize_image": "imageurl", "winners": ["Alice", "Bob"]}, - # ] - # } - # } - - for pool in pools: - this_pool_info = model_to_dict(pool) - if pool.start <= datetime.now() and (pool.end is None or pool.end >= datetime.now()): - this_pool_info["status"] = 0 - else: - this_pool_info["status"] = 1 - - this_pool_info["capacity"] = pool.get_capacity() - this_pool_items = list(pool.items.filter(prize__isnull=False).values( - "id", "origin_num", "consumed_num", "exchange_price", - "exchange_limit", "is_big_prize", - "prize__name", "prize__more_info", "prize__stock", - "prize__reference_price", "prize__image", "prize__id", "exchange_attributes", - )) - for item in this_pool_items: - item["remain_num"] = item["origin_num"] - item["consumed_num"] - this_pool_info["items"] = sorted( - this_pool_items, key=lambda x: -x["remain_num"]) # 按剩余数量降序排序,已卖完的在最后 - if pool_type != Pool.Type.EXCHANGE: - this_pool_info["my_entry_time"] = PoolRecord.objects.filter( - user=user, pool=pool).count() - this_pool_info["records_num"] = PoolRecord.objects.filter( - pool=pool).count() - if pool_type == Pool.Type.RANDOM: - for item in this_pool_items: - # 此处显示的是抽奖概率,目前使用原始的占比 - percent = (100 * item["origin_num"] / - this_pool_info["capacity"]) - if percent == int(percent): - percent = int(percent) - elif round(percent, 1) != 0: - # 保留最低精度 - percent = round(percent, 1) - item["probability"] = percent - # LOTTERY类的pool不需要capacity - else: - for item in this_pool_items: - item["my_exchange_time"] = PoolRecord.objects.filter( - user=user, pool=pool, prize=item["prize__id"]).count() - # EXCHANGE类的pool不需要capcity和records_num和my_entry_time - - if this_pool_info["status"] == 1: # 如果是刚结束的抽奖,需要填充results - big_prize_items = PoolItem.objects.filter( - pool=pool, is_big_prize=True).order_by("-prize__reference_price") - normal_prize_items = PoolItem.objects.filter( - pool=pool, is_big_prize=False).order_by("-prize__reference_price") - big_prizes_and_winners = [] - normal_prizes_and_winners = [] - - for big_prize_item in big_prize_items: - big_prizes_and_winners.append( - {"prize_name": big_prize_item.prize.name, "prize_image": big_prize_item.prize.image}) - winner_names = list(PoolRecord.objects.filter( - pool=pool, prize=big_prize_item.prize).values_list( - "user__name", flat=True)) # TODO: 需要distinct()吗? - big_prizes_and_winners[-1]["winners"] = winner_names - for normal_prize_item in normal_prize_items: - if normal_prize_item.is_empty: - continue - normal_prizes_and_winners.append( - {"prize_name": normal_prize_item.prize.name, "prize_image": normal_prize_item.prize.image}) - winner_names = list(PoolRecord.objects.filter( - pool=pool, prize=normal_prize_item.prize).values_list( - "user__name", flat=True)) # TODO: 需要distinct()吗? - normal_prizes_and_winners[-1]["winners"] = winner_names - this_pool_info["results"] = {} - this_pool_info["results"]["big_prize_results"] = big_prizes_and_winners - this_pool_info["results"]["normal_prize_results"] = normal_prizes_and_winners - - pools_info.append(this_pool_info) - - frontend_dict["pools_info"] = pools_info - - -def check_user_pool(user: User, pool: Pool) -> None | str: - """ - 检查用户 user 是否已经毕业,以及是否参加了 pool 所关联的活动;当前是否在奖池的运行时间内。 - - :param user: 当前用户 - :type user: User - :param pool: 待购买的物品所属奖池 - :type pool: Pool - :return: 如果检查通过,返回None。否则,返回一个可以作为 wrong() 函数参数的字符串。 - :rtype: None | str - """ - # 检查奖池运行时间 - if pool.start > datetime.now(): - return '当前奖池时间未开始!' - if pool.end is not None and pool.end < datetime.now(): - return '当前奖池时间已结束!' - - # 检查用户是否已经毕业 - if not user.active: - return '您已毕业!' - - # 检查用户是否参加了相关的活动 - if pool.activity is not None: - assert hasattr(user, 'naturalperson'), "非个人用户发起了奖池兑换请求" - participates = Participation.objects.filter( - activity = pool.activity, - person = user.naturalperson, - status = Participation.AttendStatus.ATTENDED, - ) - if not participates.exists(): - return '该奖池为"' + str(pool.activity) + '"活动限定奖池,请先参加再来购买!' - - return None - - -def buy_exchange_item( - user: User, poolitem_id: str, - attributes: dict[str, str] = {}) -> MESSAGECONTEXT: - """ - 购买兑换奖池的某个奖品 - - :param user: 当前用户 - :type user: User - :param poolitem_id: 待购买的奖池奖品id,因为是前端传过来的所以是str - :type poolitem_id: str - :return: 表明购买结果的warn_code和warn_message - :rtype: MESSAGECONTEXT - """ - # 检查奖品是否可以购买 - try: - poolitem_id = int(poolitem_id) - poolitem = PoolItem.objects.get( - id=poolitem_id, pool__type=Pool.Type.EXCHANGE) - except: - return wrong('奖品不存在!') - if poolitem.origin_num - poolitem.consumed_num <= 0: - return wrong('奖品已售罄!') - other_errors = check_user_pool(user, poolitem.pool) - if other_errors: - return wrong(other_errors) - - my_exchanged_time = PoolRecord.objects.filter( - user=user, pool=poolitem.pool, prize=poolitem.prize).count() - if my_exchanged_time >= poolitem.exchange_limit: - return wrong('您兑换该奖品的次数已达上限!') - - for exchange_attribute in poolitem.exchange_attributes: - if exchange_attribute['name'] not in attributes: - return wrong('请填写完整的兑换信息!') - if attributes[exchange_attribute['name']] not in exchange_attribute['range']: - return wrong('兑换信息填写错误!') - - try: - with transaction.atomic(): - poolitem = PoolItem.objects.select_for_update().get( - id=poolitem_id, pool__type=Pool.Type.EXCHANGE) - assert poolitem.pool.start <= datetime.now(), "兑换时间未开始!" - assert poolitem.pool.end is None or poolitem.pool.end >= datetime.now(), "兑换时间已结束!" - assert poolitem.origin_num - poolitem.consumed_num > 0, "奖品已售罄!" - my_exchanged_time = PoolRecord.objects.filter( - user=user, pool=poolitem.pool, prize=poolitem.prize).count() - assert my_exchanged_time < poolitem.exchange_limit, '您兑换该奖品的次数已达上限!' - assert user.YQpoint >= poolitem.exchange_price, '您的元气值不足,兑换失败!' - - # 更新奖品状态 - poolitem.consumed_num += 1 - poolitem.save() - - # 创建兑换记录 - PoolRecord.objects.create( - user=user, - pool=poolitem.pool, - prize=poolitem.prize, - attributes=attributes, - status=PoolRecord.Status.UN_REDEEM, - ) - - # 扣除元气值 - User.objects.modify_YQPoint( - user, - -poolitem.exchange_price, - source=f'兑换奖池:{poolitem.pool.title}-{poolitem.prize.name}', - source_type=YQPointRecord.SourceType.CONSUMPTION - ) - except AssertionError as e: - return wrong(str(e)) - - return succeed('兑换成功!') - - -def buy_lottery_pool(user: User, pool_id: str) -> MESSAGECONTEXT: - """ - 购买抽奖奖池 - - :param user: 当前用户 - :type user: User - :param pool_id: 待购买的奖池id,因为是前端传过来的所以是str - :type pool_id: str - :return: 表明购买结果的warn_code和warn_message - :rtype: MESSAGECONTEXT - """ - # 检查抽奖奖池状态 - try: - pool_id = int(pool_id) - pool = Pool.objects.get(id=pool_id, type=Pool.Type.LOTTERY) - except: - return wrong('抽奖不存在!') - my_entry_time = PoolRecord.objects.filter(pool=pool, user=user).count() - if my_entry_time >= pool.entry_time: - return wrong('您在本奖池中抽奖的次数已达上限!') - other_errors = check_user_pool(user, pool) - if other_errors: - return wrong(other_errors) - - try: - with transaction.atomic(): - pool = Pool.objects.select_for_update().get(id=pool_id, type=Pool.Type.LOTTERY) - assert pool.start <= datetime.now(), '抽奖未开始!' - assert pool.end is None or pool.end >= datetime.now(), '抽奖已结束!' - my_entry_time = PoolRecord.objects.filter( - pool=pool, user=user).count() - assert my_entry_time < pool.entry_time, '您在本奖池中抽奖的次数已达上限!' - assert user.YQpoint >= pool.ticket_price, '您的元气值不足,兑换失败!' - - # 创建抽奖记录 - PoolRecord.objects.create( - user=user, - pool=pool, - status=PoolRecord.Status.LOTTERING, - ) - - # 扣除元气值 - User.objects.modify_YQPoint( - user, - -pool.ticket_price, - source=f'抽奖奖池:{pool.title}', - source_type=YQPointRecord.SourceType.CONSUMPTION - ) - except AssertionError as e: - return wrong(str(e)) - - return succeed('成功进行一次抽奖!您可以在抽奖时间结束后查看抽奖结果~') - - -def select_random_prize(poolitems: QuerySet[PoolItem], select_num: Optional[int] = None) -> List[int]: - """ - 实现无放回随机抽取select_num个PoolItem(的id),初始时每种PoolItem有origin_num-consumed_num个 - - :param poolitems: 待抽取的PoolItem构成的QuerySet(每个元素表示一种PoolItem而非一个) - :type poolitems: QuerySet[PoolItem] - :param select_num: 抽几个,若为None则抽取所有奖品,也即对poolitems做一次shuffle, defaults to None - :type select_num: Optional[int], optional - :return: 抽出的poolitem的id组成的list,长度等于select_num - :rtype: List[int] - """ - assert poolitems.count() > 0 - - num_all_items = 0 # 奖品的总数 - item_dict = {} # int: PoolItem,实现把一个自然数区间映射到一种奖品 - for item in poolitems: - if item.origin_num - item.consumed_num <= 0: - continue - item_dict[num_all_items] = item - num_all_items += item.origin_num - item.consumed_num - - if select_num is None: # 不给出select_num就默认抽取所有奖品,也即对poolitems做一次shuffle - select_num = num_all_items - assert select_num <= num_all_items - - selected_idx = random.sample( - range(num_all_items), select_num) # 选出select_num个序号 - selected_items_id = [] - for idx in selected_idx: - for key in sorted(item_dict.keys(), reverse=True): - if idx >= key: # 寻找idx落入的区间 - selected_items_id.append( - item_dict[key].id) # 把idx映射为PoolItem.id - break - return selected_items_id - - -def buy_random_pool(user: User, pool_id: str) -> Tuple[MESSAGECONTEXT, int, int]: - """ - 购买盲盒 - - :param user: 当前用户 - :type user: User - :param pool_id: 待购买的奖池id,因为是前端传过来的所以是str - :type pool_id: str - :return: 表明购买结果的warn_code和warn_message;买到的prize的id(如果购买失败就是-1); - 表明盲盒结果的一个int:2表示无反应、1表示开出空盒、0表示开出奖品 - :rtype: Tuple[MESSAGECONTEXT, int, int] - """ - # 检查盲盒奖池状态 - try: - pool_id = int(pool_id) - pool = Pool.objects.get(id=pool_id, type=Pool.Type.RANDOM) - except: - return wrong('盲盒不存在!'), -1, 2 - my_entry_time = PoolRecord.objects.filter(pool=pool, user=user).count() - if my_entry_time >= pool.entry_time: - return wrong('您兑换这款盲盒的次数已达上限!'), -1, 2 - total_entry_time = PoolRecord.objects.filter(pool=pool).count() - capacity = pool.get_capacity() - if capacity <= total_entry_time: - return wrong('盲盒已售罄!'), -1, 2 - other_errors = check_user_pool(user, pool) - if other_errors: - return wrong(other_errors), -1, 2 - - try: - with transaction.atomic(): - pool = Pool.objects.select_for_update().get(id=pool_id, type=Pool.Type.RANDOM) - assert pool.start <= datetime.now(), '盲盒兑换时间未开始!' - assert pool.end is None or pool.end >= datetime.now(), '盲盒兑换时间已结束!' - my_entry_time = PoolRecord.objects.filter( - pool=pool, user=user).count() - assert my_entry_time < pool.entry_time, '您兑换这款盲盒的次数已达上限!' - assert user.YQpoint >= pool.ticket_price, '您的元气值不足,兑换失败!' - total_entry_time = PoolRecord.objects.filter(pool=pool).count() - capacity = pool.get_capacity() - assert capacity > total_entry_time, '盲盒已售罄!' - - # 开盒,修改poolitem记录,创建poolrecord记录 - items = pool.items.select_for_update().all() - real_item_id = select_random_prize(items, 1)[0] - modify_item: PoolItem = PoolItem.objects.select_for_update().get(id=real_item_id) - modify_item.consumed_num += 1 - modify_item.save() - - if modify_item.is_empty: # 如果是空盲盒,没法兑奖,record的状态记为NOT_LUCKY - item_status = PoolRecord.Status.NOT_LUCKY - else: - item_status = PoolRecord.Status.UN_REDEEM - PoolRecord.objects.create( - user=user, - pool=pool, - status=item_status, - prize=modify_item.prize, - ) - - # 扣除元气值 - User.objects.modify_YQPoint( - user, - -pool.ticket_price, - source=f'盲盒奖池:{pool.title}', - source_type=YQPointRecord.SourceType.CONSUMPTION - ) - # 如果抽到了空盒子,按照设定值对用户给予元气值补偿并返回相应的提示 - if modify_item.is_empty: - compensate_YQPoint = random.randint( - pool.empty_YQPoint_compensation_lowerbound, pool.empty_YQPoint_compensation_upperbound) - if compensate_YQPoint == 0: - return succeed(f'兑换盲盒成功!您抽到了空盒子,但是很遗憾这次没有元气值补偿QAQ'), -1, 1 - User.objects.modify_YQPoint( - user, - compensate_YQPoint, - source=f'盲盒奖池:{pool.title}空盒子补偿', - source_type=YQPointRecord.SourceType.COMPENSATION - ) - return succeed(f'兑换盲盒成功!您抽到了空盒子,获得{compensate_YQPoint}点元气值补偿!'), -1, 1 - if modify_item.prize is None: - return succeed('兑换盲盒成功!'), -1, 1 - return succeed('兑换盲盒成功!'), modify_item.prize.id, int(modify_item.is_empty) - except AssertionError as e: - return wrong(str(e)), -1, 2 - - -@transaction.atomic -def run_lottery(pool_id: int): - """ - 抽奖;更新PoolRecord表和PoolItem表;给所有参与者发送通知 - - :param pool_id: 待抽取的抽奖奖池id - :type pool_id: int - """ - # 部分参考了course_utils.py的draw_lots函数 - pool = Pool.objects.select_for_update().get(id=pool_id, type=Pool.Type.LOTTERY) - assert not PoolRecord.objects.filter( # 此时pool关联的所有records都应该是LOTTERING - pool=pool).exclude(status=PoolRecord.Status.LOTTERING).exists() - related_records = PoolRecord.objects.filter( - pool=pool, status=PoolRecord.Status.LOTTERING) - records_num = related_records.count() - if records_num == 0: - return - - # 抽奖 - record_ids_and_participant_ids = list( - related_records.values("id", "user__id")) - items = pool.items.all() - user2prize_names = {d["user__id"]: [] - for d in record_ids_and_participant_ids} # 便于发通知 - winner_record_id2item_id = {} # poolrecord.id: poolitem.id,便于更新poolrecord - loser_record_ids = [] # poolrecord.id,便于更新poolrecord - num_all_items = 0 # 该奖池中奖品总数 - for item in items: - num_all_items += item.origin_num - item.consumed_num - if num_all_items >= records_num: # 抽奖记录数少于或等于奖品数,人人有奖,给每个记录分配一个随机奖品 - shuffled_items = select_random_prize( - items, records_num) # 随机选出待发放的奖品 - for i in range(records_num): # 遍历所有记录,每个记录都有奖品 - user2prize_names[record_ids_and_participant_ids[i]["user__id"]].append( - items.get(id=shuffled_items[i]).prize.name - ) - winner_record_id2item_id[record_ids_and_participant_ids[i] - ["id"]] = shuffled_items[i] - else: # 抽奖记录数多于奖品数,给每个奖品分配一个中奖者 - for item in items: # 遍历所有奖品,每个奖品都会送给一个记录 - for i in range(item.origin_num - item.consumed_num): - winner_record_index = random.randint( - 0, len(record_ids_and_participant_ids) - 1) - user2prize_names[record_ids_and_participant_ids[winner_record_index]["user__id"]].append( - item.prize.name) - winner_record_id2item_id[record_ids_and_participant_ids[winner_record_index]["id"]] = item.id - # 因为记录多,奖品少,这里肯定不会pop成空列表 - record_ids_and_participant_ids.pop(winner_record_index) - # pop剩下的就是没中奖的那些记录 - loser_record_ids = [d["id"] - for d in record_ids_and_participant_ids] - - # 更新数据库 - for winner_record_id, poolitem_id in winner_record_id2item_id.items(): - record = PoolRecord.objects.select_for_update().get(id=winner_record_id) - item = PoolItem.objects.select_for_update().get(id=poolitem_id) - record.status = PoolRecord.Status.UN_REDEEM - record.prize = item.prize - record.time = datetime.now() - item.consumed_num += 1 - record.save() - item.save() - for loser_record_id in loser_record_ids: - record = PoolRecord.objects.select_for_update().get(id=loser_record_id) - record.status = PoolRecord.Status.NOT_LUCKY - record.time = datetime.now() - record.save() - - # 给中奖的同学发送通知 - sender = Organization.objects.get( - oname=CONFIG.yqpoint.org_name).get_user() - for user_id in user2prize_names.keys(): - receiver = User.objects.get(id=user_id) - typename = Notification.Type.NEEDREAD - title = Notification.Title.LOTTERY_INFORM - content = f"恭喜您在奖池【{pool.title}】中抽中奖品" - for prize_name in user2prize_names[user_id]: - content += f"【{prize_name}】" # 可能出现重复,即一种奖品中了好几次,不过感觉问题也不太大 - notification_create( - receiver=receiver, - sender=sender, - typename=typename, - title=title, - content=content, - # URL=f'', # TODO: 我的奖品页面? - to_wechat=dict(app=WechatApp.TO_PARTICIPANT, - level=WechatMessageLevel.IMPORTANT), - ) - - # 给没中奖的同学发送通知 - receivers = PoolRecord.objects.filter( - id__in=loser_record_ids, - ).values_list("user", flat=True) - receivers = User.objects.filter(id__in=receivers) - content = f"很抱歉通知您,您在奖池【{pool.title}】中没有中奖" - - if len(receivers) > 0: - bulk_notification_create( - receivers=receivers, - sender=sender, - typename=typename, - title=title, - content=content, - # URL=f'', # TODO: 我的奖品页面? - to_wechat=dict(app=WechatApp.TO_PARTICIPANT, - level=WechatMessageLevel.IMPORTANT), - ) - - -def get_income_expenditure( - user: User, start_time: datetime, end_time: datetime -) -> tuple[int, int]: - '''获取用户一段时间内收支情况 - - Args: - user(Usesr): 要查询的用户 - start_time(datetime): 开始时间 - end_time(datetime): 结束时间 - - Returns: - tuple[int, int]: 收入, 支出 - ''' - # 根据user选出YQPointRecord - records = YQPointRecord.objects.filter( - user=user, time__gte=start_time, time__lte=end_time) - # 统计时期内收支情况 - income = 0 - expenditure = 0 - for record in records: - if record.delta >= 0: - income += record.delta - else: - expenditure += abs(record.delta) - return income, expenditure diff --git a/app/YQPoint_views.py b/app/YQPoint_views.py deleted file mode 100644 index 25de82e1a..000000000 --- a/app/YQPoint_views.py +++ /dev/null @@ -1,154 +0,0 @@ -from generic.models import YQPointRecord -from app.views_dependency import * -from app.models import ( - Prize, - Pool, - PoolRecord, -) -from app.YQPoint_utils import ( - get_pools_and_items, - buy_exchange_item, - buy_lottery_pool, - buy_random_pool, -) -from app.utils import get_sidebar_and_navbar - -__all__ = [ - 'myYQPoint', - 'myPrize', - 'showPools', -] - - -class myYQPoint(ProfileTemplateView): - template_name = 'myYQPoint.html' - page_name = '我的元气值' - http_method_names = ['get'] - - def prepare_get(self): - html_display = {} - my_messages.transfer_message_context(self.request.GET, html_display) - html_display.update(YQPoint=self.request.user.YQpoint) - self.extra_context.update(html_display=html_display) - return self.get - - def get(self): - YQPoint = self.request.user.YQpoint - received_set = YQPointRecord.objects.filter( - user=self.request.user, - ).exclude(source_type=YQPointRecord.SourceType.CONSUMPTION).order_by("-time") - - send_set = YQPointRecord.objects.filter( - user=self.request.user, - source_type=YQPointRecord.SourceType.CONSUMPTION, - ).order_by("-time") - return self.render(YQPoint=YQPoint, received_set=received_set, send_set=send_set) - - - -class myPrize(ProfileTemplateView): - template_name = 'myPrize.html' - page_name = '我的奖品' - http_method_names = ['get'] - - def prepare_get(self): - html_display = {} - my_messages.transfer_message_context(self.request.GET, html_display) - self.extra_context.update(html_display=html_display) - return self.get - - def get(self): - lottery_set = PoolRecord.objects.filter( - user=self.request.user, - pool__type=Pool.Type.LOTTERY, - status__in=[ - PoolRecord.Status.LOTTERING, - PoolRecord.Status.NOT_LUCKY, - PoolRecord.Status.UN_REDEEM], - ).order_by("-time") - - exchange_set = PoolRecord.objects.filter( - user=self.request.user, - status__in=[ - PoolRecord.Status.UN_REDEEM, - PoolRecord.Status.REDEEMED, - PoolRecord.Status.OVERDUE], - ).order_by("-status", "-time") - return self.render(lottery_set=lottery_set, exchange_set=exchange_set) - - - -@login_required(redirect_field_name="origin") -@utils.check_user_access(redirect_url="/logout/") -@logger.secure_view() -def showPools(request: UserRequest) -> HttpResponse: - """ - 展示各种奖池的页面,可以通过POST请求发起兑换/抽奖/买盲盒 - - :param request - :type request: HttpRequest - :return - :rtype: HttpResponse - """ - if request.user.is_org(): - return redirect(message_url(wrong("只有个人账号可以进入此页面!"))) - - frontend_dict = {"exchange_pools_info": {}, - "lottery_pools_info": {}, "random_pools_info": {}} - frontend_dict["current_pool"] = -1 # 当前所在的tab - # 2表示无效果,1表示开出空盒(谢谢参与),0表示开出奖品 - frontend_dict["random_pool_effect_code"] = 2 - - # 用户是否处于活跃状态。已经毕业的用户只能查看奖池,不能参与兑换 - frontend_dict['active_user'] = request.user.active - - # POST表明发起兑换/抽奖/买盲盒 - if request.method == "POST" and request.POST: - if request.POST.get('submit_exchange', '') != '': - context = buy_exchange_item( - request.user, - poolitem_id=request.POST['submit_exchange'], - attributes={k: v for k, v in request.POST.items() - if k not in ['pool_id', 'submit_exchange']}) - my_messages.transfer_message_context( - context, frontend_dict, normalize=True) - frontend_dict["current_pool"] = int(request.POST["pool_id"]) - elif request.POST.get('submit_lottery', '') != '': - context = buy_lottery_pool( - request.user, pool_id=request.POST['submit_lottery']) - my_messages.transfer_message_context( - context, frontend_dict, normalize=True) - frontend_dict["current_pool"] = int(request.POST["submit_lottery"]) - elif request.POST.get('submit_random', '') != '': - context, prize_id, frontend_dict["random_pool_effect_code"] = buy_random_pool( - request.user, pool_id=request.POST['submit_random']) - my_messages.transfer_message_context( - context, frontend_dict, normalize=True) - if prize_id != -1: # 表明成功购买了一个盲盒 - prize = Prize.objects.get(id=prize_id) - # 供前端展示盲盒开出的结果 - frontend_dict["random_pool_effect_name"] = prize.name - frontend_dict["random_pool_effect_image"] = prize.image - frontend_dict["current_pool"] = int(request.POST["submit_random"]) - - get_pools_and_items(Pool.Type.EXCHANGE, request.user, - frontend_dict["exchange_pools_info"]) - get_pools_and_items(Pool.Type.LOTTERY, request.user, - frontend_dict["lottery_pools_info"]) # 这里包含结束一天以内的 - get_pools_and_items(Pool.Type.RANDOM, request.user, - frontend_dict["random_pools_info"]) - - frontend_dict["my_YQpoint"] = request.user.YQpoint # 元气值余额 - - if frontend_dict["current_pool"] == -1: - if len(frontend_dict["exchange_pools_info"]["pools_info"]): - frontend_dict["current_pool"] = frontend_dict["exchange_pools_info"]["pools_info"][0]["id"] - elif len(frontend_dict["lottery_pools_info"]["pools_info"]): - frontend_dict["current_pool"] = frontend_dict["lottery_pools_info"]["pools_info"][0]["id"] - elif len(frontend_dict["random_pools_info"]["pools_info"]): - frontend_dict["current_pool"] = frontend_dict["random_pools_info"]["pools_info"][0]["id"] - - - frontend_dict["bar_display"] = get_sidebar_and_navbar( - request.user, "元气值商城") - return render(request, "showPools.html", frontend_dict) diff --git a/app/academic_utils.py b/app/academic_utils.py deleted file mode 100644 index 9a6cbe1c1..000000000 --- a/app/academic_utils.py +++ /dev/null @@ -1,580 +0,0 @@ -from collections import defaultdict -from typing import cast - -from django.http import HttpRequest - -from app.utils_dependency import * -from app.models import ( - NaturalPerson, - AcademicTag, - AcademicEntry, - AcademicTagEntry, - AcademicTextEntry, - AcademicQA, - User, - Chat, -) -from app.utils import get_person_or_org -from app.comment_utils import showComment - -__all__ = [ - 'get_search_results', - 'chats2Display', - 'comments2display', - 'get_js_tag_list', - 'get_text_list', - 'get_tag_status', - 'get_text_status', - 'update_tag_entry', - 'update_text_entry', - 'update_academic_map', - 'get_wait_audit_student', - 'audit_academic_map', - 'have_entries', - 'get_tags_for_search', -] - - -def get_search_results(query: str) -> dict[str, dict[str, list[str]]]: - # TODO: 更新文档 - """ - 根据提供的关键词获取搜索结果。 - """ - - # 搜索所有含有关键词的公开的学术地图项目,忽略大小写 - academic_tags = AcademicTagEntry.objects.filter( - tag__tag_content__icontains=query, - status=AcademicEntry.EntryStatus.PUBLIC, - ).values_list( - SQ.f(AcademicEntry.person, NaturalPerson.person_id, User.username), - SQ.f(AcademicTagEntry.tag, AcademicTag.atype), - SQ.f(AcademicTagEntry.tag, AcademicTag.tag_content), - ) - academic_texts = AcademicTextEntry.objects.filter( - content__icontains=query, - status=AcademicEntry.EntryStatus.PUBLIC, - ).values_list( - SQ.f(AcademicEntry.person, NaturalPerson.person_id, User.username), - SQ.f(AcademicTextEntry.atype), - SQ.f(AcademicTextEntry.content), - ) - - # 根据tag/text对应的人,整合学术地图项目 - # 直接使用defaultdict会导致前端items不可用,因为Django先尝试以键访问,并得到空列表 - academic_map_dict = defaultdict(lambda: defaultdict(list[str])) - for sid, ty, content in academic_tags: - academic_map_dict[sid][AcademicTag.Type(ty).label].append(content) - - for sid, ty, content in academic_texts: - academic_map_dict[sid][AcademicTextEntry.Type(ty).label].append(content) - - return {k: dict(v) for k, v in academic_map_dict.items()} # 转化为字典 - - -def chats2Display(user: User, sent: bool) -> dict[str, list[dict]]: - """ - 把我收到/我发出的所有chat转化为供前端展示的两个列表,分别是进行中chat的信息、和其他chat的信息 - - :param chats: 我收到/我发出的所有chat - :type chats: QuerySet[Chat] - :param sent: 若为True则表示我发出的,否则表示我收到的 - :type sent: bool - :return: 一个词典,key为progressing和not_progressing,value分别是进行中chat的列表、和其他chat的列表 - :rtype: dict[str, list[dict]] - """ - not_progressing_chats = [] - progressing_chats = [] - - if sent: - chats = Chat.objects.filter(questioner=user).order_by( - "-modify_time", "-time") - else: - chats = Chat.objects.filter(respondent=user).order_by( - "-modify_time", "-time") - - for chat in chats: - chat_dict = {} - chat_dict['id'] = chat.id - - chat_dict['questioner_anonymous'] = chat.questioner_anonymous - # 目前根据回答者是否匿名,来区分定向和非定向提问 - chat_dict['respondent_anonymous'] = chat.respondent_anonymous - - is_questioner = user == chat.questioner - chat_dict['is_questioner'] = is_questioner - - chat_dict['questioner_name'] = get_person_or_org( - chat.questioner).get_display_name() - chat_dict['respondent_name'] = get_person_or_org( - chat.respondent).get_display_name() - - if is_questioner: - chat_dict['academic_url'] = get_person_or_org( - chat.respondent).get_absolute_url() - else: - chat_dict['academic_url'] = get_person_or_org( - chat.questioner).get_absolute_url() - - if len(chat.title) >= 12: - chat_dict['title'] = chat.title[:12] + "……" - else: - chat_dict['title'] = chat.title or "无主题" - - chat_dict['status'] = chat.get_status_display() - chat_dict['start_time'] = chat.time - chat_dict['last_modification_time'] = chat.modify_time - chat_dict['chat_url'] = f"/viewQA/{chat.id}" # 问答详情的url,超链接放在title上 - chat_dict['message_count'] = chat.comments.count() - - if chat.status == Chat.Status.PROGRESSING: - progressing_chats.append(chat_dict) - else: - not_progressing_chats.append(chat_dict) - - return { - "progressing": progressing_chats, - "not_progressing": not_progressing_chats - } - - -def comments2display(chat: Chat, user: User) -> dict: - """ - 获取一个chat中的所有comment并转化为前端展示所需的形式(复用了comment_utils.py/showComment) - """ - questioner_anonymous = chat.questioner_anonymous - respondent_anonymous = chat.respondent_anonymous - is_questioner = user == chat.questioner - - anonymous_users = [] - if questioner_anonymous: - anonymous_users.append(chat.questioner) - if respondent_anonymous: - anonymous_users.append(chat.respondent) - - context = dict() - messages = showComment(chat, anonymous_users) - # TODO: 统一用AcademicQA模型后,不建议再用chat.id,而应该使用AcademicQA的id - context.update( - title=chat.title or "无主题", - chat_id=chat.id, - messages=messages, - ) - if not messages: - context.update(not_found_messages="当前问答没有信息.") - - # 若commentable为True,则前端会给出评论区和“关闭当前问答”的按钮 - context.update( - status=chat.get_status_display(), - commentable=chat.status == Chat.Status.PROGRESSING, - anonymous_chat=chat.questioner_anonymous, - accept_anonymous=chat.respondent.accept_anonymous_chat, - answered=chat.comments.filter(commentator=chat.respondent).exists() - ) - - context.update( - is_questioner=is_questioner, - is_anonymous=questioner_anonymous if is_questioner else respondent_anonymous, - questioner_anonymous=questioner_anonymous, - respondent_anonymous=respondent_anonymous, - ) - - my_name = get_person_or_org(user).get_display_name() - questioner_info = get_person_or_org(chat.questioner) - respondent_info = get_person_or_org(chat.respondent) - academic_url = (respondent_info.get_absolute_url() - if is_questioner else questioner_info.get_absolute_url()) - - context.update( - my_name=my_name, - questioner_name=questioner_info.get_display_name(), - respondent_name=respondent_info.get_display_name(), - academic_url=academic_url - ) - - # 在对方匿名时,提供一些简单的信息 - if is_questioner: - qa: AcademicQA = AcademicQA.objects.get(chat_id=chat.id) - context['rating'] = qa.rating - if not qa.directed: - context['respondent_tags'] = list(qa.keywords) - else: - context['respondent_tags'] = [] - return context - - try: - major = AcademicTagEntry.objects.get( - person=chat.questioner, tag__atype=AcademicTag.Type.MAJOR) - major_display = major.content - except: - major_display = "" - # TODO: 暂时没用上,但是可能有用,先留着 - context['questioner_tags'] = [ - chat.questioner.username[:2] + "级", major_display - ] - return context - - -def get_js_tag_list(author: NaturalPerson, type: AcademicTag.Type, - selected: bool, - status_in: list[AcademicEntry.EntryStatus] | None = None) -> list[dict]: - """ - 用于前端显示支持搜索的专业/项目列表,返回形如[{id, content}]的列表。 - - :param author: 作者自然人信息 - :type author: NaturalPerson - :param type: 标记所需的tag类型 - :type type: AcademicTag.Type - :param selected: 用于标记是否获取本人已有的专业项目,selected代表获取前端默认选中的项目 - :type selected: bool - :param status_in: 所要检索的状态的列表,默认为None,表示搜索全部 - :type status_in: list[AcademicEntry.EntryStatus] - :return: 所有专业/项目组成的List[dict],key值如上所述 - :rtype: list[dict] - """ - if selected: - all_my_tags = AcademicTagEntry.objects.activated().filter(person=author) - if status_in is not None: - all_my_tags = all_my_tags.filter(status__in=status_in) - tags = all_my_tags.filter(tag__atype=type).values( - 'tag__id', 'tag__tag_content') - js_list = [{"id": tag['tag__id'], "text": tag['tag__tag_content']} - for tag in tags] - else: - tags = AcademicTag.objects.filter(atype=type) - js_list = [{"id": tag.id, "text": tag.tag_content} for tag in tags] - - return js_list - - -def get_text_list(author: NaturalPerson, type: AcademicTextEntry.Type, - status_in: list[AcademicEntry.EntryStatus] | None = None) -> list[str]: - """ - 获取自己的所有类型为type的TextEntry的内容列表。 - - :param author: 作者自然人信息 - :type author: NaturalPerson - :param type: TextEntry的类型 - :type type: AcademicTextEntry.Type - :param status_in: 所要检索的状态的列表,默认为None,表示搜索全部 - :type status_in: list - :return: 含有所有类型为type的TextEntry的content的list - :rtype: list[str] - """ - all_my_text = AcademicTextEntry.objects.activated().filter(person=author, atype=type) - if status_in is not None: - all_my_text = all_my_text.filter(status__in=status_in) - text_list = [text.content for text in all_my_text] - return text_list - - -def get_tag_status(person: NaturalPerson, type: AcademicTag.Type) -> str: - """ - 获取person的类型为type的TagEntry的公开状态。 - 如果person没有类型为type的TagEntry,返回"公开"。 - - :param person: 需要获取公开状态的人 - :type person: NaturalPerson - :param type: TagEntry的类型 - :type type: AcademicTag.Type - :return: 公开状态,返回"公开/私密" - :rtype: str - """ - # 首先获取person所有的TagEntry - all_tag_entries = AcademicTagEntry.objects.activated().filter( - person=person, tag__atype=type) - - if all_tag_entries.exists(): - # 因为所有类型为type的TagEntry的公开状态都一样,所以直接返回第一个entry的公开状态 - entry = all_tag_entries[0] - return "私密" if entry.status == AcademicEntry.EntryStatus.PRIVATE else "公开" - else: - return "公开" - - -def get_text_status(person: NaturalPerson, type: AcademicTextEntry.Type) -> str: - """ - 获取person的类型为type的TextEntry的公开状态。 - 如果person没有类型为type的TextEntry,返回"公开"。 - - :param person: 需要获取公开状态的人 - :type person: NaturalPerson - :param type: TextEntry的类型 - :type type: AcademicTextEntry.Type - :return: 公开状态,返回"公开/私密" - :rtype: str - """ - # 首先获取person所有的类型为type的TextEntry - all_text_entries = AcademicTextEntry.objects.activated().filter(person=person, - atype=type) - - if all_text_entries.exists(): - # 因为所有类型为type的TextEntry的公开状态都一样,所以直接返回第一个entry的公开状态 - entry = all_text_entries[0] - return "私密" if entry.status == AcademicEntry.EntryStatus.PRIVATE else "公开" - else: - return "公开" - - -def update_tag_entry(person: NaturalPerson, - tag_ids: list[str], - status: bool, - type: AcademicTag.Type) -> None: - """ - 更新TagEntry的工具函数。 - - :param person: 需要更新学术地图的人 - :type person: NaturalPerson - :param tag_ids: 含有一系列tag_id(未经类型转换)的list - :type tag_ids: list[str] - :param status: tag_ids对应的所有tags的公开状态 - :type status: bool - :param type: tag_ids对应的所有tags的类型 - :type type: AcademicTag.Type - """ - # 首先获取person所有的TagEntry - all_tag_entries = AcademicTagEntry.objects.activated().filter( - person=person, tag__atype=type) - # 标签类型无需审核 - updated_status = (AcademicEntry.EntryStatus.PUBLIC - if status == "公开" else - AcademicEntry.EntryStatus.PRIVATE) - - for entry in all_tag_entries: - if not str(entry.tag.id) in tag_ids: - # 如果用户原有的TagEntry的id在tag_ids中未出现,则将其状态设置为“已弃用” - entry.status = AcademicEntry.EntryStatus.OUTDATE - entry.save() - else: - # 如果出现,直接更新其状态,并将这个id从tag_ids移除 - entry.status = updated_status - entry.save() - tag_ids.remove(str(entry.tag.id)) - - # 接下来遍历的tag_id都是要新建的tag - for tag_id in tag_ids: - AcademicTagEntry.objects.create( - person=person, tag=AcademicTag.objects.get(id=int(tag_id)), - status=updated_status - ) - - -def update_text_entry(person: NaturalPerson, - contents: list[str], - status: bool, - type: AcademicTextEntry.Type) -> None: - """ - 更新TextEntry的工具函数。 - - :param person: 需要更新学术地图的人 - :type person: NaturalPerson - :param tag_ids: 含有一系列TextEntry的内容的list - :type tag_ids: list[str] - :param status: 该用户所有类型为type的TextEntry的公开状态 - :type status: bool - :param type: contents对应的TextEntry的类型 - :type type: AcademicTextEntry.Type - """ - # 首先获取person所有的类型为type的TextEntry - all_text_entries = AcademicTextEntry.objects.activated().filter(person=person, - atype=type) - updated_status = (AcademicEntry.EntryStatus.WAIT_AUDIT - if status == "公开" else - AcademicEntry.EntryStatus.PRIVATE) - previous_num = len(all_text_entries) - - # 即将修改/创建的entry总数一定不小于原有的,因此先遍历原有的entry,判断是否更改/删除 - for i, entry in enumerate(all_text_entries): - if entry.content != contents[i]: - # 内容发生修改,需要先将原有的content设置为“已弃用” - entry.status = AcademicEntry.EntryStatus.OUTDATE - entry.save() - if contents[i] != "": # 只有新的entry的内容不为空才创建 - AcademicTextEntry.objects.create( - person=person, atype=type, content=contents[i], - status=updated_status, - ) - elif entry.status != updated_status: - # 内容未修改但status修改,只更新entry的状态,不删除 - entry.status = updated_status - entry.save() - - # 接下来遍历的entry均为需要新建的 - for content in contents[previous_num:]: - if content != "": - AcademicTextEntry.objects.create( - person=person, atype=type, content=content, - status=AcademicEntry.EntryStatus.WAIT_AUDIT if status == "公开" - else AcademicEntry.EntryStatus.PRIVATE - ) - - -def update_academic_map(request: HttpRequest) -> dict: - """ - 从前端获取填写的学术地图信息,并在数据库中进行更新,返回含有成功/错误信息的dict。 - - :param request: http请求 - :type request: HttpRequest - :return: 含成功/错误信息的dict,用于redirect后页面的前端展示 - :rtype: dict - """ - # 首先从select栏获取所有选中的TagEntry - majors = request.POST.getlist('majors') - minors = request.POST.getlist('minors') - double_degrees = request.POST.getlist('double_degrees') - projects = request.POST.getlist('projects') - - # 然后从其余栏目获取即将更新的TextEntry - scientific_research_num = int(request.POST['scientific_research_num']) - challenge_cup_num = int(request.POST['challenge_cup_num']) - internship_num = int(request.POST['internship_num']) - scientific_direction_num = int(request.POST['scientific_direction_num']) - graduation_num = int(request.POST['graduation_num']) - scientific_research = [request.POST[f'scientific_research_{i}'] - for i in range(scientific_research_num+1)] - challenge_cup = [request.POST[f'challenge_cup_{i}'] - for i in range(challenge_cup_num+1)] - internship = [request.POST[f'internship_{i}'] - for i in range(internship_num+1)] - scientific_direction = [request.POST[f'scientific_direction_{i}'] - for i in range(scientific_direction_num+1)] - graduation = [request.POST[f'graduation_{i}'] - for i in range(graduation_num+1)] - - # 对上述五个列表中的所有填写项目,检查是否超过数据库要求的字数上限 - def max_length_of(items): return max( - [len(item) for item in items]) if len(items) > 0 else 0 - MAX_LENGTH = 4095 - if max_length_of(scientific_research) > MAX_LENGTH: - return wrong("您设置的本科生科研经历太长啦!请修改~") - elif max_length_of(challenge_cup) > MAX_LENGTH: - return wrong("您设置的挑战杯经历太长啦!请修改~") - elif max_length_of(internship) > MAX_LENGTH: - return wrong("您设置的实习经历太长啦!请修改~") - elif max_length_of(scientific_direction) > MAX_LENGTH: - return wrong("您设置的科研方向太长啦!请修改~") - elif max_length_of(graduation) > MAX_LENGTH: - return wrong("您设置的毕业去向太长啦!请修改~") - - # 从checkbox获取所有栏目的公开状态 - major_status = request.POST['major_status'] - minor_status = request.POST['minor_status'] - double_degree_status = request.POST['double_degree_status'] - project_status = request.POST['project_status'] - scientific_research_status = request.POST['scientific_research_status'] - challenge_cup_status = request.POST['challenge_cup_status'] - internship_status = request.POST['internship_status'] - scientific_direction_status = request.POST['scientific_direction_status'] - graduation_status = request.POST['graduation_status'] - - # 获取前端信息后对数据库进行更新 - with transaction.atomic(): - assert request.user.is_authenticated - user = cast(User, request.user) - me = get_person_or_org(user, User.Type.PERSON) - - # 首先更新自己的TagEntry - update_tag_entry(me, majors, major_status, AcademicTag.Type.MAJOR) - update_tag_entry(me, minors, minor_status, AcademicTag.Type.MINOR) - update_tag_entry(me, double_degrees, double_degree_status, - AcademicTag.Type.DOUBLE_DEGREE) - update_tag_entry(me, projects, project_status, - AcademicTag.Type.PROJECT) - - # 然后更新自己的TextEntry - update_text_entry( - me, scientific_research, scientific_research_status, - AcademicTextEntry.Type.SCIENTIFIC_RESEARCH - ) - update_text_entry( - me, challenge_cup, challenge_cup_status, - AcademicTextEntry.Type.CHALLENGE_CUP - ) - update_text_entry( - me, internship, internship_status, - AcademicTextEntry.Type.INTERNSHIP - ) - update_text_entry( - me, scientific_direction, scientific_direction_status, - AcademicTextEntry.Type.SCIENTIFIC_DIRECTION - ) - update_text_entry( - me, graduation, graduation_status, - AcademicTextEntry.Type.GRADUATION - ) - - # 最后更新是否允许他人提问 - accept_chat = request.POST["accept_chat"] - user.accept_chat = True if accept_chat == "True" else False - user.save() - - return succeed("学术地图修改成功!") - - -def get_wait_audit_student() -> set[NaturalPerson]: - """ - 获取当前审核中的AcademicEntry对应的学生,因为要去重,所以返回一个集合 - - :return: 当前审核中的AcademicEntry对应的NaturalPerson组成的集合 - :rtype: set[NaturalPerson] - """ - wait_audit_tag_entries = AcademicTagEntry.objects.filter( - status=AcademicEntry.EntryStatus.WAIT_AUDIT) - wait_audit_text_entries = AcademicTextEntry.objects.filter( - status=AcademicEntry.EntryStatus.WAIT_AUDIT) - - wait_audit_students = set() - for entry in wait_audit_tag_entries: - wait_audit_students.add(entry.person) - for entry in wait_audit_text_entries: - wait_audit_students.add(entry.person) - - return wait_audit_students - - -def audit_academic_map(author: NaturalPerson) -> None: - """ - 审核通过某用户的记录。 - :param author: 被审核用户 - :type author: NaturalPerson - """ - # 筛选所有待审核的记录 - AcademicTagEntry.objects.activated().filter( - person=author, status=AcademicEntry.EntryStatus.WAIT_AUDIT).update( - status=AcademicEntry.EntryStatus.PUBLIC) - - AcademicTextEntry.objects.activated().filter( - person=author, status=AcademicEntry.EntryStatus.WAIT_AUDIT).update( - status=AcademicEntry.EntryStatus.PUBLIC) - - -def have_entries(author: NaturalPerson, - status_in: list[AcademicEntry.EntryStatus]) -> bool: - """ - 判断用户有无status属性为public/wait_audit...的学术地图条目(tag和text) - :param author: 条目作者用户 - :type author: NaturalPerson - :param status_in: AcademicEntry.EntryStatus构成的list - :type status_in: list[AcademicEntry.EntryStatus] - :return: 是否有该类别的条目 - :rtype: bool - """ - all_tag_entries = AcademicTagEntry.objects.activated().filter( - person=author, status__in=status_in) - all_text_entries = (AcademicTextEntry.objects.activated().filter( - person=author, status__in=status_in)) - return bool(all_tag_entries) or bool(all_text_entries) - - -def get_tags_for_search(): - tags = AcademicTag.objects.all() - tag_contents = set() - for t in tags: - tag_contents.add(t.tag_content) - - id = 0 - tags_for_search = [] - for t in tag_contents: - tags_for_search.append({'id': id, 'text': t}) - id += 1 - - return tags_for_search diff --git a/app/academic_views.py b/app/academic_views.py deleted file mode 100644 index 201b5fb6a..000000000 --- a/app/academic_views.py +++ /dev/null @@ -1,254 +0,0 @@ -from generic.utils import to_search_indices -from app.views_dependency import * -from app.models import ( - AcademicTag, - AcademicTextEntry, - Chat, - NaturalPerson, -) -from app.academic_utils import ( - chats2Display, - comments2display, - get_js_tag_list, - get_text_list, - get_tag_status, - get_text_status, - update_academic_map, - get_wait_audit_student, - audit_academic_map, - get_tags_for_search, -) -from app.utils import ( - get_sidebar_and_navbar, - get_person_or_org, -) -from achievement.api import unlock_achievement - -__all__ = [ - 'ShowChats', - 'ViewChat', - 'modifyAcademic', - 'auditAcademic', - 'applyAuditAcademic', -] - - -class ShowChats(ProfileTemplateView): - - http_method_names = ['get'] - template_name = 'academic/showChats.html' - page_name = '学术地图问答' - - def prepare_get(self): - if not self.request.user.is_person(): - # 后续或许可以开放任意的聊天 - return self.wrong('请使用个人账号访问问答中心页面!') - return self.get - - def get(self) -> HttpResponse: - self.extra_context.update({ - 'sent_chats': chats2Display(self.request.user, sent=True), - 'received_chats': chats2Display(self.request.user, sent=False), - 'stu_list': to_search_indices(User.objects.filter_type(User.Type.PERSON)), - 'tag_list': get_tags_for_search(), - }) - - return self.render() - - -class ViewChat(ProfileTemplateView): - - http_method_names = ['get'] - template_name = 'academic/viewChat.html' - page_name = '学术地图问答' - - def setup(self, request: HttpRequest, chat_id: int): - self.chat_id = chat_id - return super().setup(request, chat_id=chat_id) - - def prepare_get(self): - possible_chat = Chat.objects.filter(id=self.chat_id).first() - if possible_chat is None: - return self.wrong('问答不存在') - chat: Chat = possible_chat - if self.request.user not in [chat.questioner, chat.respondent]: - return self.wrong('您只能访问自己参与的问答!') - self.chat = chat - return self.get - - def get(self) -> HttpResponse: - user = self.request.user - self.extra_context.update(comments2display(self.chat, user)) - return self.render() - - -@login_required(redirect_field_name="origin") -@utils.check_user_access(redirect_url="/logout/") -@logger.secure_view() -def modifyAcademic(request: UserRequest) -> HttpResponse: - """ - 学术地图编辑界面 - - :param request: http请求 - :type request: HttpRequest - :return: http响应 - :rtype: HttpResponse - """ - frontend_dict = {} - - if not request.user.is_person(): - return redirect(message_url(wrong("只有个人才可以修改自己的学术地图!"))) - - # POST表明编辑界面发起修改 - if request.method == "POST" and request.POST: - try: - context = update_academic_map(request) - if context["warn_code"] == 1: # 填写的TextEntry太长导致填写失败 - return redirect(message_url(context, "/modifyAcademic/")) - else: # warn_code == 2,表明填写成功 - unlock_achievement(request.user, "编辑自己的学术地图") # 解锁成就-编辑自己的学术地图 - return redirect(message_url(context, "/stuinfo/#tab=academic_map")) - except: - return redirect(message_url(wrong("修改过程中出现意料之外的错误,请联系工作人员处理!"))) - - # 不是POST,说明用户希望编辑学术地图,下面准备前端展示量 - # 获取所有专业/项目的列表,左右前端select框的下拉选项 - me = get_person_or_org(request.user) - frontend_dict.update( - major_list=get_js_tag_list(me, AcademicTag.Type.MAJOR, selected=False), - minor_list=get_js_tag_list(me, AcademicTag.Type.MINOR, selected=False), - double_degree_list=get_js_tag_list( - me, AcademicTag.Type.DOUBLE_DEGREE, selected=False), - project_list=get_js_tag_list( - me, AcademicTag.Type.PROJECT, selected=False), - ) - - # 获取用户已有的专业/项目的列表,用于select的默认选中项 - frontend_dict.update( - selected_major_list=get_js_tag_list( - me, AcademicTag.Type.MAJOR, selected=True), - selected_minor_list=get_js_tag_list( - me, AcademicTag.Type.MINOR, selected=True), - selected_double_degree_list=get_js_tag_list( - me, AcademicTag.Type.DOUBLE_DEGREE, selected=True), - selected_project_list=get_js_tag_list( - me, AcademicTag.Type.PROJECT, selected=True), - ) - - # 获取用户已有的TextEntry的contents,用于TextEntry填写栏的前端预填写 - scientific_research_list = get_text_list( - me, AcademicTextEntry.Type.SCIENTIFIC_RESEARCH - ) - challenge_cup_list = get_text_list( - me, AcademicTextEntry.Type.CHALLENGE_CUP - ) - internship_list = get_text_list( - me, AcademicTextEntry.Type.INTERNSHIP - ) - scientific_direction_list = get_text_list( - me, AcademicTextEntry.Type.SCIENTIFIC_DIRECTION - ) - graduation_list = get_text_list( - me, AcademicTextEntry.Type.GRADUATION - ) - frontend_dict.update( - scientific_research_list=scientific_research_list, - challenge_cup_list=challenge_cup_list, - internship_list=internship_list, - scientific_direction_list=scientific_direction_list, - graduation_list=graduation_list, - scientific_research_num=len(scientific_research_list), - challenge_cup_num=len(challenge_cup_list), - internship_num=len(internship_list), - scientific_direction_num=len(scientific_direction_list), - graduation_num=len(graduation_list), - ) - - # 最后获取每一种atype对应的entry的公开状态,如果没有则默认为公开 - major_status = get_tag_status(me, AcademicTag.Type.MAJOR) - minor_status = get_tag_status(me, AcademicTag.Type.MINOR) - double_degree_status = get_tag_status(me, AcademicTag.Type.DOUBLE_DEGREE) - project_status = get_tag_status(me, AcademicTag.Type.PROJECT) - scientific_research_status = get_text_status( - me, AcademicTextEntry.Type.SCIENTIFIC_RESEARCH - ) - challenge_cup_status = get_text_status( - me, AcademicTextEntry.Type.CHALLENGE_CUP - ) - internship_status = get_text_status( - me, AcademicTextEntry.Type.INTERNSHIP - ) - scientific_direction_status = get_text_status( - me, AcademicTextEntry.Type.SCIENTIFIC_DIRECTION - ) - graduation_status = get_text_status( - me, AcademicTextEntry.Type.GRADUATION - ) - - status_dict = dict( - major_status=major_status, - minor_status=minor_status, - double_degree_status=double_degree_status, - project_status=project_status, - scientific_research_status=scientific_research_status, - challenge_cup_status=challenge_cup_status, - internship_status=internship_status, - scientific_direction_status=scientific_direction_status, - graduation_status=graduation_status, - ) - frontend_dict.update(status_dict) - - # 获取“全部公开”checkbox的选中状态与公开的type数量/总type数 - frontend_dict["all_status"] = "私密" if "私密" in status_dict.values() else "公开" - frontend_dict["public_num"] = list(status_dict.values()).count("公开") - frontend_dict["total_num"] = len(status_dict) - - # 获取用户是否允许他人提问 - frontend_dict["accept_chat"] = request.user.accept_chat - - # 最后获取侧边栏信息 - frontend_dict["bar_display"] = get_sidebar_and_navbar( - request.user, "修改学术地图") - frontend_dict["warn_code"] = request.GET.get('warn_code', 0) - frontend_dict["warn_message"] = request.GET.get('warn_message', "") - return render(request, "academic/modify.html", frontend_dict) - - -@login_required(redirect_field_name="origin") -@utils.check_user_access(redirect_url="/logout/") -@logger.secure_view() -def auditAcademic(request: UserRequest) -> HttpResponse: - """ - 供教师使用的页面,展示所有待审核的学术地图 - - :param request - :type request: HttpRequest - :return - :rtype: HttpResponse - """ - # 身份检查 - person = get_person_or_org(request.user) - if not (request.user.is_person() and person.is_teacher()): - return redirect(message_url(wrong('只有教师账号可进入学术地图审核页面!'))) - - frontend_dict = {} - frontend_dict["bar_display"] = get_sidebar_and_navbar( - request.user, "审核学术地图") - frontend_dict["student_list"] = get_wait_audit_student() - - return render(request, "academic/audit.html", frontend_dict) - - -@login_required(redirect_field_name="origin") -@utils.check_user_access(redirect_url="/logout/") -@logger.secure_view() -def applyAuditAcademic(request: HttpRequest): - if not NaturalPerson.objects.get_by_user(request.user).is_teacher(): - return JsonResponse(wrong("只有老师才能执行审核操作!")) - try: - author = NaturalPerson.objects.get_by_user(request.POST.get("author_id")) - # 需要回传作者的person_id.id - audit_academic_map(author) - return JsonResponse(succeed("审核成功!")) - except: - return JsonResponse(wrong("审核发布时发生未知错误,请联系管理员!")) diff --git a/app/activity_utils.py b/app/activity_utils.py deleted file mode 100644 index 15f0240dd..000000000 --- a/app/activity_utils.py +++ /dev/null @@ -1,978 +0,0 @@ -""" -activity_utils.py - -这个文件应该只被 ./activity_views.py, ./scheduler_func.py 依赖 -依赖于 ./utils.py, ./wechat_send.py, ./notification_utils.py - -scheduler_func 依赖于 wechat_send 依赖于 utils - -文件中参数存在 activity 的函数需要在 transaction.atomic() 块中进行。 -如果存在预期异常,抛出 ActivityException,否则抛出其他异常 -""" -import io -import base64 -import random -from typing import Iterable -from datetime import datetime, timedelta - -import qrcode - -from utils.http.utils import build_full_url -import utils.models.query as SQ -from generic.models import User, YQPointRecord -from scheduler.adder import ScheduleAdder -from scheduler.cancel import remove_job -from app.utils_dependency import * -from app.models import ( - User, - NaturalPerson as Person, - Organization, - Organization as Org, - OrganizationType as OrgType, - Position, - Activity, - Participation, - Notification, - ActivityPhoto, -) -from app.utils import get_person_or_org, if_image -from app.notification_utils import ( - notification_create, - bulk_notification_create, - notification_status_change, -) -from app.extern.wechat import WechatApp, WechatMessageLevel -from app.log import logger - - -__all__ = [ - 'changeActivityStatus', - 'notifyActivity', - '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', -] - - -""" -使用方式: -scheduler.add_job(changeActivityStatus, "date", - id=f"activity_{aid}_{to_status}", run_date, args) -注意: - 1、当 cur_status 不为 None 时,检查活动是否为给定状态 - 2、一个活动每一个目标状态最多保留一个定时任务 -允许的状态变换: - 2、报名中 -> 等待中 - 3、等待中 -> 进行中 - 4、进行中 -> 已结束 -活动变更为进行中时,更新报名成功人员状态 -""" - - -@logger.secure_func('活动状态更新异常') -@logger.secure_func('检查活动状态', exc_type=AssertionError) -@transaction.atomic -def changeActivityStatus(aid, cur_status, to_status): - ''' - 幂等;可能发生异常;包装器负责处理异常 - 必须提供cur_status,则会在转换状态前检查前后状态,每次只能变更一个阶段 - 即:报名中->等待中->进行中->结束 - 状态不符合时,抛出AssertionError - ''' - activity = Activity.objects.select_for_update().get(id=aid) - if cur_status is None: - raise AssertionError('未提供当前状态,不允许进行活动状态修改') - if cur_status == Activity.Status.CANCELED: - return - assert cur_status == activity.status, f'<{activity.status}>与期望的状态<{cur_status}>不符' - - FSM = [ - (Activity.Status.APPLYING, Activity.Status.WAITING), - (Activity.Status.WAITING, Activity.Status.PROGRESSING), - (Activity.Status.PROGRESSING, Activity.Status.END), - ] - LIMITED_STATUS = list(set(map(lambda x: x[0], FSM))) - if cur_status in LIMITED_STATUS: - assert (cur_status, to_status) in FSM, f'不能从{cur_status}变更到{to_status}' - - if to_status == Activity.Status.WAITING: - if activity.bidding: - draw_lots(activity) - - # 活动变更为进行中时,修改参与人参与状态 - elif to_status == Activity.Status.PROGRESSING: - unchecked = SQ.sfilter(Participation.activity, activity).filter( - status=Participation.AttendStatus.APPLYSUCCESS) - if activity.need_checkin: - unchecked.update(status=Participation.AttendStatus.UNATTENDED) - else: - unchecked.update(status=Participation.AttendStatus.ATTENDED) - - # if not activity.valid: - # # 活动开始后,未审核自动通过 - # activity.valid = True - # notification = Notification.objects.get( - # relate_instance=activity, - # status=Notification.Status.UNDONE, - # title=Notification.Title.VERIFY_INFORM - # ) - # notification_status_change(notification, Notification.Status.DONE) - - # 结束,计算元气值 - elif (to_status == Activity.Status.END - and activity.category != Activity.ActivityCategory.COURSE): - activity.settle_yqpoint(status=to_status) - # 过早进行这个修改,将被写到activity待执行的保存中,导致失败后调用activity.save仍会调整状态 - activity.status = to_status - activity.save() - - -""" -需要在 transaction 中使用 -所有涉及到 activity 的函数,都应该先锁 activity -""" - - -def draw_lots(activity: Activity): - participation = SQ.sfilter(Participation.activity, activity) - participation: QuerySet[Participation] - l = len(participation.filter(status=Participation.AttendStatus.APPLYING)) - - engaged = len(participation.filter( - status__in=[Participation.AttendStatus.APPLYSUCCESS, - Participation.AttendStatus.UNATTENDED, - Participation.AttendStatus.ATTENDED] - )) - - leftQuota = activity.capacity - engaged - - if l <= leftQuota: - participation.filter( - status__in=[Participation.AttendStatus.APPLYING, - Participation.AttendStatus.APPLYFAILED] - ).update(status=Participation.AttendStatus.APPLYSUCCESS) - activity.current_participants = engaged + l - else: - lucky_ones = random.sample(range(l), leftQuota) - activity.current_participants = activity.capacity - for i, participant in enumerate(participation.select_for_update().filter( - status__in=[Participation.AttendStatus.APPLYING, - Participation.AttendStatus.APPLYFAILED] - )): - if i in lucky_ones: - participant.status = Participation.AttendStatus.APPLYSUCCESS - else: - participant.status = Participation.AttendStatus.APPLYFAILED - participant.save() - # 签到成功的转发通知和微信通知 - receivers = SQ.qsvlist(participation.filter( - status=Participation.AttendStatus.APPLYSUCCESS - ), Participation.person, Person.person_id) - receivers = User.objects.filter(id__in=receivers) - sender = activity.organization_id.get_user() - typename = Notification.Type.NEEDREAD - content = f'您好!您参与抽签的活动“{activity.title}”报名成功!请准时参加活动!' - URL = f'/viewActivity/{activity.id}' - if len(receivers) > 0: - bulk_notification_create( - receivers=receivers, - sender=sender, - typename=typename, - title=Notification.Title.ACTIVITY_INFORM, - content=content, - URL=URL, - to_wechat=dict(app=WechatApp.TO_PARTICIPANT, level=WechatMessageLevel.IMPORTANT), - ) - # 抽签失败的同学发送通知 - receivers = SQ.qsvlist(participation.filter( - status=Participation.AttendStatus.APPLYFAILED - ), Participation.person, Person.person_id) - receivers = User.objects.filter(id__in=receivers) - content = f'很抱歉通知您,您参与抽签的活动“{activity.title}”报名失败!' - if len(receivers) > 0: - bulk_notification_create( - receivers=receivers, - sender=sender, - typename=typename, - title=Notification.Title.ACTIVITY_INFORM, - content=content, - URL=URL, - to_wechat=dict(app=WechatApp.TO_PARTICIPANT, level=WechatMessageLevel.IMPORTANT), - ) - - -""" -使用方式: -scheduler.add_job(notifyActivityStart, "date", - id=f"activity_{aid}_{start_notification}", run_date, args) -""" - - -def _participant_uids(activity: Activity) -> list[int]: - participant_person_id = SQ.qsvlist(Participation.objects.filter( - SQ.sq(Participation.activity, activity), - SQ.mq(Participation.status, IN=[Participation.AttendStatus.APPLYSUCCESS, - Participation.AttendStatus.APPLYING]) - ), Participation.person) - return SQ.qsvlist(Person.objects.activated().filter( - id__in=participant_person_id), Person.person_id) - - -def _subscriber_uids(activity: Activity) -> list[int]: - return SQ.qsvlist(Person.objects.activated().exclude( - id__in=activity.organization_id.unsubscribers.all() - ), Person.person_id) - - -@logger.secure_func('活动消息发送异常') -def notifyActivity(aid: int, msg_type: str, msg=""): - activity = Activity.objects.get(id=aid) - inner = activity.inner - title = Notification.Title.ACTIVITY_INFORM - if msg_type == "newActivity": - title = activity.title - msg = f"您关注的小组{activity.organization_id.oname}发布了新的活动。" - msg += f"\n开始时间: {activity.start.strftime('%Y-%m-%d %H:%M')}" - msg += f"\n活动地点: {activity.location}" - receivers = User.objects.filter(id__in=_subscriber_uids(activity)) - publish_kws = dict(app=WechatApp.TO_SUBSCRIBER) - elif msg_type == "remind": - with transaction.atomic(): - activity = Activity.objects.select_for_update().get(id=aid) - nowtime = datetime.now() - notifications = Notification.objects.filter( - relate_instance=activity, - start_time__gt=nowtime + timedelta(seconds=60), - title=Notification.Title.PENDING_INFORM, - ) - if len(notifications) > 0: - return - msg = f"您参与的活动 <{activity.title}> 即将开始。" - msg += f"\n开始时间: {activity.start.strftime('%Y-%m-%d %H:%M')}" - msg += f"\n活动地点: {activity.location}" - receivers = User.objects.filter(id__in=_participant_uids(activity)) - publish_kws = dict(app=WechatApp.TO_PARTICIPANT) - - elif msg_type == 'modification_sub': - receivers = User.objects.filter(id__in=_subscriber_uids(activity)) - publish_kws = dict(app=WechatApp.TO_SUBSCRIBER) - elif msg_type == 'modification_par': - receivers = User.objects.filter(id__in=_participant_uids(activity)) - publish_kws = dict( - app=WechatApp.TO_PARTICIPANT, - level=WechatMessageLevel.IMPORTANT, - ) - elif msg_type == "modification_sub_ex_par": - receiver_uids = list(set(_subscriber_uids(activity)) - - set(_participant_uids(activity))) - receivers = User.objects.filter(id__in=receiver_uids) - publish_kws = dict(app=WechatApp.TO_SUBSCRIBER) - - # 应该用不到了,调用的时候分别发给 par 和 sub - # 主要发给两类用户的信息往往是不一样的 - elif msg_type == 'modification_all': - receiver_uids = list(set(_subscriber_uids(activity)) | - set(_participant_uids(activity))) - receivers = User.objects.filter(id__in=receiver_uids) - publish_kws = dict(app=WechatApp.TO_SUBSCRIBER) - elif msg_type == 'newCourseActivity': - title = activity.title - msg = f"课程{activity.organization_id.oname}发布了新的课程活动。" - msg += f"\n开始时间: {activity.start.strftime('%Y-%m-%d %H:%M')}" - msg += f"\n活动地点: {activity.location}" - receiver_uids = list(set(_subscriber_uids(activity)) | - set(_participant_uids(activity))) - receivers = User.objects.filter(id__in=receiver_uids) - publish_kws = dict(app=WechatApp.TO_SUBSCRIBER) - else: - raise ValueError(f"msg_type参数错误: {msg_type}") - - if inner and publish_kws.get('app') == WechatApp.TO_SUBSCRIBER: - member_id_list = SQ.qsvlist(Position.objects.activated().filter( - org=activity.organization_id), Position.person, Person.person_id) - receivers = receivers.filter(id__in=member_id_list) - - success, _ = bulk_notification_create( - receivers=list(receivers), - sender=activity.organization_id.get_user(), - typename=Notification.Type.NEEDREAD, - title=title, - content=msg, - URL=f"/viewActivity/{aid}", - relate_instance=activity, - to_wechat=publish_kws, - ) - assert success, "批量创建通知并发送时失败" - - -def get_activity_QRcode(activity): - auth_code = GLOBAL_CONFIG.hasher.encode(str(activity.id)) - url = build_full_url(f'checkinActivity/{activity.id}?auth={auth_code}') - qr = qrcode.QRCode( - version=2, - error_correction=qrcode.constants.ERROR_CORRECT_L, - box_size=5, - border=5, - ) - qr.add_data(url) - qr.make(fit=True) - img = qr.make_image() - io_buffer = io.BytesIO() - img.save(io_buffer, "png") - data = base64.encodebytes(io_buffer.getvalue()).decode() - return "data:image/png;base64," + str(data) - - -class ActivityException(Exception): - def __init__(self, msg): - self.msg = msg - - def __str__(self): - return self.msg - - -# 时间合法性的检查,检查时间是否在当前时间的一个月以内,并且检查开始的时间是否早于结束的时间, -def check_ac_time(start_time: datetime, end_time: datetime) -> bool: - now_time = datetime.now() - month_late = now_time + timedelta(days=30) - if not start_time < end_time: - return False - if now_time < start_time < month_late: - return True # 时间所处范围正确 - - return False - - -def activity_base_check(request, edit=False): - '''正常情况下检查出错误会抛出不含错误信息的AssertionError,不抛出ActivityException''' - - context = dict() - - # title, introduction, location 创建时不能为空 - context["title"] = request.POST["title"] - context["introduction"] = request.POST["introduction"] - context["location"] = request.POST["location"] - assert len(context["title"]) > 0 - assert len(context["introduction"]) > 0 - assert len(context["location"]) > 0 - - # url,就不支持了 http 了,真的没必要 - context["url"] = request.POST["URL"] - if context["url"] != "": - assert context["url"].startswith( - "http://") or context["url"].startswith("https://") - - # 在审核通过后,这些不可修改 - signscheme = int(request.POST["signscheme"]) - if signscheme: - context["bidding"] = True - else: - context["bidding"] = False - - # examine_teacher 需要特殊检查 - context["examine_teacher"] = request.POST.get("examine_teacher") - - # 预报备 - context["recorded"] = False - if request.POST.get("recorded"): - context["recorded"] = True - - # 时间 - act_start = datetime.strptime( - request.POST["actstart"], "%Y-%m-%d %H:%M") # 活动报名时间 - act_end = datetime.strptime( - request.POST["actend"], "%Y-%m-%d %H:%M") # 活动报名结束时间 - context["start"] = act_start - context["end"] = act_end - assert check_ac_time(act_start, act_end) - - # create 或者调整报名时间,都是要确保活动不要立刻截止报名 - now_time = datetime.now() - if not edit or request.POST.get("adjust_apply_ddl"): - prepare_scheme = int(request.POST["prepare_scheme"]) - prepare_times = Activity.EndBeforeHours.prepare_times - prepare_time = prepare_times[prepare_scheme] - signup_end = act_start - timedelta(hours=prepare_time) - assert now_time <= signup_end - context["endbefore"] = prepare_scheme - context["signup_end"] = signup_end - else: - # 修改但不调整报名截止时间,后面函数自己查 - context["adjust_apply"] = False - - # 人数限制 - capacity = request.POST.get("maxpeople") - no_limit = request.POST.get("unlimited_capacity") - if no_limit is not None: - capacity = 10000 - if capacity is not None and capacity != "": - capacity = int(capacity) - assert capacity >= 0 - context["capacity"] = capacity - - # 需要签到 - if request.POST.get("need_checkin"): - context["need_checkin"] = True - - # 内部活动 - if request.POST.get("inner"): - context["inner"] = True - else: - context["inner"] = False - - # 图片 优先使用上传的图片 - announcephoto = request.FILES.getlist("images") - if len(announcephoto) > 0: - pic = announcephoto[0] - assert if_image(pic) == 2 - else: - if request.POST.get("picture1"): - pic = request.POST.get("picture1") - elif request.POST.get("picture2"): - pic = request.POST.get("picture2") - elif request.POST.get("picture3"): - pic = request.POST.get("picture3") - elif request.POST.get("picture4"): - pic = request.POST.get("picture4") - else: - pic = request.POST.get("picture5") - - if pic is None: - template_id = request.POST.get("template_id") - if template_id: - context["template_id"] = int(template_id) - else: - assert edit - else: - context["pic"] = pic - - return context - - -def _set_change_status(activity: Activity, current, next, time, replace): - ScheduleAdder(changeActivityStatus, id=f'activity_{activity.id}_{next}', - run_time=time, replace=replace)(activity.id, current, next) - - -def _set_jobs_to_status(activity: Activity, replace: bool) -> Activity.Status: - now_time = datetime.now() - status = Activity.Status.END - if now_time < activity.end: - status, next = Activity.Status.PROGRESSING, Activity.Status.END - _set_change_status(activity, status, next, activity.end, replace) - if now_time < activity.start: - status, next = Activity.Status.WAITING, Activity.Status.PROGRESSING - _set_change_status(activity, status, next, activity.start, replace) - if now_time < activity.apply_end: - status, next = Activity.Status.APPLYING, Activity.Status.WAITING - _set_change_status(activity, status, next, activity.apply_end, replace) - if now_time < activity.start - timedelta(minutes=15): - reminder = ScheduleAdder(notifyActivity, id=f'activity_{activity.id}_remind', - run_time=activity.start - timedelta(minutes=15), replace=replace) - reminder(activity.id, 'remind') - return status - - -def create_activity(request): - ''' - 检查活动,合法时寻找该活动,不存在时创建 - 返回(activity.id, created) - - --- - 检查不合格时抛出AssertionError - - 不再假设ActivityException特定语义,暂不抛出该类异常 - ''' - - context = activity_base_check(request) - - # 查找是否有类似活动存在 - old_ones = Activity.objects.activated().filter( - title=context["title"], - start=context["start"], - introduction=context["introduction"], - location=context["location"] - ) - if len(old_ones) == 0: - old_ones = Activity.objects.filter( - title=context["title"], - start=context["start"], - introduction=context["introduction"], - location=context["location"], - status=Activity.Status.REVIEWING, - ) - if len(old_ones): - assert len(old_ones) == 1, "创建活动时,已存在的相似活动不唯一" - return old_ones[0].id, False - - # 审批老师存在 - examine_teacher = Person.objects.get_teacher(context["examine_teacher"]) - - # 检查完毕,创建活动 - org = get_person_or_org(request.user, UTYPE_ORG) - activity = Activity.objects.create( - title=context["title"], - organization_id=org, - examine_teacher=examine_teacher, - introduction=context["introduction"], - location=context["location"], - capacity=context["capacity"], - URL=context["url"], - start=context["start"], - end=context["end"], - bidding=context["bidding"], - apply_end=context["signup_end"], - inner=context["inner"], - ) - activity.endbefore = context["endbefore"] - if context.get("need_checkin"): - activity.need_checkin = True - if context["recorded"]: - # 预报备活动,先开放报名,再审批 - activity.recorded = True - activity.status = Activity.Status.APPLYING - notifyActivity(activity.id, "newActivity") - _set_jobs_to_status(activity, replace=False) - - activity.save() - - if context.get("template_id"): - template = Activity.objects.get(id=context["template_id"]) - photo = ActivityPhoto.objects.get( - type=ActivityPhoto.PhotoType.ANNOUNCE, activity=template) - photo.pk = None - photo.id = None - photo.activity = activity - photo.save() - else: - ActivityPhoto.objects.create( - image=context["pic"], type=ActivityPhoto.PhotoType.ANNOUNCE, activity=activity) - - notification_create( - receiver=examine_teacher.person_id, - sender=request.user, - typename=Notification.Type.NEEDDO, - title=Notification.Title.VERIFY_INFORM, - content="您有一个活动待审批", - URL=f"/examineActivity/{activity.id}", - relate_instance=activity, - to_wechat=dict(app=WechatApp.AUDIT), - ) - - return activity.id, True - - -def modify_activity(request, activity): - - if activity.status == Activity.Status.REVIEWING: - modify_reviewing_activity(request, activity) - elif activity.status == Activity.Status.APPLYING or activity.status == Activity.Status.WAITING: - modify_accepted_activity(request, activity) - else: - raise ValueError - - -""" -检查 修改审核中活动 的 request -审核中,只需要修改内容,不需要通知 -但如果修改了审核老师,需要再通知新的审核老师,并 close 原审核请求 -""" - - -def modify_reviewing_activity(request, activity): - - context = activity_base_check(request, edit=True) - - if context.get("adjust_apply") is not None: - # 注意这里是不调整 - assert context["adjust_apply"] == False - assert activity.apply_end <= context["start"] - timedelta(hours=1) - else: - activity.apply_end = context["signup_end"] - activity.endbefore = context["endbefore"] - - activity.title = context["title"] - activity.introduction = context["introduction"] - activity.location = context["location"] - activity.capacity = context["capacity"] - activity.URL = context["url"] - activity.start = context["start"] - activity.end = context["end"] - activity.bidding = context["bidding"] - if context.get("need_checkin"): - activity.need_checkin = True - else: - activity.need_checkin = False - if context.get("inner"): - activity.inner = True - else: - activity.inner = False - activity.save() - - # 图片 - if context.get("pic") is not None: - pic = activity.photos.get(type=ActivityPhoto.PhotoType.ANNOUNCE) - pic.image = context["pic"] - pic.save() - - -""" -对已经通过审核的活动进行修改 -不能修改预算,元气值支付模式,审批老师 -只能修改时间,地点,URL, 简介,向同学收取元气值时的元气值数量 - -# 这个实际上应该是 activated/valid activity -""" - - -def modify_accepted_activity(request, activity): - - # TODO 删除任务,注册新任务 - - to_participants = [f"您参与的活动{activity.title}发生变化"] - # to_subscribers = [f"您关注的活动{activity.title}发生变化"] - if activity.location != request.POST["location"]: - to_participants.append("活动地点修改为" + request.POST["location"]) - activity.location = request.POST["location"] - - # 时间改变 - act_start = datetime.strptime(request.POST["actstart"], "%Y-%m-%d %H:%M") - now_time = datetime.now() - assert now_time < act_start - - if request.POST.get("adjust_apply_ddl"): - prepare_scheme = int(request.POST["prepare_scheme"]) - prepare_times = Activity.EndBeforeHours.prepare_times - prepare_time = prepare_times[prepare_scheme] - signup_end = act_start - timedelta(hours=prepare_time) - assert now_time <= signup_end - activity.apply_end = signup_end - activity.endbefore = prepare_scheme - # to_subscribers.append(f"活动报名截止时间调整为{signup_end.strftime('%Y-%m-%d %H:%M')}") - to_participants.append( - f"活动报名截止时间调整为{signup_end.strftime('%Y-%m-%d %H:%M')}") - else: - signup_end = activity.apply_end - assert signup_end + timedelta(hours=1) <= act_start - - if activity.start != act_start: - # to_subscribers.append(f"活动开始时间调整为{act_start.strftime('%Y-%m-%d %H:%M')}") - to_participants.append( - f"活动开始时间调整为{act_start.strftime('%Y-%m-%d %H:%M')}") - activity.start = act_start - - if signup_end < now_time and activity.status == Activity.Status.WAITING: - activity.status = Activity.Status.APPLYING - - if request.POST.get("unlimited_capacity"): - capacity = 10000 - else: - capacity = int(request.POST["maxpeople"]) - assert capacity > 0 - if capacity < len(SQ.sfilter(Participation.activity, activity).filter( - status=Participation.AttendStatus.APPLYSUCCESS - )): - raise ActivityException(f"当前成功报名人数已超过{capacity}人!") - activity.capacity = capacity - - if request.POST.get("need_checkin"): - activity.need_checkin = True - else: - activity.need_checkin = False - - # 内部活动 - if request.POST.get("inner"): - activity.inner = True - else: - activity.inner = False - - activity.end = datetime.strptime(request.POST["actend"], "%Y-%m-%d %H:%M") - assert activity.start < activity.end - activity.URL = request.POST["URL"] - activity.introduction = request.POST["introduction"] - activity.save() - - _set_jobs_to_status(activity, replace=True) - - # if len(to_subscribers) > 1: - # notifyActivity(activity.id, "modification_sub_ex_par", "\n".join(to_subscribers)) - if len(to_participants) > 1: - notifyActivity(activity.id, "modification_par", - "\n".join(to_participants)) - - -def accept_activity(request, activity): - - # 审批通过 - activity.valid = True - - # 通知 - notification = Notification.objects.get( - relate_instance=activity, - status=Notification.Status.UNDONE, - title=Notification.Title.VERIFY_INFORM - ) - notification_status_change(notification, Notification.Status.DONE) - - notification_create( - receiver=activity.organization_id.get_user(), - sender=request.user, - typename=Notification.Type.NEEDREAD, - title=Notification.Title.ACTIVITY_INFORM, - content=f"您的活动{activity.title}已通过审批。", - URL=f"/viewActivity/{activity.id}", - relate_instance=activity, - to_wechat=dict(app=WechatApp.AUDIT), - ) - - if activity.status == Activity.Status.REVIEWING: - activity.status = _set_jobs_to_status(activity, replace=False) - - activity.save() - - -def _remove_jobs(activity: Activity, *jobs): - for job in jobs: - remove_job(f"activity_{activity.id}_{job}") - - -def _remove_activity_jobs(activity: Activity): - _remove_jobs(activity, 'remind', Activity.Status.WAITING, - Activity.Status.PROGRESSING, Activity.Status.END) - - -def reject_activity(request, activity): - # 审批过,这个叫 valid 不太合适...... - activity.valid = True - - # 通知 - notification = Notification.objects.get( - relate_instance=activity, - status=Notification.Status.UNDONE, - title=Notification.Title.VERIFY_INFORM - ) - notification_status_change(notification, Notification.Status.DONE) - - if activity.status == Activity.Status.REVIEWING: - activity.status = Activity.Status.REJECT - else: - Notification.objects.filter( - relate_instance=activity - ).update(status=Notification.Status.DELETE) - # 曾将所有报名的人的状态改为申请失败 - notifyActivity(activity.id, "modification_par", - f"您报名的活动{activity.title}已取消。") - activity.status = Activity.Status.CANCELED - _remove_activity_jobs(activity) - - notification = notification_create( - receiver=activity.organization_id.get_user(), - sender=request.user, - typename=Notification.Type.NEEDREAD, - title=Notification.Title.ACTIVITY_INFORM, - content=f"您的活动{activity.title}被拒绝。", - URL=f"/viewActivity/{activity.id}", - relate_instance=activity, - to_wechat=dict(app=WechatApp.AUDIT), - ) - - activity.save() - - -# 调用的时候用 try -def apply_activity(request, activity: Activity): - '''这个函数在正常情况下只应该抛出提示错误信息的ActivityException''' - context = dict() - context["success"] = False - - payer = Person.objects.get_by_user(request.user) - - if activity.inner: - position = Position.objects.activated().filter( - person=payer, org=activity.organization_id) - if len(position) == 0: - # 按理说这里也是走不到的,前端会限制 - raise ActivityException( - f"该活动是{activity.organization_id}内部活动,暂不开放对外报名。") - - try: - participant = Participation.objects.select_for_update().get( - SQ.sq(Participation.activity, activity), - SQ.sq(Participation.person, payer), - ) - participated = True - except: - participated = False - if participated: - if ( - participant.status == Participation.AttendStatus.APPLYSUCCESS or - participant.status == Participation.AttendStatus.APPLYING - ): - raise ActivityException("您已报名该活动。") - elif participant.status != Participation.AttendStatus.CANCELED: - raise ActivityException(f"您的报名状态异常,当前状态为:{participant.status}") - - if not activity.bidding: - if activity.current_participants < activity.capacity: - activity.current_participants += 1 - else: - raise ActivityException("活动已报满,请稍后再试。") - else: - activity.current_participants += 1 - - if not participated: - participant = Participation.objects.create(**dict([ - (SQ.f(Participation.activity), activity), - (SQ.f(Participation.person), payer), - ])) - if not activity.bidding: - participant.status = Participation.AttendStatus.APPLYSUCCESS - else: - participant.status = Participation.AttendStatus.APPLYING - - participant.save() - activity.save() - - -def cancel_activity(request, activity): - - if activity.status == Activity.Status.REVIEWING: - activity.status = Activity.Status.ABORT - activity.save() - # 修改老师的通知 - notification = Notification.objects.get( - relate_instance=activity, - status=Notification.Status.UNDONE - ) - notification_status_change(notification, Notification.Status.DELETE) - return - - if activity.status == Activity.Status.PROGRESSING: - if activity.start.day == datetime.now().day and datetime.now() < activity.start + timedelta(days=1): - pass - else: - raise ActivityException("活动已于一天前开始,不能取消。") - - if activity.status == Activity.Status.CANCELED: - raise ActivityException("活动已取消。") - - org = Organization.objects.select_for_update().get( - organization_id=request.user - ) - - activity.status = Activity.Status.CANCELED - notifyActivity(activity.id, "modification_par", - f"您报名的活动{activity.title}已取消。") - notification = Notification.objects.get( - relate_instance=activity, - typename=Notification.Type.NEEDDO - ) - notification_status_change(notification, Notification.Status.DELETE) - - # 活动取消后,曾将所有相关状态变为申请失败 - - _remove_activity_jobs(activity) - activity.save() - - -def withdraw_activity(request, activity: Activity): - - np = Person.objects.get_by_user(request.user) - participant = Participation.objects.select_for_update().get( - SQ.sq(Participation.activity, activity), - SQ.sq(Participation.person, np), - status__in=[ - Activity.Status.WAITING, - Participation.AttendStatus.APPLYING, - Participation.AttendStatus.APPLYSUCCESS, - Participation.AttendStatus.CANCELED, - ], - ) - if participant.status == Participation.AttendStatus.CANCELED: - raise ActivityException("已退出活动。") - participant.status = Participation.AttendStatus.CANCELED - activity.current_participants -= 1 - - participant.save() - activity.save() - - -@transaction.atomic -def create_participate_infos(activity: Activity, persons: Iterable[Person], **fields): - '''批量创建一系列参与信息,并返回创建的参与信息''' - participantion = [ - Participation(**dict([ - (SQ.f(Participation.activity), activity), - (SQ.f(Participation.person), person), - ]), **fields) for person in persons - ] - return Participation.objects.bulk_create(participantion) - - -@transaction.atomic -def _update_new_participants(activity: Activity, new_participant_uids: list[str]) -> int: - '''更新活动的参与者,返回新增的参与者数量,不修改活动''' - status = Participation.AttendStatus.ATTENDED - participants = SQ.qsvlist(SQ.sfilter(Participation.activity, activity).filter( - status=status).select_for_update(), Participation.person) - # 获取需要添加的参与者 - new_participant_nps = SQ.mfilter(Person.person_id, User.username, - IN=new_participant_uids) - new_participant_nps = new_participant_nps.exclude(id__in=participants) - # 此处必须执行查询,否则是lazy query,会导致后面的bulk_increase_YQPoint出错 - new_participant_ids = SQ.qsvlist(new_participant_nps, Person.person_id) - new_participants = User.objects.filter(id__in=new_participant_ids) - # 为添加的参与者增加元气值 - point = activity.eval_point() - User.objects.bulk_increase_YQPoint( - new_participants, point, '参加活动', YQPointRecord.SourceType.ACTIVITY) - create_participate_infos(activity, new_participant_nps, status=status) - return len(new_participant_nps) - - -@transaction.atomic -def _delete_outdate_participants(activity: Activity, new_participant_uids: list[str]): - '''删除过期的参与者,收回元气值,返回删除的参与者的数量,不修改活动''' - # 获取需要删除的参与者 - participation = SQ.sfilter(Participation.activity, activity).filter( - status=Participation.AttendStatus.ATTENDED).select_for_update() - removed_participation = participation.exclude( - SQ.mq(Participation.person, Person.person_id, User.username, - IN=new_participant_uids)) - removed_participant_ids = SQ.qsvlist(removed_participation, - Participation.person, Person.person_id) - removed_participants = User.objects.filter(id__in=removed_participant_ids) - # 为删除的参与者撤销元气值发放 - point = activity.eval_point() - User.objects.bulk_withdraw_YQPoint(removed_participants, point, - '撤销参加活动', YQPointRecord.SourceType.CONSUMPTION) - return removed_participation.delete()[0] - - -@transaction.atomic -def modify_participants(activity: Activity, new_participant_uids: list[str]): - '''将参与信息修改为指定的参与者,参与者已经检查合法性,暂不允许删除''' - new_uids = new_participant_uids - # activity.current_participants -= _delete_outdate_participants(activity, new_uids) - activity.current_participants += _update_new_participants(activity, new_uids) - activity.save() - - -def weekly_summary_orgs() -> QuerySet[Organization]: - '''允许进行每周总结的组织''' - VALID_ORG_TYPES = ['团委', '学学学委员会', '学学学学会', '学生会'] - return SQ.mfilter(Org.otype, OrgType.otype_name, IN=VALID_ORG_TYPES) - - -def available_participants() -> QuerySet[User]: - '''允许参与活动的人''' - return SQ.mfilter(User.id, IN=SQ.qsvlist(Person.objects.activated(), Person.person_id)) diff --git a/app/activity_views.py b/app/activity_views.py deleted file mode 100644 index ffe262020..000000000 --- a/app/activity_views.py +++ /dev/null @@ -1,1278 +0,0 @@ -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 diff --git a/app/admin.py b/app/admin.py index 1de136d0c..5421f4b2e 100644 --- a/app/admin.py +++ b/app/admin.py @@ -9,7 +9,6 @@ from utils.admin_utils import * from app.models import * from scheduler.cancel import remove_job -from app.YQPoint_utils import run_lottery from app.org_utils import accept_modifyorg_submit # 通用内联模型 @@ -25,22 +24,6 @@ class PositionInline(admin.TabularInline): ] show_change_link = True -@readonly_inline -class ParticipationInline(admin.TabularInline): - model = Participation - classes = ['collapse'] - ordering = ['-' + f(model.activity)] - fields = [f(model.activity), f(model.person), f(model.status)] - show_change_link = True - -@readonly_inline -class CourseParticipantInline(admin.TabularInline): - model = CourseParticipant - classes = ['collapse'] - ordering = ['-id'] - fields = ['course', 'person', 'status'] - show_change_link = True - # 后台模型 @admin.register(NaturalPerson) @@ -59,7 +42,7 @@ class NaturalPersonAdmin(admin.ModelAdmin): f(_m.stu_grade), f(_m.stu_class), ] - inlines = [PositionInline, ParticipationInline, CourseParticipantInline] + inlines = [PositionInline] def _show_by_option(self, obj: NaturalPerson | None, option: str, detail: str): if obj is None or getattr(obj, option): @@ -309,267 +292,267 @@ def refresh(self, request, queryset): return self.message_user(request, f'修改成功!新增职务:{new}') -@admin.register(Activity) -class ActivityAdmin(admin.ModelAdmin): - list_display = ["title", 'id', "organization_id", - "status", "participant_diaplay", - "publish_time", "start", "end",] - search_fields = ('id', "title", "organization_id__oname", - "current_participants",) +# @admin.register(Activity) +# class ActivityAdmin(admin.ModelAdmin): +# list_display = ["title", 'id', "organization_id", +# "status", "participant_diaplay", +# "publish_time", "start", "end",] +# search_fields = ('id', "title", "organization_id__oname", +# "current_participants",) - class ErrorFilter(admin.SimpleListFilter): - title = '错误状态' # 过滤标题显示为"以 错误状态" - parameter_name = 'wrong_status' # 过滤器使用的过滤字段 +# class ErrorFilter(admin.SimpleListFilter): +# title = '错误状态' # 过滤标题显示为"以 错误状态" +# parameter_name = 'wrong_status' # 过滤器使用的过滤字段 - def lookups(self, request, model_admin): - '''针对字段值设置过滤器的显示效果''' - return ( - ('all', '全部错误状态'), - ('not_waiting', '未进入 等待中 状态'), - ('not_processing', '未进入 进行中 状态'), - ('not_end', '未进入 已结束 状态'), - ('review_end', '已结束的未审核'), - ('normal', '正常'), - ) +# def lookups(self, request, model_admin): +# '''针对字段值设置过滤器的显示效果''' +# return ( +# ('all', '全部错误状态'), +# ('not_waiting', '未进入 等待中 状态'), +# ('not_processing', '未进入 进行中 状态'), +# ('not_end', '未进入 已结束 状态'), +# ('review_end', '已结束的未审核'), +# ('normal', '正常'), +# ) - def queryset(self, request, queryset): - '''定义过滤器的过滤动作''' - now = datetime.now() - error_id_set = set() - activate_queryset = queryset.exclude( - status__in=[ - Activity.Status.REVIEWING, - Activity.Status.CANCELED, - Activity.Status.REJECT, - Activity.Status.ABORT, - ]) - if self.value() in ['not_waiting', 'all', 'normal']: - error_id_set.update(activate_queryset.exclude( - status=Activity.Status.WAITING).filter( - apply_end__lte=now, - start__gt=now, - ).values_list('id', flat=True)) - if self.value() in ['not_processing', 'all', 'normal']: - error_id_set.update(activate_queryset.exclude( - status=Activity.Status.PROGRESSING).filter( - start__lte=now, - end__gt=now, - ).values_list('id', flat=True)) - if self.value() in ['not_end', 'all', 'normal']: - error_id_set.update(activate_queryset.exclude( - status=Activity.Status.END).filter( - end__lte=now, - ).values_list('id', flat=True)) - if self.value() in ['review_end', 'all', 'normal']: - error_id_set.update(queryset.filter( - status=Activity.Status.REVIEWING, - end__lte=now, - ).values_list('id', flat=True)) - - if self.value() == 'normal': - return queryset.exclude(id__in=error_id_set) - elif self.value() is not None: - return queryset.filter(id__in=error_id_set) - return queryset +# def queryset(self, request, queryset): +# '''定义过滤器的过滤动作''' +# now = datetime.now() +# error_id_set = set() +# activate_queryset = queryset.exclude( +# status__in=[ +# Activity.Status.REVIEWING, +# Activity.Status.CANCELED, +# Activity.Status.REJECT, +# Activity.Status.ABORT, +# ]) +# if self.value() in ['not_waiting', 'all', 'normal']: +# error_id_set.update(activate_queryset.exclude( +# status=Activity.Status.WAITING).filter( +# apply_end__lte=now, +# start__gt=now, +# ).values_list('id', flat=True)) +# if self.value() in ['not_processing', 'all', 'normal']: +# error_id_set.update(activate_queryset.exclude( +# status=Activity.Status.PROGRESSING).filter( +# start__lte=now, +# end__gt=now, +# ).values_list('id', flat=True)) +# if self.value() in ['not_end', 'all', 'normal']: +# error_id_set.update(activate_queryset.exclude( +# status=Activity.Status.END).filter( +# end__lte=now, +# ).values_list('id', flat=True)) +# if self.value() in ['review_end', 'all', 'normal']: +# error_id_set.update(queryset.filter( +# status=Activity.Status.REVIEWING, +# end__lte=now, +# ).values_list('id', flat=True)) + +# if self.value() == 'normal': +# return queryset.exclude(id__in=error_id_set) +# elif self.value() is not None: +# return queryset.filter(id__in=error_id_set) +# return queryset - list_filter = ( - "status", - 'year', 'semester', 'category', - "organization_id__otype", - "inner", "need_checkin", "valid", - ErrorFilter, - 'endbefore', - "publish_time", 'start', 'end', - ) - date_hierarchy = 'start' - - def participant_diaplay(self, obj): - return f'{obj.current_participants}/{"无限" if obj.capacity == 10000 else obj.capacity}' - participant_diaplay.short_description = "报名情况" - - inlines = [ParticipationInline] - - actions = [] - - @as_action("更新 报名人数", actions, update=True) - def refresh_count(self, request, queryset: QuerySet[Activity]): - for activity in queryset: - activity.current_participants = sfilter( - Participation.activity, activity).filter( - status__in=[ - Participation.AttendStatus.ATTENDED, - Participation.AttendStatus.UNATTENDED, - Participation.AttendStatus.APPLYSUCCESS, - ]).count() - activity.save() - return self.message_user(request=request, message='修改成功!') +# list_filter = ( +# "status", +# 'year', 'semester', 'category', +# "organization_id__otype", +# "inner", "need_checkin", "valid", +# ErrorFilter, +# 'endbefore', +# "publish_time", 'start', 'end', +# ) +# date_hierarchy = 'start' + +# def participant_diaplay(self, obj): +# return f'{obj.current_participants}/{"无限" if obj.capacity == 10000 else obj.capacity}' +# participant_diaplay.short_description = "报名情况" + +# inlines = [ParticipationInline] + +# actions = [] + +# @as_action("更新 报名人数", actions, update=True) +# def refresh_count(self, request, queryset: QuerySet[Activity]): +# for activity in queryset: +# activity.current_participants = sfilter( +# Participation.activity, activity).filter( +# status__in=[ +# Participation.AttendStatus.ATTENDED, +# Participation.AttendStatus.UNATTENDED, +# Participation.AttendStatus.APPLYSUCCESS, +# ]).count() +# activity.save() +# return self.message_user(request=request, message='修改成功!') - @as_action('设为 普通活动', actions, update=True) - def set_normal_category(self, request, queryset): - queryset.update(category=Activity.ActivityCategory.NORMAL) - return self.message_user(request=request, message='修改成功!') - - @as_action('设为 课程活动', actions, update=True) - def set_course_category(self, request, queryset): - queryset.update(category=Activity.ActivityCategory.COURSE) - return self.message_user(request=request, message='修改成功!') - - def _change_status(self, activity, from_status, to_status): - from app.activity_utils import changeActivityStatus - changeActivityStatus(activity.id, from_status, to_status) - if remove_job(f'activity_{activity.id}_{to_status}'): - return '修改成功, 并移除了定时任务!' - else: - return '修改成功!' - - @as_action("进入 等待中 状态", actions, single=True) - def to_waiting(self, request, queryset): - _from, _to = Activity.Status.APPLYING, Activity.Status.WAITING - msg = self._change_status(queryset[0], _from, _to) - return self.message_user(request, msg) +# @as_action('设为 普通活动', actions, update=True) +# def set_normal_category(self, request, queryset): +# queryset.update(category=Activity.ActivityCategory.NORMAL) +# return self.message_user(request=request, message='修改成功!') + +# @as_action('设为 课程活动', actions, update=True) +# def set_course_category(self, request, queryset): +# queryset.update(category=Activity.ActivityCategory.COURSE) +# return self.message_user(request=request, message='修改成功!') + +# def _change_status(self, activity, from_status, to_status): +# from app.activity_utils import changeActivityStatus +# changeActivityStatus(activity.id, from_status, to_status) +# if remove_job(f'activity_{activity.id}_{to_status}'): +# return '修改成功, 并移除了定时任务!' +# else: +# return '修改成功!' + +# @as_action("进入 等待中 状态", actions, single=True) +# def to_waiting(self, request, queryset): +# _from, _to = Activity.Status.APPLYING, Activity.Status.WAITING +# msg = self._change_status(queryset[0], _from, _to) +# return self.message_user(request, msg) - @as_action("进入 进行中 状态", actions, single=True) - def to_processing(self, request, queryset): - _from, _to = Activity.Status.WAITING, Activity.Status.PROGRESSING - msg = self._change_status(queryset[0], _from, _to) - return self.message_user(request, msg) +# @as_action("进入 进行中 状态", actions, single=True) +# def to_processing(self, request, queryset): +# _from, _to = Activity.Status.WAITING, Activity.Status.PROGRESSING +# msg = self._change_status(queryset[0], _from, _to) +# return self.message_user(request, msg) - @as_action("进入 已结束 状态", actions, single=True) - def to_end(self, request, queryset): - _from, _to = Activity.Status.PROGRESSING, Activity.Status.END - msg = self._change_status(queryset[0], _from, _to) - return self.message_user(request, msg) - - @as_action("取消 定时任务", actions) - def cancel_scheduler(self, request, queryset): - success_list = [] - failed_list = [] - CANCEL_STATUSES = [ - 'remind', - Activity.Status.END, - Activity.Status.PROGRESSING, - Activity.Status.WAITING, - ] - for activity in queryset: - failed_statuses = [] - for status in CANCEL_STATUSES: - if not remove_job(f'activity_{activity.id}_{status}'): - failed_statuses.append(status) - if failed_statuses: - if len(failed_statuses) != len(CANCEL_STATUSES): - failed_list.append(f'{activity.id}: {",".join(failed_statuses)}') - else: - failed_list.append(f'{activity.id}') - else: - success_list.append(f'{activity.id}') +# @as_action("进入 已结束 状态", actions, single=True) +# def to_end(self, request, queryset): +# _from, _to = Activity.Status.PROGRESSING, Activity.Status.END +# msg = self._change_status(queryset[0], _from, _to) +# return self.message_user(request, msg) + +# @as_action("取消 定时任务", actions) +# def cancel_scheduler(self, request, queryset): +# success_list = [] +# failed_list = [] +# CANCEL_STATUSES = [ +# 'remind', +# Activity.Status.END, +# Activity.Status.PROGRESSING, +# Activity.Status.WAITING, +# ] +# for activity in queryset: +# failed_statuses = [] +# for status in CANCEL_STATUSES: +# if not remove_job(f'activity_{activity.id}_{status}'): +# failed_statuses.append(status) +# if failed_statuses: +# if len(failed_statuses) != len(CANCEL_STATUSES): +# failed_list.append(f'{activity.id}: {",".join(failed_statuses)}') +# else: +# failed_list.append(f'{activity.id}') +# else: +# success_list.append(f'{activity.id}') - msg = f'成功取消{len(success_list)}项活动的定时任务!' if success_list else '未能完全取消任何任务' - if failed_list: - msg += f'\n{len(failed_list)}项活动取消失败:\n{";".join(failed_list)}' - return self.message_user(request=request, message=msg) - - -@admin.register(Participation) -class ParticipationAdmin(admin.ModelAdmin): - _m = Participation - _act = _m.activity - list_display = ['id', f(_act), f(_m.person), f(_m.status)] - search_fields = ['id', f(_act, 'id'), f(_act, Activity.title), - f(_m.person, NaturalPerson.name)] - list_filter = [ - f(_m.status), f(_act, Activity.category), - f(_act, Activity.year), f(_act, Activity.semester), - ] - - -@admin.register(Notification) -class NotificationAdmin(admin.ModelAdmin): - list_display = ["id", "receiver", "sender", "title", "start_time"] - search_fields = ('id', "receiver__username", "sender__username", 'title') - list_filter = ('start_time', 'status', 'typename', "finish_time") - - actions = [ - 'set_delete', - 'republish', - 'republish_bulk_at_promote', 'republish_bulk_at_message', - ] - - @as_action("设置状态为 删除", update=True) - def set_delete(self, request, queryset): - queryset.update(status=Notification.Status.DELETE) - return self.message_user(request=request, - message='修改成功!') - - @as_action("重发 单个通知") - def republish(self, request, queryset): - if len(queryset) != 1: - return self.message_user(request=request, - message='一次只能重发一个通知!', - level='error') - notification = queryset[0] - from app.extern.wechat import publish_notification, WechatApp - if not publish_notification( - notification, - app=WechatApp.NORMAL, - ): - return self.message_user(request=request, - message='发送失败!请检查通知内容!', - level='error') - return self.message_user(request=request, - message='已成功定时,将发送至默认窗口!') +# msg = f'成功取消{len(success_list)}项活动的定时任务!' if success_list else '未能完全取消任何任务' +# if failed_list: +# msg += f'\n{len(failed_list)}项活动取消失败:\n{";".join(failed_list)}' +# return self.message_user(request=request, message=msg) + + +# @admin.register(Participation) +# class ParticipationAdmin(admin.ModelAdmin): +# _m = Participation +# _act = _m.activity +# list_display = ['id', f(_act), f(_m.person), f(_m.status)] +# search_fields = ['id', f(_act, 'id'), f(_act, Activity.title), +# f(_m.person, NaturalPerson.name)] +# list_filter = [ +# f(_m.status), f(_act, Activity.category), +# f(_act, Activity.year), f(_act, Activity.semester), +# ] + + +# @admin.register(Notification) +# class NotificationAdmin(admin.ModelAdmin): +# list_display = ["id", "receiver", "sender", "title", "start_time"] +# search_fields = ('id', "receiver__username", "sender__username", 'title') +# list_filter = ('start_time', 'status', 'typename', "finish_time") + +# actions = [ +# 'set_delete', +# 'republish', +# 'republish_bulk_at_promote', 'republish_bulk_at_message', +# ] + +# @as_action("设置状态为 删除", update=True) +# def set_delete(self, request, queryset): +# queryset.update(status=Notification.Status.DELETE) +# return self.message_user(request=request, +# message='修改成功!') + +# @as_action("重发 单个通知") +# def republish(self, request, queryset): +# if len(queryset) != 1: +# return self.message_user(request=request, +# message='一次只能重发一个通知!', +# level='error') +# notification = queryset[0] +# from app.extern.wechat import publish_notification, WechatApp +# if not publish_notification( +# notification, +# app=WechatApp.NORMAL, +# ): +# return self.message_user(request=request, +# message='发送失败!请检查通知内容!', +# level='error') +# return self.message_user(request=request, +# message='已成功定时,将发送至默认窗口!') - def republish_bulk(self, request, queryset, app): - if not request.user.is_superuser: - return self.message_user(request=request, - message='操作失败,没有权限,请联系老师!', - level='warning') - if len(queryset) != 1: - return self.message_user(request=request, - message='一次只能选择一个通知!', - level='error') - bulk_identifier = queryset[0].bulk_identifier - if not bulk_identifier: - return self.message_user(request=request, - message='该通知不存在批次标识!', - level='error') - try: - from app.extern.wechat import publish_notifications - except Exception as e: - return self.message_user(request=request, - message=f'导入失败, 原因: {e}', - level='error') - if not publish_notifications( - filter_kws={'bulk_identifier': bulk_identifier}, - app=app, - ): - return self.message_user(request=request, - message='发送失败!请检查通知内容!', - level='error') - return self.message_user(request=request, - message=f'已成功定时!标识为{bulk_identifier}') - republish_bulk.short_description = "错误的重发操作" - - @as_action("重发 所在批次 于 订阅窗口") - def republish_bulk_at_promote(self, request, queryset): - try: - from app.extern.wechat import WechatApp - app = WechatApp._PROMOTE - except Exception as e: - return self.message_user(request=request, - message=f'导入失败, 原因: {e}', - level='error') - return self.republish_bulk(request, queryset, app) - - @as_action("重发 所在批次 于 消息窗口") - def republish_bulk_at_message(self, request, queryset): - try: - from app.extern.wechat import WechatApp - app = WechatApp._MESSAGE - except Exception as e: - return self.message_user(request=request, - message=f'导入失败, 原因: {e}', - level='error') - return self.republish_bulk(request, queryset, app) +# def republish_bulk(self, request, queryset, app): +# if not request.user.is_superuser: +# return self.message_user(request=request, +# message='操作失败,没有权限,请联系老师!', +# level='warning') +# if len(queryset) != 1: +# return self.message_user(request=request, +# message='一次只能选择一个通知!', +# level='error') +# bulk_identifier = queryset[0].bulk_identifier +# if not bulk_identifier: +# return self.message_user(request=request, +# message='该通知不存在批次标识!', +# level='error') +# try: +# from app.extern.wechat import publish_notifications +# except Exception as e: +# return self.message_user(request=request, +# message=f'导入失败, 原因: {e}', +# level='error') +# if not publish_notifications( +# filter_kws={'bulk_identifier': bulk_identifier}, +# app=app, +# ): +# return self.message_user(request=request, +# message='发送失败!请检查通知内容!', +# level='error') +# return self.message_user(request=request, +# message=f'已成功定时!标识为{bulk_identifier}') +# republish_bulk.short_description = "错误的重发操作" + +# @as_action("重发 所在批次 于 订阅窗口") +# def republish_bulk_at_promote(self, request, queryset): +# try: +# from app.extern.wechat import WechatApp +# app = WechatApp._PROMOTE +# except Exception as e: +# return self.message_user(request=request, +# message=f'导入失败, 原因: {e}', +# level='error') +# return self.republish_bulk(request, queryset, app) + +# @as_action("重发 所在批次 于 消息窗口") +# def republish_bulk_at_message(self, request, queryset): +# try: +# from app.extern.wechat import WechatApp +# app = WechatApp._MESSAGE +# except Exception as e: +# return self.message_user(request=request, +# message=f'导入失败, 原因: {e}', +# level='error') +# return self.republish_bulk(request, queryset, app) @admin.register(Help) @@ -648,268 +631,8 @@ def approve_requests(self, request, queryset: QuerySet['ModifyOrganization']): accept_modifyorg_submit(application) self.message_user(request, '操作成功完成!') -@admin.register(Course) -class CourseAdmin(admin.ModelAdmin): - list_display = [ - "name", - "organization", - "type", - "participant_diaplay", - "status", - ] - search_fields = ( - "name", "organization__oname", - 'classroom', 'teacher', - ) - list_filter = ("year", "semester", "type", "status",) - autocomplete_fields = ['organization'] - - class CourseTimeInline(admin.TabularInline): - model = CourseTime - classes = ['collapse'] - extra = 1 - - inlines = [CourseTimeInline, CourseParticipantInline] - - def participant_diaplay(self, obj): - return f'{obj.current_participants}/{"无限" if obj.capacity == 10000 else obj.capacity}' - participant_diaplay.short_description = "报名情况" - - actions = [] - - @as_action("更新课程状态", actions) - def refresh_status(self, request, queryset): - from app.course_utils import register_selection - register_selection() - return self.message_user(request=request, - message='已设置定时任务!') - - @as_action("更新课程状态 延迟2分钟", actions) - def refresh_status_delay2(self, request, queryset): - from app.course_utils import register_selection - from datetime import timedelta - register_selection(wait_for=timedelta(minutes=2)) - return self.message_user(request=request, - message='已设置定时任务!') - - -@admin.register(CourseParticipant) -class CourseParticipantAdmin(admin.ModelAdmin): - list_display = ["course", "person", "status",] - search_fields = ("course__name", "person__name",) - autocomplete_fields = ['course', 'person'] - - -@admin.register(CourseRecord) -class CourseRecordAdmin(admin.ModelAdmin): - _m = CourseRecord - list_display = [ - _m.get_course_name, f(_m.person), - f(_m.year), f(_m.semester), - f(_m.attend_times), f(_m.total_hours), - f(_m.invalid), - ] - search_fields = [ - f(_m.course, Course.name), f(_m.extra_name), - f(_m.person, NaturalPerson.name), - f(_m.person, NaturalPerson.person_id, User.username), - ] - class TypeFilter(admin.SimpleListFilter): - title = '学时类别' - parameter_name = 'type' # 过滤器使用的过滤字段 - - def lookups(self, request, model_admin): - '''针对字段值设置过滤器的显示效果''' - # 自带一个None, '全部' - return ( - ('null', '无'), - ('any', '任意'), - ) + tuple(Course.CourseType.choices) - - def queryset(self, request, queryset): - '''定义过滤器的过滤动作''' - if self.value() == 'null': - return queryset.filter(course__isnull=True) - elif self.value() == 'any': - return queryset.exclude(course__isnull=True) - elif self.value() in map(str, Course.CourseType.values): - return queryset.filter(course__type=self.value()) - return queryset - list_filter = [TypeFilter, f(_m.year), f(_m.semester), f(_m.invalid)] - - autocomplete_fields = [f(_m.person), f(_m.course)] - - actions = [] - - @as_action('更新来源名称', actions, update=True) - def update_extra_name(self, request, queryset: QuerySet[CourseRecord]): - records = queryset.filter(course__isnull=False) - for record in records.select_related(f(CourseRecord.course)): - record.extra_name = record.course.name - record.save() - return self.message_user(request=request, message='已更新关联学时名称!') - - @as_action("设置为 无效学时", actions, update=True) - def set_invalid(self, request, queryset): - queryset.update(invalid=True) - return self.message_user(request=request, message='修改成功!') - - @as_action("设置为 有效学时", actions, update=True) - def set_valid(self, request, queryset): - queryset.update(invalid=False) - return self.message_user(request=request, message='修改成功!') - - -@admin.register(AcademicTag) -class AcademicTagAdmin(admin.ModelAdmin): - list_display = ["atype", "tag_content"] - search_fields = ("atype", "tag_content") - list_filter = ["atype"] - - -class AcademicEntryAdmin(admin.ModelAdmin): - actions = [] - - @as_action("通过审核", actions, 'change', update=True) - def accept(self, request, queryset): - queryset.filter(status=AcademicEntry.EntryStatus.WAIT_AUDIT - ).update(status=AcademicEntry.EntryStatus.PUBLIC) - return self.message_user(request, '修改成功!') - - @as_action("取消公开", actions, 'change', update=True) - def reject(self, request, queryset): - queryset.filter(status=AcademicEntry.EntryStatus.PUBLIC - ).update(status=AcademicEntry.EntryStatus.WAIT_AUDIT) - return self.message_user(request, '修改成功!') - - -@admin.register(AcademicTagEntry) -class AcademicTagEntryAdmin(AcademicEntryAdmin): - list_display = ["person", "status", "tag"] - search_fields = ("person__name", "tag__tag_content") - list_filter = ["tag__atype", "status"] - - -@admin.register(AcademicTextEntry) -class AcademicTextEntryAdmin(AcademicEntryAdmin): - list_display = ["person", "status", "atype", "content"] - search_fields = ("person__name", "content") - list_filter = ["atype", "status"] - - -class PoolItemInline(admin.TabularInline): - model = PoolItem - classes = ['collapse'] - ordering = ['-id'] - fields = ['pool', 'prize', 'origin_num', 'consumed_num', 'exchange_limit', 'exchange_price'] - show_change_link = True -PoolItemInline = readonly_inline(PoolItemInline, can_add=True) - - -@admin.register(Prize) -class PrizeAdmin(admin.ModelAdmin): - autocomplete_fields = ['provider'] - inlines = [PoolItemInline] - - -@admin.register(Pool) -class PoolAdmin(admin.ModelAdmin): - inlines = [PoolItemInline] - actions = [] - - def _do_draw_lots(self, request, queryset: QuerySet['Pool']): - '''对queryset中所有未完成抽奖的抽奖奖池进行奖品分配。 - - 这个函数假定queryset已经被select_for_update锁定,所以可以安全地查找“奖池记录”中与该奖池有关的行。 - ''' - lottery_pool_ids = list(queryset.filter(type = Pool.Type.LOTTERY).values_list('id', flat = True)) - for pool_id in lottery_pool_ids: - pool_title = Pool.objects.get(id = pool_id).title - if PoolRecord.objects.filter( - pool__id = pool_id - ).exclude( - status = PoolRecord.Status.LOTTERING - ).exists(): - self.message_user(request, "奖池【" + pool_title + "】在调用前已完成抽奖", 'warning') - continue - run_lottery(pool_id) - self.message_user(request, "奖池【" + pool_title + "】抽奖已完成") - - @as_action('立即抽奖', actions, 'change', update = True) - def draw_lots(self, request, queryset: QuerySet['Pool']): - self._do_draw_lots(request, queryset) - - @as_action('立即停止并抽奖', actions, 'change', update = True) - def stop_and_draw(self, request, queryset: QuerySet['Pool']): - queryset.update(end = datetime.now()) - self.message_user(request, "已将选中奖池全部停止") - self._do_draw_lots(request, queryset) - - -@admin.register(PoolRecord) -class PoolRecordAdmin(admin.ModelAdmin): - list_display = ['user_display', 'pool', 'status', 'prize', 'time'] - search_fields = ['user__name'] - list_filter = [ - 'status', 'prize', 'time', - ('prize__provider', admin.RelatedOnlyFieldListFilter), - ] - readonly_fields = ['time'] - autocomplete_fields = ['user'] - actions = [] - - @as_display('用户') - def user_display(self, obj: PoolRecord): - return obj.user.name - - def has_manage_permission(self, request: HttpRequest, record: PoolRecord = None) -> bool: - if not request.user.is_authenticated: - return False - if record is not None: - return record.prize.provider == request.user - return Prize.objects.filter(provider=request.user).exists() - # return super().get_queryset(request).filter(prize__provider=request.user).exists() - - def has_module_permission(self, request: HttpRequest) -> bool: - return super().has_module_permission(request) or self.has_manage_permission(request) - - def has_view_permission(self, request: HttpRequest, obj: PoolRecord = None) -> bool: - return super().has_view_permission(request, obj) or self.has_manage_permission(request, obj) - - def get_queryset(self, request: HttpRequest): - qs = super().get_queryset(request) - if not self.has_change_permission(request) and self.has_manage_permission(request): - qs = qs.filter(prize__provider=request.user) - return qs - - @as_action('兑换', actions, ['change', 'manage'], update=True, single=False) # 修改single为False以允许选择多个记录 - def redeem_prize(self, request, queryset): - # 检查权限和记录状态,统计可以兑换的记录数 - redeemable_records = [] - for record in queryset: - if not (self.has_change_permission(request, record) or self.has_manage_permission(request, record)): - return self.message_user(request, '无权负责一部分选定的礼品兑换!', 'warning') - if record.status != PoolRecord.Status.UN_REDEEM: - return self.message_user(request, f'奖品 {record.prize.name} 已被兑换或不可兑换!', 'warning') - redeemable_records.append(record) - - # 如果没有可兑换的记录,提前返回 - if not redeemable_records: - return self.message_usesr(request, '没有可兑换的奖品!', 'error') - - # 对可以兑换的记录进行兑换处理 - for record in redeemable_records: - if record.prize.name.startswith('信用分'): - User.objects.modify_credit(record.user, 1, '元气值:兑换') - record.status = PoolRecord.Status.REDEEMED - record.redeem_time = datetime.now() - record.save() - - # 返回成功信息 - self.message_user(request, f'成功兑换 {len(redeemable_records)} 个奖品!') admin.site.register(OrganizationTag) admin.site.register(Comment) admin.site.register(CommentPhoto) -admin.site.register(PoolItem) -admin.site.register(ActivitySummary) +# admin.site.register(ActivitySummary) diff --git a/app/chat_api.py b/app/chat_api.py deleted file mode 100644 index bfb4df899..000000000 --- a/app/chat_api.py +++ /dev/null @@ -1,78 +0,0 @@ -from app.views_dependency import * -from app.models import Chat -from app.chat_utils import ( - change_chat_status, - select_by_keywords, - modify_rating, - create_QA, - add_comment_to_QA, -) - -__all__ = [ - 'StartChat', 'AddComment', 'CloseChat', 'StartUndirectedChat', 'RateAnswer' -] - - -class StartChat(ProfileJsonView): - def prepare_post(self): - self.receiver_id = self.request.POST['receiver_id'] - self.questioner_anonymous = self.request.POST['comment_anonymous'] == 'true' - return self.post - - def post(self): - '''创建一条新的chat''' - respondent = User.objects.get(username=self.receiver_id) - - context = create_QA(self.request, respondent, directed=True, - questioner_anonymous=self.questioner_anonymous) - return self.message_response(context) - - -class AddComment(ProfileJsonView): - need_prepare = False - def post(self): - '''向聊天中添加对话''' - return self.message_response(add_comment_to_QA(self.request)) - - -class CloseChat(ProfileJsonView): - def prepare_post(self): - self.chat_id = int(self.request.POST['chat_id']) - return self.post - - def post(self): - '''终止聊天''' - message_context = change_chat_status(self.chat_id, Chat.Status.CLOSED) - return self.message_response(message_context) - - -class StartUndirectedChat(ProfileJsonView): - def prepare_post(self): - self.questioner_anonymous = self.request.POST['comment_anonymous'] == 'true' - self.keywords = self.request.POST['keywords'].split(sep=',') - return self.post - - def post(self): - """ - 开始非定向问答 - """ - respondent, message_context = select_by_keywords( - self.request.user, self.questioner_anonymous, self.keywords) - if respondent is None: - return self.message_response(message_context) - - context = create_QA(self.request, respondent, directed=False, - questioner_anonymous=self.questioner_anonymous, - keywords=self.keywords) - return self.message_response(context) - - -class RateAnswer(ProfileJsonView): - def prepare_post(self): - self.chat_id = int(self.request.POST['chat_id']) - self.rating = int(self.request.POST['rating']) - return self.post - - def post(self): - '''提问方对回答质量给出评价''' - return self.message_response(modify_rating(self.chat_id, self.rating)) diff --git a/app/chat_utils.py b/app/chat_utils.py deleted file mode 100644 index 5396cf95c..000000000 --- a/app/chat_utils.py +++ /dev/null @@ -1,293 +0,0 @@ -from collections import Counter -from random import sample -from typing import Tuple - -from django.http import HttpRequest - -from generic.models import YQPointRecord -from app.utils_dependency import * -from app.models import ( - AcademicEntry, - AcademicTagEntry, - AcademicTextEntry, - User, - Chat, - AcademicQA, - AcademicQAAwards, -) -from app.comment_utils import addComment -from achievement.api import unlock_achievement - -__all__ = [ - 'change_chat_status', - 'select_by_keywords', - 'create_QA', - 'add_comment_to_QA', - 'modify_rating', -] - - -def change_chat_status(chat_id: int, to_status: Chat.Status) -> MESSAGECONTEXT: - """ - 修改chat的状态 - - :param chat_id - :type chat_id: int - :param to_status: 目标状态 - :type to_status: Chat.Status - :return: 表明成功与否的MESSAGECONTEXT - :rtype: MESSAGECONTEXT - """ - # 参考了notification_utils.py的notification_status_change - context = wrong("在修改问答状态的过程中发生错误,请联系管理员!") - with transaction.atomic(): - try: - chat: Chat = Chat.objects.select_for_update().get(id=chat_id) - except: - return wrong("该问答不存在!", context) - - if chat.status == to_status: - return succeed("问答状态无需改变!", context) - - if to_status == Chat.Status.CLOSED: - chat.status = Chat.Status.CLOSED - chat.save() - succeed("您已成功关闭当前提问!", context) - elif to_status == Chat.Status.PROGRESSING: # 这个目前没有用到 - raise NotImplementedError - chat.status = Chat.Status.PROGRESSING - chat.save() - succeed("您已成功开放一个问答!", context) - return context - - -def add_chat_message(request: HttpRequest, chat: Chat) -> MESSAGECONTEXT: - """ - 给一个chat发送新comment,并给接收者发通知(复用了comment_utils.py/addComment) - - :param request: addComment函数会用到,其user即为发送comment的用户,其POST参数至少应当包括:comment_submit(点击“回复”按钮),comment(回复内容) - :type request: HttpRequest - :param chat - :type chat: Chat - :return: 表明发送结果的MESSAGECONTEXT - :rtype: MESSAGECONTEXT - """ - # 只能发给PROGRESSING的chat - if chat.status == Chat.Status.CLOSED: - return wrong("当前问答已关闭,无法发送新信息!") - if (not chat.respondent.accept_anonymous_chat - ) and chat.questioner_anonymous: - if request.user == chat.respondent: - return wrong("您目前处于禁用匿名提问状态!") - else: - return wrong("对方目前不允许匿名提问!") - - if request.user == chat.questioner: - receiver = chat.respondent # 我是这个chat的提问方,则我发送新comment时chat的接收方会收到通知 - anonymous = chat.questioner_anonymous # 如果chat是匿名提问的,则我作为提问方发送新comment时需要匿名 - else: - receiver = chat.questioner # 我是这个chat的接收方,则我发送新comment时chat的提问方会收到通知 - anonymous = False # 接收方发送的comment一定是实名的 - - comment_context = addComment( # 复用comment_utils.py,这个函数包含了通知发送功能 - request, - chat, - receiver, - anonymous=anonymous, - notification_title='学术地图问答信息') - return comment_context - - -def create_chat( - request: HttpRequest, - respondent: User, - title: str, - questioner_anonymous: bool = False, - respondent_anonymous: bool = False -) -> Tuple[int | None, MESSAGECONTEXT]: - """ - 创建新chat并调用add_chat_message发送首条提问 - - :param request: add_chat_message会用到 - :type request: HttpRequest - :param respondent: 被提问的人 - :type respondent: User - :param title: chat主题,不超过50字 - :type title: str - :param anonymous: chat是否匿名, defaults to False - :type anonymous: bool, optional - :return: 新chat的id(创建失败为None)和表明创建chat/发送提问结果的MESSAGECONTEXT - :rtype: Tuple[int | None, MESSAGECONTEXT] - """ - if (not respondent.accept_anonymous_chat) and questioner_anonymous: - return None, wrong("对方不允许匿名提问!") - - # 目前提问方回答方都需要是自然人 - if not request.user.is_person() or not respondent.is_person(): - return None, wrong("目前只允许个人用户进行问答!") - - if len(title) > 50: # Chat.title的max_length为50 - return None, wrong("主题过长!请勿超过50字") - if len(request.POST["comment"]) == 0: - return None, wrong("提问内容不能为空!") - - with transaction.atomic(): - chat = Chat.objects.create( - questioner=request.user, - respondent=respondent, - title=title, - questioner_anonymous=questioner_anonymous, - respondent_anonymous=respondent_anonymous, - ) - # 创建chat后没有发送通知,随后创建chat的第一条comment时会发送通知 - comment_context = add_chat_message(request, chat) - - return chat.id, comment_context - - -def get_matched_users(query: str, current_user: User, anonymous: bool): - """ - 根据提供的关键词获取搜索结果 - 比academic_utils中的类似函数更加精简 - """ - # 搜索所有含有关键词的公开的学术地图项目,忽略大小写 - match_with_tags = SQ.qsvlist(AcademicTagEntry.objects.filter( - tag__tag_content__icontains=query, - status=AcademicEntry.EntryStatus.PUBLIC, - ), AcademicEntry.person, NaturalPerson.person_id) - - match_with_texts = SQ.qsvlist(AcademicTextEntry.objects.filter( - content__icontains=query, - status=AcademicEntry.EntryStatus.PUBLIC, - ), AcademicEntry.person, NaturalPerson.person_id) - - matched_ids = match_with_tags + match_with_texts - matched_users = User.objects.exclude( - username=current_user.username).filter(id__in=matched_ids) - - if anonymous: - return matched_users.filter(accept_anonymous_chat=True) - else: - return matched_users - - -def select_by_keywords( - user: User, anonymous: bool, - keywords: list[str]) -> Tuple[User | None, MESSAGECONTEXT]: - """ - 根据关键词从学生中抽取一个回答者 - """ - matched_users = [] - for k in keywords: - matched_users.extend(list(get_matched_users(k, user, anonymous))) - - counted_users = Counter() - counted_users.update(matched_users) - greatest_occurrence = counted_users.most_common(1)[0][1] - most_matched_users = [ - user[0] for user in counted_users.items() - if user[1] == greatest_occurrence - ] - - if not most_matched_users: - return None, wrong("没有和标签匹配的对象!") - - chosen_users = sample(most_matched_users, k=1)[0] - return chosen_users, succeed("成功找到回答者") - - -def create_QA(request: HttpRequest, - respondent: User, - directed: bool, - questioner_anonymous: bool, - keywords: None | list[str] = None) -> MESSAGECONTEXT: - """ - 创建学术地图问答,包括定向和非定向 - - :param respondent: 回答者 - :type respondent: User - :param directed: 是否为定向问答 - :type directed: bool - :param questioner_anonymous: 提问者是否匿名 - :type questioner_anonymous: bool - :param keywords: 关键词,暂时只在非定向问答中使用,用来定位回答者。 - :type keywords: None | list[str] - """ - respondent_anonymous = not directed - chat_id, message_context = create_chat( - request, - respondent=respondent, - title=request.POST.get('comment_title'), - questioner_anonymous=questioner_anonymous, - respondent_anonymous=respondent_anonymous, - ) - if chat_id is None: - return message_context - - with transaction.atomic(): - AcademicQA.objects.create( - chat_id=chat_id, - keywords=keywords, - directed=directed, - ) - - # 解锁成就-参与学术问答 - unlock_achievement(request.user, "参与学术问答") - - # 奖励仅限第一次发起非定向提问 - if directed: - return succeed("提问成功") - - award_points = 5 - award, created = AcademicQAAwards.objects.get_or_create(user=request.user) - if created or not award.created_undirected_qa: - User.objects.modify_YQPoint( - request.user, award_points, "学术地图: 首次发起非定向提问", source_type=YQPointRecord.SourceType.ACHIEVE) - award.created_undirected_qa = True - award.save() - return succeed(f"首次发起非定向提问,奖励{award_points}元气值~") - - return succeed("提问成功") - - -def modify_rating(chat_id: int, rating: int) -> MESSAGECONTEXT: - assert rating >= 0 and rating <= 3 - award_points = [0, 5, 8, 12] # 评价对应的奖励值 - with transaction.atomic(): - qa: AcademicQA = AcademicQA.objects.get(chat_id=chat_id) - qa.rating = rating - User.objects.modify_YQPoint(qa.chat.respondent, award_points[rating], - "学术问答: 回答得到好评", - YQPointRecord.SourceType.ACHIEVE) - qa.save() - - return succeed("成功修改评价") - - -def add_comment_to_QA(request: HttpRequest) -> MESSAGECONTEXT: - try: - # TODO: 以后换成AcademicQA的id - chat = Chat.objects.get(id=request.POST.get('chat_id')) - except: - return wrong('问答不存在!') - - message_context = add_chat_message(request, chat) - if message_context[my_messages.CODE_FIELD] == my_messages.WRONG: - return message_context - - anonymous = request.POST.get('anonymous') - if anonymous == 'true': - return message_context - - with transaction.atomic(): - if request.user == chat.respondent: - Chat.objects.select_for_update().filter(id=chat.id).update( - respondent_anonymous=False) - else: - Chat.objects.select_for_update().filter(id=chat.id).update( - questioner_anonymous=False) - - # 解锁成就-参与学术问答 - unlock_achievement(request.user, "参与学术问答") - - return message_context diff --git a/app/course_utils.py b/app/course_utils.py deleted file mode 100644 index 9ac24dcfb..000000000 --- a/app/course_utils.py +++ /dev/null @@ -1,1491 +0,0 @@ -""" -course_utils.py - -course_views.py的依赖函数 - -registration_status_check: 检查学生选课状态变化的合法性 -registration_status_change: 改变学生选课状态 -course_to_display: 把课程信息转换为方便前端呈现的形式 -draw_lots: 预选阶段结束时执行抽签 -change_course_status: 改变课程的选课阶段 -remaining_willingness_point(暂不启用): 计算学生剩余的意愿点数 -process_time: 把datetime对象转换成人类可读的时间表示 -check_course_time_conflict: 检查当前选择的课是否与已选的课上课时间冲突 -""" -from app.utils_dependency import * -from app.models import ( - User, - NaturalPerson, - Activity, - Notification, - ActivityPhoto, - Position, - Participation, - Course, - CourseTime, - CourseParticipant, - CourseRecord, - Semester, -) -from app.utils import ( - get_person_or_org, - if_image, -) -from app.notification_utils import ( - bulk_notification_create, - notification_create, - notification_status_change, -) -from app.activity_utils import ( - changeActivityStatus, - notifyActivity, - create_participate_infos, -) -from app.extern.wechat import WechatApp, WechatMessageLevel -from app.log import logger - -import openpyxl -import openpyxl.worksheet.worksheet -from random import sample -from urllib.parse import quote -from collections import Counter -from datetime import datetime, timedelta -from typing import Tuple, List - -from django.http import HttpRequest, HttpResponse -from django.db import transaction -from django.db.models import F, Q, Sum, Prefetch - -from scheduler.adder import ScheduleAdder, MultipleAdder -from scheduler.cancel import remove_job -from utils.config.cast import str_to_time -from achievement.api import unlock_achievement - -__all__ = [ - 'check_ac_time_course', - 'course_activity_base_check', - 'create_single_course_activity', - 'modify_course_activity', - 'cancel_course_activity', - 'registration_status_change', - 'course_to_display', - 'change_course_status', - 'course_base_check', - 'create_course', - 'cal_participate_num', - 'check_post_and_modify', - 'finish_course', - 'download_course_record', - 'download_select_info', -] - -APP_CONFIG = CONFIG.course - - -def check_ac_time_course(start_time: datetime, end_time: datetime) -> bool: - """ - 时间合法性的检查:开始早于结束 - - :param start_time: 活动开始时间 - :type start_time: datetime - :param end_time: 活动结束时间 - :type end_time: datetime - :return: 是否合法 - :rtype: bool - """ - if not start_time < end_time: - return False - return True - - -def course_activity_base_check(request: HttpRequest) -> dict: - """ - 检查课程活动,是activity_base_check的简化版,失败时抛出AssertionError - - :param request: 修改/发起单次课程活动的请求 - :type request: HttpRequest - :raises AssertionError: 活动时间非法/需要报名的活动必须提前至少一小时发起 - :return: context - :rtype: dict - """ - context = dict() - - # 读取活动名称和地点,检查合法性 - context["title"] = request.POST.get("title", "") - # context["introduction"] = request.POST["introduction"] # 暂定不需要简介 - context["location"] = request.POST.get("location", "") - assert len(context["title"]) > 0, "标题不能为空" - # assert len(context["introduction"]) > 0 # 暂定不需要简介 - assert len(context["location"]) > 0, "地点不能为空" - - # 读取活动时间,检查合法性 - try: - act_start = datetime.strptime( - request.POST["lesson_start"], "%Y-%m-%d %H:%M") # 活动开始时间 - act_end = datetime.strptime( - request.POST["lesson_end"], "%Y-%m-%d %H:%M") # 活动结束时间 - act_publish_day = { - "instant": Course.PublishDay.instant, - "3": Course.PublishDay.threeday, - "2": Course.PublishDay.twoday, - "1": Course.PublishDay.oneday, - }[request.POST.get("publish_day")] # 活动发布提前日期 - - if act_publish_day == Course.PublishDay.instant: - act_publish_time = datetime.now() + timedelta(seconds=10) # 活动发布时间,立即发布 - else: - act_publish_time = datetime.strptime( - request.POST["publish_time"], "%Y-%m-%d %H:%M") # 活动发布时间,指定的发布时间 - except: - raise AssertionError("活动时间非法") - context["start"] = act_start - context["end"] = act_end - context["publish_day"] = act_publish_day - context["publish_time"] = act_publish_time - - if request.POST["need_apply"] == "True": - assert datetime.now() < context["start"] - \ - timedelta(hours=1), "需要报名的活动必须提前至少一小时发起" - - assert check_ac_time_course(act_start, act_end), "活动时间非法" - - # 默认需要签到 - context["need_checkin"] = True - # 默认不需要报名 - context["need_apply"] = request.POST["need_apply"] == "True" - context["post_type"] = str(request.POST.get("post_type", "")) - return context - - -def create_single_course_activity(request: HttpRequest) -> Tuple[int, bool]: - """ - 创建单次课程活动,是create_activity的简化版 - 错误提示通过AssertionError抛出 - - :param request: 发起单次课程活动的请求 - :type request: HttpRequest - :return: 课程活动id,True(成功创建)/False(相似活动已存在) - :rtype: Tuple[int, bool] - """ - context = course_activity_base_check(request) - - # 获取组织和课程 - org = get_person_or_org(request.user, UTYPE_ORG) - course = Course.objects.activated().get(organization=org) - - # 查找是否有类似活动存在 - old_ones = Activity.objects.activated().filter( - title=context["title"], - start=context["start"], - # introduction=context["introduction"], # 暂定不需要简介 - location=context["location"], - category=Activity.ActivityCategory.COURSE, # 查重时要求是课程活动 - ) - if len(old_ones): - assert len(old_ones) == 1, "创建活动时,已存在的相似活动不唯一" - return old_ones[0].id, False - - # 获取默认审核老师 - examine_teacher = NaturalPerson.objects.get_teacher( - APP_CONFIG.audit_teachers[0]) - - # 获取活动所属课程的图片,用于viewActivity, examineActivity等页面展示 - image = str(course.photo) - assert image, "获取课程图片失败" - - # 创建活动 - activity = Activity.objects.create( - title=context["title"], - organization_id=org, - examine_teacher=examine_teacher, - # introduction=context["introduction"], # 暂定不需要简介 - location=context["location"], - start=context["start"], - end=context["end"], - category=Activity.ActivityCategory.COURSE, - need_checkin=True, # 默认需要签到 - - recorded=True, - status=Activity.Status.UNPUBLISHED, - publish_day=context["publish_day"], # 发布提前天数 - publish_time=context["publish_time"], # 发布时间 - need_apply=context["need_apply"] # 是否需要报名 - - # capacity, URL, bidding, - # inner, end_before均为default - ) - - if context["need_apply"]: - activity.endbefore = Activity.EndBefore.onehour - activity.apply_end = activity.start - timedelta(hours=1) - - if not activity.need_apply: - # 选课人员自动报名活动 - # 选课结束以后,活动参与人员从小组成员获取 - person_pos = list(Position.objects.activated().filter( - org=course.organization).values_list("person", flat=True)) - if course.status == Course.Status.STAGE2: - # 如果处于补退选阶段,活动参与人员从课程选课情况获取 - selected_person = list(CourseParticipant.objects.filter( - course=course, - status=CourseParticipant.Status.SUCCESS, - ).values_list("person", flat=True)) - person_pos += selected_person - person_pos = list(set(person_pos)) - members = NaturalPerson.objects.filter( - id__in=person_pos) - status = Participation.AttendStatus.APPLYSUCCESS - create_participate_infos(activity, members, status=status) - - activity.current_participants = len(person_pos) - activity.capacity = len(person_pos) - activity.save() - - # 在活动发布时通知参与成员,创建定时任务并修改活动状态 - if activity.need_apply: - ScheduleAdder(changeActivityStatus, id=f"activity_{activity.id}_{Activity.Status.APPLYING}", - run_time=activity.publish_time)(activity.id, Activity.Status.UNPUBLISHED, Activity.Status.APPLYING) # OK - ScheduleAdder(changeActivityStatus, id=f"activity_{activity.id}_{Activity.Status.WAITING}", - run_time=activity.start - timedelta(hours=1))(activity.id, Activity.Status.APPLYING, Activity.Status.WAITING) # OK - else: - ScheduleAdder(changeActivityStatus, id=f"activity_{activity.id}_{Activity.Status.WAITING}", - run_time=activity.publish_time)(activity.id, Activity.Status.UNPUBLISHED, Activity.Status.WAITING) # OK - - ScheduleAdder(notifyActivity, id=f"activity_{activity.id}_newCourseActivity", - run_time=activity.publish_time)(activity.id, "newCourseActivity") # OK - - # 引入定时任务:提前15min提醒、活动状态由WAITING变PROGRESSING再变END - ScheduleAdder(notifyActivity, id=f"activity_{activity.id}_remind", - run_time=activity.start - timedelta(minutes=15))(activity.id, "remind") # OK - ScheduleAdder(changeActivityStatus, id=f"activity_{activity.id}_{Activity.Status.PROGRESSING}", - run_time=activity.start)(activity.id, Activity.Status.WAITING, Activity.Status.PROGRESSING) - ScheduleAdder(changeActivityStatus, id=f"activity_{activity.id}_{Activity.Status.END}", - run_time=activity.end)(activity.id, Activity.Status.PROGRESSING, Activity.Status.END) - activity.save() - - # 设置活动照片 - ActivityPhoto.objects.create( - image=image, type=ActivityPhoto.PhotoType.ANNOUNCE, activity=activity) - - # 通知审核老师 - notification_create( - receiver=examine_teacher.person_id, - sender=request.user, - typename=Notification.Type.NEEDDO, - title=Notification.Title.VERIFY_INFORM, - content="您有一个单次课程活动待审批", - URL=f"/examineActivity/{activity.id}", - relate_instance=activity, - to_wechat=dict(app=WechatApp.AUDIT), - ) - - return activity.id, True - - -def modify_course_activity(request: HttpRequest, activity: Activity): - """ - 修改单次课程活动信息,是modify_activity的简化版 - 错误提示通过AssertionError抛出 - - :param request: 修改单次课程活动的请求 - :type request: HttpRequest - :param activity: 待修改的活动 - :type activity: Activity - """ - # 课程活动仅在待发布状态下可以修改 - assert activity.status == Activity.Status.UNPUBLISHED, \ - "课程活动只有在待发布状态才能修改。" - - context = course_activity_base_check(request) - - # 记录旧信息(以便发通知),写入新信息 - old_title = activity.title - activity.title = context["title"] - # activity.introduction = context["introduction"]# 暂定不需要简介 - old_location = activity.location - activity.location = context["location"] - old_start = activity.start - activity.start = context["start"] - old_end = activity.end - activity.end = context["end"] - old_publish_day = activity.publish_day - activity.publish_day = context["publish_day"] - old_publish_time = activity.publish_time - activity.publish_time = context["publish_time"] - old_need_apply = activity.need_apply - activity.need_apply = context["need_apply"] - - if context["need_apply"]: - activity.endbefore = Activity.EndBefore.onehour - activity.apply_end = activity.start - timedelta(hours=1) - - activity.save() - - # 修改所有该时段的时间、地点 - if context["post_type"] == "modify_all" and activity.course_time is not None: - course_time = CourseTime.objects.select_for_update().get( - id=activity.course_time.id) - course = course_time.course - schedule_start = course_time.start - schedule_end = course_time.end - # 设置CourseTime初始时间为对应的 周几:hour:minute:second - # 设置周几 - schedule_start += timedelta( - days=(context["start"].weekday() - schedule_start.weekday())) - schedule_end += timedelta( - days=(context["end"].weekday() - schedule_end.weekday())) - # 设置每周上课时间:hour:minute:second - schedule_start = schedule_start.replace(hour=context["start"].hour, - minute=context["start"].minute, - second=context["start"].second) - schedule_end = schedule_end.replace(hour=context["end"].hour, - minute=context["end"].minute, - second=context["end"].second) - course_time.start = schedule_start - course_time.end = schedule_end - # 设置地点 - course.classroom = context["location"] - course.need_apply = context["need_apply"] - course.publish_day = context["publish_day"] - course.save() - course_time.save() - # 目前只要编辑了活动信息,无论活动处于什么状态,都通知全体选课同学 - # if activity.status != Activity.Status.APPLYING and activity.status != Activity.Status.WAITING: - # return - - # 发布前参与同学无法获取课程信息,因此不需要发送通知 - # to_participants = [f"您参与的书院课程活动{old_title}发生变化"] - # if old_title != activity.title: - # to_participants.append(f"活动更名为{activity.title}") - # if old_location != activity.location: - # to_participants.append(f"活动地点修改为{activity.location}") - # if old_start != activity.start: - # to_participants.append( - # f"活动开始时间调整为{activity.start.strftime('%Y-%m-%d %H:%M')}") - - # 更新定时任务 - if old_need_apply: - # 删除报名中的状态阶段 - remove_job(job_id=f"activity_{activity.id}_{Activity.Status.APPLYING}") - - if activity.need_apply: - ScheduleAdder(changeActivityStatus, id=f"activity_{activity.id}_{Activity.Status.APPLYING}", - run_time=activity.publish_time)(activity.id, Activity.Status.UNPUBLISHED, Activity.Status.APPLYING) # OK - ScheduleAdder(changeActivityStatus, id=f"activity_{activity.id}_{Activity.Status.WAITING}", - run_time=activity.start - timedelta(hours=1))(activity.id, Activity.Status.APPLYING, Activity.Status.WAITING) # OK - else: - ScheduleAdder(changeActivityStatus, id=f"activity_{activity.id}_{Activity.Status.WAITING}", - run_time=activity.publish_time)(activity.id, Activity.Status.UNPUBLISHED, Activity.Status.WAITING) # OK - - ScheduleAdder(notifyActivity, id=f"activity_{activity.id}_newCourseActivity", - run_time=activity.publish_time)(activity.id, "newCourseActivity") # OK - ScheduleAdder(notifyActivity, id=f"activity_{activity.id}_remind", - run_time=activity.start - timedelta(minutes=15))(activity.id, "remind") # OK - ScheduleAdder(changeActivityStatus, id=f"activity_{activity.id}_{Activity.Status.PROGRESSING}", - run_time=activity.start)(activity.id, Activity.Status.WAITING, Activity.Status.PROGRESSING) # OK - ScheduleAdder(changeActivityStatus, id=f"activity_{activity.id}_{Activity.Status.END}", - run_time=activity.end)(activity.id, Activity.Status.PROGRESSING, Activity.Status.END) # OK - - # 发通知 - # notifyActivity(activity.id, "modification_par", "\n".join(to_participants)) - - -def cancel_course_activity(request: HttpRequest, activity: Activity, cancel_all: bool = False): - """ - 取消课程活动,是cancel_activity的简化版,在聚合页面被调用 - - 在聚合页面中,应确保activity是课程活动,并且应检查activity.status, - 如果不是WAITING或PROGRESSING,不应调用本函数 - - 成功无返回值,失败返回错误消息 - (或者也可以在聚合页面判断出来能不能取消) - - :param request: 取消单次课程活动的请求 - :type request: HttpRequest - :param activity: 待取消的活动 - :type activity: Activity - :param cancel_all: 取消该时段所有活动, defaults to False - :type cancel_all: bool, optional - :return: 取消失败的话返回错误信息 - :rtype: string - """ - # 只有UNPUBLISHED,WAITING和PROGRESSING允许取消 - if activity.status not in [ - Activity.Status.UNPUBLISHED, - Activity.Status.WAITING, - Activity.Status.PROGRESSING, - ]: - return f"课程活动状态为{activity.get_status_display()},不可取消。" - - # 课程活动已于一天前开始则不能取消,这一点也可以在聚合页面进行判断 - if activity.status == Activity.Status.PROGRESSING: - if activity.start.day != datetime.now().day: - return "课程活动已于一天前开始,不能取消。" - - # 取消活动 - activity.status = Activity.Status.CANCELED - # 目前只要取消了活动信息,无论活动处于什么状态,都通知全体选课同学 - notifyActivity(activity.id, "modification_par", - f"您报名的书院课程活动{activity.title}已取消(活动原定开始于{activity.start.strftime('%Y-%m-%d %H:%M')})。") - - # 删除老师的审核通知(如果有) - notification = Notification.objects.get( - relate_instance=activity, - typename=Notification.Type.NEEDDO - ) - notification_status_change(notification, Notification.Status.DELETE) - - # 取消定时任务(需要先判断一下是否已经被执行了) - if activity.start - timedelta(minutes=15) > datetime.now(): - remove_job(f"activity_{activity.id}_remind") - if activity.start > datetime.now(): - remove_job(f"activity_{activity.id}_{Activity.Status.PROGRESSING}") - if activity.end > datetime.now(): - remove_job(f"activity_{activity.id}_{Activity.Status.END}") - - activity.save() - - # 取消该时段所有活动! - if cancel_all: - # 设置结束 若cur_week >= end_week 则每周定时任务无需执行 - activity.course_time.end_week = activity.course_time.cur_week - activity.course_time.save() - - -def remaining_willingness_point(user: NaturalPerson) -> int: - """ - 计算用户剩余的意愿点 - - :param user: 当前用户 - :type user: NaturalPerson - :raises NotImplementedError: 暂不启用 - :return: 剩余的意愿点值 - :rtype: int - """ - raise NotImplementedError("暂时不使用意愿点") - # 当前用户已经预选的课 - # 由于participant可能为空,重新启用的时候注意修改代码 - courses = Course.objects.filter( - participant_set__person=user, - participant_set__status=CourseParticipant.Status.SELECT) - - initial_point = 99 # 初始意愿点 - cost_point = courses.aggregate(Sum('bidding')) # 已经使用的意愿点 - - if cost_point: - # cost_point可能为None - return initial_point - cost_point['bidding__sum'] - else: - return initial_point - - -def registration_status_check(course_status: Course.Status, - cur_status: CourseParticipant.Status, - to_status: CourseParticipant.Status) -> None: - """ - 检查选课状态的变化是否合法 - - 1. 预选阶段允许的状态变化: SELECT <-> UNSELECT - 2. 补退选阶段允许的状态变化: SUCCESS -> UNSELECT; FAILED -> SUCCESS; UNSELECT -> SUCCESS - - 异常: 抛出AssertionError,在调用处解决 - - :param course_status: 课程所处的选课阶段 - :type course_status: Course.Status - :param cur_status: 当前选课状态 - :type cur_status: CourseParticipant.Status - :param to_status: 希望转变为的选课状态 - :type to_status: CourseParticipant.Status - """ - - if course_status == Course.Status.STAGE1: - assert ((cur_status == CourseParticipant.Status.SELECT - and to_status == CourseParticipant.Status.UNSELECT) - or (cur_status == CourseParticipant.Status.UNSELECT - and to_status == CourseParticipant.Status.SELECT)) - else: - assert ((cur_status == CourseParticipant.Status.SUCCESS - and to_status == CourseParticipant.Status.UNSELECT) - or (cur_status == CourseParticipant.Status.FAILED - and to_status == CourseParticipant.Status.SUCCESS) - or (cur_status == CourseParticipant.Status.UNSELECT - and to_status == CourseParticipant.Status.SUCCESS)) - - -def check_course_time_conflict(current_course: Course, - user: NaturalPerson) -> Tuple[bool, str]: - """ - 检查当前选择课程的时间和已选课程是否冲突 - - :param current_course: 用户当前想选的课程 - :type current_course: Course - :param user: 当前用户 - :type user: NaturalPerson - :return: 是否冲突、发生冲突的具体原因 - :rtype: Tuple[bool, str] - """ - selected_courses = Course.objects.selected( - user, unfailed=True).prefetch_related("time_set") - - def time_hash(time: datetime): - return time.weekday() * 1440 + time.hour * 60 + time.minute - - # 因为选择的课最多只能有6门,所以暂时用暴力算法 - for current_course_time in current_course.time_set.all(): - - # 当前选择课程的上课时间 - current_start_time = current_course_time.start - current_end_time = current_course_time.end - - for course in selected_courses: - for course_time in course.time_set.all(): - start_time = course_time.start - end_time = course_time.end - - # 效率不高,有待改进 - if not (time_hash(current_start_time) >= time_hash(end_time) or - time_hash(current_end_time) <= time_hash(start_time)): - # 发生冲突 - return True, \ - f"《{current_course.name}》和《{course.name}》的上课时间发生冲突!" - - # 没有冲突 - return False, "" - ''' - # 循环较少的写法 - from django.db.models import Q - conflict_course_names = set() - for current_course_time in current_course.time_set.all(): - # 冲突时间 - conflict_times = CourseTime.objects.filter( - # 已选的课程 - Q(course__in=selected_courses), - # 开始比当前的结束时间早 - (Q(start__week_day=current_course_time.end.weekday() + 1, - start__time__lte=current_course_time.end.time()) - | Q(start__week_day__lt=current_course_time.end.weekday() + 1)) - # 结束比当前的开始时间晚 - & (Q(end__week_day=current_course_time.start.weekday() + 1, - end__time__gte=current_course_time.start.time()) - | Q(end__week_day__gt=current_course_time.start.weekday() + 1)) - ) - if conflict_times.exists(): - # return True, f'《{conflict_times.first().course.name}》' - conflict_course_names.union( - conflict_times.values_list('course__name', flat=True)) - - conflict_count = len(conflict_course_names) - # 有冲突 - if conflict_count: - return conflict_count, f'《{"》《".join(conflict_course_names)}》' - # 没有冲突 - return conflict_count, "" - ''' - - -@logger.secure_func() -def registration_status_change(course_id: int, user: NaturalPerson, - action: str) -> MESSAGECONTEXT: - """ - 学生点击选课或者取消选课后,更改学生的选课状态 - - :param course_id: 当前课程的编号 - :type course_id: int - :param user: 当前用户 - :type user: NaturalPerson - :param action: 希望进行的操作,可能为"select"或"cancel" - :type action: str - :return: 操作是否成功执行 - :rtype: MESSAGECONTEXT - """ - context = wrong("在修改选课状态的过程中发生错误,请联系管理员!") - - # 在外部保证课程ID是存在的 - course = Course.objects.get(id=course_id) - course_status = course.status - - if (course_status != Course.Status.STAGE1 - and course_status != Course.Status.STAGE2): - return wrong("在非选课阶段不能选课!") - - need_to_create = False - - if action == "select": - if CourseParticipant.objects.filter(course_id=course_id, - person=user).exists(): - participant_info = CourseParticipant.objects.get( - course_id=course_id, person=user) - cur_status = participant_info.status - else: - need_to_create = True - cur_status = CourseParticipant.Status.UNSELECT - - if course_status == Course.Status.STAGE1: - to_status = CourseParticipant.Status.SELECT - else: - to_status = CourseParticipant.Status.SUCCESS - - # 选课不能超过6门 - if Course.objects.selected(user, unfailed=True).count() >= 6: - return wrong("每位同学同时预选或选上的课程数最多为6门!") - - # 检查选课时间是否冲突 - is_conflict, message = check_course_time_conflict(course, user) - - if is_conflict: - return wrong(message) - # return wrong(f'与{is_conflict}门已选课程时间冲突: {message}') - - # 解锁成就-首次报名书院课程 - unlock_achievement(user, '首次报名书院课程') - - else: - # action为取消预选或退选 - to_status = CourseParticipant.Status.UNSELECT - - # 不允许状态不存在,除非发生了严重的错误 - try: - participant_info = CourseParticipant.objects.get( - course_id=course_id, person=user) - cur_status = participant_info.status - except: - return context - - # 检查当前选课状态、选课阶段和操作的一致性 - try: - registration_status_check(course_status, cur_status, to_status) - except AssertionError: - return wrong("非法的选课状态修改!") - - # 暂时不使用意愿点选课 - # if (course_status == Course.Status.STAGE1 - # and remaining_willingness_point(user) < course.bidding): - # context["warn_message"] = "剩余意愿点不足" - - # 更新选课状态 - try: - with transaction.atomic(): - if to_status == CourseParticipant.Status.UNSELECT: - Course.objects.filter(id=course_id).select_for_update().update( - current_participants=F("current_participants") - 1) - CourseParticipant.objects.filter(course_id=course_id, - person=user).delete() - succeed("成功取消选课!", context) - else: - # 处理并发问题 - course = Course.objects.select_for_update().get(id=course_id) - if (course_status == Course.Status.STAGE2 - and course.current_participants >= course.capacity): - wrong("选课人数已满!", context) - else: - course.current_participants += 1 - course.save() - - # 由于不同用户之间的状态不共享,这个更新应该可以不加锁 - if need_to_create: - CourseParticipant.objects.create(course_id=course_id, - person=user, - status=to_status) - else: - CourseParticipant.objects.filter( - course_id=course_id, - person=user).update(status=to_status) - succeed("选课成功!", context) - except: - return context - return context - - -def process_time(start: datetime, end: datetime) -> str: - """ - 把datetime对象转换成可读的时间表示 - - :param start: 课程的开始时间 - :type start: datetime - :param end: 课程的结束时间 - :type end: datetime - :return: 可读的时间表示 - :rtype: str - """ - chinese_display = ["一", "二", "三", "四", "五", "六", "日"] - start_time = start.strftime("%H:%M") - end_time = end.strftime("%H:%M") - return f"周{chinese_display[start.weekday()]} {start_time}-{end_time}" - - -def course_to_display(courses: QuerySet[Course], - user: NaturalPerson, - detail: bool = False) -> List[dict]: - """ - 将课程信息转换为列表,方便前端呈现 - - :param courses: 课程集合 - :type courses: QuerySet[Course] - :param user: 当前用户 - :type user: NaturalPerson - :param detail: 是否显示课程的详细信息, defaults to False - :type detail: bool - :return: 课程信息列表,用一个字典来传递课程的全部信息 - :rtype: List[dict] - """ - display = [] - - if detail: - courses = courses.select_related('organization').prefetch_related( - "time_set") - else: - # 预取,同时不查询不需要的字段 - courses = courses.defer( - "classroom", - "teacher", - "introduction", - "photo", - "teaching_plan", - "record_cal_method", - "QRcode", - ).select_related('organization').prefetch_related( - Prefetch('participant_set', - queryset=CourseParticipant.objects.filter(person=user), - to_attr='participants'), "time_set") - - # 获取课程的基本信息 - for course in courses: - course_info = {} - - # 选课页面和详情页面共用的信息 - course_info["name"] = course.name - course_info["type"] = course.get_type_display() # 课程类型 - course_info["avatar_path"] = course.organization.get_user_ava() - - course_time = [] - for time in course.time_set.all(): - course_time.append(process_time(time.start, time.end)) - course_info["time_set"] = course_time - - def linebreak(str): - from re import sub - return sub("((\r|\\\)+n)|((\r|\\\)+\n)", "\n", str) - - if detail: - # 在课程详情页才展示的信息 - course_info["times"] = course.times # 课程周数 - course_info["classroom"] = course.classroom - course_info["teacher"] = course.teacher - course_info["introduction"] = linebreak(course.introduction) - course_info["teaching_plan"] = linebreak(course.teaching_plan) - course_info["record_cal_method"] = linebreak(course.record_cal_method) - course_info["organization_name"] = course.organization.oname - course_info["have_QRcode"] = bool(course.QRcode) - course_info["photo_path"] = course.get_photo_path() - course_info["QRcode"] = course.get_QRcode_path() - display.append(course_info) - continue - - course_info["course_id"] = course.id - course_info["capacity"] = course.capacity - course_info["current_participants"] = course.current_participants - course_info["status"] = course.get_status_display() # 课程所处的选课阶段 - - # 暂时不启用意愿点机制 - # course_info["bidding"] = int(course.bidding) - - # 当前学生的选课状态(注:course.participants是一个list) - if course.participants: - course_info["student_status"] = course.participants[ - 0].get_status_display() - else: - course_info["student_status"] = "未选课" - - display.append(course_info) - - return display - - -@logger.secure_func() -def draw_lots(): - """ - 等额抽签选出成功选课的学生,并修改学生的选课状态 - """ - courses = Course.objects.activated().filter(status=Course.Status.DRAWING) - for course in courses: - with transaction.atomic(): - participants = CourseParticipant.objects.filter( - course=course, status=CourseParticipant.Status.SELECT) - - participants_num = participants.count() - if participants_num <= 0: - continue - - participants_id = list(participants.values_list("id", flat=True)) - capacity = course.capacity - - if participants_num <= capacity: - # 选课人数少于课程容量,不用抽签 - CourseParticipant.objects.filter( - id__in=participants_id).select_for_update().update( - status=CourseParticipant.Status.SUCCESS) - Course.objects.filter(id=course.id).select_for_update().update( - current_participants=participants_num) - else: - # 抽签;可能实现得有一些麻烦 - lucky_ones = sample(participants_id, capacity) - unlucky_ones = list( - set(participants_id).difference(set(lucky_ones))) - # 不确定是否要加悲观锁 - CourseParticipant.objects.filter( - id__in=lucky_ones).select_for_update().update( - status=CourseParticipant.Status.SUCCESS) - CourseParticipant.objects.filter( - id__in=unlucky_ones).select_for_update().update( - status=CourseParticipant.Status.FAILED) - Course.objects.filter(id=course.id).select_for_update().update( - current_participants=capacity) - - # 给选课成功的同学发送通知 - receivers = SQ.qsvlist(CourseParticipant.objects.filter( - course=course, - status=CourseParticipant.Status.SUCCESS, - ), CourseParticipant.person, NaturalPerson.person_id) - receivers = User.objects.filter(id__in=receivers) - sender = course.organization.get_user() - typename = Notification.Type.NEEDREAD - title = Notification.Title.ACTIVITY_INFORM - content = f"您好!您已成功选上课程《{course.name}》!" - - # 课程详情页面 - URL = f"/viewCourse/?courseid={course.id}" - - # 批量发送通知 - bulk_notification_create( - receivers=receivers, - sender=sender, - typename=typename, - title=title, - content=content, - URL=URL, - to_wechat=dict(app=WechatApp.TO_PARTICIPANT, - level=WechatMessageLevel.IMPORTANT) - ) - - # 给选课失败的同学发送通知 - - receivers = SQ.qsvlist(CourseParticipant.objects.filter( - course=course, - status=CourseParticipant.Status.FAILED, - ), CourseParticipant.person, NaturalPerson.person_id) - receivers = User.objects.filter(id__in=receivers) - content = f"很抱歉通知您,您未选上课程《{course.name}》。" - if len(receivers) > 0: - bulk_notification_create( - receivers=receivers, - sender=sender, - typename=typename, - title=title, - content=content, - URL=URL, - to_wechat=dict(app=WechatApp.TO_PARTICIPANT, - level=WechatMessageLevel.IMPORTANT), - ) - - -@logger.secure_func() -def change_course_status(cur_status: Course.Status, to_status: Course.Status) -> None: - """ - 作为定时任务,在课程设定的时间改变课程的选课阶段 - - example: - scheduler.add_job(change_course_status, "date", - id=f"course_{course_id}_{to_status}, run_date, args) - - :param cur_status: 课程的当前选课阶段 - :type cur_status: Course.Status - :param to_status: 希望课程转变到的选课阶段 - :type to_status: Course.Status - :raises AssertionError: 选课已经结束 / 两个状态间不匹配 - :raises AssertionError: 未提供当前阶段的信息 - """ - # 以下进行状态的合法性检查 - if cur_status is not None: - if cur_status == Course.Status.WAITING: - assert to_status == Course.Status.STAGE1, \ - f"不能从{cur_status}变更到{to_status}" - elif cur_status == Course.Status.STAGE1: - assert to_status == Course.Status.DRAWING, \ - f"不能从{cur_status}变更到{to_status}" - elif cur_status == Course.Status.DRAWING: - assert to_status == Course.Status.STAGE2, \ - f"不能从{cur_status}变更到{to_status}" - elif cur_status == Course.Status.STAGE2: - assert to_status == Course.Status.SELECT_END, \ - f"不能从{cur_status}变更到{to_status}" - else: - raise AssertionError("选课已经结束,不能再变化状态") - else: - raise AssertionError("未提供当前状态,不允许进行选课状态修改") - courses = Course.objects.activated().filter(status=cur_status) - if to_status == Course.Status.SELECT_END: - courses = courses.select_related('organization') - with transaction.atomic(): - for course in courses: - if to_status == Course.Status.SELECT_END: - # 选课结束,将选课成功的同学批量加入小组 - participants = CourseParticipant.objects.filter( - course=course, - status=CourseParticipant.Status.SUCCESS).select_related( - 'person') - organization = course.organization - positions = [] - for participant in participants: - # 如果已有当前的离职状态,改成在职成员 - Position.objects.current().filter( - person=participant.person, - org=organization, - status=Position.Status.DEPART, - ).update(pos=10, - is_admin=False, - semester=GLOBAL_CONFIG.semester, - status=Position.Status.INSERVICE) - # 检查是否已经加入小组 - if not Position.objects.activated().filter( - person=participant.person, - org=organization).exists(): - position = Position(person=participant.person, - org=organization, - semester=GLOBAL_CONFIG.semester) - - positions.append(position) - if positions: - with transaction.atomic(): - Position.objects.bulk_create(positions) - # 更新目标状态 - courses.select_for_update().update(status=to_status) - - -@logger.secure_func() -def register_selection(wait_for: timedelta | None = None): - """ - 添加定时任务,实现课程状态转变,每次发起课程时调用 - """ - # 预选和补退选的开始和结束时间 - year = GLOBAL_CONFIG.acadamic_year - semester = GLOBAL_CONFIG.semester.value - now = datetime.now() - if wait_for is not None: - now += wait_for - stage1_start = str_to_time(APP_CONFIG.yx_election_start) - stage1_start = max(stage1_start, now + timedelta(seconds=5)) - stage1_end = str_to_time(APP_CONFIG.yx_election_end) - stage1_end = max(stage1_end, now + timedelta(seconds=10)) - publish_time = str_to_time(APP_CONFIG.publish_time) - publish_time = max(publish_time, now + timedelta(seconds=15)) - stage2_start = str_to_time(APP_CONFIG.btx_election_start) - stage2_start = max(stage2_start, now + timedelta(seconds=20)) - stage2_end = str_to_time(APP_CONFIG.btx_election_end) - stage2_end = max(stage2_end, now + timedelta(seconds=25)) - # 定时任务:修改课程状态 - adder = MultipleAdder(change_course_status) - adder.schedule(f'{year}_{semester}_选课_stage1_start', - run_time=stage1_start)(Course.Status.WAITING, Course.Status.STAGE1) - adder.schedule(f'{year}_{semester}_选课_stage1_end', - run_time=stage1_end)(Course.Status.STAGE1, Course.Status.DRAWING) - ScheduleAdder(draw_lots, id=f'{year}_{semester}_选课_publish', - run_time=publish_time)() - adder.schedule(f'{year}_{semester}_选课_stage2_start', - run_time=stage2_start)(Course.Status.DRAWING, Course.Status.STAGE2) - adder.schedule(f'{year}_{semester}_选课_stage2_end', - run_time=stage2_end)(Course.Status.STAGE2, Course.Status.SELECT_END) - # 状态随时间的变化: WAITING-STAGE1-WAITING-STAGE2-END - - -def course_base_check(request, if_new=None): - """ - 选课单变量合法性检查并准备变量 - """ - context = dict() - # 字符串字段合法性检查 - try: - # name, introduction, classroom 创建时不能为空 - context = my_messages.read_content( - request.POST, - "name", - 'teacher', - "introduction", - "classroom", - "teaching_plan", - "record_cal_method", - "need_apply", - "publish_day", - _trans_func=str, - _default="", - ) - assert len(context["name"]) > 0, "课程名称不能为空!" - assert len(context["introduction"]) > 0, "课程介绍不能为空!" - assert len(context["teaching_plan"]) > 0, "教学计划不能为空!" - assert len(context["record_cal_method"]) > 0, "学时计算方法不能为空!" - assert len(context["classroom"]) > 0, "上课地点不能为空!" - assert context["need_apply"] in ["True", "False"], "是否需要报名必须为给定值!" - assert context["publish_day"] in [ - "instant", "1", "2", "3"], "信息发布时间必须为给定值!" - except Exception as e: - return wrong(str(e)) - # int类型合法性检查 - - type_num = request.POST.get("type", "") # 课程类型 - capacity = request.POST.get("capacity", -1) - # context['times'] = int(request.POST["times"]) #课程上课周数 - try: - cur_info = "记得选择课程类型哦!" - type_num = int(type_num) - cur_info = "课程类型仅包括德智体美劳五种!" - assert 0 <= type_num < 5 - cur_info = "课程容量应当大于0!" - assert int(capacity) > 0 - except: - return wrong(cur_info) - context['type'] = type_num - context['capacity'] = capacity - - # 图片类型合法性检查 - try: - announcephoto = request.FILES.get("photo") - pic = None - if announcephoto: - pic = announcephoto - assert if_image(pic) == 2, "课程预告图片文件类型错误!" - else: - for i in range(5): - if request.POST.get(f'picture{i+1}'): - pic = request.POST.get(f'picture{i+1}') - context["photo"] = pic - context["QRcode"] = request.FILES.get("QRcode") - if if_new is None: - assert context["photo"] is not None, "缺少课程预告图片!" - assert if_image(context["QRcode"]) != 1, "微信群二维码图片文件类型错误!" - except Exception as e: - return wrong(str(e)) - - # 每周课程时间合法性检查 - # TODO: 需要增加是否可以修改时间的安全性检查 - course_starts = request.POST.getlist("start") - course_ends = request.POST.getlist("end") - course_starts = [ - datetime.strptime(course_start, "%Y-%m-%d %H:%M") - for course_start in course_starts - if course_start != '' - ] - course_ends = [ - datetime.strptime(course_end, "%Y-%m-%d %H:%M") - for course_end in course_ends - if course_end != '' - ] - try: - for i in range(len(course_starts)): - assert check_ac_time_course( - course_starts[i], course_ends[i]), f'第{i+1}次上课时间起止时间有误!' - # 课程每周同一次课的开始和结束时间应当处于同一天 - assert course_starts[i].date( - ) == course_ends[i].date(), f'第{i+1}次上课起止时间应当为同一天' - except Exception as e: - return wrong(str(e)) - context['course_starts'] = course_starts - context['course_ends'] = course_ends - context['publish_day'] = { - "instant": Course.PublishDay.instant, - "3": Course.PublishDay.threeday, - "2": Course.PublishDay.twoday, - "1": Course.PublishDay.oneday, - }[context['publish_day']] - org = get_person_or_org(request.user, UTYPE_ORG) - context['organization'] = org - - succeed("合法性检查通过!", context) - return context - - -def create_course(request, course_id=None): - ''' - 检查课程,合法时寻找该课程,不存在时创建 - 返回(course.id, created) - ''' - context = dict() - - try: - context = course_base_check(request, course_id) - if context["warn_code"] == 1: # 合法性检查出错! - return context - except: - return wrong("检查参数合法性时遇到不可预料的错误。如有需要,请联系管理员解决!") - - # 编辑已有课程 - if course_id is not None: - try: - course = Course.objects.get(id=int(course_id)) - with transaction.atomic(): - if course.status in [Course.Status.WAITING]: - course_time = course.time_set.all() - course_time.delete() - course.name = context["name"] - course.classroom = context["classroom"] - course.teacher = context['teacher'] - course.introduction = context["introduction"] - course.teaching_plan = context["teaching_plan"] - course.record_cal_method = context["record_cal_method"] - course.type = context['type'] - course.capacity = context["capacity"] - course.need_apply = context["need_apply"] - course.publish_day = context["publish_day"] - course.photo = context['photo'] if context['photo'] is not None else course.photo - if context['QRcode']: - course.QRcode = context["QRcode"] - course.save() - - if course.status in [Course.Status.WAITING]: - for i in range(len(context['course_starts'])): - CourseTime.objects.create( - course=course, - start=context['course_starts'][i], - end=context['course_ends'][i], - ) - except: - return wrong("修改课程时遇到不可预料的错误。如有需要,请联系管理员解决!") - context["cid"] = course_id - context["warn_code"] = 2 - context["warn_message"] = "修改课程成功!" - # 创建新课程 - else: - try: - with transaction.atomic(): - course = Course.objects.create( - name=context["name"], - organization=context['organization'], - classroom=context["classroom"], - teacher=context['teacher'], - introduction=context["introduction"], - teaching_plan=context["teaching_plan"], - record_cal_method=context["record_cal_method"], - type=context['type'], - capacity=context["capacity"], - need_apply=context["need_apply"], - publish_day=context["publish_day"] - ) - course.photo = context['photo'] - if context['QRcode']: - course.QRcode = context["QRcode"] - course.save() - - for i in range(len(context['course_starts'])): - CourseTime.objects.create( - course=course, - start=context['course_starts'][i], - end=context['course_ends'][i], - ) - register_selection() # 每次发起课程,创建定时任务 - except: - return wrong("创建课程时遇到不可预料的错误。如有需要,请联系管理员解决!") - context["cid"] = course.id - context["warn_code"] = 2 - context["warn_message"] = "创建课程成功!" - - return context - - -def cal_participate_num(course: Course) -> dict: - """ - 计算该课程对应组织所有成员的参与次数 - return {Naturalperson.id:参与次数} - 前端使用的时候直接读取字典的值就好了 - :param course: 选择要计算的课程 - :type course: Course - :return: 返回统计数据 - :rtype: dict - """ - org = course.organization - activities = Activity.objects.activated().filter( - organization_id=org, - status=Activity.Status.END, - category=Activity.ActivityCategory.COURSE, - ) - # 只有小组成员才可以有学时 - members = Position.objects.activated().filter( - pos__gte=1, - person__identity=NaturalPerson.Identity.STUDENT, - org=org, - ).values_list("person", flat=True) - all_participants = SQ.qsvlist( - Participation.objects.activated(no_unattend=True) - .filter(SQ.mq(Participation.activity, IN=activities), - SQ.mq(Participation.person, IN=members)), - Participation.person) - participate_num = dict(Counter(all_participants)) - # 没有参加的参与次数设置为0 - participate_num.update( - {id: 0 for id in members if id not in participate_num}) - return participate_num - - -def check_post_and_modify(records: list, post_data: dict) -> MESSAGECONTEXT: - """ - records和post_data分别为原先和更新后的list - 检查post表单是否可以为这个course对应的内容, - 如果可以,修改学时 - - 返回wrong|succeed - - 不抛出异常 - :param records: 原本的学时数据 - :type records: list - :param post_data: 由前端上传上来的修改结果 - :type post_data: dict - :return: 检查结果 - :rtype: MESSAGECONTEXT - """ - try: - # 对每一条记录而言 - for record in records: - # 选取id作为匹配键 - key = str(record.person.id) - assert key in post_data.keys(), "提交的人员信息不匹配,请联系管理员!" - - # 读取小时数 - hours = post_data.get(str(key), -1) - assert float(hours) >= 0, "学时数据为负数,请检查输入数据!" - record.total_hours = float(hours) - # 更新是否有效 - record.invalid = (record.total_hours < - APP_CONFIG.least_record_hours) - - CourseRecord.objects.bulk_update(records, ["total_hours", "invalid"]) - return succeed("修改学时信息成功!") - except AssertionError as e: - # 此时相当于出现用户应该知晓的信息 - return wrong(str(e)) - except: - return wrong("数据格式异常,请检查输入数据!") - - -def finish_course(course): - """ - 结束课程 - 设置课程状态为END 生成学时表并通知同学该课程已结束。 - """ - # 若存在课程活动未结束则无法结束课程。 - cur_activities = Activity.objects.activated().filter( - organization_id=course.organization, - category=Activity.ActivityCategory.COURSE).exclude( - status__in=[ - Activity.Status.CANCELED, - Activity.Status.END, - ]) - if cur_activities.exists(): - return wrong("存在尚未结束的课程活动,请在所有课程活结束以后再结束课程。") - - try: - # 取消发布每周定时活动 - course_times = course.time_set - for course_time in course_times.all(): - course_time.end_week = course_time.cur_week - course_time.save() - except: - return wrong("取消课程每周定时活动时失败,请联系管理员!") - try: - # 生成学时表 - participate_num = cal_participate_num(course) - participants = NaturalPerson.objects.activated().filter( - id__in=participate_num.keys()) - course_record_list = [] - for participant in participants: - # 如果存在相同学期的学时表,则不创建 - if not CourseRecord.objects.current().filter( - person=participant, course=course - ).exists(): - course_record_list.append(CourseRecord( - person=participant, - course=course, - attend_times=participate_num[participant.id], - total_hours=2 * participate_num[participant.id], - invalid=(2 * participate_num[participant.id] < 8), - )) - CourseRecord.objects.bulk_create(course_record_list) - except: - return wrong("生成学时表失败,请联系管理员!") - try: - # 通知课程小组成员该课程已结束 - title = f'课程结束通知!' - msg = f'{course.name}在本学期的课程已结束!' - receivers = SQ.qsvlist(participants, NaturalPerson.person_id) - receivers = User.objects.filter(id__in=receivers) - bulk_notification_create( - receivers=list(receivers), - sender=course.organization.get_user(), - typename=Notification.Type.NEEDREAD, - title=title, - content=msg, - URL=f"/viewCourse/?courseid={course.id}", - to_wechat=dict(app=WechatApp.TO_PARTICIPANT), - ) - # 设置课程状态 - except: - return wrong("生成通知失败,请联系管理员!") - course.status = Course.Status.END - course.save() - return succeed("结束课程成功!") - - -def _excel_response(workbook: openpyxl.Workbook, file_name: str) -> HttpResponse: - '''创建Excel文件回应,file_name为未转义的文件名(不含后缀)''' - response = HttpResponse(content_type='application/vnd.ms-excel') - response['Content-Disposition'] = f'attachment; filename={quote(file_name)}.xlsx' - workbook.save(response) - return response - - -def _write_detail_sheet(detail_sheet: openpyxl.worksheet.worksheet.Worksheet, - records: QuerySet[CourseRecord], title: str = '详情') -> None: - '''将学时信息写入Excel文件的detail_sheet中''' - # 注意,标题中的中文符号如:无法被解读 - detail_sheet.title = title - # 从第一行开始写,因为Excel文件的行号是从1开始,列号也是从1开始 - detail_header = ['课程', '姓名', '学号', '次数', '学时', '学年', '学期', '有效'] - detail_sheet.append(detail_header) - _M = CourseRecord - for record in records.values_list( - SQ.f(_M.course, Course.name), SQ.f(_M.extra_name), - SQ.f(_M.person, NaturalPerson.name), - SQ.f(_M.person, NaturalPerson.person_id, User.username), - SQ.f(_M.attend_times), SQ.f(_M.total_hours), - SQ.f(_M.year), SQ.f(_M.semester), SQ.f(_M.invalid), - ): - record_info = [ - record[0] or record[1], - *record[2:6], - f'{record[6]}-{record[6] + 1}', - '春' if record[7] == Semester.SPRING else '秋', - '否' if record[8] else '是', - ] - # 将每一个对象的所有字段的信息写入一行内 - detail_sheet.append(record_info) - - -def download_course_record(course: Course = None, year: int = None, semester: Semester = None) -> HttpResponse: - """返回需要导出的学时信息文件 - course: - 提供course时为单个课程服务,只导出该课程的相关人员的学时信息 - 不提供时下载所有学时信息,注意,只有相关负责老师可以访问! - :param course: 所选择的课程, defaults to None - :type course: Course, optional - :param year: 所选择的学年, defaults to None - :type year: int, optional - :param semester: 所选择的学期, defaults to None - :type semester: Semester, optional - :return: 返回下载的文件数据 - :rtype: HttpResponse - """ - wb = openpyxl.Workbook() # 生成一个工作簿(即一个Excel文件) - wb.encoding = 'utf-8' - # 学时筛选内容 - filter_kws = {} - if course is not None: - filter_kws[SQ.f(CourseRecord.course)] = course - if year is not None: - filter_kws[SQ.f(CourseRecord.year)] = year - if semester is not None: - filter_kws[SQ.f(CourseRecord.semester)] = semester - - if course is not None: - # 助教下载自己课程的学时 - records = CourseRecord.objects.filter(**filter_kws) - _write_detail_sheet(wb.active, records) - now = datetime.now().strftime('%Y-%m-%d %H:%M') - return _excel_response(wb, f'{course.name}-{now}') - - # 老师下载所有课程的学时 - # 获取第一个工作表(detail_sheet) - detail_sheet = wb.active - # 设置明细和汇总两个sheet的相关信息 - total_sheet: openpyxl.worksheet.worksheet.Worksheet = wb.create_sheet('汇总', 0) - first_line = ['学号', '姓名', '总有效学时', '总无效学时'] - first_line.extend(Course.CourseType.labels) - first_line.append('其他') - total_sheet.append(first_line) - - # 下载所有学时信息,包括无效学时 - all_person = NaturalPerson.objects.activated().filter( - identity=NaturalPerson.Identity.STUDENT) - - # 汇总表信息,姓名,学号,总学时 - relate = SQ.Reverse(CourseRecord.person) - person_record = all_person.annotate( - record_hours=Sum(SQ.f(relate, CourseRecord.total_hours), - filter=SQ.mq(relate, invalid=False, **filter_kws)), - invalid_hours=Sum(SQ.f(relate, CourseRecord.total_hours), - filter=SQ.mq(relate, invalid=True, **filter_kws)), - ).order_by(SQ.f(NaturalPerson.person_id, User.username)) - - def _sum_hours(records: QuerySet[CourseRecord]) -> float: - agg = records.filter(**filter_kws).aggregate(sum=Sum('total_hours')) - return agg['sum'] or 0 - - for person in person_record.select_related(SQ.f(NaturalPerson.person_id)): - line = [person.person_id.username, person.name, - person.record_hours or 0, - person.invalid_hours or 0] - valid_records = SQ.sfilter(CourseRecord.person, person).exclude(invalid=True) - # 计算每个类别的学时 - for course_type in list(Course.CourseType): - line.append(_sum_hours(valid_records.filter(course__type=course_type))) - # 计算没有对应Course的学时 - line.append(_sum_hours(valid_records.filter(course__isnull=True))) - total_sheet.append(line) - - # 详细信息 - records = SQ.mfilter(CourseRecord.person, IN=all_person).filter(**filter_kws) - order = SQ.f(CourseRecord.person, NaturalPerson.person_id, User.username) - _write_detail_sheet(detail_sheet, records.order_by(order)) - now = datetime.now().strftime('%Y-%m-%d %H:%M') - return _excel_response(wb, f'学时汇总-{now}') - - -def download_select_info(single_course: Course | None = None): - """ - 下载选课信息 - single_course: - 提供single_course时为单个课程服务,只导出该课程的相关人员的选课信息 - 不提供时下载所有选课信息,注意,此时只有相关负责老师可以访问! - 不提供手动选课人员名单。 - :return: 返回下载的文件数据 - :rtype: HttpResponse - """ - wb = openpyxl.Workbook() # 生成一个工作簿(即一个Excel文件) - wb.encoding = 'utf-8' - sheet_header = ['姓名', '学号'] - if single_course is not None: - courses = [single_course] - else: - courses = Course.objects.activated() - # 为每一门课创建一个新的sheet - for course in courses: - # 生成新的sheet,并设置表头 - sheet = wb.create_sheet(title=f'{course.name}') - sheet.append(sheet_header) - # 导出相关信息,不包括手动选课 - lucky_ones = CourseParticipant.objects.filter( - course=course, status=CourseParticipant.Status.SUCCESS) - for info in lucky_ones.values_list( - SQ.f(CourseParticipant.person, NaturalPerson.name), - SQ.f(CourseParticipant.person, NaturalPerson.person_id, User.username) - ): - person_info = [ - info[0], - info[1] - ] - sheet.append(person_info) - # 删除多余的第一个sheet - wb.remove_sheet(wb.active) - # 设置文件名 - semester = "春" if courses[0].semester == Semester.SPRING else "秋" - year = (courses[0].year + 1) if semester == "春" else courses[0].year - ctime = datetime.now().strftime('%Y-%m-%d %H:%M') - if single_course is not None: - file_name = f'{year}{semester}{course.name}选课名单-{ctime}' - else: - file_name = f'{year}{semester}选课名单汇总-{ctime}' - # 保存并返回 - return _excel_response(wb, file_name) diff --git a/app/course_views.py b/app/course_views.py deleted file mode 100644 index b26d7825d..000000000 --- a/app/course_views.py +++ /dev/null @@ -1,744 +0,0 @@ -""" -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= & 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= - :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() diff --git a/app/jobs.py b/app/jobs.py index a2a5e8308..5726fe12f 100644 --- a/app/jobs.py +++ b/app/jobs.py @@ -20,21 +20,8 @@ NaturalPerson, Organization, OrganizationType, - Activity, - ActivityPhoto, - ActivitySummary, - Participation, Notification, Position, - Course, - CourseTime, - CourseParticipant, -) -from app.activity_utils import ( - changeActivityStatus, - notifyActivity, - create_participate_infos, - weekly_summary_orgs, ) from app.notification_utils import ( bulk_notification_create, @@ -84,48 +71,6 @@ def send_to_orgs(title, message, url='/index/'): ) -@periodical('interval', job_id='activityStatusUpdater', minutes=5) -def changeAllActivities(): - """ - 频繁执行,添加更新其他活动的定时任务,主要是为了异步调度 - 对于被多次落下的活动,每次更新一步状态 - """ - def next_time_generator(first: timedelta | datetime, step: timedelta): - while True: - yield first - first += step - now = datetime.now() - times = next_time_generator( - now + timedelta(seconds=20), timedelta(seconds=5)) - adder = MultipleAdder(changeActivityStatus) - - def _update_all(_cur, _next, activities): - for activity in activities: - adder.schedule(f'activity_{activity.id}_{_next}', - run_time=next(times))(activity.id, _cur, _next) - - applying_activities = Activity.objects.filter( - status=Activity.Status.APPLYING, - apply_end__lte=now, - ) - _update_all(Activity.Status.APPLYING, - Activity.Status.WAITING, applying_activities) - - waiting_activities = Activity.objects.filter( - status=Activity.Status.WAITING, - start__lte=now, - ) - _update_all(Activity.Status.WAITING, - Activity.Status.PROGRESSING, waiting_activities) - - progressing_activities = Activity.objects.filter( - status=Activity.Status.PROGRESSING, - end__lte=now, - ) - _update_all(Activity.Status.PROGRESSING, - Activity.Status.END, progressing_activities) - - @periodical('interval', job_id="get weather per hour", hours=1) def get_weather_async(): city = "Haidian" @@ -156,142 +101,6 @@ def get_weather() -> Dict[str, Any]: return json.load(f) -def add_week_course_activity(course_id: int, weektime_id: int, cur_week: int, course_stage2: bool): - """ - 添加每周的课程活动 - """ - course: Course = Course.objects.get(id=course_id) - examine_teacher = NaturalPerson.objects.get_teacher( - CONFIG.course.audit_teachers[0]) - # 当前课程在学期已举办的活动 - conducted_num = Activity.objects.activated().filter( - organization_id=course.organization, - category=Activity.ActivityCategory.COURSE).count() - # 发起活动,并设置报名 - with transaction.atomic(): - week_time = CourseTime.objects.select_for_update().get(id=weektime_id) - if week_time.cur_week != cur_week: - return False - start_time = week_time.start + timedelta(days=7 * cur_week) - end_time = week_time.end + timedelta(days=7 * cur_week) - activity = Activity.objects.create( - title=f'{course.name}-第{conducted_num+1}次课', - organization_id=course.organization, - examine_teacher=examine_teacher, - location=course.classroom, - start=start_time, - end=end_time, - category=Activity.ActivityCategory.COURSE, - ) - activity.status = Activity.Status.UNPUBLISHED - activity.publish_day = course.publish_day - if course.publish_day == Course.PublishDay.instant: - # 指定为立即发布的活动在上一周结束后一天发布 - activity.publish_time = week_time.end + \ - timedelta(days=7 * cur_week - 6) - else: - activity.publish_time = week_time.start + \ - timedelta(days=7 * cur_week - course.publish_day) - - activity.need_apply = course.need_apply # 是否需要报名 - - if course.need_apply: - activity.endbefore = Activity.EndBefore.onehour - activity.apply_end = activity.start - timedelta(hours=1) - - activity.need_checkin = True # 需要签到 - activity.recorded = True - activity.course_time = week_time - activity.introduction = f'{course.organization.oname}每周课程活动' - ActivityPhoto.objects.create(image=course.photo, - type=ActivityPhoto.PhotoType.ANNOUNCE, - activity=activity) - if not activity.need_apply: - # 选课人员自动报名活动 - # 选课结束以后,活动参与人员从小组成员获取 - person_pos = list(Position.objects.activated().filter( - org=course.organization).values_list("person", flat=True)) - if course_stage2: - # 如果处于补退选阶段,活动参与人员从课程选课情况获取 - selected_person = list(CourseParticipant.objects.filter( - course=course, - status=CourseParticipant.Status.SUCCESS, - ).values_list("person", flat=True)) - person_pos += selected_person - person_pos = list(set(person_pos)) - members = NaturalPerson.objects.filter( - id__in=person_pos) - status = Participation.AttendStatus.APPLYSUCCESS - create_participate_infos(activity, members, status=status) - - participate_num = len(person_pos) - activity.capacity = participate_num - activity.current_participants = participate_num - - week_time.cur_week += 1 - week_time.save() - activity.save() - # 在活动发布时通知参与成员,创建定时任务并修改活动状态 - changer = MultipleAdder(changeActivityStatus) - notifier = MultipleAdder(notifyActivity) - # TODO: 修改UNPUBLISHED状态的诡异逻辑和状态切换 - if activity.need_apply: - changer.schedule(f'activity_{activity.id}_{Activity.Status.APPLYING}', - run_time=activity.publish_time - )(activity.id, Activity.Status.UNPUBLISHED, Activity.Status.APPLYING) - changer.schedule(f'activity_{activity.id}_{Activity.Status.WAITING}', - run_time=activity.start - timedelta(hours=1) - )(activity.id, Activity.Status.APPLYING, Activity.Status.WAITING) - else: - changer.schedule(f'activity_{activity.id}_{Activity.Status.WAITING}', - run_time=activity.publish_time - )(activity.id, Activity.Status.UNPUBLISHED, Activity.Status.WAITING) - - notifier.schedule(f'activity_{activity.id}_newCourseActivity', - run_time=activity.publish_time)(activity.id, "newCourseActivity") - notifier.schedule(f'activity_{activity.id}_remind', - run_time=activity.start - timedelta(minutes=15))(activity.id, "remind") - changer.schedule(f'activity_{activity.id}_{Activity.Status.PROGRESSING}', - run_time=activity.start - )(activity.id, Activity.Status.WAITING, Activity.Status.PROGRESSING) - changer.schedule(f'activity_{activity.id}_{Activity.Status.END}', - run_time=activity.end - )(activity.id, Activity.Status.PROGRESSING, Activity.Status.END) - - notification_create( - receiver=examine_teacher.person_id, - sender=course.organization.get_user(), - typename=Notification.Type.NEEDDO, - title=Notification.Title.VERIFY_INFORM, - content="新增了一个已审批的课程活动", - URL=f"/examineActivity/{activity.id}", - relate_instance=activity, - to_wechat={"app": WechatApp.AUDIT, 'level': WechatMessageLevel.INFO}, - ) - - -@periodical('interval', 'courseWeeklyActivitylauncher', minutes=5) -def longterm_launch_course(): - """ - 定时发起长期课程活动 - 提前一周发出课程,一般是在本周课程活动结束时发出 - 本函数的循环不幂等,幂等通过课程活动创建函数的幂等实现 - """ - courses = Course.objects.activated().filter( - status__in=[Course.Status.SELECT_END, Course.Status.STAGE2]) - for course in courses: - for week_time in course.time_set.all(): - cur_week = week_time.cur_week - end_week = week_time.end_week - if cur_week < end_week: # end_week默认16周,允许助教修改 - # 在本周课程结束后生成下一周课程活动 - due_time = week_time.end + timedelta(days=7 * cur_week) - if due_time - timedelta(days=7) < datetime.now() < due_time: - # 如果处于补退选阶段: - course_stage2 = True if course.status == Course.Status.STAGE2 else False - add_week_course_activity( - course.id, week_time.id, cur_week, course_stage2) - @periodical('cron', 'active_score_updater', hour=1) def update_active_score_per_day(days=14): @@ -370,26 +179,3 @@ def happy_birthday(): Notification.Type.NEEDREAD, title, message, url, to_wechat=dict(level=WechatMessageLevel.IMPORTANT, show_source=False), ) - - -@script -@periodical('cron', 'weekly_activity_summary_reminder', hour=20, minute=0, day_of_week='sun') -def weekly_activity_summary_reminder(): - '''提醒组织负责人填写每周活动总结 - - 每周日晚上8点提醒所有组织负责人通过每周活动总结填写未在系统中申报的活动 - 目前仅限于团委,学学学委员会,学学学学会,学生会 - ''' - today = date.today() - cur_semester = current_semester() - if not cur_semester.start_date <= today <= cur_semester.end_date: - return - notify_orgs = weekly_summary_orgs() - sender = User.objects.get(username='zz00000') - for org in notify_orgs.select_related(SQ.f(Organization.organization_id)): - notification_create( - org.get_user(), sender, - Notification.Type.NEEDREAD, '每周活动总结提醒', - '如果本周举办了未在系统中申报的活动,请通过每周活动总结及时填报!', - to_wechat=dict(show_source=False), - ) diff --git a/app/migrations/0001_initial.py b/app/migrations/0001_initial.py index 327f9be8e..aa76924b1 100644 --- a/app/migrations/0001_initial.py +++ b/app/migrations/0001_initial.py @@ -1,550 +1,808 @@ -# Generated by Django 4.2.3 on 2023-10-18 18:25 +# Generated by Django 5.0.9 on 2024-11-16 15:13 import app.models -import datetime -from django.db import migrations, models import django.db.models.deletion import django_mysql.models +from django.conf import settings +from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name='AcademicQA', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('keywords', models.JSONField(blank=True, null=True, verbose_name='关键词')), - ('directed', models.BooleanField(default=False, verbose_name='是否定向')), - ('rating', models.IntegerField(default=0, verbose_name='评价')), - ], - options={ - 'verbose_name': 'P.学术问答', - 'verbose_name_plural': 'P.学术问答', - }, - ), - migrations.CreateModel( - name='AcademicQAAwards', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_undirected_qa', models.BooleanField(default=False, verbose_name='是否发起过非定向提问')), - ], - options={ - 'verbose_name': 'P.学术问答相关奖励', - 'verbose_name_plural': 'P.学术问答相关奖励', - }, - ), - migrations.CreateModel( - name='AcademicTag', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('atype', models.SmallIntegerField(choices=[(0, '主修专业'), (1, '辅修专业'), (2, '双学位专业'), (3, '参与项目')], verbose_name='标签类型')), - ('tag_content', models.CharField(max_length=63, verbose_name='标签内容')), - ], - options={ - 'verbose_name': 'P.学术地图标签', - 'verbose_name_plural': 'P.学术地图标签', - }, - ), - migrations.CreateModel( - name='AcademicTagEntry', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('status', models.SmallIntegerField(choices=[(0, '不公开'), (1, '待审核'), (2, '已公开'), (3, '已弃用')], verbose_name='记录状态')), - ], - options={ - 'verbose_name': 'P.学术地图标签项目', - 'verbose_name_plural': 'P.学术地图标签项目', - }, - ), - migrations.CreateModel( - name='AcademicTextEntry', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('status', models.SmallIntegerField(choices=[(0, '不公开'), (1, '待审核'), (2, '已公开'), (3, '已弃用')], verbose_name='记录状态')), - ('atype', models.SmallIntegerField(choices=[(0, '本科生科研'), (1, '挑战杯'), (2, '实习经历'), (3, '科研方向'), (4, '毕业去向')], verbose_name='类型')), - ('content', models.CharField(max_length=4095, verbose_name='内容')), - ], - options={ - 'verbose_name': 'P.学术地图文本项目', - 'verbose_name_plural': 'P.学术地图文本项目', - }, - ), - migrations.CreateModel( - name='ActivityPhoto', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('type', models.SmallIntegerField(choices=[(0, '预告图片'), (1, '总结图片')])), - ('image', models.ImageField(blank=True, null=True, upload_to='activity/photo/%Y/%m/', verbose_name='活动图片')), - ('time', models.DateTimeField(auto_now_add=True, verbose_name='上传时间')), - ], - options={ - 'verbose_name': '3.活动图片', - 'verbose_name_plural': '3.活动图片', - 'ordering': ['-time'], - }, - ), - migrations.CreateModel( - name='ActivitySummary', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('status', models.SmallIntegerField(choices=[(0, '待审核'), (1, '已通过'), (2, '已取消'), (3, '已拒绝')], default=0)), - ('image', models.ImageField(blank=True, null=True, upload_to='ActivitySummary/photo/%Y/%m/', verbose_name='活动总结图片')), - ('time', models.DateTimeField(auto_now_add=True, verbose_name='申请时间')), - ], - options={ - 'verbose_name': '3.活动总结', - 'verbose_name_plural': '3.活动总结', - 'ordering': ['-time'], - }, - ), - migrations.CreateModel( - name='Comment', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('text', models.TextField(blank=True, default='', verbose_name='文字内容')), - ('time', models.DateTimeField(auto_now_add=True, verbose_name='评论时间')), - ], - options={ - 'verbose_name': '2.评论', - 'verbose_name_plural': '2.评论', - 'ordering': ['-time'], - }, - ), - migrations.CreateModel( - name='CommentBase', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('typename', models.CharField(default='commentbase', max_length=32, verbose_name='模型类型')), - ('time', models.DateTimeField(auto_now_add=True, verbose_name='发起时间')), - ('modify_time', models.DateTimeField(auto_now=True, verbose_name='上次修改时间')), - ], - options={ - 'verbose_name': '2.带有评论', - 'verbose_name_plural': '2.带有评论', - }, - ), - migrations.CreateModel( - name='CommentPhoto', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('image', models.ImageField(blank=True, null=True, upload_to='comment/%Y/%m/', verbose_name='评论图片')), - ], - options={ - 'verbose_name': '2.评论图片', - 'verbose_name_plural': '2.评论图片', - }, - ), - migrations.CreateModel( - name='Course', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=60, verbose_name='课程名称')), - ('year', models.IntegerField(default=app.models.current_year, verbose_name='开课年份')), - ('semester', models.CharField(choices=[('Fall', '秋'), ('Spring', '春'), ('Fall+Spring', '春秋')], default=app.models.current_semester, max_length=15, verbose_name='开课学期')), - ('times', models.SmallIntegerField(default=16, verbose_name='课程开设周数')), - ('classroom', models.CharField(blank=True, default='', max_length=60, verbose_name='预期上课地点')), - ('teacher', models.CharField(blank=True, default='', max_length=48, verbose_name='授课教师')), - ('introduction', models.TextField(blank=True, default='这里暂时没有介绍哦~', verbose_name='课程简介')), - ('teaching_plan', models.TextField(blank=True, default='暂无', verbose_name='教学计划')), - ('record_cal_method', models.TextField(blank=True, default='暂无', verbose_name='学时计算方式')), - ('status', models.SmallIntegerField(choices=[(0, '已撤销'), (1, '未开始'), (2, '预选'), (3, '抽签中'), (4, '补退选'), (5, '选课结束'), (6, '已结束')], default=1, verbose_name='开课状态')), - ('type', models.SmallIntegerField(choices=[(0, '德'), (1, '智'), (2, '体'), (3, '美'), (4, '劳')], verbose_name='课程类型')), - ('capacity', models.IntegerField(default=100, verbose_name='课程容量')), - ('current_participants', models.IntegerField(default=0, verbose_name='当前选课人数')), - ('publish_day', models.SmallIntegerField(default=3, verbose_name='信息发布时间')), - ('need_apply', models.BooleanField(default=False, verbose_name='是否需要报名')), - ('photo', models.ImageField(blank=True, upload_to='course/photo/%Y/', verbose_name='宣传图片')), - ('QRcode', models.ImageField(blank=True, null=True, upload_to='course/QRcode/%Y/')), - ], - options={ - 'verbose_name': '4.书院课程', - 'verbose_name_plural': '4.书院课程', - 'ordering': ['id'], - }, - ), - migrations.CreateModel( - name='CourseParticipant', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('status', models.SmallIntegerField(choices=[(0, '已选课'), (1, '未选课'), (2, '选课成功'), (3, '选课失败')], default=1, verbose_name='选课状态')), - ], - options={ - 'verbose_name': '4.课程报名情况', - 'verbose_name_plural': '4.课程报名情况', - }, - ), - migrations.CreateModel( - name='CourseRecord', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('extra_name', models.CharField(blank=True, max_length=60, verbose_name='课程名称额外标注')), - ('year', models.IntegerField(default=app.models.current_year, verbose_name='课程所在学年')), - ('semester', models.CharField(choices=[('Fall', '秋'), ('Spring', '春'), ('Fall+Spring', '春秋')], default=app.models.current_semester, max_length=15, verbose_name='课程所在学期')), - ('total_hours', models.FloatField(verbose_name='总计参加学时')), - ('attend_times', models.IntegerField(default=0, verbose_name='参加课程次数')), - ('invalid', models.BooleanField(default=False, verbose_name='无效')), - ], - options={ - 'verbose_name': '4.学时表', - 'verbose_name_plural': '4.学时表', - }, - ), - migrations.CreateModel( - name='CourseTime', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('start', models.DateTimeField(verbose_name='开始时间')), - ('end', models.DateTimeField(verbose_name='结束时间')), - ('cur_week', models.IntegerField(default=0, verbose_name='已生成周数')), - ('end_week', models.IntegerField(default=16, verbose_name='总周数')), - ], - options={ - 'verbose_name': '4.上课时间', - 'verbose_name_plural': '4.上课时间', - 'ordering': ['start'], - }, - ), - migrations.CreateModel( - name='Freshman', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('sid', models.CharField(max_length=20, unique=True, verbose_name='学号')), - ('name', models.CharField(max_length=10, verbose_name='姓名')), - ('gender', models.CharField(max_length=5, verbose_name='性别')), - ('birthday', models.DateField(verbose_name='生日')), - ('place', models.CharField(blank=True, default='其它', max_length=32, verbose_name='生源地')), - ('grade', models.CharField(blank=True, max_length=5, null=True, verbose_name='年级')), - ('status', models.SmallIntegerField(choices=[(0, '未注册'), (1, '已注册')], default=0, verbose_name='注册状态')), - ], - options={ - 'verbose_name': '0.新生信息', - 'verbose_name_plural': '0.新生信息', - }, - ), - migrations.CreateModel( - name='Help', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=20, verbose_name='帮助标题')), - ('content', models.TextField(max_length=500, verbose_name='帮助内容')), - ], - options={ - 'verbose_name': '~A.页面帮助', - 'verbose_name_plural': '~A.页面帮助', - }, - ), - migrations.CreateModel( - name='ModifyRecord', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('usertype', models.CharField(blank=True, default='', max_length=16, verbose_name='用户类型')), - ('name', models.CharField(blank=True, default='', max_length=32, verbose_name='名称')), - ('info', models.TextField(blank=True, default='', verbose_name='相关信息')), - ('time', models.DateTimeField(auto_now_add=True, verbose_name='修改时间')), - ], - options={ - 'verbose_name': '~R.修改记录', - 'verbose_name_plural': '~R.修改记录', - 'ordering': ['-time'], - }, - ), - migrations.CreateModel( - name='NaturalPerson', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('stu_id_dbonly', models.CharField(blank=True, max_length=150, verbose_name='学号——仅数据库')), - ('name', models.CharField(max_length=10, verbose_name='姓名')), - ('nickname', models.CharField(blank=True, max_length=20, null=True, verbose_name='昵称')), - ('gender', models.SmallIntegerField(blank=True, choices=[(0, '男'), (1, '女')], null=True, verbose_name='性别')), - ('birthday', models.DateField(blank=True, null=True, verbose_name='生日')), - ('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='邮箱')), - ('telephone', models.CharField(blank=True, max_length=20, null=True, verbose_name='电话')), - ('biography', models.TextField(default='还没有填写哦~', max_length=1024, verbose_name='自我介绍')), - ('avatar', models.ImageField(blank=True, upload_to='avatar/')), - ('wallpaper', models.ImageField(blank=True, upload_to='wallpaper/')), - ('inform_share', models.BooleanField(default=True)), - ('last_time_login', models.DateTimeField(blank=True, null=True, verbose_name='上次登录时间')), - ('QRcode', models.ImageField(blank=True, upload_to='QRcode/')), - ('visit_times', models.IntegerField(default=0, verbose_name='浏览次数')), - ('identity', models.SmallIntegerField(choices=[(0, '教职工'), (1, '学生')], default=1, verbose_name='身份')), - ('stu_class', models.CharField(blank=True, max_length=5, null=True, verbose_name='班级')), - ('stu_major', models.CharField(blank=True, max_length=25, null=True, verbose_name='专业')), - ('stu_grade', models.CharField(blank=True, max_length=5, null=True, verbose_name='年级')), - ('stu_dorm', models.CharField(blank=True, max_length=6, null=True, verbose_name='宿舍')), - ('status', models.SmallIntegerField(choices=[(0, '未毕业'), (1, '已毕业')], default=0, verbose_name='在校状态')), - ('show_nickname', models.BooleanField(default=False)), - ('show_birthday', models.BooleanField(default=False)), - ('show_gender', models.BooleanField(default=True)), - ('show_email', models.BooleanField(default=False)), - ('show_tel', models.BooleanField(default=False)), - ('show_major', models.BooleanField(default=True)), - ('show_dorm', models.BooleanField(default=False)), - ('wechat_receive_level', models.IntegerField(choices=[(0, '接收全部消息'), (500, '仅重要通知')], default=0, help_text='允许微信接收的最低消息等级,更低等级的通知类消息将被屏蔽', verbose_name='微信接收等级')), - ('accept_promote', models.BooleanField(default=True)), - ('active_score', models.FloatField(default=0, verbose_name='活跃度')), - ], - options={ - 'verbose_name': '0.自然人', - 'verbose_name_plural': '0.自然人', - }, - ), - migrations.CreateModel( - name='Notification', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('status', models.SmallIntegerField(choices=[(0, '已处理'), (1, '待处理'), (2, '已删除')], default=1)), - ('title', models.CharField(blank=True, max_length=50, null=True, verbose_name='通知标题')), - ('content', models.TextField(blank=True, verbose_name='通知内容')), - ('start_time', models.DateTimeField(auto_now_add=True, verbose_name='通知发出时间')), - ('finish_time', models.DateTimeField(blank=True, null=True, verbose_name='通知处理时间')), - ('typename', models.SmallIntegerField(choices=[(0, '知晓类'), (1, '处理类')], default=0)), - ('URL', models.URLField(blank=True, max_length=1024, null=True, verbose_name='相关网址')), - ('bulk_identifier', models.CharField(db_index=True, default='', max_length=64, verbose_name='批量信息标识')), - ('anonymous_flag', models.BooleanField(default=False, verbose_name='是否匿名')), - ], - options={ - 'verbose_name': 'o.通知消息', - 'verbose_name_plural': 'o.通知消息', - 'ordering': ['id'], - }, - ), - migrations.CreateModel( - name='Organization', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('oname', models.CharField(max_length=32, unique=True)), - ('status', models.BooleanField(default=True, verbose_name='激活状态')), - ('introduction', models.TextField(blank=True, default='这里暂时没有介绍哦~', null=True, verbose_name='介绍')), - ('avatar', models.ImageField(blank=True, upload_to='avatar/')), - ('QRcode', models.ImageField(blank=True, upload_to='QRcode/')), - ('wallpaper', models.ImageField(blank=True, upload_to='wallpaper/')), - ('visit_times', models.IntegerField(default=0, verbose_name='浏览次数')), - ('inform_share', models.BooleanField(default=True)), - ], - options={ - 'verbose_name': '0.小组', - 'verbose_name_plural': '0.小组', - }, - ), - migrations.CreateModel( - name='OrganizationTag', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=10, verbose_name='标签名')), - ('color', models.CharField(choices=[('#C1C1C1', '灰色'), ('#DC143C', '红色'), ('#FFA500', '橙色'), ('#FFD700', '黄色'), ('#3CB371', '绿色'), ('#1E90FF', '蓝色'), ('#800080', '紫色'), ('#FF69B4', '粉色'), ('#DAA520', '棕色'), ('#8B4513', '咖啡色')], max_length=10, verbose_name='颜色')), - ], - options={ - 'verbose_name': '1.组织类型标签', - 'verbose_name_plural': '1.组织类型标签', - }, - ), - migrations.CreateModel( - name='OrganizationType', - fields=[ - ('otype_id', models.SmallIntegerField(primary_key=True, serialize=False, unique=True, verbose_name='小组类型编号')), - ('otype_name', models.CharField(max_length=25, verbose_name='小组类型名称')), - ('otype_superior_id', models.SmallIntegerField(default=0, verbose_name='上级小组类型编号')), - ('job_name_list', django_mysql.models.ListCharField(models.CharField(max_length=10), max_length=44, size=4)), - ('allow_unsubscribe', models.BooleanField(default=True, verbose_name='允许取关?')), - ('control_pos_threshold', models.SmallIntegerField(default=0, verbose_name='管理者权限等级上限')), - ], - options={ - 'verbose_name': '1.小组类型', - 'verbose_name_plural': '1.小组类型', - 'ordering': ['otype_name'], - }, - ), - migrations.CreateModel( - name='Participant', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('status', models.CharField(choices=[('申请中', 'Applying'), ('活动申请失败', 'Applyfailed'), ('已报名', 'Applysuccess'), ('已参与', 'Attended'), ('未签到', 'Unattended'), ('放弃', 'Canceled')], default='申请中', max_length=32, verbose_name='学生参与活动状态')), - ], - options={ - 'verbose_name': '3.活动参与情况', - 'verbose_name_plural': '3.活动参与情况', - 'ordering': ['activity_id'], - }, - ), - migrations.CreateModel( - name='Pool', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=50, verbose_name='名称')), - ('type', models.CharField(choices=[('兑换', '兑换奖池'), ('抽奖', '抽奖奖池'), ('盲盒', '盲盒奖池')], max_length=15, verbose_name='类型')), - ('entry_time', models.IntegerField(default=1, verbose_name='进入次数')), - ('ticket_price', models.IntegerField(default=0, verbose_name='抽奖费')), - ('start', models.DateTimeField(verbose_name='开始时间')), - ('end', models.DateTimeField(blank=True, null=True, verbose_name='结束时间')), - ('redeem_start', models.DateTimeField(blank=True, null=True, verbose_name='兑奖开始时间')), - ('redeem_end', models.DateTimeField(blank=True, null=True, verbose_name='兑奖结束时间')), - ], - options={ - 'verbose_name': '5.奖池', - 'verbose_name_plural': '5.奖池', - }, - ), - migrations.CreateModel( - name='PoolItem', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('origin_num', models.IntegerField(verbose_name='初始数量')), - ('consumed_num', models.IntegerField(default=0, verbose_name='已兑换')), - ('exchange_limit', models.IntegerField(default=0, verbose_name='单人兑换上限')), - ('exchange_price', models.IntegerField(blank=True, null=True, verbose_name='价格')), - ('is_big_prize', models.BooleanField(default=False, verbose_name='是否特别奖品')), - ], - options={ - 'verbose_name': '5.奖池奖品', - 'verbose_name_plural': '5.奖池奖品', - }, - ), - migrations.CreateModel( - name='PoolRecord', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('status', models.CharField(choices=[('抽奖中', '抽奖中'), ('未中奖', '未中奖'), ('未兑奖', '未兑奖'), ('已兑奖', '已兑奖'), ('已失效', '已失效')], max_length=15, verbose_name='状态')), - ('time', models.DateTimeField(auto_now_add=True, verbose_name='记录时间')), - ('redeem_time', models.DateTimeField(blank=True, null=True, verbose_name='兑奖时间')), - ], - options={ - 'verbose_name': '5.奖池记录', - 'verbose_name_plural': '5.奖池记录', - }, - ), - migrations.CreateModel( - name='Position', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('pos', models.SmallIntegerField(default=10, verbose_name='职务等级')), - ('is_admin', models.BooleanField(default=False, verbose_name='是否是负责人')), - ('show_post', models.BooleanField(default=True)), - ('year', models.IntegerField(default=app.models.current_year, verbose_name='当前学年')), - ('semester', models.CharField(choices=[('Fall', '秋'), ('Spring', '春'), ('Fall+Spring', '春秋')], default='Fall+Spring', max_length=15, verbose_name='当前学期')), - ('status', models.CharField(choices=[('在职', 'Inservice'), ('离职', 'Depart')], default='在职', max_length=32, verbose_name='职务状态')), - ], - options={ - 'verbose_name': '1.职务', - 'verbose_name_plural': '1.职务', - }, - ), - migrations.CreateModel( - name='Prize', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=50, verbose_name='名称')), - ('more_info', models.CharField(blank=True, default='', max_length=255, verbose_name='详情')), - ('stock', models.IntegerField(default=0, verbose_name='参考库存')), - ('reference_price', models.IntegerField(verbose_name='参考价格')), - ('image', models.ImageField(blank=True, null=True, upload_to='prize/%Y-%m/', verbose_name='图片')), - ], - options={ - 'verbose_name': '5.奖品', - 'verbose_name_plural': '5.奖品', - }, - ), - migrations.CreateModel( - name='Wishes', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('text', models.TextField(blank=True, default='', verbose_name='心愿内容')), - ('time', models.DateTimeField(auto_now_add=True, verbose_name='发布时间')), - ('background', models.TextField(default=app.models.Wishes.rand_color, verbose_name='颜色编码')), - ], - options={ - 'verbose_name': '~A.心愿', - 'verbose_name_plural': '~A.心愿', - 'ordering': ['-time'], - }, - ), - migrations.CreateModel( - name='Activity', - fields=[ - ('commentbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='app.commentbase')), - ('title', models.CharField(max_length=50, verbose_name='活动名称')), - ('year', models.IntegerField(default=app.models.current_year, verbose_name='活动年份')), - ('semester', models.CharField(choices=[('Fall', '秋'), ('Spring', '春'), ('Fall+Spring', '春秋')], default=app.models.current_semester, max_length=15, verbose_name='活动学期')), - ('publish_day', models.SmallIntegerField(default=3, verbose_name='信息发布提前时间')), - ('publish_time', models.DateTimeField(default=datetime.datetime.now, verbose_name='信息发布时间')), - ('need_apply', models.BooleanField(default=False, verbose_name='是否需要报名')), - ('endbefore', models.SmallIntegerField(choices=[(0, '一小时'), (1, '一天'), (2, '三天'), (3, '一周')], default=1, verbose_name='报名截止于')), - ('apply_end', models.DateTimeField(blank=True, default=datetime.datetime.now, verbose_name='报名截止时间')), - ('start', models.DateTimeField(blank=True, default=datetime.datetime.now, verbose_name='活动开始时间')), - ('end', models.DateTimeField(blank=True, default=datetime.datetime.now, verbose_name='活动结束时间')), - ('location', models.CharField(blank=True, max_length=200, verbose_name='活动地点')), - ('introduction', models.TextField(blank=True, max_length=225, verbose_name='活动简介')), - ('QRcode', models.ImageField(blank=True, upload_to='QRcode/')), - ('bidding', models.BooleanField(default=False, verbose_name='是否投点竞价')), - ('need_checkin', models.BooleanField(default=False, verbose_name='是否需要签到')), - ('visit_times', models.IntegerField(default=0, verbose_name='浏览次数')), - ('recorded', models.BooleanField(default=False, verbose_name='是否预报备')), - ('valid', models.BooleanField(default=False, verbose_name='是否已审核')), - ('inner', models.BooleanField(default=False, verbose_name='内部活动')), - ('capacity', models.IntegerField(default=100, verbose_name='活动最大参与人数')), - ('current_participants', models.IntegerField(default=0, verbose_name='活动当前报名人数')), - ('URL', models.URLField(blank=True, default='', max_length=1024, verbose_name='活动相关(推送)网址')), - ('status', models.CharField(choices=[('审核中', 'Reviewing'), ('已撤销', 'Abort'), ('未过审', 'Reject'), ('已取消', 'Canceled'), ('报名中', 'Applying'), ('待发布', 'Unpublished'), ('等待中', 'Waiting'), ('进行中', 'Progressing'), ('已结束', 'End')], default='审核中', max_length=32, verbose_name='活动状态')), - ('category', models.SmallIntegerField(choices=[(0, '普通活动'), (1, '课程活动')], default=0, verbose_name='活动类别')), - ], - options={ - 'verbose_name': '3.活动', - 'verbose_name_plural': '3.活动', - }, - bases=('app.commentbase',), - ), - migrations.CreateModel( - name='Chat', - fields=[ - ('commentbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='app.commentbase')), - ('title', models.CharField(default='', max_length=50, verbose_name='主题')), - ('questioner_anonymous', models.BooleanField(default=True, verbose_name='提问方是否匿名')), - ('respondent_anonymous', models.BooleanField(default=False, verbose_name='回答方是否匿名')), - ('status', models.SmallIntegerField(choices=[(0, '进行中'), (1, '已关闭')], default=0)), - ], - options={ - 'verbose_name': '2.对话', - 'verbose_name_plural': '2.对话', - }, - bases=('app.commentbase',), - ), - migrations.CreateModel( - name='ModifyOrganization', - fields=[ - ('commentbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='app.commentbase')), - ('oname', models.CharField(max_length=32)), - ('introduction', models.TextField(blank=True, default='这里暂时没有介绍哦~', null=True, verbose_name='介绍')), - ('application', models.TextField(blank=True, default='这里暂时还没写申请理由哦~', null=True, verbose_name='申请理由')), - ('avatar', models.ImageField(blank=True, default='avatar/org_default.png', null=True, upload_to='avatar/', verbose_name='小组头像')), - ('status', models.SmallIntegerField(choices=[(0, '审核中'), (1, '已通过'), (2, '已取消'), (3, '已拒绝')], default=0)), - ('tags', models.CharField(blank=True, default='', max_length=100)), - ], - options={ - 'verbose_name': '1.新建小组', - 'verbose_name_plural': '1.新建小组', - 'ordering': ['-modify_time', '-time'], - }, - bases=('app.commentbase',), - ), - migrations.CreateModel( - name='ModifyPosition', - fields=[ - ('commentbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='app.commentbase')), - ('pos', models.SmallIntegerField(blank=True, null=True, verbose_name='申请职务等级')), - ('reason', models.TextField(blank=True, default='这里暂时还没写申请理由哦~', null=True, verbose_name='申请理由')), - ('status', models.SmallIntegerField(choices=[(0, '审核中'), (1, '已通过'), (2, '已取消'), (3, '已拒绝')], default=0)), - ('apply_type', models.CharField(choices=[('加入小组', 'Join'), ('修改职位', 'Transfer'), ('退出小组', 'Withdraw')], max_length=32, verbose_name='申请类型')), - ], - options={ - 'verbose_name': '1.成员申请详情', - 'verbose_name_plural': '1.成员申请详情', - 'ordering': ['-modify_time', '-time'], - }, - bases=('app.commentbase',), + name="CommentBase", + fields=[ + ("id", models.AutoField(primary_key=True, serialize=False)), + ( + "typename", + models.CharField( + default="commentbase", max_length=32, verbose_name="模型类型" + ), + ), + ("time", models.DateTimeField(auto_now_add=True, verbose_name="发起时间")), + ( + "modify_time", + models.DateTimeField(auto_now=True, verbose_name="上次修改时间"), + ), + ], + options={ + "verbose_name": "2.带有评论", + "verbose_name_plural": "2.带有评论", + }, + ), + migrations.CreateModel( + name="Freshman", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "sid", + models.CharField(max_length=20, unique=True, verbose_name="学号"), + ), + ("name", models.CharField(max_length=10, verbose_name="姓名")), + ("gender", models.CharField(max_length=5, verbose_name="性别")), + ("birthday", models.DateField(verbose_name="生日")), + ( + "place", + models.CharField( + blank=True, default="其它", max_length=32, verbose_name="生源地" + ), + ), + ( + "grade", + models.CharField( + blank=True, max_length=5, null=True, verbose_name="年级" + ), + ), + ( + "status", + models.SmallIntegerField( + choices=[(0, "未注册"), (1, "已注册")], default=0, verbose_name="注册状态" + ), + ), + ], + options={ + "verbose_name": "0.新生信息", + "verbose_name_plural": "0.新生信息", + }, + ), + migrations.CreateModel( + name="Help", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=20, verbose_name="帮助标题")), + ("content", models.TextField(max_length=500, verbose_name="帮助内容")), + ], + options={ + "verbose_name": "~A.页面帮助", + "verbose_name_plural": "~A.页面帮助", + }, + ), + migrations.CreateModel( + name="OrganizationTag", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=10, verbose_name="标签名")), + ( + "color", + models.CharField( + choices=[ + ("#C1C1C1", "灰色"), + ("#DC143C", "红色"), + ("#FFA500", "橙色"), + ("#FFD700", "黄色"), + ("#3CB371", "绿色"), + ("#1E90FF", "蓝色"), + ("#800080", "紫色"), + ("#FF69B4", "粉色"), + ("#DAA520", "棕色"), + ("#8B4513", "咖啡色"), + ], + max_length=10, + verbose_name="颜色", + ), + ), + ], + options={ + "verbose_name": "1.组织类型标签", + "verbose_name_plural": "1.组织类型标签", + }, + ), + migrations.CreateModel( + name="Wishes", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("text", models.TextField(blank=True, default="", verbose_name="心愿内容")), + ("time", models.DateTimeField(auto_now_add=True, verbose_name="发布时间")), + ( + "background", + models.TextField( + default=app.models.Wishes.rand_color, verbose_name="颜色编码" + ), + ), + ], + options={ + "verbose_name": "~A.心愿", + "verbose_name_plural": "~A.心愿", + "ordering": ["-time"], + }, + ), + migrations.CreateModel( + name="Comment", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("text", models.TextField(blank=True, default="", verbose_name="文字内容")), + ("time", models.DateTimeField(auto_now_add=True, verbose_name="评论时间")), + ( + "commentator", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + verbose_name="评论者", + ), + ), + ( + "commentbase", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="comments", + to="app.commentbase", + ), + ), + ], + options={ + "verbose_name": "2.评论", + "verbose_name_plural": "2.评论", + "ordering": ["-time"], + }, + ), + migrations.CreateModel( + name="CommentPhoto", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "image", + models.ImageField( + blank=True, + null=True, + upload_to="comment/%Y/%m/", + verbose_name="评论图片", + ), + ), + ( + "comment", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="comment_photos", + to="app.comment", + ), + ), + ], + options={ + "verbose_name": "2.评论图片", + "verbose_name_plural": "2.评论图片", + }, + ), + migrations.CreateModel( + name="ModifyRecord", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "usertype", + models.CharField( + blank=True, default="", max_length=16, verbose_name="用户类型" + ), + ), + ( + "name", + models.CharField( + blank=True, default="", max_length=32, verbose_name="名称" + ), + ), + ("info", models.TextField(blank=True, default="", verbose_name="相关信息")), + ("time", models.DateTimeField(auto_now_add=True, verbose_name="修改时间")), + ( + "user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="modify_records", + to=settings.AUTH_USER_MODEL, + to_field="username", + ), + ), + ], + options={ + "verbose_name": "~R.修改记录", + "verbose_name_plural": "~R.修改记录", + "ordering": ["-time"], + }, + ), + migrations.CreateModel( + name="Notification", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "status", + models.SmallIntegerField( + choices=[(0, "已处理"), (1, "待处理"), (2, "已删除")], default=1 + ), + ), + ( + "title", + models.CharField( + blank=True, max_length=50, null=True, verbose_name="通知标题" + ), + ), + ("content", models.TextField(blank=True, verbose_name="通知内容")), + ( + "start_time", + models.DateTimeField(auto_now_add=True, verbose_name="通知发出时间"), + ), + ( + "finish_time", + models.DateTimeField(blank=True, null=True, verbose_name="通知处理时间"), + ), + ( + "typename", + models.SmallIntegerField( + choices=[(0, "知晓类"), (1, "处理类")], default=0 + ), + ), + ( + "URL", + models.URLField( + blank=True, max_length=1024, null=True, verbose_name="相关网址" + ), + ), + ( + "bulk_identifier", + models.CharField( + db_index=True, default="", max_length=64, verbose_name="批量信息标识" + ), + ), + ( + "anonymous_flag", + models.BooleanField(default=False, verbose_name="是否匿名"), + ), + ( + "receiver", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="recv_notice", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "relate_instance", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="relate_notifications", + to="app.commentbase", + ), + ), + ( + "sender", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="send_notice", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "verbose_name": "o.通知消息", + "verbose_name_plural": "o.通知消息", + "ordering": ["id"], + }, + ), + migrations.CreateModel( + name="Organization", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("oname", models.CharField(max_length=32, unique=True)), + ("status", models.BooleanField(default=True, verbose_name="激活状态")), + ( + "introduction", + models.TextField( + blank=True, default="这里暂时没有介绍哦~", null=True, verbose_name="介绍" + ), + ), + ("avatar", models.ImageField(blank=True, upload_to="avatar/")), + ("QRcode", models.ImageField(blank=True, upload_to="QRcode/")), + ("wallpaper", models.ImageField(blank=True, upload_to="wallpaper/")), + ("visit_times", models.IntegerField(default=0, verbose_name="浏览次数")), + ("inform_share", models.BooleanField(default=True)), + ( + "organization_id", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ("tags", models.ManyToManyField(to="app.organizationtag")), + ], + options={ + "verbose_name": "0.小组", + "verbose_name_plural": "0.小组", + }, + ), + migrations.CreateModel( + name="NaturalPerson", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "stu_id_dbonly", + models.CharField( + blank=True, max_length=150, verbose_name="学号——仅数据库" + ), + ), + ("name", models.CharField(max_length=10, verbose_name="姓名")), + ( + "nickname", + models.CharField( + blank=True, max_length=20, null=True, verbose_name="昵称" + ), + ), + ( + "gender", + models.SmallIntegerField( + blank=True, + choices=[(0, "男"), (1, "女")], + null=True, + verbose_name="性别", + ), + ), + ( + "birthday", + models.DateField(blank=True, null=True, verbose_name="生日"), + ), + ( + "email", + models.EmailField( + blank=True, max_length=254, null=True, verbose_name="邮箱" + ), + ), + ( + "telephone", + models.CharField( + blank=True, max_length=20, null=True, verbose_name="电话" + ), + ), + ( + "biography", + models.TextField( + default="还没有填写哦~", max_length=1024, verbose_name="自我介绍" + ), + ), + ("avatar", models.ImageField(blank=True, upload_to="avatar/")), + ("wallpaper", models.ImageField(blank=True, upload_to="wallpaper/")), + ("inform_share", models.BooleanField(default=True)), + ( + "last_time_login", + models.DateTimeField(blank=True, null=True, verbose_name="上次登录时间"), + ), + ("QRcode", models.ImageField(blank=True, upload_to="QRcode/")), + ("visit_times", models.IntegerField(default=0, verbose_name="浏览次数")), + ( + "identity", + models.SmallIntegerField( + choices=[(0, "教职工"), (1, "学生")], default=1, verbose_name="身份" + ), + ), + ( + "stu_class", + models.CharField( + blank=True, max_length=5, null=True, verbose_name="班级" + ), + ), + ( + "stu_major", + models.CharField( + blank=True, max_length=25, null=True, verbose_name="专业" + ), + ), + ( + "stu_grade", + models.CharField( + blank=True, max_length=5, null=True, verbose_name="年级" + ), + ), + ( + "stu_dorm", + models.CharField( + blank=True, max_length=6, null=True, verbose_name="宿舍" + ), + ), + ( + "status", + models.SmallIntegerField( + choices=[(0, "未毕业"), (1, "已毕业")], default=0, verbose_name="在校状态" + ), + ), + ("show_nickname", models.BooleanField(default=False)), + ("show_birthday", models.BooleanField(default=False)), + ("show_gender", models.BooleanField(default=True)), + ("show_email", models.BooleanField(default=False)), + ("show_tel", models.BooleanField(default=False)), + ("show_major", models.BooleanField(default=True)), + ("show_dorm", models.BooleanField(default=False)), + ( + "wechat_receive_level", + models.IntegerField( + choices=[(0, "接收全部消息"), (500, "仅重要通知")], + default=0, + help_text="允许微信接收的最低消息等级,更低等级的通知类消息将被屏蔽", + verbose_name="微信接收等级", + ), + ), + ("accept_promote", models.BooleanField(default=True)), + ("active_score", models.FloatField(default=0, verbose_name="活跃度")), + ( + "person_id", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "unsubscribe_list", + models.ManyToManyField( + db_index=True, + related_name="unsubscribers", + to="app.organization", + ), + ), + ], + options={ + "verbose_name": "0.自然人", + "verbose_name_plural": "0.自然人", + }, + ), + migrations.CreateModel( + name="OrganizationType", + fields=[ + ( + "otype_id", + models.SmallIntegerField( + primary_key=True, + serialize=False, + unique=True, + verbose_name="小组类型编号", + ), + ), + ("otype_name", models.CharField(max_length=25, verbose_name="小组类型名称")), + ( + "otype_superior_id", + models.SmallIntegerField(default=0, verbose_name="上级小组类型编号"), + ), + ( + "job_name_list", + django_mysql.models.ListCharField( + models.CharField(max_length=10), max_length=44, size=4 + ), + ), + ( + "allow_unsubscribe", + models.BooleanField(default=True, verbose_name="允许取关?"), + ), + ( + "control_pos_threshold", + models.SmallIntegerField(default=0, verbose_name="管理者权限等级上限"), + ), + ( + "incharge", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="incharge", + to="app.naturalperson", + ), + ), + ], + options={ + "verbose_name": "1.小组类型", + "verbose_name_plural": "1.小组类型", + "ordering": ["otype_name"], + }, + ), + migrations.AddField( + model_name="organization", + name="otype", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="app.organizationtype" + ), + ), + migrations.CreateModel( + name="Position", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("pos", models.SmallIntegerField(default=10, verbose_name="职务等级")), + ("is_admin", models.BooleanField(default=False, verbose_name="是否是负责人")), + ("show_post", models.BooleanField(default=True)), + ( + "year", + models.IntegerField( + default=app.models.current_year, verbose_name="当前学年" + ), + ), + ( + "semester", + models.CharField( + choices=[("Fall", "秋"), ("Spring", "春"), ("Fall+Spring", "春秋")], + default="Fall+Spring", + max_length=15, + verbose_name="当前学期", + ), + ), + ( + "status", + models.CharField( + choices=[("在职", "Inservice"), ("离职", "Depart")], + default="在职", + max_length=32, + verbose_name="职务状态", + ), + ), + ( + "org", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="position_set", + to="app.organization", + ), + ), + ( + "person", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="position_set", + to="app.naturalperson", + ), + ), + ], + options={ + "verbose_name": "1.职务", + "verbose_name_plural": "1.职务", + }, + ), + migrations.CreateModel( + name="ModifyOrganization", + fields=[ + ( + "commentbase_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="app.commentbase", + ), + ), + ("oname", models.CharField(max_length=32)), + ( + "introduction", + models.TextField( + blank=True, default="这里暂时没有介绍哦~", null=True, verbose_name="介绍" + ), + ), + ( + "application", + models.TextField( + blank=True, + default="这里暂时还没写申请理由哦~", + null=True, + verbose_name="申请理由", + ), + ), + ( + "avatar", + models.ImageField( + blank=True, + default="avatar/org_default.png", + null=True, + upload_to="avatar/", + verbose_name="小组头像", + ), + ), + ( + "status", + models.SmallIntegerField( + choices=[(0, "审核中"), (1, "已通过"), (2, "已取消"), (3, "已拒绝")], + default=0, + ), + ), + ("tags", models.CharField(blank=True, default="", max_length=100)), + ( + "otype", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="app.organizationtype", + ), + ), + ( + "pos", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "verbose_name": "1.新建小组", + "verbose_name_plural": "1.新建小组", + "ordering": ["-modify_time", "-time"], + }, + bases=("app.commentbase",), + ), + migrations.CreateModel( + name="ModifyPosition", + fields=[ + ( + "commentbase_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="app.commentbase", + ), + ), + ( + "pos", + models.SmallIntegerField( + blank=True, null=True, verbose_name="申请职务等级" + ), + ), + ( + "reason", + models.TextField( + blank=True, + default="这里暂时还没写申请理由哦~", + null=True, + verbose_name="申请理由", + ), + ), + ( + "status", + models.SmallIntegerField( + choices=[(0, "审核中"), (1, "已通过"), (2, "已取消"), (3, "已拒绝")], + default=0, + ), + ), + ( + "apply_type", + models.CharField( + choices=[ + ("加入小组", "Join"), + ("修改职位", "Transfer"), + ("退出小组", "Withdraw"), + ], + max_length=32, + verbose_name="申请类型", + ), + ), + ( + "org", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="position_application", + to="app.organization", + ), + ), + ( + "person", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="position_application", + to="app.naturalperson", + ), + ), + ], + options={ + "verbose_name": "1.成员申请详情", + "verbose_name_plural": "1.成员申请详情", + "ordering": ["-modify_time", "-time"], + }, + bases=("app.commentbase",), ), ] diff --git a/app/migrations/0002_initial.py b/app/migrations/0002_initial.py deleted file mode 100644 index 4d7d56aac..000000000 --- a/app/migrations/0002_initial.py +++ /dev/null @@ -1,243 +0,0 @@ -# Generated by Django 4.2.3 on 2023-10-18 18:25 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('app', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='prize', - name='provider', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='提供者'), - ), - migrations.AddField( - model_name='position', - name='org', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='position_set', to='app.organization'), - ), - migrations.AddField( - model_name='position', - name='person', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='position_set', to='app.naturalperson'), - ), - migrations.AddField( - model_name='poolrecord', - name='pool', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.pool', verbose_name='奖池'), - ), - migrations.AddField( - model_name='poolrecord', - name='prize', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='app.prize', verbose_name='奖品'), - ), - migrations.AddField( - model_name='poolrecord', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='用户'), - ), - migrations.AddField( - model_name='poolitem', - name='pool', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.pool', verbose_name='奖池'), - ), - migrations.AddField( - model_name='poolitem', - name='prize', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='app.prize', verbose_name='奖品'), - ), - migrations.AddField( - model_name='participant', - name='person_id', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.naturalperson'), - ), - migrations.AddField( - model_name='organizationtype', - name='incharge', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='incharge', to='app.naturalperson'), - ), - migrations.AddField( - model_name='organization', - name='organization_id', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='organization', - name='otype', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.organizationtype'), - ), - migrations.AddField( - model_name='organization', - name='tags', - field=models.ManyToManyField(to='app.organizationtag'), - ), - migrations.AddField( - model_name='notification', - name='receiver', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='recv_notice', to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='notification', - name='relate_instance', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='relate_notifications', to='app.commentbase'), - ), - migrations.AddField( - model_name='notification', - name='sender', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='send_notice', to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='naturalperson', - name='person_id', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='naturalperson', - name='unsubscribe_list', - field=models.ManyToManyField(db_index=True, related_name='unsubscribers', to='app.organization'), - ), - migrations.AddField( - model_name='modifyrecord', - name='user', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='modify_records', to=settings.AUTH_USER_MODEL, to_field='username'), - ), - migrations.AddField( - model_name='coursetime', - name='course', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='time_set', to='app.course'), - ), - migrations.AddField( - model_name='courserecord', - name='course', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='app.course'), - ), - migrations.AddField( - model_name='courserecord', - name='person', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.naturalperson'), - ), - migrations.AddField( - model_name='courseparticipant', - name='course', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='participant_set', to='app.course'), - ), - migrations.AddField( - model_name='courseparticipant', - name='person', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.naturalperson'), - ), - migrations.AddField( - model_name='course', - name='organization', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.organization', verbose_name='开课组织'), - ), - migrations.AddField( - model_name='commentphoto', - name='comment', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comment_photos', to='app.comment'), - ), - migrations.AddField( - model_name='comment', - name='commentator', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='评论者'), - ), - migrations.AddField( - model_name='comment', - name='commentbase', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='app.commentbase'), - ), - migrations.AddField( - model_name='academictextentry', - name='person', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.naturalperson'), - ), - migrations.AddField( - model_name='academictagentry', - name='person', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.naturalperson'), - ), - migrations.AddField( - model_name='academictagentry', - name='tag', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.academictag'), - ), - migrations.AddField( - model_name='academicqaawards', - name='user', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='participant', - name='activity_id', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.activity'), - ), - migrations.AddField( - model_name='modifyposition', - name='org', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='position_application', to='app.organization'), - ), - migrations.AddField( - model_name='modifyposition', - name='person', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='position_application', to='app.naturalperson'), - ), - migrations.AddField( - model_name='modifyorganization', - name='otype', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.organizationtype'), - ), - migrations.AddField( - model_name='modifyorganization', - name='pos', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='chat', - name='questioner', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='send_chat_set', to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='chat', - name='respondent', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='receive_chat_set', to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='activitysummary', - name='activity', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.activity'), - ), - migrations.AddField( - model_name='activityphoto', - name='activity', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='photos', to='app.activity'), - ), - migrations.AddField( - model_name='activity', - name='course_time', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='app.coursetime', verbose_name='课程每周活动时间'), - ), - migrations.AddField( - model_name='activity', - name='examine_teacher', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.naturalperson', verbose_name='审核老师'), - ), - migrations.AddField( - model_name='activity', - name='organization_id', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.organization'), - ), - migrations.AddField( - model_name='academicqa', - name='chat', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='app.chat'), - ), - ] diff --git a/app/migrations/0003_rename_activity_id_participant_activity_and_more.py b/app/migrations/0003_rename_activity_id_participant_activity_and_more.py deleted file mode 100644 index b5138abf6..000000000 --- a/app/migrations/0003_rename_activity_id_participant_activity_and_more.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 4.2.5 on 2023-10-18 19:00 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('app', '0002_initial'), - ] - - operations = [ - migrations.RenameField( - model_name='participant', - old_name='activity_id', - new_name='activity', - ), - migrations.RenameField( - model_name='participant', - old_name='person_id', - new_name='person', - ), - ] diff --git a/app/migrations/0004_rename_participant_participation.py b/app/migrations/0004_rename_participant_participation.py deleted file mode 100644 index c4a29b379..000000000 --- a/app/migrations/0004_rename_participant_participation.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 4.2.5 on 2023-10-18 19:08 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('app', '0003_rename_activity_id_participant_activity_and_more'), - ] - - operations = [ - migrations.RenameModel( - old_name='Participant', - new_name='Participation', - ), - ] diff --git a/app/migrations/0005_alter_participation_activity_and_more.py b/app/migrations/0005_alter_participation_activity_and_more.py deleted file mode 100644 index 6129a817e..000000000 --- a/app/migrations/0005_alter_participation_activity_and_more.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 4.2.5 on 2023-10-20 22:30 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('app', '0004_rename_participant_participation'), - ] - - operations = [ - migrations.AlterField( - model_name='participation', - name='activity', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='app.activity'), - ), - migrations.AlterField( - model_name='participation', - name='person', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='app.naturalperson'), - ), - ] diff --git a/app/migrations/0006_pool_empty_yqpoint_compensation_lowerbound_and_more.py b/app/migrations/0006_pool_empty_yqpoint_compensation_lowerbound_and_more.py deleted file mode 100644 index bc5f46fe7..000000000 --- a/app/migrations/0006_pool_empty_yqpoint_compensation_lowerbound_and_more.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 5.0.1 on 2024-01-30 10:29 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("app", "0005_alter_participation_activity_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="pool", - name="empty_YQPoint_compensation_lowerbound", - field=models.IntegerField(default=0, verbose_name="空盒元气值补偿下限"), - ), - migrations.AddField( - model_name="pool", - name="empty_YQPoint_compensation_upperbound", - field=models.IntegerField(default=0, verbose_name="空盒元气值补偿上限"), - ), - migrations.AddField( - model_name="poolitem", - name="is_empty_prize", - field=models.BooleanField(default=False, verbose_name="是否空盒"), - ), - ] diff --git a/app/migrations/0007_poolitem_exchange_attributes_poolrecord_attributes.py b/app/migrations/0007_poolitem_exchange_attributes_poolrecord_attributes.py deleted file mode 100644 index 0ea977c82..000000000 --- a/app/migrations/0007_poolitem_exchange_attributes_poolrecord_attributes.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 5.0.1 on 2024-08-27 20:42 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("app", "0006_pool_empty_yqpoint_compensation_lowerbound_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="poolitem", - name="exchange_attributes", - field=models.JSONField(default=list, verbose_name="属性"), - ), - migrations.AddField( - model_name="poolrecord", - name="attributes", - field=models.JSONField(default=dict, verbose_name="属性"), - ), - ] diff --git a/app/migrations/0008_pool_activity_alter_poolitem_exchange_attributes_and_more.py b/app/migrations/0008_pool_activity_alter_poolitem_exchange_attributes_and_more.py deleted file mode 100644 index 210287d63..000000000 --- a/app/migrations/0008_pool_activity_alter_poolitem_exchange_attributes_and_more.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 5.0.1 on 2024-08-31 15:14 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("app", "0007_poolitem_exchange_attributes_poolrecord_attributes"), - ] - - operations = [ - migrations.AddField( - model_name="pool", - name="activity", - field=models.ForeignKey( - blank=True, - default=None, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="app.activity", - ), - ), - migrations.AlterField( - model_name="poolitem", - name="exchange_attributes", - field=models.JSONField(blank=True, default=list, verbose_name="属性"), - ), - migrations.AlterField( - model_name="poolrecord", - name="attributes", - field=models.JSONField(blank=True, default=dict, verbose_name="属性"), - ), - ] diff --git a/app/models.py b/app/models.py index 54efa978b..c5f9bcdd5 100644 --- a/app/models.py +++ b/app/models.py @@ -64,11 +64,7 @@ 'Semester', 'Organization', 'Position', - 'Course', 'CommentBase', - 'Activity', - 'ActivityPhoto', - 'Participation', 'Notification', 'Comment', 'CommentPhoto', @@ -77,22 +73,6 @@ 'Help', 'Wishes', 'ModifyRecord', - 'Course', - 'CourseTime', - 'CourseParticipant', - 'CourseRecord', - 'AcademicTag', - 'AcademicEntry', - 'AcademicTagEntry', - 'AcademicTextEntry', - 'AcademicQA', - 'AcademicQAAwards', - 'Chat', - 'Prize', - 'Pool', - 'PoolItem', - 'PoolRecord', - 'ActivitySummary', ] @@ -731,71 +711,71 @@ def get_pos_number(self): # 返回对应的pos number 并作超出处理 return min(len(self.org.otype.job_name_list), self.pos) -class ActivityManager(models.Manager['Activity']): - def activated(self, only_displayable=True, noncurrent=False): - # 选择学年相同,并且学期相同或者覆盖的 - # 请保证query_range是一个queryset,将manager的行为包装在query_range计算完之前 - if only_displayable: - query_range = self.displayable() - else: - query_range = self.all() - return select_current(query_range, noncurrent=noncurrent) - - def displayable(self): - # REVIEWING, ABORT 状态的活动,只对创建者和审批者可见,对其他人不可见 - # 过审后被取消的活动,还是可能被看到,也应该让学生看到这个活动被取消了 - return self.exclude(status__in=[ - Activity.Status.REVIEWING, - # Activity.Status.CANCELED, - Activity.Status.ABORT, - Activity.Status.REJECT - ]) - - def get_newlyended_activity(self): - # 一周内结束的活动 - nowtime = datetime.now() - mintime = nowtime - timedelta(days=7) - return select_current( - self.filter(end__gt=mintime, status=Activity.Status.END)) - - def get_recent_activity(self): - # 开始时间在前后一周内,除了取消和审核中的活动。按时间逆序排序 - nowtime = datetime.now() - mintime = nowtime - timedelta(days=7) - maxtime = nowtime + timedelta(days=7) - return select_current(self.filter( - start__gt=mintime, - start__lt=maxtime, - status__in=[ - Activity.Status.APPLYING, - Activity.Status.WAITING, - Activity.Status.PROGRESSING, - Activity.Status.END - ], - )).order_by("category", "-start") - - def get_newlyreleased_activity(self): - nowtime = datetime.now() - return select_current(self.filter( - publish_time__gt=nowtime - timedelta(days=7), - status__in=[ - Activity.Status.APPLYING, - Activity.Status.WAITING, - Activity.Status.PROGRESSING - ], - )).order_by("category", "-publish_time") - - def get_today_activity(self): - # 开始时间在今天的活动,且不展示结束的活动。按开始时间由近到远排序 - nowtime = datetime.now() - return self.filter( - status__in=[ - Activity.Status.APPLYING, - Activity.Status.WAITING, - Activity.Status.PROGRESSING, - ] - ).filter(start__date=nowtime.date(), - ).order_by("start") +# class ActivityManager(models.Manager['Activity']): +# def activated(self, only_displayable=True, noncurrent=False): +# # 选择学年相同,并且学期相同或者覆盖的 +# # 请保证query_range是一个queryset,将manager的行为包装在query_range计算完之前 +# if only_displayable: +# query_range = self.displayable() +# else: +# query_range = self.all() +# return select_current(query_range, noncurrent=noncurrent) + +# def displayable(self): +# # REVIEWING, ABORT 状态的活动,只对创建者和审批者可见,对其他人不可见 +# # 过审后被取消的活动,还是可能被看到,也应该让学生看到这个活动被取消了 +# return self.exclude(status__in=[ +# Activity.Status.REVIEWING, +# # Activity.Status.CANCELED, +# Activity.Status.ABORT, +# Activity.Status.REJECT +# ]) + +# def get_newlyended_activity(self): +# # 一周内结束的活动 +# nowtime = datetime.now() +# mintime = nowtime - timedelta(days=7) +# return select_current( +# self.filter(end__gt=mintime, status=Activity.Status.END)) + +# def get_recent_activity(self): +# # 开始时间在前后一周内,除了取消和审核中的活动。按时间逆序排序 +# nowtime = datetime.now() +# mintime = nowtime - timedelta(days=7) +# maxtime = nowtime + timedelta(days=7) +# return select_current(self.filter( +# start__gt=mintime, +# start__lt=maxtime, +# status__in=[ +# Activity.Status.APPLYING, +# Activity.Status.WAITING, +# Activity.Status.PROGRESSING, +# Activity.Status.END +# ], +# )).order_by("category", "-start") + +# def get_newlyreleased_activity(self): +# nowtime = datetime.now() +# return select_current(self.filter( +# publish_time__gt=nowtime - timedelta(days=7), +# status__in=[ +# Activity.Status.APPLYING, +# Activity.Status.WAITING, +# Activity.Status.PROGRESSING +# ], +# )).order_by("category", "-publish_time") + +# def get_today_activity(self): +# # 开始时间在今天的活动,且不展示结束的活动。按开始时间由近到远排序 +# nowtime = datetime.now() +# return self.filter( +# status__in=[ +# Activity.Status.APPLYING, +# Activity.Status.WAITING, +# Activity.Status.PROGRESSING, +# ] +# ).filter(start__date=nowtime.date(), +# ).order_by("start") class CommentBase(models.Model): @@ -867,269 +847,266 @@ def get_instance(self): return self -class Activity(CommentBase): - class Meta: - verbose_name = "3.活动" - verbose_name_plural = verbose_name - - """ - Jul 30晚, Activity类经历了较大的更新, 请阅读群里[活动发起逻辑]文档,看一下活动发起需要用到的变量 - (1) 删除是否允许改变价格, 直接允许价格变动, 取消政策见文档【不允许投点的价格变动】 - (2) 取消活动报名时间的填写, 改为选择在活动结束前多久结束报名,选项见EndBefore - (3) 活动容量[capacity]允许是正无穷 - (4) 增加活动状态类, 恢复之前的活动状态记录方式, 通过定时任务来改变 #TODO - (5) 除了定价方式[bidding]之外的量都可以改变, 其中[capicity]不能低于目前已经报名人数, 活动的开始时间不能早于当前时间+1h - (6) 修改活动时间同步导致报名时间的修改, 当然也需要考虑EndBefore的修改; 这部分修改通过定时任务的时间体现, 详情请见地下室schedule任务的新建和取消 - (7) 增加活动立项的接口, activated, 筛选出这个学期的活动(见class [ActivityManager]) - """ - - title = models.CharField("活动名称", max_length=50) - organization_id = models.ForeignKey( - Organization, - on_delete=models.CASCADE, - ) - - year = models.IntegerField("活动年份", default=current_year) - - semester = models.CharField( - "活动学期", - choices=Semester.choices, - max_length=15, - default=current_semester, - ) - - class PublishDay(models.IntegerChoices): - instant = (0, "立即发布") - oneday = (1, "提前一天") - twoday = (2, "提前两天") - threeday = (3, "提前三天") - - publish_day = models.SmallIntegerField( - "信息发布提前时间", default=PublishDay.threeday) # 默认为提前三天时间 - publish_time = models.DateTimeField( - "信息发布时间", default=datetime.now) # 默认为当前时间,可以被覆盖 - need_apply = models.BooleanField("是否需要报名", default=False) - - # 删除显示报名时间, 保留一个字段表示报名截止于活动开始前多久:1h / 1d / 3d / 7d - class EndBefore(models.IntegerChoices): - onehour = (0, "一小时") - oneday = (1, "一天") - threeday = (2, "三天") - oneweek = (3, "一周") - - class EndBeforeHours: - prepare_times = [1, 24, 72, 168] - - # TODO: 修改默认报名截止时间为活动开始前(5分钟) - endbefore = models.SmallIntegerField( - "报名截止于", choices=EndBefore.choices, default=EndBefore.oneday - ) - - apply_end = models.DateTimeField( - "报名截止时间", blank=True, default=datetime.now) - start = models.DateTimeField("活动开始时间", blank=True, default=datetime.now) - end = models.DateTimeField("活动结束时间", blank=True, default=datetime.now) - # prepare_time = models.FloatField("活动准备小时数", default=24.0) - # apply_start = models.DateTimeField("报名开始时间", blank=True, default=datetime.now) - - location = models.CharField("活动地点", blank=True, max_length=200) - introduction = models.TextField("活动简介", max_length=225, blank=True) - - QRcode = models.ImageField(upload_to=f"QRcode/", blank=True) # 二维码字段 - - # url,活动二维码 - - bidding = models.BooleanField("是否投点竞价", default=False) - - need_checkin = models.BooleanField("是否需要签到", default=False) - - visit_times = models.IntegerField("浏览次数", default=0) - - examine_teacher = models.ForeignKey( - NaturalPerson, on_delete=models.CASCADE, verbose_name="审核老师") - # recorded 其实是冗余,但用着方便,存了吧,activity_show.html用到了 - recorded = models.BooleanField("是否预报备", default=False) - valid = models.BooleanField("是否已审核", default=False) - - inner = models.BooleanField("内部活动", default=False) - - # 允许是正无穷, 可以考虑用INTINF - capacity = models.IntegerField("活动最大参与人数", default=100) - current_participants = models.IntegerField("活动当前报名人数", default=0) - - URL = models.URLField("活动相关(推送)网址", max_length=1024, - default="", blank=True) - - def __str__(self): - return str(self.title) - - class Status(models.TextChoices): - REVIEWING = "审核中" - ABORT = "已撤销" - REJECT = "未过审" - CANCELED = "已取消" - APPLYING = "报名中" - UNPUBLISHED = "待发布" - WAITING = "等待中" - PROGRESSING = "进行中" - END = "已结束" - - # 恢复活动状态的类别 - status = models.CharField( - "活动状态", choices=Status.choices, default=Status.REVIEWING, max_length=32 - ) - - objects: ActivityManager = ActivityManager() - - class ActivityCategory(models.IntegerChoices): - NORMAL = (0, "普通活动") - COURSE = (1, "课程活动") - - category = models.SmallIntegerField( - "活动类别", choices=ActivityCategory.choices, default=0 - ) - course_time = models.ForeignKey("CourseTime", on_delete=models.SET_NULL, - null=True, blank=True, - verbose_name="课程每周活动时间") - - def save(self, *args, **kwargs): - self.typename = "activity" - super().save(*args, **kwargs) - - def related_job_ids(self): - jobids = [] - try: - jobids.append(f'activity_{self.id}_remind') - jobids.append(f'activity_{self.id}_{Activity.Status.APPLYING}') - jobids.append(f'activity_{self.id}_{Activity.Status.WAITING}') - jobids.append(f'activity_{self.id}_{Activity.Status.PROGRESSING}') - jobids.append(f'activity_{self.id}_{Activity.Status.END}') - except: - pass - return jobids - - def popular_level(self, any_status=False): - if not any_status and not self.status in [ - Activity.Status.WAITING, - Activity.Status.PROGRESSING, - Activity.Status.END, - ]: - return 0 - if self.current_participants >= self.capacity: - return 2 - if (self.current_participants >= 30 - or (self.capacity >= 10 and self.current_participants >= self.capacity * 0.85) - ): - return 1 - return 0 - - def has_tag(self): - if self.need_checkin or self.inner: - return True - if self.status == Activity.Status.APPLYING: - return True - if self.popular_level(): - return True - return False - - def eval_point(self) -> int: - '''计算价值的活动积分''' - # TODO: 添加到模型字段,固定每个活动的积分 - hours = (self.end - self.start).seconds / 3600 - if hours > CONFIG.yqpoint.activity.invalid_hour: - return 0 - point = ceil(CONFIG.yqpoint.activity.per_hour * hours) - # 单次活动记录的积分上限,默认无上限 - if CONFIG.yqpoint.activity.max is not None: - point = min(CONFIG.yqpoint.activity.max, point) - return point - - @transaction.atomic - def settle_yqpoint(self, status: Status | None = None, point: int | None = None): - '''结算活动积分,应仅在活动结束时调用''' - if status is None: - status = self.status # type: ignore - assert status == Activity.Status.END, "活动未结束,不能结算积分" - if point is None: - point = self.eval_point() - assert point >= 0, "活动积分不能为负" - # 活动积分为0时,不记录 - if point == 0: - return - - self = Activity.objects.select_for_update().get(pk=self.pk) - participation = SQ.sfilter(Participation.activity, self).filter( - status=Participation.AttendStatus.ATTENDED) - participant_ids = SQ.qsvlist(participation, - Participation.person, NaturalPerson.person_id) - participants = User.objects.filter(id__in=participant_ids) - User.objects.bulk_increase_YQPoint( - participants, point, "参加活动", YQPointRecord.SourceType.ACTIVITY) - - -class ActivityPhoto(models.Model): - class Meta: - verbose_name = "3.活动图片" - verbose_name_plural = verbose_name - ordering = ["-time"] - - class PhotoType(models.IntegerChoices): - ANNOUNCE = (0, "预告图片") - SUMMARY = (1, "总结图片") - - type = models.SmallIntegerField(choices=PhotoType.choices) - image = models.ImageField( - upload_to=f"activity/photo/%Y/%m/", verbose_name=u'活动图片', null=True, blank=True) - activity = models.ForeignKey( - Activity, related_name="photos", on_delete=models.CASCADE) - activity_id: int - time = models.DateTimeField("上传时间", auto_now_add=True) - - def get_image_path(self): - return image_url(self.image, enable_abs=True) - - -class ParticipationManager(models.Manager['Participation']): - def activated(self, no_unattend=False): - '''返回成功报名的参与信息''' - exclude_status = [ - Participation.AttendStatus.CANCELED, - Participation.AttendStatus.APPLYFAILED, - ] - if no_unattend: - exclude_status.append(Participation.AttendStatus.UNATTENDED) - return self.exclude(status__in=exclude_status) - - -class Participation(models.Model): - class Meta: - verbose_name = "3.活动参与情况" - verbose_name_plural = verbose_name - ordering = ["activity_id"] - - activity = models.ForeignKey(Activity, on_delete=models.CASCADE, related_name='+') - person = models.ForeignKey(NaturalPerson, on_delete=models.CASCADE, related_name='+') - - @necessary_for_frontend(person) - def get_participant(self): - '''供前端使用,追踪该字段的函数''' - return self.person - - class AttendStatus(models.TextChoices): - APPLYING = "申请中" - APPLYFAILED = "活动申请失败" - APPLYSUCCESS = "已报名" - ATTENDED = "已参与" - UNATTENDED = "未签到" - CANCELED = "放弃" - - status = models.CharField( - "学生参与活动状态", - choices=AttendStatus.choices, - default=AttendStatus.APPLYING, - max_length=32, - ) - objects: ParticipationManager = ParticipationManager() +# class Activity(CommentBase): +# class Meta: +# verbose_name = "3.活动" +# verbose_name_plural = verbose_name + +# """ +# Jul 30晚, Activity类经历了较大的更新, 请阅读群里[活动发起逻辑]文档,看一下活动发起需要用到的变量 +# (1) 删除是否允许改变价格, 直接允许价格变动, 取消政策见文档【不允许投点的价格变动】 +# (2) 取消活动报名时间的填写, 改为选择在活动结束前多久结束报名,选项见EndBefore +# (3) 活动容量[capacity]允许是正无穷 +# (4) 增加活动状态类, 恢复之前的活动状态记录方式, 通过定时任务来改变 #TODO +# (5) 除了定价方式[bidding]之外的量都可以改变, 其中[capicity]不能低于目前已经报名人数, 活动的开始时间不能早于当前时间+1h +# (6) 修改活动时间同步导致报名时间的修改, 当然也需要考虑EndBefore的修改; 这部分修改通过定时任务的时间体现, 详情请见地下室schedule任务的新建和取消 +# (7) 增加活动立项的接口, activated, 筛选出这个学期的活动(见class [ActivityManager]) +# """ + +# title = models.CharField("活动名称", max_length=50) +# organization_id = models.ForeignKey( +# Organization, +# on_delete=models.CASCADE, +# ) + +# year = models.IntegerField("活动年份", default=current_year) + +# semester = models.CharField( +# "活动学期", +# choices=Semester.choices, +# max_length=15, +# default=current_semester, +# ) + +# class PublishDay(models.IntegerChoices): +# instant = (0, "立即发布") +# oneday = (1, "提前一天") +# twoday = (2, "提前两天") +# threeday = (3, "提前三天") + +# publish_day = models.SmallIntegerField( +# "信息发布提前时间", default=PublishDay.threeday) # 默认为提前三天时间 +# publish_time = models.DateTimeField( +# "信息发布时间", default=datetime.now) # 默认为当前时间,可以被覆盖 +# need_apply = models.BooleanField("是否需要报名", default=False) + +# # 删除显示报名时间, 保留一个字段表示报名截止于活动开始前多久:1h / 1d / 3d / 7d +# class EndBefore(models.IntegerChoices): +# onehour = (0, "一小时") +# oneday = (1, "一天") +# threeday = (2, "三天") +# oneweek = (3, "一周") + +# class EndBeforeHours: +# prepare_times = [1, 24, 72, 168] + +# # TODO: 修改默认报名截止时间为活动开始前(5分钟) +# endbefore = models.SmallIntegerField( +# "报名截止于", choices=EndBefore.choices, default=EndBefore.oneday +# ) + +# apply_end = models.DateTimeField( +# "报名截止时间", blank=True, default=datetime.now) +# start = models.DateTimeField("活动开始时间", blank=True, default=datetime.now) +# end = models.DateTimeField("活动结束时间", blank=True, default=datetime.now) +# # prepare_time = models.FloatField("活动准备小时数", default=24.0) +# # apply_start = models.DateTimeField("报名开始时间", blank=True, default=datetime.now) + +# location = models.CharField("活动地点", blank=True, max_length=200) +# introduction = models.TextField("活动简介", max_length=225, blank=True) + +# QRcode = models.ImageField(upload_to=f"QRcode/", blank=True) # 二维码字段 + +# # url,活动二维码 + +# bidding = models.BooleanField("是否投点竞价", default=False) + +# need_checkin = models.BooleanField("是否需要签到", default=False) + +# visit_times = models.IntegerField("浏览次数", default=0) + +# examine_teacher = models.ForeignKey( +# NaturalPerson, on_delete=models.CASCADE, verbose_name="审核老师") +# # recorded 其实是冗余,但用着方便,存了吧,activity_show.html用到了 +# recorded = models.BooleanField("是否预报备", default=False) +# valid = models.BooleanField("是否已审核", default=False) + +# inner = models.BooleanField("内部活动", default=False) + +# # 允许是正无穷, 可以考虑用INTINF +# capacity = models.IntegerField("活动最大参与人数", default=100) +# current_participants = models.IntegerField("活动当前报名人数", default=0) + +# URL = models.URLField("活动相关(推送)网址", max_length=1024, +# default="", blank=True) + +# def __str__(self): +# return str(self.title) + +# class Status(models.TextChoices): +# REVIEWING = "审核中" +# ABORT = "已撤销" +# REJECT = "未过审" +# CANCELED = "已取消" +# APPLYING = "报名中" +# UNPUBLISHED = "待发布" +# WAITING = "等待中" +# PROGRESSING = "进行中" +# END = "已结束" + +# # 恢复活动状态的类别 +# status = models.CharField( +# "活动状态", choices=Status.choices, default=Status.REVIEWING, max_length=32 +# ) + +# objects: ActivityManager = ActivityManager() + +# class ActivityCategory(models.IntegerChoices): +# NORMAL = (0, "普通活动") +# COURSE = (1, "课程活动") + +# category = models.SmallIntegerField( +# "活动类别", choices=ActivityCategory.choices, default=0 +# ) + +# def save(self, *args, **kwargs): +# self.typename = "activity" +# super().save(*args, **kwargs) + +# def related_job_ids(self): +# jobids = [] +# try: +# jobids.append(f'activity_{self.id}_remind') +# jobids.append(f'activity_{self.id}_{Activity.Status.APPLYING}') +# jobids.append(f'activity_{self.id}_{Activity.Status.WAITING}') +# jobids.append(f'activity_{self.id}_{Activity.Status.PROGRESSING}') +# jobids.append(f'activity_{self.id}_{Activity.Status.END}') +# except: +# pass +# return jobids + +# def popular_level(self, any_status=False): +# if not any_status and not self.status in [ +# Activity.Status.WAITING, +# Activity.Status.PROGRESSING, +# Activity.Status.END, +# ]: +# return 0 +# if self.current_participants >= self.capacity: +# return 2 +# if (self.current_participants >= 30 +# or (self.capacity >= 10 and self.current_participants >= self.capacity * 0.85) +# ): +# return 1 +# return 0 + +# def has_tag(self): +# if self.need_checkin or self.inner: +# return True +# if self.status == Activity.Status.APPLYING: +# return True +# if self.popular_level(): +# return True +# return False + +# def eval_point(self) -> int: +# '''计算价值的活动积分''' +# # TODO: 添加到模型字段,固定每个活动的积分 +# hours = (self.end - self.start).seconds / 3600 +# if hours > CONFIG.yqpoint.activity.invalid_hour: +# return 0 +# point = ceil(CONFIG.yqpoint.activity.per_hour * hours) +# # 单次活动记录的积分上限,默认无上限 +# if CONFIG.yqpoint.activity.max is not None: +# point = min(CONFIG.yqpoint.activity.max, point) +# return point + +# @transaction.atomic +# def settle_yqpoint(self, status: Status | None = None, point: int | None = None): +# '''结算活动积分,应仅在活动结束时调用''' +# if status is None: +# status = self.status # type: ignore +# assert status == Activity.Status.END, "活动未结束,不能结算积分" +# if point is None: +# point = self.eval_point() +# assert point >= 0, "活动积分不能为负" +# # 活动积分为0时,不记录 +# if point == 0: +# return + +# self = Activity.objects.select_for_update().get(pk=self.pk) +# participation = SQ.sfilter(Participation.activity, self).filter( +# status=Participation.AttendStatus.ATTENDED) +# participant_ids = SQ.qsvlist(participation, +# Participation.person, NaturalPerson.person_id) +# participants = User.objects.filter(id__in=participant_ids) +# User.objects.bulk_increase_YQPoint( +# participants, point, "参加活动", YQPointRecord.SourceType.ACTIVITY) + + +# class ActivityPhoto(models.Model): +# class Meta: +# verbose_name = "3.活动图片" +# verbose_name_plural = verbose_name +# ordering = ["-time"] + +# class PhotoType(models.IntegerChoices): +# ANNOUNCE = (0, "预告图片") +# SUMMARY = (1, "总结图片") + +# type = models.SmallIntegerField(choices=PhotoType.choices) +# image = models.ImageField( +# upload_to=f"activity/photo/%Y/%m/", verbose_name=u'活动图片', null=True, blank=True) +# activity = models.ForeignKey( +# Activity, related_name="photos", on_delete=models.CASCADE) +# activity_id: int +# time = models.DateTimeField("上传时间", auto_now_add=True) + +# def get_image_path(self): +# return image_url(self.image, enable_abs=True) + + +# class ParticipationManager(models.Manager['Participation']): +# def activated(self, no_unattend=False): +# '''返回成功报名的参与信息''' +# exclude_status = [ +# Participation.AttendStatus.CANCELED, +# Participation.AttendStatus.APPLYFAILED, +# ] +# if no_unattend: +# exclude_status.append(Participation.AttendStatus.UNATTENDED) +# return self.exclude(status__in=exclude_status) + + +# class Participation(models.Model): +# class Meta: +# verbose_name = "3.活动参与情况" +# verbose_name_plural = verbose_name +# ordering = ["activity_id"] + +# activity = models.ForeignKey(Activity, on_delete=models.CASCADE, related_name='+') +# person = models.ForeignKey(NaturalPerson, on_delete=models.CASCADE, related_name='+') + +# @necessary_for_frontend(person) +# def get_participant(self): +# '''供前端使用,追踪该字段的函数''' +# return self.person + +# class AttendStatus(models.TextChoices): +# APPLYING = "申请中" +# APPLYFAILED = "活动申请失败" +# APPLYSUCCESS = "已报名" +# ATTENDED = "已参与" +# UNATTENDED = "未签到" +# CANCELED = "放弃" + +# status = models.CharField( +# "学生参与活动状态", +# choices=AttendStatus.choices, +# default=AttendStatus.APPLYING, +# max_length=32, +# ) +# objects: ParticipationManager = ParticipationManager() class NotificationManager(models.Manager['Notification']): @@ -1160,20 +1137,13 @@ class Type(models.IntegerChoices): NEEDDO = (1, "处理类") # 需要处理的事务 class Title(models.TextChoices): - # 等待逻辑补充,可以自定义 - TRANSFER_INFORM = "元气值入账通知" - TRANSFER_CONFIRM = "转账确认通知" ACTIVITY_INFORM = "活动状态通知" VERIFY_INFORM = "审核信息通知" POSITION_INFORM = "成员变动通知" TRANSFER_FEEDBACK = "转账回执" NEW_ORGANIZATION = "新建小组通知" - YQ_DISTRIBUTION = "元气值发放通知" PENDING_INFORM = "事务开始通知" FEEDBACK_INFORM = "反馈通知" - YPLIB_INFORM = "元培书房通知" - LOTTERY_INFORM = "抽奖结果通知" - ACHIEVE_INFORM = "解锁成就通知" status = models.SmallIntegerField(choices=Status.choices, default=1) title = models.CharField("通知标题", blank=True, null=True, max_length=50) @@ -1432,503 +1402,35 @@ class Meta: time = models.DateTimeField('修改时间', auto_now_add=True) -class CourseManager(models.Manager['Course']): - def activated(self, noncurrent: bool | None = False): - # 选择当前学期的开设课程 - # 不显示已撤销的课程信息 - return select_current( - self.exclude(status=Course.Status.ABORT), noncurrent=noncurrent) - - def selected(self, person: NaturalPerson, unfailed=False): - all_status = [ - CourseParticipant.Status.SELECT, - CourseParticipant.Status.SUCCESS, - ] - if not unfailed: - all_status.append(CourseParticipant.Status.FAILED) - # 返回当前学生所选的所有课程,选课失败也要算入 - # participant_set是对CourseParticipant的反向查询 - return self.activated().filter(participant_set__person=person, - participant_set__status__in=all_status) - - def unselected(self, person: NaturalPerson): - # 返回当前学生没选上的所有课程 - my_course_list = self.selected( - person, unfailed=True).values_list("id", flat=True) - return self.activated().exclude(id__in=my_course_list) - - -class Course(models.Model): - """ - 助教发布课程需要填写的信息 - """ - class Meta: - verbose_name = "4.书院课程" - verbose_name_plural = verbose_name - ordering = ["id"] - - name = models.CharField("课程名称", max_length=60) - organization = models.ForeignKey(Organization, on_delete=models.CASCADE, - verbose_name="开课组织") - - year = models.IntegerField("开课年份", default=current_year) - - semester = models.CharField("开课学期", - choices=Semester.choices, - max_length=15, - default=current_semester) +# class ActivitySummary(models.Model): +# class Meta: +# verbose_name = "3.活动总结" +# verbose_name_plural = verbose_name +# ordering = ["-time"] - # 课程开设的周数 - times = models.SmallIntegerField("课程开设周数", default=16) - classroom = models.CharField("预期上课地点", - max_length=60, - default="", - blank=True) - teacher = models.CharField("授课教师", max_length=48, default="", blank=True) +# class Status(models.IntegerChoices): +# WAITING = (0, "待审核") +# CONFIRMED = (1, "已通过") +# CANCELED = (2, "已取消") +# REFUSED = (3, "已拒绝") - introduction = models.TextField("课程简介", blank=True, default="这里暂时没有介绍哦~") - teaching_plan = models.TextField("教学计划", blank=True, default="暂无") - record_cal_method = models.TextField("学时计算方式", blank=True, default="暂无") +# activity = models.ForeignKey(Activity, on_delete=models.CASCADE) - class Status(models.IntegerChoices): - # 预选前和预选结束到补退选开始都是WAITING状态 - ABORT = (0, "已撤销") - WAITING = (1, "未开始") - STAGE1 = (2, "预选") - DRAWING = (3, "抽签中") - STAGE2 = (4, "补退选") - SELECT_END = (5, "选课结束") - END = (6, "已结束") - - status = models.SmallIntegerField("开课状态", - choices=Status.choices, - default=Status.WAITING) - - class CourseType(models.IntegerChoices): - # 课程类型 - MORAL = (0, "德") - INTELLECTUAL = (1, "智") - PHYSICAL = (2, "体") - AESTHETICS = (3, "美") - LABOUR = (4, "劳") - - type = models.SmallIntegerField("课程类型", choices=CourseType.choices) - - capacity = models.IntegerField("课程容量", default=100) - current_participants = models.IntegerField("当前选课人数", default=0) - - class PublishDay(models.IntegerChoices): - instant = (0, "立即发布") - oneday = (1, "提前一天") - twoday = (2, "提前两天") - threeday = (3, "提前三天") - - publish_day = models.SmallIntegerField( - "信息发布时间", default=PublishDay.threeday) # 默认为提前三天时间 - need_apply = models.BooleanField("是否需要报名", default=False) - # 暂时只允许上传一张图片 - photo = models.ImageField(verbose_name="宣传图片", - upload_to=f"course/photo/%Y/", - blank=True) - - # 课程群二维码 - QRcode = models.ImageField(upload_to=f"course/QRcode/%Y/", - blank=True, - null=True) - - objects: CourseManager = CourseManager() - - def save(self, *args, **kwargs): - super().save(*args, **kwargs) - - @invalid_for_frontend - def __str__(self): - return f'{self.name}_{self.year}{self.get_semester_display()}' - - def get_photo_path(self): - # 暂不要求课程的宣传图片必须存在 报错更令人烦恼 - return image_url(self.photo, enable_abs=True) - - def get_QRcode_path(self): - return image_url(self.QRcode) - - -class CourseTime(models.Model): - """ - 记录课程每周的上课时间,同一课程可以对应多个上课时间 - """ - class Meta: - verbose_name = "4.上课时间" - verbose_name_plural = verbose_name - ordering = ["start"] +# status = models.SmallIntegerField(choices=Status.choices, default=0) +# image = models.ImageField(upload_to=f"ActivitySummary/photo/%Y/%m/", +# verbose_name='活动总结图片', null=True, blank=True) +# time = models.DateTimeField("申请时间", auto_now_add=True) - course = models.ForeignKey(Course, on_delete=models.CASCADE, - related_name="time_set") +# def __str__(self): +# return f'{self.activity.title}活动总结' - # 开始时间和结束时间指的是一次课程的上课时间和下课时间 - # 需要提醒助教,填写的时间是第一周上课的时间,这影响到课程活动的统一开设。 - start = models.DateTimeField("开始时间") - end = models.DateTimeField("结束时间") - cur_week = models.IntegerField("已生成周数", default=0) - end_week = models.IntegerField("总周数", default=16) - - -class CourseParticipant(models.Model): - """ - 学生的选课情况 - """ - class Meta: - verbose_name = "4.课程报名情况" - verbose_name_plural = verbose_name - - course = models.ForeignKey(Course, on_delete=models.CASCADE, - related_name="participant_set") - person = models.ForeignKey(NaturalPerson, on_delete=models.CASCADE) - - class Status(models.IntegerChoices): - SELECT = (0, "已选课") - UNSELECT = (1, "未选课") - SUCCESS = (2, "选课成功") - FAILED = (3, "选课失败") - - status = models.SmallIntegerField( - "选课状态", - choices=Status.choices, - default=Status.UNSELECT, - ) - - -class CourseRecordManager(models.Manager['CourseRecord']): - def current(self): - # 选择当前学期的学时 - return select_current(self) - - def past(self): - # 只存在当前学期和过去的,非本学期即是过去 - return select_current(self, noncurrent=True) - - def valid(self, noncurrent=None): - '''有效学时,可加入参数来过滤时间''' - return select_current(self.filter(invalid=False), noncurrent=noncurrent) - - def invalid(self, noncurrent=None): - '''无效学时,可加入参数来过滤时间''' - return select_current(self.filter(invalid=True), noncurrent=noncurrent) - - -class CourseRecord(models.Model): - """ - 学时表 - """ - class Meta: - verbose_name = "4.学时表" - verbose_name_plural = verbose_name - - person = models.ForeignKey(NaturalPerson, on_delete=models.CASCADE) - course = models.ForeignKey( - Course, on_delete=models.SET_NULL, null=True, blank=True, - ) - # 长度兼容组织和课程名 - extra_name = models.CharField("课程名称额外标注", max_length=60, blank=True) - - year = models.IntegerField("课程所在学年", default=current_year) - semester = models.CharField( - "课程所在学期", - choices=Semester.choices, - default=current_semester, - max_length=15, - ) - total_hours = models.FloatField("总计参加学时") - attend_times = models.IntegerField("参加课程次数", default=0) - invalid = models.BooleanField("无效", default=False) - - objects: CourseRecordManager = CourseRecordManager() - - def get_course_name(self): - if self.course is not None: - return self.course.name - return self.extra_name - get_course_name.short_description = "课程名" - - -# 学术地图相关模型 -class AcademicTag(models.Model): - class Meta: - verbose_name = "P.学术地图标签" - verbose_name_plural = verbose_name - - class Type(models.IntegerChoices): - MAJOR = (0, '主修专业') - MINOR = (1, '辅修专业') - DOUBLE_DEGREE = (2, '双学位专业') - PROJECT = (3, '参与项目') - - atype = models.SmallIntegerField('标签类型', choices=Type.choices) - tag_content = models.CharField('标签内容', max_length=63) - - def __str__(self) -> str: - return self.get_atype_display() + ' - ' + self.tag_content - - -class AcademicEntryManager(models.Manager['AcademicEntry']): - def activated(self): - # 筛选未被删除的entry - return self.exclude(status=AcademicEntry.EntryStatus.OUTDATE) - - -class AcademicEntry(models.Model): - class Meta: - abstract = True - - class EntryStatus(models.IntegerChoices): - PRIVATE = (0, '不公开') - WAIT_AUDIT = (1, '待审核') - PUBLIC = (2, '已公开') - OUTDATE = (3, '已弃用') - - person = models.ForeignKey(NaturalPerson, on_delete=models.CASCADE) - status = models.SmallIntegerField('记录状态', choices=EntryStatus.choices) - - objects: AcademicEntryManager = AcademicEntryManager() - - -class AcademicTagEntry(AcademicEntry): - class Meta: - verbose_name = "P.学术地图标签项目" - verbose_name_plural = verbose_name - - tag = models.ForeignKey(AcademicTag, on_delete=models.CASCADE) - - @property - def content(self) -> str: - return self.tag.tag_content - - -class AcademicTextEntry(AcademicEntry): - class Meta: - verbose_name = "P.学术地图文本项目" - verbose_name_plural = verbose_name - - class Type(models.IntegerChoices): - SCIENTIFIC_RESEARCH = (0, '本科生科研') - CHALLENGE_CUP = (1, '挑战杯') - INTERNSHIP = (2, '实习经历') - SCIENTIFIC_DIRECTION = (3, '科研方向') - GRADUATION = (4, '毕业去向') - - atype = models.SmallIntegerField('类型', choices=Type.choices) - content = models.CharField('内容', max_length=4095) - - -class ChatManager(models.Manager['Chat']): - def activated(self): - # 筛选进行中的对话 - return self.filter(status=Chat.Status.PROGRESSING) - - -class Chat(CommentBase): - """ - 每个Chat支持一对用户间的多次通信,每一条信息是一个Comment记录 - 一对用户间可以开启多个Chat,进行不同主题的讨论 - - 应用于学术地图的QA功能 - """ - class Meta: - verbose_name = "2.对话" - verbose_name_plural = verbose_name - - questioner = models.ForeignKey(User, on_delete=models.CASCADE, - related_name="send_chat_set") - respondent = models.ForeignKey(User, on_delete=models.CASCADE, - related_name="receive_chat_set") - title = models.CharField("主题", default="", max_length=50) - questioner_anonymous = models.BooleanField("提问方是否匿名", default=True) - respondent_anonymous = models.BooleanField("回答方是否匿名", default=False) - - class Status(models.IntegerChoices): - PROGRESSING = (0, "进行中") - CLOSED = (1, "已关闭") # 发送方或接收方选择关闭时转入该状态,此后双方不能再向该Chat发消息 - status = models.SmallIntegerField(choices=Status.choices, default=0) - - objects: ChatManager = ChatManager() - - def save(self, *args, **kwargs): - self.typename = "Chat" - super().save(*args, **kwargs) - - -class AcademicQAManager(models.Manager['AcademicQA']): - def activated(self): - return self.filter(chat__status=Chat.Status.PROGRESSING) - - -class AcademicQA(models.Model): - class Meta: - verbose_name = "P.学术问答" - verbose_name_plural = verbose_name - - chat = models.OneToOneField(to=Chat, on_delete=models.CASCADE) - keywords = models.JSONField('关键词', null=True, blank=True) - directed = models.BooleanField('是否定向', default=False) - rating = models.IntegerField('评价', default=0) - - objects: AcademicQAManager = AcademicQAManager() - - -class AcademicQAAwards(models.Model): - class Meta: - verbose_name = "P.学术问答相关奖励" - verbose_name_plural = verbose_name - - user = models.OneToOneField(User, on_delete=models.CASCADE) - created_undirected_qa = models.BooleanField("是否发起过非定向提问", default=False) - - -class Prize(models.Model): - class Meta: - verbose_name = '5.奖品' - verbose_name_plural = verbose_name - - name = models.CharField('名称', max_length=50) - more_info = models.CharField('详情', max_length=255, default='', blank=True) - stock = models.IntegerField('参考库存', default=0) - reference_price = models.IntegerField('参考价格') - image = models.ImageField( - '图片', upload_to=f'prize/%Y-%m/', null=True, blank=True) - provider = models.ForeignKey(User, verbose_name='提供者', on_delete=models.CASCADE, - null=True, blank=True) - - @invalid_for_frontend - def __str__(self): - return self.name - - -class Pool(models.Model): - class Meta: - verbose_name = '5.奖池' - verbose_name_plural = verbose_name - - class Type(models.TextChoices): - EXCHANGE = '兑换', '兑换奖池' - # 等结束抽签 - LOTTERY = '抽奖', '抽奖奖池' - # 类似盲盒,每次抽可以获得一件物品,但不一定是大奖 - RANDOM = '盲盒', '盲盒奖池' - - title = models.CharField('名称', max_length=50) - type = models.CharField('类型', choices=Type.choices, max_length=15) - # 类型为兑换池时无效 - entry_time = models.IntegerField('进入次数', default=1) - ticket_price = models.IntegerField('抽奖费', default=0) - empty_YQPoint_compensation_lowerbound = models.IntegerField( - '空盒元气值补偿下限', default=0) - empty_YQPoint_compensation_upperbound = models.IntegerField( - '空盒元气值补偿上限', default=0) - start = models.DateTimeField('开始时间') - end = models.DateTimeField('结束时间', null=True, blank=True) - redeem_start = models.DateTimeField( - '兑奖开始时间', null=True, blank=True) # 指线下获取奖品实物 - redeem_end = models.DateTimeField('兑奖结束时间', null=True, blank=True) - # 如果activity非空,只有参加了该活动的用户可以参与这个奖池的兑换。 - activity = models.ForeignKey(Activity, on_delete=models.SET_NULL, - null=True, blank=True, default=None) - - @invalid_for_frontend - def __str__(self): - return self.title - - @property - def items(self) -> 'models.manager.RelatedManager[PoolItem]': - return self.poolitem_set - - @invalid_for_frontend - def get_capacity(self): - return self.items.aggregate(Sum('origin_num'))['origin_num__sum'] or 0 - - -class PoolItem(models.Model): - class Meta: - verbose_name = '5.奖池奖品' - verbose_name_plural = verbose_name - - pool = models.ForeignKey(Pool, verbose_name='奖池', on_delete=models.CASCADE) - prize = models.ForeignKey(Prize, verbose_name='奖品', on_delete=models.CASCADE, - null=True, blank=True) - origin_num = models.IntegerField('初始数量') - consumed_num = models.IntegerField('已兑换', default=0) - # 下面三个在 pool 类型为兑换奖池时有效 - exchange_limit = models.IntegerField('单人兑换上限', default=0) - exchange_price = models.IntegerField('价格', null=True, blank=True) - exchange_attributes = models.JSONField('属性', default=list, blank=True) - # 下面这个在抽奖/盲盒奖池中有效 - is_big_prize = models.BooleanField('是否特别奖品', default=False) - is_empty_prize = models.BooleanField('是否空盒', default=False) - - @property - def is_empty(self) -> bool: - return self.is_empty_prize or (self.prize is None) - - @invalid_for_frontend - def __str__(self): - if self.is_empty: - return f'{self.pool} 空盒' - return f'{self.pool} {self.prize}' - - -class PoolRecord(models.Model): - class Meta: - verbose_name = '5.奖池记录' - verbose_name_plural = verbose_name - - class Status(models.TextChoices): - # 抽奖奖池时有效 - LOTTERING = '抽奖中', '抽奖中' - NOT_LUCKY = '未中奖', '未中奖' - UN_REDEEM = '未兑奖', '未兑奖' # 指线下获取奖品实物 - REDEEMED = '已兑奖', '已兑奖' - OVERDUE = '已失效', '已失效' - - user = models.ForeignKey(User, verbose_name='用户', on_delete=models.CASCADE) - pool = models.ForeignKey(Pool, verbose_name='奖池', on_delete=models.CASCADE) - prize = models.ForeignKey( - Prize, verbose_name='奖品', on_delete=models.CASCADE, - null=True, blank=True, - ) - attributes = models.JSONField('属性', default=dict, blank=True) - status = models.CharField('状态', choices=Status.choices, max_length=15) - time = models.DateTimeField('记录时间', auto_now_add=True) - redeem_time = models.DateTimeField('兑奖时间', null=True, blank=True) - - -class ActivitySummary(models.Model): - class Meta: - verbose_name = "3.活动总结" - verbose_name_plural = verbose_name - ordering = ["-time"] - - class Status(models.IntegerChoices): - WAITING = (0, "待审核") - CONFIRMED = (1, "已通过") - CANCELED = (2, "已取消") - REFUSED = (3, "已拒绝") - - activity = models.ForeignKey(Activity, on_delete=models.CASCADE) - - status = models.SmallIntegerField(choices=Status.choices, default=0) - image = models.ImageField(upload_to=f"ActivitySummary/photo/%Y/%m/", - verbose_name='活动总结图片', null=True, blank=True) - time = models.DateTimeField("申请时间", auto_now_add=True) - - def __str__(self): - return f'{self.activity.title}活动总结' - - def is_pending(self): # 表示是不是pending状态 - return self.status == ActivitySummary.Status.WAITING +# def is_pending(self): # 表示是不是pending状态 +# return self.status == ActivitySummary.Status.WAITING - @necessary_for_frontend('activity.organization_id') - def get_org(self): - return self.activity.organization_id +# @necessary_for_frontend('activity.organization_id') +# def get_org(self): +# return self.activity.organization_id - @necessary_for_frontend('activity.title', '__str__') - def get_audit_display(self): - return f'{self.activity.title}总结' +# @necessary_for_frontend('activity.title', '__str__') +# def get_audit_display(self): +# return f'{self.activity.title}总结' diff --git a/app/urls.py b/app/urls.py index 5e14be527..0c416da73 100644 --- a/app/urls.py +++ b/app/urls.py @@ -14,11 +14,6 @@ from app import ( views, org_views, - activity_views, - course_views, - academic_views, - chat_api, - YQPoint_views, ) # 尽量不使用, 不支持 @@ -43,23 +38,6 @@ name="subscribeOrganization"), path("saveSubscribeStatus", views.saveSubscribeStatus, name="saveSubscribeStatus"), -] + [ - # 活动 - path("viewActivity/", activity_views.viewActivity, name="viewActivity"), - path("getActivityInfo/", activity_views.getActivityInfo, name="getActivityInfo"), - path("checkinActivity/", - activity_views.checkinActivity, name="checkinActivity"), - path("addActivity/", activity_views.addActivity, name="addActivity"), - path("showActivity/", activity_views.activityCenter, name="showActivity"), - path("editActivity/", activity_views.addActivity, name="editActivity"), - path("examineActivity/", - activity_views.examineActivity, name="examineActivity"), - path("offlineCheckinActivity/", - activity_views.offlineCheckinActivity, name="offlineCheckinActivity"), - path("endActivity/", activity_views.finishedActivityCenter, name="endActivity"), - path("modifyEndActivity/", activity_views.activitySummary, - name="modifyEndActivity"), - path("weekly-activity-summary/", activity_views.WeeklyActivitySummary.as_view(), name="weekly_activity_summary"), ] + [ # 组织相关操作 path("saveShowPositionStatus", org_views.saveShowPositionStatus, @@ -71,48 +49,4 @@ path("modifyOrganization/", org_views.modifyOrganization, name="modifyOrganization"), path("sendMessage/", org_views.sendMessage, name="sendMessage"), - # path("applyPosition/", views.apply_position, name="applyPosition"), 弃用多年 -] + [ - # 发布选课相关操作 - path("addCourse/", course_views.addCourse, name="addCourse"), - path("editCourse/", course_views.addCourse, name="editCourse"), - # 选课相关操作 - path("selectCourse/", course_views.selectCourse, name="selectCourse"), - path("viewCourse/", course_views.viewCourse, name="viewCourse"), - # 课程相关操作 - path("addSingleCourseActivity/", course_views.addSingleCourseActivity, - name="addSingleCourseActivity"), - path("editCourseActivity/", - course_views.editCourseActivity, name="editCourseActivity"), - path("showCourseActivity/", course_views.showCourseActivity, - name="showCourseActivity"), - path("showCourseRecord/", course_views.showCourseRecord, - name="showCourseRecord"), - # 数据导出 - path("outputRecord/", course_views.outputRecord, name="outputRecord"), - path("outputSelectInfo/", course_views.outputSelectInfo, - name="outputSelectInfo"), - path("output-all-select-info/", course_views.outputAllSelectInfo, - name="output-all-select-info"), -] + [ - # 学术地图 - path("modifyAcademic/", academic_views.modifyAcademic, name="modifyAcademic"), - path("AcademicQA/", academic_views.ShowChats.as_view(), name="showChats"), - path("viewQA/", academic_views.ViewChat.as_view(), name="viewChat"), - path("auditAcademic/", academic_views.auditAcademic, name="auditAcademic"), - path("applyAuditAcademic/", academic_views.applyAuditAcademic, - name="applyAuditAcademic"), -] + [ - # 问答相关 - # TODO: url等合并前端后再改 - path("addChatComment/", chat_api.AddComment.as_view(), name="addComment"), - path("closeChat/", chat_api.CloseChat.as_view(), name="closeChat"), - path("startChat/", chat_api.StartChat.as_view(), name="startChat"), - path("startUndirectedChat/", chat_api.StartUndirectedChat.as_view(), name="startUndirectedChat"), - path("rateAnswer/", chat_api.RateAnswer.as_view(), name="rateAnswer") -] + [ - # 元气值 - path("myYQPoint/", YQPoint_views.myYQPoint.as_view(), name="myYQPoint"), - path("showPools/", YQPoint_views.showPools, name="showPools"), - path("myPrize/", YQPoint_views.myPrize.as_view(), name="myPrize"), ] diff --git a/app/utils.py b/app/utils.py index 4c72dfe03..60284c6e7 100644 --- a/app/utils.py +++ b/app/utils.py @@ -22,7 +22,6 @@ Position, Notification, Help, - Participation, ModifyRecord, ) @@ -250,8 +249,6 @@ def get_sidebar_and_navbar(user: User, navbar_name="", title_name=""): if navbar_name: help_key = navbar_name - if help_key == "我的元气值": - help_key += _utype.lower() help_info = Help.objects.filter(title=navbar_name).first() bar_display.update( help_message=CONFIG.help_message.get(help_key, ""), @@ -482,60 +479,6 @@ def check_account_setting(request: UserRequest): attr_dict['tags_modify'] = request.POST['tags_modify'] return attr_dict, show_dict, html_display -# 导出Excel文件 - - -def export_activity(activity, inf_type): - - # 设置HTTPResponse的类型 - response = HttpResponse(content_type='application/vnd.ms-excel') - if activity is None: - return response - response['Content-Disposition'] = f'attachment;filename={activity.title}.xls' - participants: QuerySet[Participation] = SQ.sfilter(Participation.activity, activity) - if inf_type == "sign": # 签到信息 - participants = participants.filter(status=Participation.AttendStatus.ATTENDED) - elif inf_type == "enroll": # 报名信息 - participants = participants.exclude(status=Participation.AttendStatus.CANCELED) - else: - return response - """导出excel表""" - if len(participants) > 0: - # 创建工作簿 - ws = xlwt.Workbook(encoding='utf-8') - # 添加第一页数据表 - w = ws.add_sheet('sheet1') # 新建sheet(sheet的名称为"sheet1") - # 写入表头 - w.write(0, 0, u'姓名') - w.write(0, 1, u'学号') - w.write(0, 2, u'年级/班级') - if inf_type == "enroll": - w.write(0, 3, u'报名状态') - w.write(0, 4, u'注:报名状态为“已参与”时表示报名成功并成功签到,“未签到”表示报名成功但未签到,' - u'"已报名"表示报名成功,“活动申请失败”表示在抽签模式中落选,“申请中”则表示抽签尚未开始。') - # 写入数据 - excel_row = 1 - for participant in participants: - name = participant.person.name - Sno = participant.person.person_id.username - grade = str(participant.person.stu_grade) + '级' + \ - str(participant.person.stu_class) + '班' - if inf_type == "enroll": - status = participant.status - w.write(excel_row, 3, status) - # 写入每一行对应的数据 - w.write(excel_row, 0, name) - w.write(excel_row, 1, Sno) - w.write(excel_row, 2, grade) - excel_row += 1 - # 写出到IO - output = BytesIO() - ws.save(output) - # 重新定位到开始 - output.seek(0) - response.write(output.getvalue()) - return response - # 导出小组成员信息Excel文件 def export_orgpos_info(org): diff --git a/app/views.py b/app/views.py index 36aeb9a22..a9aaf6942 100644 --- a/app/views.py +++ b/app/views.py @@ -18,21 +18,12 @@ NaturalPerson, Freshman, Position, - AcademicTag, - AcademicEntry, - AcademicTextEntry, Organization, OrganizationTag, OrganizationType, - Activity, - ActivityPhoto, - Participation, Notification, Wishes, - Course, - CourseRecord, Semester, - AcademicQA, ) from app.utils import ( get_person_or_org, @@ -47,19 +38,7 @@ notification_status_change, notification2Display, ) -from app.YQPoint_utils import add_signin_point -from app.academic_utils import ( - get_search_results, - comments2display, - get_js_tag_list, - get_text_list, - have_entries, - get_tag_status, - get_text_status, -) -from achievement.utils import personal_achievements -from achievement.api import unlock_achievement, unlock_YQPoint_achievements from semester.api import current_semester @@ -334,109 +313,40 @@ def _get_org_latest_pos(positions: QuerySet[Position], org): ) # ----------------------------------- 活动卡片 ----------------------------------- # - # ------------------ 学时查询 ------------------ # - - # 只有是自己的主页时才显示学时 - if is_myself: - # 把当前学期的活动去除 - past_courses = CourseRecord.objects.past().filter(person=oneself) - - # 无效学时,在前端呈现 - useless_courses = ( - past_courses - .filter(invalid=True) - ) - - # 特判,需要一定时长才能计入总学时 - past_courses = ( - past_courses - .exclude(invalid=True) - ) - - past_courses = past_courses.order_by('year', 'semester') - useless_courses = useless_courses.order_by('year', 'semester') - - progress_list = [] - - # 计算每个类别的学时 - for course_type in list(Course.CourseType): # CourseType.values亦可 - progress_list.append(( - past_courses - .filter(course__type=course_type) - .aggregate(Sum('total_hours')) - )['total_hours__sum'] or 0) - # 计算没有对应Course的学时 - progress_list.append(( - past_courses - .filter(course__isnull=True) - .aggregate(Sum('total_hours')) - )['total_hours__sum'] or 0) - - # 每个人的规定学时,按年级讨论 - try: - # 本科生 - if int(oneself.stu_grade) <= 2018: - ruled_hours = 0 - elif int(oneself.stu_grade) == 2019: - ruled_hours = 32 - else: - ruled_hours = 64 - except: - # 其它,如老师和住宿辅导员等 - ruled_hours = 0 - - # 计算总学时 - complete_hours = sum(progress_list) - # 用于算百分比的实际总学时(考虑到可能会超学时),仅后端使用 - actual_total_hours = max(complete_hours, ruled_hours) - if actual_total_hours > 0: - progress_list = [ - hour / actual_total_hours * 100 for hour in progress_list - ] - - course_context = dict( - ruled_hours=ruled_hours, - complete_hours=complete_hours, - past_courses=past_courses, - useless_courses=useless_courses, - progress_list=progress_list, - ) - render_context.update(Course=course_context) - - # ------------------ 活动参与 ------------------ # - - participants = Participation.objects.activated().filter(SQ.sq( - Participation.person, person)) - activities = Activity.objects.activated().filter( - # ~Q(status=Activity.Status.CANCELED), # 暂时可以呈现已取消的活动 - id__in=SQ.qsvlist(participants, Participation.activity), - ) - if request.user.is_person(): - # 因为上面筛选过活动,这里就不用筛选了 - # 之前那个写法是O(nm)的 - activities_me = Participation.objects.activated().filter(SQ.sq( - Participation.person, oneself)) - activities_me = set(SQ.qsvlist(activities_me, Participation.activity)) - else: - activities_me = activities.filter(organization_id=oneself) - activities_me = set(activities_me.values_list("id", flat=True)) - activity_is_same = [ - activity in activities_me - for activity in activities.values_list("id", flat=True) - ] - activity_info = list(zip(activities, activity_is_same)) - activity_info.sort(key=lambda a: a[0].start, reverse=True) - html_display["activity_info"] = list(activity_info) or None - - # 呈现历史活动,不考虑共同活动的规则,直接全部呈现 - history_activities = list( - Activity.objects.activated(noncurrent=True).filter( - # ~Q(status=Activity.Status.CANCELED), # 暂时可以呈现已取消的活动 - id__in=SQ.qsvlist(participants, Participation.activity), - )) - history_activities.sort(key=lambda a: a.start, reverse=True) - html_display["history_act_info"] = list(history_activities) or None + # # ------------------ 活动参与 ------------------ # + + # participants = Participation.objects.activated().filter(SQ.sq( + # Participation.person, person)) + # activities = Activity.objects.activated().filter( + # # ~Q(status=Activity.Status.CANCELED), # 暂时可以呈现已取消的活动 + # id__in=SQ.qsvlist(participants, Participation.activity), + # ) + # if request.user.is_person(): + # # 因为上面筛选过活动,这里就不用筛选了 + # # 之前那个写法是O(nm)的 + # activities_me = Participation.objects.activated().filter(SQ.sq( + # Participation.person, oneself)) + # activities_me = set(SQ.qsvlist(activities_me, Participation.activity)) + # else: + # activities_me = activities.filter(organization_id=oneself) + # activities_me = set(activities_me.values_list("id", flat=True)) + # activity_is_same = [ + # activity in activities_me + # for activity in activities.values_list("id", flat=True) + # ] + # activity_info = list(zip(activities, activity_is_same)) + # activity_info.sort(key=lambda a: a[0].start, reverse=True) + # html_display["activity_info"] = list(activity_info) or None + + # # 呈现历史活动,不考虑共同活动的规则,直接全部呈现 + # history_activities = list( + # Activity.objects.activated(noncurrent=True).filter( + # # ~Q(status=Activity.Status.CANCELED), # 暂时可以呈现已取消的活动 + # id__in=SQ.qsvlist(participants, Participation.activity), + # )) + # history_activities.sort(key=lambda a: a.start, reverse=True) + # html_display["history_act_info"] = list(history_activities) or None # 警告呈现信息 @@ -444,98 +354,6 @@ def _get_org_latest_pos(positions: QuerySet[Position], org): if request.GET.get("modinfo", "") == "success": succeed("修改个人信息成功!", html_display) - # ----------------------------------- 学术地图 ----------------------------------- # - # ------------------ 提问区 or 进行中的问答------------------ # - progressing_chat = AcademicQA.objects.activated().filter( - directed=True, - chat__questioner=request.user, - chat__respondent=person.get_user() - ) - if progressing_chat.exists(): - chat_qa = progressing_chat.first() - comment_display = comments2display(chat_qa.chat, request.user) - # TODO: 字典的key有冲突风险 - html_display.update(comment_display) - html_display["have_progressing_chat"] = True - else: # 没有进行中的问答,显示提问区 - html_display["have_progressing_chat"] = False - html_display["accept_chat"] = person.get_user().accept_chat - html_display["accept_anonymous"] = person.get_user().accept_anonymous_chat - - # ------------------ 查看学术地图 ------------------ # - status_in = [AcademicEntry.EntryStatus.PUBLIC] - is_teacher = request.user.is_person() and oneself.is_teacher() - if is_myself: - status_in = None - elif is_teacher: - status_in.append(AcademicEntry.EntryStatus.WAIT_AUDIT) - - # 判断用户是否有可以展示的内容 - content_status = status_in - if is_myself: - content_status = [AcademicEntry.EntryStatus.PUBLIC, - AcademicEntry.EntryStatus.WAIT_AUDIT] - academic_params = dict() - academic_params.update( - is_inspector=is_teacher, - author_id=person.person_id.id, - have_content=have_entries(person, content_status), - have_unaudit=have_entries(person, [AcademicEntry.EntryStatus.WAIT_AUDIT]), - ) - - # 获取用户已有的专业/项目的列表,用于select的默认选中项 - selected_dict = dict( - selected_major_list=AcademicTag.Type.MAJOR, - selected_minor_list=AcademicTag.Type.MINOR, - selected_double_degree_list=AcademicTag.Type.DOUBLE_DEGREE, - selected_project_list=AcademicTag.Type.PROJECT, - ) - academic_params.update({ - name: get_js_tag_list(person, type, selected=True, status_in=status_in) - for name, type in selected_dict.items() - }) - - # 获取用户已有的TextEntry的contents,用于TextEntry填写栏的前端预填写 - text_dict = dict( - scientific_research_list=AcademicTextEntry.Type.SCIENTIFIC_RESEARCH, - challenge_cup_list=AcademicTextEntry.Type.CHALLENGE_CUP, - internship_list=AcademicTextEntry.Type.INTERNSHIP, - scientific_direction_list=AcademicTextEntry.Type.SCIENTIFIC_DIRECTION, - graduation_list=AcademicTextEntry.Type.GRADUATION, - ) - academic_params.update({ - name: get_text_list(person, type, status_in) - for name, type in text_dict.items() - }) - - # 最后获取每一种atype对应的entry的公开状态,如果没有则默认为公开 - tag_status_dict = dict( - major_status=AcademicTag.Type.MAJOR, - minor_status=AcademicTag.Type.MINOR, - double_degree_status=AcademicTag.Type.DOUBLE_DEGREE, - project_status=AcademicTag.Type.PROJECT, - ) - academic_params.update({ - name: get_tag_status(person, type) - for name, type in tag_status_dict.items() - }) - text_status_dict = dict( - scientific_research_status=AcademicTextEntry.Type.SCIENTIFIC_RESEARCH, - challenge_cup_status=AcademicTextEntry.Type.CHALLENGE_CUP, - internship_status=AcademicTextEntry.Type.INTERNSHIP, - scientific_direction_status=AcademicTextEntry.Type.SCIENTIFIC_DIRECTION, - graduation_status=AcademicTextEntry.Type.GRADUATION, - ) - academic_params.update({ - name: get_text_status(person, type) - for name, type in text_status_dict.items() - }) - render_context.update(Academic=academic_params) - - # ------------------ 成就卡片 ------------------ # - _, _, achievement_by_types = personal_achievements(person.get_user()) - achievement_params = dict(type_order_displays=achievement_by_types) - render_context.update(Achievement=achievement_params) # ------------------ 前端准备 ------------------ # # 存储被查询人的信息 @@ -643,9 +461,7 @@ def orginfo(request: UserRequest): html_display = {} html_display["is_myself"] = is_myself - html_display["is_course"] = ( - Course.objects.activated().filter(organization=org).exists() - ) + inform_share, alert_message = utils.get_inform_share( me, is_myself=is_myself) @@ -672,55 +488,55 @@ def orginfo(request: UserRequest): org.save() return redirect("/welcome/") - # 该学年、该学期、该小组的 活动的信息,分为 未结束continuing 和 已结束ended ,按时间顺序降序展现 - continuing_activities = ( - Activity.objects.activated() - .filter(organization_id=org) - .filter( - status__in=[ - Activity.Status.REVIEWING, - Activity.Status.APPLYING, - Activity.Status.WAITING, - Activity.Status.PROGRESSING, - ] - ) - .order_by("-start") - ) + # # 该学年、该学期、该小组的 活动的信息,分为 未结束continuing 和 已结束ended ,按时间顺序降序展现 + # continuing_activities = ( + # Activity.objects.activated() + # .filter(organization_id=org) + # .filter( + # status__in=[ + # Activity.Status.REVIEWING, + # Activity.Status.APPLYING, + # Activity.Status.WAITING, + # Activity.Status.PROGRESSING, + # ] + # ) + # .order_by("-start") + # ) - ended_activities = ( - Activity.objects.activated() - .filter(organization_id=org) - .filter(status__in=[Activity.Status.CANCELED, Activity.Status.END]) - .order_by("-start") - ) + # ended_activities = ( + # Activity.objects.activated() + # .filter(organization_id=org) + # .filter(status__in=[Activity.Status.CANCELED, Activity.Status.END]) + # .order_by("-start") + # ) - # 筛选历史活动,具体为不是这个学期的活动 - history_activities = ( - Activity.objects.activated(noncurrent=True) - .filter(organization_id=org) - .order_by("-start") - ) + # # 筛选历史活动,具体为不是这个学期的活动 + # history_activities = ( + # Activity.objects.activated(noncurrent=True) + # .filter(organization_id=org) + # .order_by("-start") + # ) - # 如果是用户登陆的话,就记录一下用户有没有加入该活动,用字典存每个活动的状态,再把字典存在列表里 - - def _display_activities(activities: QuerySet[Activity]) -> list[dict]: - displays = [] - for act in activities: - dictmp = {} - dictmp["act"] = act - hours = Activity.EndBeforeHours.prepare_times[act.endbefore] - dictmp["endbefore"] = act.start - timedelta(hours=hours) - if request.user.is_person(): - participation = Participation.objects.filter( - SQ.sq(Participation.activity, act), SQ.sq(Participation.person, me), - ).first() - dictmp["status"] = participation.status if participation else "无记录" - displays.append(dictmp) - return displays - - continuing_activity_list_participantrec = _display_activities(continuing_activities) - ended_activity_list_participantrec = _display_activities(ended_activities) - history_activity_list_participantrec = _display_activities(history_activities) + # # 如果是用户登陆的话,就记录一下用户有没有加入该活动,用字典存每个活动的状态,再把字典存在列表里 + + # def _display_activities(activities: QuerySet[Activity]) -> list[dict]: + # displays = [] + # for act in activities: + # dictmp = {} + # dictmp["act"] = act + # hours = Activity.EndBeforeHours.prepare_times[act.endbefore] + # dictmp["endbefore"] = act.start - timedelta(hours=hours) + # if request.user.is_person(): + # participation = Participation.objects.filter( + # SQ.sq(Participation.activity, act), SQ.sq(Participation.person, me), + # ).first() + # dictmp["status"] = participation.status if participation else "无记录" + # displays.append(dictmp) + # return displays + + # continuing_activity_list_participantrec = _display_activities(continuing_activities) + # ended_activity_list_participantrec = _display_activities(ended_activities) + # history_activity_list_participantrec = _display_activities(history_activities) # 判断我是不是老大, 首先设置为false, 然后如果有id和user一样, 就为True html_display["isboss"] = False @@ -788,56 +604,36 @@ def homepage(request: UserRequest): my_messages.transfer_message_context(request.GET, html_display) nowtime = datetime.now() - # 今天第一次访问 welcome 界面,积分增加 - if request.user.is_person(): - with transaction.atomic(): - np = NaturalPerson.objects.get_by_user(request.user, update=True) - if np.last_time_login is None or np.last_time_login.date() != nowtime.date(): - np.last_time_login = nowtime - np.save() - add_point, html_display['signin_display'] = add_signin_point( - request.user) - html_display['first_signin'] = True # 前端显示 - - # 解锁成就-注册智慧书院 - # 如果放在注册页面结束判定 则已经注册好的用户获取不到该成就 - unlock_achievement(request.user, '注册智慧书院') - - # 元气满满系列更新 - semester = current_semester() - start_datetime = datetime.combine(semester.start_date, datetime.min.time()) - end_datetime = datetime.combine(semester.end_date, datetime.max.time()) - unlock_YQPoint_achievements(request.user, start_datetime, end_datetime) - - # 开始时间在前后一周内,除了取消和审核中的活动。按时间逆序排序 - recentactivity_list = Activity.objects.get_recent_activity( - ).select_related('organization_id') - - # 开始时间在今天的活动,且不展示结束的活动。按开始时间由近到远排序 - activities = Activity.objects.get_today_activity().select_related('organization_id') - activities_start = [ - activity.start.strftime("%H:%M") for activity in activities - ] - html_display['today_activities'] = list( - zip(activities, activities_start)) or None - - # 最新一周内发布的活动,按发布的时间逆序 - newlyreleased_list = Activity.objects.get_newlyreleased_activity( - ).select_related('organization_id') - - # 即将截止的活动,按截止时间正序 - prepare_times = Activity.EndBeforeHours.prepare_times - - signup_list = [] - signup_rec = Activity.objects.activated().select_related( - 'organization_id').filter(status=Activity.Status.APPLYING).order_by("category", "apply_end")[:10] - for act in signup_rec: - deadline = act.apply_end - dictmp = {} - dictmp["deadline"] = deadline - dictmp["act"] = act - dictmp["tobestart"] = (deadline - nowtime).total_seconds()//360/10 - signup_list.append(dictmp) + + # # 开始时间在前后一周内,除了取消和审核中的活动。按时间逆序排序 + # recentactivity_list = Activity.objects.get_recent_activity( + # ).select_related('organization_id') + + # # 开始时间在今天的活动,且不展示结束的活动。按开始时间由近到远排序 + # activities = Activity.objects.get_today_activity().select_related('organization_id') + # activities_start = [ + # activity.start.strftime("%H:%M") for activity in activities + # ] + # html_display['today_activities'] = list( + # zip(activities, activities_start)) or None + + # # 最新一周内发布的活动,按发布的时间逆序 + # newlyreleased_list = Activity.objects.get_newlyreleased_activity( + # ).select_related('organization_id') + + # # 即将截止的活动,按截止时间正序 + # prepare_times = Activity.EndBeforeHours.prepare_times + + # signup_list = [] + # signup_rec = Activity.objects.activated().select_related( + # 'organization_id').filter(status=Activity.Status.APPLYING).order_by("category", "apply_end")[:10] + # for act in signup_rec: + # deadline = act.apply_end + # dictmp = {} + # dictmp["deadline"] = deadline + # dictmp["act"] = act + # dictmp["tobestart"] = (deadline - nowtime).total_seconds()//360/10 + # signup_list.append(dictmp) # 如果提交了心愿,发生如下的操作 if request.method == "POST" and request.POST: @@ -877,26 +673,26 @@ def homepage(request: UserRequest): # (firstpic, firsturl), guidepics = guidepics[0], guidepics[1:] # firstpic是第一个导航图,不是第一张图片,现在把这个逻辑在模板处理了 - """ - 取出过去一周的所有活动,filter出上传了照片的活动,从每个活动的照片中随机选择一张 - 如果列表为空,那么添加一张default,否则什么都不加。 - """ - all_photo_display = ActivityPhoto.objects.filter( - type=ActivityPhoto.PhotoType.SUMMARY).order_by('-time') - photo_display, _aid_set = list(), set() # 实例的哈希值未定义,不可靠 - count = 9 - len(guidepics) # 算第一张导航图 - for photo in all_photo_display: - # 不用activity,因为外键需要访问数据库 - if photo.activity_id not in _aid_set and photo.image: - # 数据库设成了image可以为空而不是空字符串,str的判断对None没有意义 - - photo.image = MEDIA_URL + str(photo.image) - photo_display.append(photo) - _aid_set.add(photo.activity_id) - count -= 1 - - if count <= 0: # 目前至少能显示一个,应该也合理吧 - break + # """ + # 取出过去一周的所有活动,filter出上传了照片的活动,从每个活动的照片中随机选择一张 + # 如果列表为空,那么添加一张default,否则什么都不加。 + # """ + # all_photo_display = ActivityPhoto.objects.filter( + # type=ActivityPhoto.PhotoType.SUMMARY).order_by('-time') + # photo_display, _aid_set = list(), set() # 实例的哈希值未定义,不可靠 + # count = 9 - len(guidepics) # 算第一张导航图 + # for photo in all_photo_display: + # # 不用activity,因为外键需要访问数据库 + # if photo.activity_id not in _aid_set and photo.image: + # # 数据库设成了image可以为空而不是空字符串,str的判断对None没有意义 + + # photo.image = MEDIA_URL + str(photo.image) + # photo_display.append(photo) + # _aid_set.add(photo.activity_id) + # count -= 1 + + # if count <= 0: # 目前至少能显示一个,应该也合理吧 + # break photo_display = () if photo_display: guidepics = guidepics[1:] # 第一张只是封面图,如果有需要呈现的内容就不显示 @@ -1024,8 +820,6 @@ def accountSetting(request: UserRequest): modify_msg = '\n'.join(modify_info) record_modify_with_session(request, f"修改了{expr}项信息:\n{modify_msg}") - # 解锁成就-更新一次个人档案 - unlock_achievement(request.user, '更新一次个人档案') return redirect("/stuinfo/?modinfo=success") # else: 没有更新 @@ -1348,13 +1142,13 @@ def search(request: HttpRequest): now = datetime.now() - def get_recent_activity(org): - activities = Activity.objects.activated().filter(Q(organization_id=org.id) - & ~Q(status=Activity.Status.CANCELED) - & ~Q(status=Activity.Status.REJECT)) - activities = list(activities) - activities.sort(key=lambda activity: abs(now - activity.start)) - return None if len(activities) == 0 else activities[0:3] + # def get_recent_activity(org): + # activities = Activity.objects.activated().filter(Q(organization_id=org.id) + # & ~Q(status=Activity.Status.CANCELED) + # & ~Q(status=Activity.Status.REJECT)) + # activities = list(activities) + # activities.sort(key=lambda activity: abs(now - activity.start)) + # return None if len(activities) == 0 else activities[0:3] org_display_list = [] for org in organization_list: @@ -1374,7 +1168,7 @@ def get_recent_activity(org): # .values("person__name") # ) # ], - "activities": get_recent_activity(org), + # "activities": get_recent_activity(org), "get_user_ava": org.get_user_ava() } ) @@ -1382,13 +1176,13 @@ def get_recent_activity(org): # 小组要呈现的具体内容 organization_field = ["小组名称", "小组类型", "负责人", "近期活动"] - # 搜索活动 - activity_list = Activity.objects.activated().filter( - Q(title__icontains=query) | Q(organization_id__oname__icontains=query) & ~Q( - status=Activity.Status.CANCELED) - & ~Q(status=Activity.Status.REJECT) - & ~Q(status=Activity.Status.REVIEWING) & ~Q(status=Activity.Status.ABORT) - ) + # # 搜索活动 + # activity_list = Activity.objects.activated().filter( + # Q(title__icontains=query) | Q(organization_id__oname__icontains=query) & ~Q( + # status=Activity.Status.CANCELED) + # & ~Q(status=Activity.Status.REJECT) + # & ~Q(status=Activity.Status.REVIEWING) & ~Q(status=Activity.Status.ABORT) + # ) # 活动要呈现的内容 activity_field = ["活动名称", "承办小组", "状态"] @@ -1403,18 +1197,6 @@ def get_recent_activity(org): # | Q(org__oname__icontains=query) # ) - # 学术地图内容 - academic_map_dict = get_search_results(query) - academic_list = [] - for username, contents in academic_map_dict.items(): - np: NaturalPerson = SQ.mget(NaturalPerson.person_id, username=username) - info = dict( - ref=np.get_absolute_url() + '#tab=academic_map', - sname=np.name, - avatar=np.get_user_ava(), - ) - academic_list.append((info, contents)) - # 新版侧边栏, 顶栏等的呈现,采用 bar_display, 必须放在render前最后一步 bar_display = utils.get_sidebar_and_navbar(request.user, "信息搜索") return render(request, "search.html", locals()) diff --git a/boot/settings.py b/boot/settings.py index 7266f2c2c..a65d636b5 100644 --- a/boot/settings.py +++ b/boot/settings.py @@ -67,11 +67,7 @@ class SettingConfig(Config): "Appointment", 'dm', "scheduler", - "yp_library", - "questionnaire", - "dormitory", - "feedback", - "achievement", + # "feedback", ] diff --git a/boot/urls.py b/boot/urls.py index 0c7c3e9a9..2b6c670ff 100644 --- a/boot/urls.py +++ b/boot/urls.py @@ -11,16 +11,10 @@ path("admin/", admin.site.urls), path('api-auth/', include('rest_framework.urls')), path("yppf/", include("app.urls")), - # TODO: Why it is here? - # path("yppf/", include("feedback.urls")), path("underground/", include("Appointment.urls")), - path("yplibrary/", include("yp_library.urls")), - path("questionnaire/", include("questionnaire.urls")), - path("dormitory/", include("dormitory.urls")), path("", include("generic.urls")), path("", include("record.urls")), path("", include("app.urls")), - path("", include("feedback.urls")), ] # 生产环境下自动返回空列表,请通过docker或服务器设置手动serve静态文件和媒体文件 diff --git a/dm/dump_funcs.py b/dm/dump_funcs.py index b82ef71f7..fbceba4d6 100644 --- a/dm/dump_funcs.py +++ b/dm/dump_funcs.py @@ -12,7 +12,6 @@ from generic.models import * from record.models import * from app.models import * -from feedback.models import Feedback from Appointment.models import Appoint @@ -177,53 +176,3 @@ def dump(cls, hash_func: Callable = None, **options) -> pd.DataFrame: participants_data['用户'].map(hash_func) return participants_data - -class PersonCourseDump(BaseDump): - """个人书院课程参与记录 - 包含:课程数量,有效次数,无效次数,有效时长,无效时长 - """ - - @classmethod - def dump(cls, hash_func: Callable = None, **options) -> pd.DataFrame: - _m = CourseRecord - course_data = pd.DataFrame( - cls.time_filter(CourseRecord, year=options.get('year', None), - semester=options.get('semester', None)) - .values_list(SQ.f(_m.person)) - .annotate(course_num=Count('id'), - record_times=Sum('attend_times', filter=Q(invalid=False)), - invalid_times=Sum('attend_times', filter=Q(invalid=True)), - record_hours=Sum('total_hours', filter=Q(invalid=False)), - invalid_hours=Sum('total_hours', filter=Q(invalid=True))) - .values_list( - SQ.f(_m.person, NaturalPerson.person_id, User.username), - 'course_num', 'record_times', 'invalid_times', - 'record_hours', 'invalid_hours'), - columns=('用户', '课程数量', '有效次数', '无效次数', '有效时长', '无效时长')) - if hash_func is not None: - course_data['用户'].map(hash_func) - return course_data - - -class PersonFeedbackDump(BaseDump): - """个人反馈数据记录 - 包含:提交反馈数、解决反馈数。 - """ - @classmethod - def dump(cls, hash_func: Callable = None, **options) -> pd.DataFrame: - _m = Feedback - feedback_data = pd.DataFrame( - cls.time_filter(Feedback, start_time=options.get('start_time', None), - end_time=options.get('end_time', None), - start_time_field='feedback_time', - end_time_field='feedback_time') - .values_list(SQ.f(_m.person)) - .annotate(total_num=Count('id'), - solved_num=Count('id', filter=Q(solve_status=Feedback.SolveStatus.SOLVED))) - .values_list( - SQ.f(_m.person, NaturalPerson.person_id, User.username), - 'total_num', 'solved_num'), - columns=('用户', '提交反馈数', '已解决反馈数')) - if hash_func is not None: - feedback_data['用户'].map(hash_func) - return feedback_data diff --git a/dm/load_funcs.py b/dm/load_funcs.py index f1cfe847f..1232c54bc 100644 --- a/dm/load_funcs.py +++ b/dm/load_funcs.py @@ -18,19 +18,11 @@ Organization, OrganizationTag, OrganizationType, - Activity, Help, - Course, - CourseRecord, Semester, Comment, - AcademicTag, ) from app.utils import random_code_init -from feedback.models import ( - FeedbackType, - Feedback, -) @@ -40,11 +32,8 @@ 'create_person_account', 'create_org_account', # load functions 'load_stu', 'load_orgtype', 'load_org', - 'load_activity', - 'load_freshman', 'load_help', 'load_course_record', - 'load_org_tag', 'load_old_org_tags', 'load_feedback_type', - 'load_feedback', 'load_feedback_comments', 'load_major', - 'load_minor', 'load_double_degree', 'load_project' + 'load_freshman', 'load_help', + 'load_org_tag', 'load_old_org_tags', ] @@ -304,51 +293,6 @@ def load_org(filepath: str, output_func: Callable=None, html=False): return try_output(msg, output_func, html) -def load_activity(filepath: str, output_func: Callable=None, html=False): - act_df = load_file(filepath) - act_list = [] - for _, act_dict in act_df.iterrows(): - organization_id = str(act_dict["organization_id"]) - - try: - user = User.objects.get(username=organization_id) - org = Organization.objects.get(organization_id=user) - except: - msg = "请先导入小组信息!{username}".format(username=organization_id) - if output_func is not None: - output_func(msg) - return - else: - return msg - title = act_dict["title"] - start = act_dict["start"] - end = act_dict["end"] - start = datetime.strptime(start, "%m/%d/%Y %H:%M %p") - end = datetime.strptime(end, "%m/%d/%Y %H:%M %p") - location = act_dict["location"] - introduction = act_dict["introduction"] - capacity = int(act_dict["capacity"]) - URL = act_dict["URL"] - - act_list.append( - Activity( - title=title, - organization_id=org, - start=start, - end=end, - location=location, - introduction=introduction, - capacity=capacity, - URL=URL, - examine_teacher = NaturalPerson.objects.get(name="YPadmin") - ) - ) - # Activity.objects.bulk_create(act_list) - for act in act_list: - act.save() - return try_output("导入活动信息成功!", output_func, html) - - def load_stu(filepath: str, output_func: Callable=None, html=False): stu_df = load_file(filepath) total = 0 @@ -463,231 +407,6 @@ def load_help(filepath: str, output_func: Callable=None, html=False): return try_output("成功导入帮助信息!", output_func, html) -def load_course_record(filepath: str, output_func: Callable=None, html:bool=False) -> str: - """从文件中导入学时信息 - - :param filepath: 文件路径,放在test文件夹内 - :type filepath: str - :param output_func: 输出函数, defaults to None - :type output_func: Callable, optional - :param html: 允许以HTML格式输出,否则将br标签替换为\n, defaults to False - :type html: bool, optional - :return: 返回导入结果的提示 - :rtype: str - """ - - try: - courserecord_file = load_file(filepath) - except: - return try_output(f"没有找到{filepath},请确认该文件已经在test_data中。", output_func, html) - - # 学年,学期和课程的德智体美劳信息都是在文件的info这个sheet中读取的 - year = courserecord_file['info'].iloc[1,1] - semester = courserecord_file['info'].iloc[2,1] - semester = Semester.get(semester) - - course_type_all = { - "德" : Course.CourseType.MORAL , - "智" : Course.CourseType.INTELLECTUAL , - "体" : Course.CourseType.PHYSICAL , - "美" : Course.CourseType.AESTHETICS, - "劳" : Course.CourseType.LABOUR, - } - course_info = courserecord_file['info'] #info这个sheet - info_height, info_width = course_info.shape - # ---- 以下为读取info里面的课程信息并自动注册course ------ - for i in range(4, info_height): - course_name = course_info.iloc[i,0] - course_type = course_info.iloc[i,1] #德智体美劳 - #备注:由于一些课程名称所包含的符号不能被包含在excel文件的sheet的命名中(会报错), - #所以考虑到这种情况,使用模糊查询的方式,sheet的命名只写一部分就可以了 - orga_found = Organization.objects.filter(oname=course_name) - if not orga_found.exists(): #若查询不到,使用模糊查询 - orga_found = Organization.objects.filter(oname__contains=course_name) - - if orga_found.exists(): - course_found = Course.objects.filter( - name = orga_found[0].oname, - type__in = Course.CourseType, - year = year, - semester = semester, - ) - if not course_found.exists(): #新建课程 - Course.objects.create( - name = orga_found[0].oname, - organization = orga_found[0], - type = course_type_all[course_type], - status = Course.Status.END, - year = year, - semester = semester, - photo = ( - '/static/assets/img/announcepics/' - f'{course_type_all[course_type].value+1}.JPG' - ), - ) - - # ---- 以下为读取其他sheet并导入学时记录 ------- - info_show = { #储存异常信息 - 'type error': [], - 'stuID miss' :[], - 'person not found' : [], - 'course not found' : [], - 'data miss':[], - } - - for course in courserecord_file.keys(): #遍历各个sheet - if course in ['汇总','info']: continue - - course_df = courserecord_file[course] #文件里的一个sheet - height, width = course_df.shape - course_found = False #是否查询到sheet名称所对应的course - - course_get = Course.objects.filter( - name=course, - year=year, - semester=semester, - ) - if not course_get.exists(): - course_get = Course.objects.filter( - name__contains=course, - year=year, - semester=semester, - ) - - if course_get.exists(): #查找到了相应course - course_found = True - else: - info_show["course not found"].append(course) - - for i in range(4,height): - #每个sheet开头有几行不是学时信息,所以跳过 - sid: str = course_df.iloc[i, 1] #学号 - name: str = course_df.iloc[i, 2] - times: int = course_df.iloc[i, 3] - hours: float = course_df.iloc[i, 4] - record_view = f'{course} {sid} {name} {times} {hours}' - if not isinstance(name, str) and sid is numpy.nan: #允许中间有空行 - continue - if times is numpy.nan or hours is numpy.nan: #次数和学时缺少 - info_show["data miss"].append(record_view) - continue - try: - sid = '' if sid is numpy.nan else str(int(float(sid))) - times, hours = int(times), float(hours) - name = str(name) - except: - info_show["type error"].append(record_view) - continue - - person = NaturalPerson.objects.filter(name=name) - if not sid: #没有学号 - info_show["stuID miss"].append(record_view) - else: #若有学号,则根据学号继续查找(排除重名) - person = person.filter(SQ.sq( - [NaturalPerson.person_id, User.username], sid)) - - if not person.exists(): - error_info = [record_view] - #若同时按照学号和姓名查找不到的话,则只用姓名或者只用学号查找可能的人员 - person_guess_byname = NaturalPerson.objects.filter(name=name) - if sid: #若填了学号的话,则试着查找 - person_guess_byId = NaturalPerson.objects.filter(SQ.sq( - [NaturalPerson.person_id, User.username], sid)) - else: - person_guess_byId = None - error_info += [person_guess_byname, person_guess_byId] - info_show["person not found"].append(error_info) - continue - - record = CourseRecord.objects.filter( #查询是否已经有记录 - person=person[0], - year=year, - semester=semester, - ) - record_search_course = record.filter(course__name=course) - record_search_extra = record.filter(extra_name=course) - # 需要时临时修改即可 - invalid = float(hours) < CONFIG.course.least_record_hours - - if record_search_course.exists(): - record_search_course.update( - invalid = invalid, - attend_times = times, - total_hours = hours - ) - elif record_search_extra.exists(): - record_search_extra.update( - invalid = invalid, - attend_times = times, - total_hours = hours - ) - else: - newrecord = CourseRecord.objects.create( - person = person[0], - extra_name = course, - attend_times = times, - total_hours = hours, - year = year, - semester = semester, - invalid = invalid, - ) - if course_found: - newrecord.course = course_get[0] - newrecord.save() - - # ----- 以下为前端展示导入的结果 ------ - display_message = '导入完成\n' - print_show = [ - '
未查询到该人员:
', - '
是不是想导入以下学生?:
', - '
未查询到以下课程,已通过额外字段定义课程名称
', - '
表格内容错误
', - '
数据缺失
', - '
未填写学号,已导入但请注意排除学生同名的可能
', - '
新建的学时数据统计:
', - '
更新的学时数据统计:
' - ] - - - if info_show['person not found']: - display_message += print_show[0] - for person in info_show['person not found']: - display_message += '未查询到 ' + person[0] + '
' + print_show[1] - if person[1].exists(): - for message in person[1]: - message: NaturalPerson - display_message += '
' + message.name + ' ' + message.person_id.username + '
' - if person[2] != None and person[2].exists(): - for message in person[2]: - message: NaturalPerson - display_message += '
' + message.name + ' ' + message.person_id.username + '
' - elif not person[1].exists(): - display_message += '
未查询到类似数据
' - display_message += '
' - - if info_show['course not found']: - display_message += print_show[2] - for course in info_show['course not found']: - display_message += '
' + course + '
' - - if info_show['type error']: - display_message += print_show[3] - for error in info_show['type error']: - display_message += '
' + '表格内容: ' + error + '
' - - if info_show['data miss']: - display_message += print_show[4] - for error in info_show['data miss']: - display_message += '
' + '表格内容: ' + error + '
' - - if info_show['stuID miss']: - display_message += print_show[5] - for stu in info_show['stuID miss']: - display_message += '
' + '表格内容: ' + stu + '
' - - return try_output(display_message, output_func, html) - - def load_org_tag(filepath: str, output_func: Callable=None, html=False): try: org_tag_def = load_file(filepath) @@ -738,239 +457,6 @@ def load_old_org_tags(filepath: str, output_func: Callable=None, html=False): return try_output(msg, output_func, html) -def load_feedback(filepath: str, output_func: Callable=None, html=False): - '''该函数用于导入反馈详情的数据(csv)''' - try: - feedback_df = load_file(filepath) - except: - return try_output(f"没有找到{filepath},请确认该文件已经在test_data中。", output_func, html) - error_dict = {} - feedback_num = 0 - for _, feedback_dict in feedback_df.iterrows(): - feedback_num += 1 - err = False - try: - feedback, mid = Feedback.objects.get_or_create( - type=FeedbackType.objects.get(name=feedback_dict["type"]), - person=NaturalPerson.objects.get(name=feedback_dict["person"]), - org=Organization.objects.get(oname=feedback_dict["org"]), - ) - - feedback.title = feedback_dict["title"] - feedback.content = feedback_dict["content"] - - issue_status_dict = {"草稿": 0, "已发布": 1, "已删除": 2,} - read_status_dict = {"已读": 0, "未读": 1,} - solve_status_dict = {"已解决": 0, "解决中": 1, "无法解决": 2,} - public_status_dict = {"公开": 0, "未公开": 1, "撤销公开": 2, "强制不公开": 3,} - - assert feedback_dict["issue_status"] in issue_status_dict.keys() - feedback.issue_status = issue_status_dict[feedback_dict["issue_status"]] - - assert feedback_dict["read_status"] in read_status_dict.keys() - feedback.read_status = read_status_dict[feedback_dict["read_status"]] - - assert feedback_dict["solve_status"] in solve_status_dict.keys() - feedback.solve_status = solve_status_dict[feedback_dict["solve_status"]] - - assert feedback_dict["public_status"] in public_status_dict.keys() - feedback.public_status = public_status_dict[feedback_dict["public_status"]] - - if feedback_dict["publisher_public"].lower() == "true": - feedback.publisher_public = True - else: - feedback.publisher_public = False - - if feedback_dict["org_public"].lower() == "true": - feedback.org_public = True - else: - feedback.org_public = False - - except Exception as e: - err = True - error_dict["{}: {}".format(feedback_num, feedback_dict["title"])] = ''' - 填写状态信息有误,请再次检查发布/阅读/解决/公开状态(文字)是否填写正确! - ''' if isinstance(e,AssertionError) else e - feedback.delete() - - if not err: - feedback.save() - - msg = '
'.join(( - f"共尝试导入{feedback_num}条反馈的详细信息", - f"导入成功的反馈:{feedback_num - len(error_dict)}条", - f"导入失败的反馈:{len(error_dict)}条", - f'错误原因:' if error_dict else '' - ) + tuple(f'{fb}:{err}' for fb, err in error_dict.items() - ) + ('', - f"请注意:下面的字段必须填写对应的选项,否则反馈信息将无法保存!", - f"(1)issue_status:草稿 / 已发布 / 已删除", - f"(2)read_status:已读 / 未读", - f"(3)solve_status:已解决 / 解决中 / 无法解决", - f"(4)public_status:公开 / 未公开 / 撤销公开 / 强制不公开" - )) - return try_output(msg, output_func, html) - - -def load_feedback_type(filepath: str, output_func: Callable=None, html=False): - '''该函数用于导入反馈类型的数据(csv)''' - try: - feedback_type_df = load_file(filepath) - except: - return try_output(f"没有找到{filepath},请确认该文件已经在test_data中。", output_func, html) - type_list = [] - for _, type_dict in feedback_type_df.iterrows(): - type_id = int(type_dict["id"]) - type_name = type_dict["name"] - flexible = int(type_dict["flexible"]) - if flexible == 0: - feedbacktype, mid = FeedbackType.objects.get_or_create(id=type_id) - elif flexible == 1: - otype = type_dict["org_type"] - otype_id = OrganizationType.objects.get(otype_name=otype).otype_id - feedbacktype, mid = FeedbackType.objects.get_or_create( - id=type_id, org_type_id=otype_id, - ) - else: - otype = type_dict["org_type"] - org = type_dict["org"] - otype_id = OrganizationType.objects.get(otype_name=otype).otype_id - org_id = Organization.objects.get(oname=org).id - feedbacktype, mid = FeedbackType.objects.get_or_create( - id=type_id, org_type_id=otype_id, org_id=org_id, - ) - feedbacktype.name = type_name - feedbacktype.flexible = flexible - feedbacktype.save() - - FeedbackType.objects.bulk_create(type_list) - return try_output("导入反馈类型信息成功!", output_func, html) - - -def load_feedback_comments(filepath: str, output_func: Callable=None, html=False): - '''该函数用于导入反馈的评论(feedbackcomments.csv) - 需要先导入feedbackinf.csv''' - try: - feedback_df = load_file(filepath) - except: - return try_output(f"没有找到{filepath},请确认该文件已经在test_data中。", output_func, html) - error_dict = {} - comment_num = 0 - for _, comment_dict in feedback_df.iterrows(): - comment_num += 1 - err = False - try: - feedback = Feedback.objects.get(id=comment_dict["fid"]) - commentator = get_user_by_name(comment_dict["commentator"]) - comment_time = datetime.strptime(comment_dict["time"], "%m/%d/%Y %H:%M %p") - - comment = Comment.objects.create( - commentbase=feedback, commentator=commentator, text=comment_dict["text"], time=comment_time - ) - - except Exception as e: - err = True - error_dict["{}: {}".format(comment_num, comment_dict["fid"])] = ''' - 填写状态信息有误,请再次检查发布/阅读/解决/公开状态(文字)是否填写正确! - ''' if isinstance(e,AssertionError) else e - comment.delete() - - if not err: - comment.save() - - msg = '
'.join(( - f"共尝试导入{comment}条反馈评论", - f"导入成功的反馈:{comment_num - len(error_dict)}条", - f"导入失败的反馈:{len(error_dict)}条", - f'错误原因:' if error_dict else '' - ) + tuple(f'{fb}:{err}' for fb, err in error_dict.items() - )) - return try_output(msg, output_func, html) - - -def load_major(filepath: str, output_func: Callable=None, html=False): - '''该函数用于导入学术地图中的主修专业标签(文件须为txt格式)''' - if not filepath.endswith('txt'): - return try_output("请确保数据文件为txt格式!", output_func, html) - - try: - file = open(filepath, 'r') - except: - return try_output(f"没有找到{filepath},请确认该文件已经在test_data中。", output_func, html) - - lines = [line.strip() for line in file.readlines()] - majors = [line for i, line in enumerate(lines) if (line != '') and (i > 0 and lines[i-1] != '')] - for major in majors: - AcademicTag.objects.get_or_create( - atype=AcademicTag.Type.MAJOR, - tag_content=major, - ) - file.close() - return try_output("导入主修专业信息成功!", output_func, html) - - -def load_minor(filepath: str, output_func: Callable=None, html=False): - '''该函数用于导入学术地图中的辅修专业标签(文件须为txt格式)''' - if not filepath.endswith('txt'): - return try_output("请确保数据文件为txt格式!", output_func, html) - - try: - file = open(filepath, 'r') - except: - return try_output(f"没有找到{filepath},请确认该文件已经在test_data中。", output_func, html) - - lines = [line.strip() for line in file.readlines()] - minors = [line for i, line in enumerate(lines) if (line != '') and (i > 0 and lines[i-1] != '')] - for minor in minors: - AcademicTag.objects.get_or_create( - atype=AcademicTag.Type.MINOR, - tag_content=minor, - ) - file.close() - return try_output("导入辅修专业信息成功!", output_func, html) - - -def load_double_degree(filepath: str, output_func: Callable=None, html=False): - '''该函数用于导入学术地图中的双学位专业标签(文件须为txt格式)''' - if not filepath.endswith('txt'): - return try_output("请确保数据文件为txt格式!", output_func, html) - - try: - file = open(filepath, 'r') - except: - return try_output(f"没有找到{filepath},请确认该文件已经在test_data中。", output_func, html) - - lines = [line.strip() for line in file.readlines()] - majors = [line for i, line in enumerate(lines) if (line != '') and (i > 0 and lines[i-1] != '')] - for major in majors: - AcademicTag.objects.get_or_create( - atype=AcademicTag.Type.DOUBLE_DEGREE, - tag_content=major, - ) - file.close() - return try_output("导入双学位专业信息成功!", output_func, html) - - -def load_project(filepath: str, output_func: Callable=None, html=False): - '''该函数用于导入学术地图中的项目标签(文件须为txt格式)''' - if not filepath.endswith('txt'): - return try_output("请确保数据文件为txt格式!", output_func, html) - - try: - file = open(filepath, 'r') - except: - return try_output(f"没有找到{filepath},请确认该文件已经在test_data中。", output_func, html) - - lines = [line.strip() for line in file.readlines()] - projects = [line for line in lines if line != ''] - for project in projects: - AcademicTag.objects.get_or_create( - atype=AcademicTag.Type.PROJECT, - tag_content=project, - ) - file.close() - return try_output("导入项目信息成功!", output_func, html) - def load_birthday(filepath: str, use_name: bool=False, slash: bool=False): ''' 用来从csv中导入用户生日的函数,调用频率很低 diff --git a/dm/management/__init__.py b/dm/management/__init__.py index da8055ec9..765d4fc15 100644 --- a/dm/management/__init__.py +++ b/dm/management/__init__.py @@ -51,8 +51,6 @@ def register_dump_groups(group: str, tasks: List[str]): register_dump('org_activity', OrgActivityDump) register_dump('person_position', PersonPosDump, accept_params=['year', 'semester']) register_dump('person_activity', PersonActivityDump, accept_params=['year', 'semester']) -register_dump('person_feedback', PersonFeedbackDump) -register_dump('person_course', PersonCourseDump, accept_params=['year', 'semester']) register_dump_groups('tracking', ['page', 'module']) register_dump_groups('activity', ['org_activity', 'person_activity']) @@ -66,13 +64,4 @@ def register_dump_groups(group: str, tasks: List[str]): register_load('org', load_org, 'orginf.csv') register_load('orgtag', load_org_tag, 'orgtag.csv') register_load('oldorgtags', load_old_org_tags, 'oldorgtags.csv') -register_load('activity', load_activity, 'activityinfo.csv') register_load('help', load_help, 'help.csv') -register_load('courserecord', load_course_record, 'coursetime.xlsx') -register_load('feedbackType', load_feedback_type, 'feedbacktype.csv') -register_load('feedback', load_feedback, 'feedbackinf.csv') -register_load('feedbackComments', load_feedback_comments, 'feedbackcomments.csv') -register_load('major', load_major, 'major.txt') -register_load('minor', load_minor, 'minor.txt') -register_load('doubleDegree', load_double_degree, 'doubledegree.txt') -register_load('project', load_project, 'project.txt') diff --git a/dm/summary.py b/dm/summary.py deleted file mode 100644 index e3b3a1316..000000000 --- a/dm/summary.py +++ /dev/null @@ -1,483 +0,0 @@ -"""Generating summary data -TODO: Remove type errors -""" - -from typing import Dict, Any -from datetime import * -from collections import defaultdict, Counter - -from django.db.models import * - -from utils.models.query import * -from app.config import * -from app.models import * -from Appointment.models import Appoint, CardCheckInfo, Room -from Appointment.utils.identity import get_participant -from Appointment.config import CONFIG - -SUMMARY_YEAR = 2021 -SUMMARY_SEM_START = datetime(2021, 9, 1) -SUMMARY_SEM_END: datetime = CONFIG.semester_start - - -def remove_local_var(d: Dict[str, Any]): - keys = list(d.keys()) - for k in keys: - if k.startswith('_'): - d.pop(k) - if k == 'np': - d.pop(k) - return d - - -def generic_info(): - generic_info = {} - generic_info.update(cal_all_org()) - generic_info.update(cal_all_course()) - return generic_info - - -def person_info(np: 'NaturalPerson|User'): - if isinstance(np, User): - np = NaturalPerson.objects.get_by_user(np) - person_info = dict(Sname=np.name) - person_info.update(cal_study_room(np)) - person_info.update(cal_early_room(np)) - person_info.update(cal_late_room(np)) - person_info.update(cal_appoint(np)) - person_info.update(cal_appoint_kw(np)) - person_info.update(cal_co_appoint(np)) - person_info.update(cal_sharp_appoint(np)) - person_info.update(cal_appoint_sum(np)) - person_info.update(cal_act(np)) - person_info.update(cal_course(np)) - return person_info - - -def person_infos(min=0, max=10000, count=10000): - npd = {} - for np in NaturalPerson.objects.filter( - mq(NaturalPerson.person_id, User.id, gte=min, lte=max) - ).select_related(f(NaturalPerson.person_id)): - npd[np.person_id.username] = person_info(np) - count -= 1 - if count <= 0: - break - return npd - - -# 通用统计部分从此开始 -__generics = None - - -def cal_all_org(): - total_club_num = ModifyOrganization.objects.filter( - otype__otype_name='学生小组', status=ModifyOrganization.Status.CONFIRMED).count() - total_courseorg_num = Organization.objects.filter( - otype__otype_name='书院课程').count() - total_act = Activity.objects.exclude(status__in=[ - Activity.Status.REVIEWING, - Activity.Status.CANCELED, - Activity.Status.ABORT, - Activity.Status.REJECT - ]).filter(year=SUMMARY_YEAR) - total_act_num = total_act.count() - total_act_hour: timedelta = sum( - [(a.end-a.start) for a in total_act], timedelta(0)) - total_act_hour = round(total_act_hour.total_seconds() / 3600, 2) - return dict(total_club_num=total_club_num, total_courseorg_num=total_courseorg_num, - total_act_num=total_act_num, total_act_hour=total_act_hour - ) - - -def cal_all_course(): - total_course_num = Course.objects.exclude(status=Course.Status.ABORT).filter( - year=SUMMARY_YEAR).count() - course_act = Activity.objects.exclude(status__in=[ - Activity.Status.REVIEWING, - Activity.Status.CANCELED, - Activity.Status.ABORT, - Activity.Status.REJECT - ]).filter( - year=SUMMARY_YEAR, category=Activity.ActivityCategory.COURSE) - - total_course_act_num = len(course_act) - total_course_act_hour: timedelta = sum( - [(a.end-a.start) for a in course_act], timedelta(0)) - total_course_act_hour = round( - total_course_act_hour.total_seconds() / 3600, 2) - - persons = NaturalPerson.objects.annotate(cc=Count( - 'courseparticipant', filter=Q( - courseparticipant__course__year=SUMMARY_YEAR, - courseparticipant__status__in=[ - CourseParticipant.Status.SELECT, - CourseParticipant.Status.SUCCESS], - ))) - have_course_num = persons.filter(cc__gte=1).count() - have_three_course_num = persons.filter(cc__gte=3).count() - - return dict(total_course_num=total_course_num, - total_course_act_num=total_course_act_num, total_course_act_hour=total_course_act_hour, - have_course_num=have_course_num, have_three_course_num=have_three_course_num - ) - - -__persons = None - -def test_wrapper(func): - def _(np): - try: - return func(np) - except: - return {} - return _ - - -def cal_sharp_appoint(np: NaturalPerson): - appoints = Appoint.objects.filter( - Astart__gte=SUMMARY_SEM_START, - Astart__lt=SUMMARY_SEM_END, - major_student__Sid=np.person_id) - sharp_appoints = appoints.exclude(Atype=Appoint.Type.TEMPORARY).filter( - Astart__lt=F('Atime') + timedelta(minutes=30)) - sharp_appoint_num = sharp_appoints.count() - if not sharp_appoint_num: - return dict(sharp_appoint_num=sharp_appoint_num) - sharp_appoint: Appoint = min( - sharp_appoints, key=lambda x: x.Astart-x.Atime) - sharp_appoint_day = sharp_appoint.Astart.strftime('%Y年%m月%d日') - sharp_appoint_reason = sharp_appoint.Ausage - sharp_appoint_min = (sharp_appoint.Astart - sharp_appoint.Atime).total_seconds() - if sharp_appoint_min < 60: - sharp_appoint_min = f'{round(sharp_appoint_min)}秒' - else: - sharp_appoint_min = f'{round((sharp_appoint_min / 60) % 60)}分钟' - sharp_appoint_room = str(sharp_appoint.Room) - disobey_num = appoints.filter(Astatus=Appoint.Status.VIOLATED).count() - return dict( - sharp_appoint_num=sharp_appoint_num, - sharp_appoint_day=sharp_appoint_day, - sharp_appoint_reason=sharp_appoint_reason, - sharp_appoint_min=sharp_appoint_min, - sharp_appoint_room=sharp_appoint_room, - disobey_num=disobey_num - ) - - -def cal_appoint_sum(np: NaturalPerson): - appoints = Appoint.objects.not_canceled().filter( - Astart__gte=SUMMARY_SEM_START, - Astart__lt=SUMMARY_SEM_END, - major_student__Sid=np.person_id - ) - total_time = appoints.aggregate( - time=Sum(F('Afinish')-F('Astart')))['time'] or timedelta() - appoint_hour = round(total_time.total_seconds() / 3600, 1) - appoint_num = appoints.count() - - return dict( - appoint_hour=appoint_hour, appoint_num=appoint_num, - # poem_word=?? - ) - - -def cal_act(np: NaturalPerson): - orgs = ModifyOrganization.objects.filter( - pos=np.person_id, status=ModifyOrganization.Status.CONFIRMED) - IScreate = bool(orgs) - myclub_name = '' - if IScreate: - myclub_name = ','.join(orgs.values_list('oname', flat=True)) - pos = Position.objects.activated(noncurrent=None).filter( - person=np, - year=SUMMARY_YEAR - ) - club_num = pos.filter(org__otype__otype_name='学生小组').count() - course_org_num = pos.filter(org__otype__otype_name='书院课程').count() - act_num = Participation.objects.activated().filter( - sq(Participation.person, np), - sq([Participation.activity, Activity.year], SUMMARY_YEAR), - ).count() - position_num = pos.count() - return dict( - IScreate=IScreate, myclub_name=myclub_name, - club_num=club_num, course_org_num=course_org_num, act_num=act_num, - position_num=position_num, - ) - - -def cal_course(np: NaturalPerson): - # course_num = Course.objects.exclude( - # status=Course.Status.ABORT).filter( - # year=SUMMARY_YEAR, - # participant_set__person=np, - # participant_set__status__in=[ - # CourseParticipant.Status.SELECT, - # CourseParticipant.Status.SUCCESS, - # CourseParticipant.Status.FAILED, - # ]).count() - - course_me_past = CourseRecord.objects.filter( - person=np, invalid=False, year=SUMMARY_YEAR) - - course_num = course_me_past.count() - - pro = [] - # 计算每个类别的学时 - for course_type in list(Course.CourseType): # CourseType.values亦可 - t = course_me_past.filter(course__type=course_type) - if not t: - continue - t = t.aggregate(Sum('total_hours'), Sum('attend_times'), count=Count('*')) - pro.append([course_type.label, t['total_hours__sum'] - or 0, t['attend_times__sum'] or 0, t['count'] or 0]) - - unclassified_hour = course_me_past.filter(course__isnull=True).aggregate( - Sum('total_hours'))['total_hours__sum'] or 0 - course_hour = 0 - - types = [] - max_type_info = '无', 0 - for label, hour, _, count in pro: - if count > max_type_info[1]: - max_type_info = label, count - types.append(label) - course_hour += hour - - if unclassified_hour: - types.append('其它') - course_hour += unclassified_hour - - course_type = '/'.join(types) + f' {len(types)}' - type_count = len(types) - - if course_me_past: - most_time: CourseRecord = max( - course_me_past, key=lambda x: x.total_hours) - most_num: CourseRecord = max( - course_me_past, key=lambda x: x.attend_times) - course_most_time_name, course_most_hour = most_time.get_course_name(), most_time.total_hours - course_most_num_name, course_most_num = most_num.get_course_name(), most_num.attend_times - else: - course_most_time_name, course_most_hour = "无", 0 - course_most_num_name, course_most_num = "无", 0 - - return dict(course_num=course_num, course_hour=course_hour, course_type=course_type, - course_most_time_name=course_most_time_name, course_most_hour=course_most_hour, - course_most_num_name=course_most_num_name, course_most_num=course_most_num, - max_type_info=max_type_info, type_count=type_count - ) - - -def cal_study_room(np: NaturalPerson): - - _start_time = SUMMARY_SEM_START - _end_time = SUMMARY_SEM_END - - _user = np.get_user() - _par = get_participant(_user) - if _par is None: - return {} - - _study_room_record_filter = Q(Cardroom__Rtitle__contains='自习', - Cardtime__gt=_start_time, - Cardtime__lt=_end_time, - Cardstudent=_par) - - _study_room_reords = CardCheckInfo.objects.filter( - _study_room_record_filter) - - if not _study_room_reords.exists(): - return dict(study_room_num=0) - study_room_num = _study_room_reords.aggregate(cnt=Count('*')).get('cnt', 0) - study_room_day = _study_room_reords.values_list('Cardtime__date').annotate( - cnt=Count('*')).aggregate(cnt=Count('*')).get('cnt', 0) - _cnt_dict = defaultdict(int) - for r, _, _ in _study_room_reords.values_list('Cardroom__Rid', 'Cardtime__date').annotate(cnt=Count('*')): - _cnt_dict[r] += 1 - study_room_top, study_room_top_day = max( - [(r, cnt) for r, cnt in _cnt_dict.items()], key=lambda x: x[1]) - return dict(study_room_num=study_room_num, - study_room_day=study_room_day, - study_room_top=study_room_top, - study_room_top_day=study_room_top_day, - ) - - -def cal_early_room(np: NaturalPerson): - - _start_time = SUMMARY_SEM_START - _end_time = SUMMARY_SEM_END - - _user = np.get_user() - _par = get_participant(_user) - if _par is None: - return {} - - _record_filter = Q(Cardtime__gt=_start_time, - Cardtime__lt=_end_time, - Cardstudent=_par, - Cardtime__hour__lt=8, - Cardtime__hour__gte=6) - _room_reords = CardCheckInfo.objects.filter(_record_filter) - if not _room_reords.exists(): - return dict(early_day_num=0) - early_day_num = _room_reords.values_list('Cardtime__date').annotate( - cnt=Count('*')).aggregate(cnt=Count('*')).get('cnt', 0) - if early_day_num: - early_room, early_room_day, early_room_time = min(list(_room_reords.values_list( - 'Cardroom__Rid', 'Cardtime__date').annotate(time=Min('Cardtime__time'))), key=lambda x: x[2]) - return remove_local_var(locals()) - - -def cal_late_room(np: NaturalPerson): - - _start_time = SUMMARY_SEM_START - _end_time = SUMMARY_SEM_END - - _user = np.get_user() - _par = get_participant(_user) - if _par is None: - return {} - - _record_filter = Q(Cardtime__gt=_start_time, - Cardtime__lt=_end_time, - Cardstudent=_par) - _late_filter_night = Q(Cardtime__hour__gte=23) - _late_filter_dawn = Q(Cardtime__hour__lt=5) - _room_reords = CardCheckInfo.objects.filter(_record_filter) - late_room_num = len(list(set(_room_reords.filter( - _late_filter_night).values_list('Cardtime__date')))) - if not late_room_num: - return dict(late_room_num=0) - _dawn_records = list(_room_reords.filter(_late_filter_dawn).values_list( - 'Cardroom', 'Cardtime__date', 'Cardtime__time')) - if _dawn_records: - _latest_record = max(_dawn_records, key=lambda x: x[2]) - else: - _latest_record = max(_room_reords.filter(_late_filter_night).values_list( - 'Cardroom', 'Cardtime__date', 'Cardtime__time'), key=lambda x: x[2]) - late_room, late_room_date, late_room_time = _latest_record - _late_room_ref_date = late_room_date - if late_room_time.hour < 23: - _late_room_ref_date = late_room_date - timedelta(days=1) - late_room_people = len(list(set(CardCheckInfo.objects.filter(Cardtime__gt=_start_time, - Cardtime__lt=_end_time, - Cardtime__date=_late_room_ref_date, - Cardtime__hour__gte=22 - ).values_list('Cardstudent')))) - return locals() - - -def cal_appoint(np: NaturalPerson): - - _start_time = SUMMARY_SEM_START - _end_time = SUMMARY_SEM_END - - _user = np.get_user() - _par = get_participant(_user) - if _par is None: - return {} - - _talk_rooms = Room.objects.talk_rooms().values_list('Rid') - _func_rooms = Room.objects.function_rooms().values_list('Rid') - _me_act_appoint = Appoint.objects.not_canceled().filter( - students=_par, Astart__gt=_start_time, Astart__lt=_end_time) - _me_act_talk_appoint = _me_act_appoint.filter(Room__in=_talk_rooms) - if not _me_act_appoint.exists(): - return {} - if not _me_act_talk_appoint.exists(): - discuss_appoint_num = 0 - else: - discuss_appoint_num = _me_act_talk_appoint.aggregate(cnt=Count('*'))['cnt'] - - discuss_appoint_hour = sum([(finish - start).seconds for start, - finish in _me_act_talk_appoint.values_list('Astart', 'Afinish')])//3600 - _my_talk_rooms = _me_act_talk_appoint.values_list('Room') - discuss_appoint_long_room, discuss_appoint_long_hour = max([(r[0], _me_act_appoint.filter(Room=r).aggregate( - tol=Sum(F('Afinish') - F('Astart')))['tol'].total_seconds()//3600) for r in _my_talk_rooms], key=lambda x: x[1]) - appiont_most_day, appoint_most_num = Counter( - _me_act_appoint.values_list('Astart__date')).most_common(1)[0] - appiont_most_day = appiont_most_day[0].strftime('%m月%d日') - - _me_act_func_appoint = _me_act_appoint.filter(Room__in=_func_rooms) - if not _me_act_func_appoint.exists(): - func_appoint_num = func_appoint_hour = 0 - else: - func_appoint_num = _me_act_func_appoint.aggregate(cnt=Count('*'))['cnt'] - func_appoint_hour = _me_act_func_appoint.aggregate( - tol=Sum(F('Afinish') - F('Astart')))['tol'].total_seconds()//3600 - # django 的 groupby 真的烂 - # func_appoint_most = _me_act_func_appoint.values_list('Room').annotate(cnt=Count('*')) - func_appoint_most, func_appoint_most_hour = Counter( - _me_act_func_appoint.values_list('Room__Rtitle')).most_common(1)[0] - func_appoint_most = func_appoint_most[0] - return remove_local_var(locals()) - - -def cal_appoint_kw(np: NaturalPerson): - import jieba - - _start_time = SUMMARY_SEM_START - _end_time = SUMMARY_SEM_END - - _user = np.get_user() - _par = get_participant(_user) - if _par is None: - return {} - - _talk_rooms = Room.objects.talk_rooms().values_list('Rid') - _func_rooms = Room.objects.function_rooms().values_list('Rid') - _me_act_appoint = Appoint.objects.not_canceled().filter( - students=_par, Astart__gt=_start_time, Astart__lt=_end_time).exclude(Atype=Appoint.Type.TEMPORARY) - - _key_words = [] - for _usage in _me_act_appoint.values_list('Ausage'): - _key_words.extend(jieba.cut(_usage[0])) - Skeywords = Counter(_key_words).most_common(3) - return remove_local_var(locals()) - - -def cal_co_appoint(np: NaturalPerson): - _start_time = SUMMARY_SEM_START - _end_time = SUMMARY_SEM_END - - _user = np.get_user() - _par = get_participant(_user) - if _par is None: - return {} - - _me_act_appoint = Appoint.objects.not_canceled().filter( - students=_par, Astart__gt=_start_time, Astart__lt=_end_time) - _co_np_list = [] - for appoint in _me_act_appoint: - for _co_np in appoint.students.all(): - if _co_np != _par: - _co_np_list.append(_co_np) - if not _co_np_list: - return {} - co_mate, co_appoint_num = Counter(_co_np_list).most_common(1)[0] - _co_act_appoint = _me_act_appoint.filter(students=co_mate) - co_appoint_hour = _co_act_appoint.aggregate( - tol=Sum(F('Afinish') - F('Astart')))['tol'].total_seconds()//3600 - co_mate = co_mate.name - co_title = '' - if co_appoint_hour > 30: - co_title = '最好的朋友' - elif co_appoint_hour > 15: - co_title = '形影不离' - elif co_appoint_hour > 8: - co_title = '结伴同行' - elif co_appoint_hour > 3: - co_title = '感谢相遇' - - import jieba - _key_words = [] - for usage in _co_act_appoint.values_list('Ausage'): - if usage[0] in ['临时预约', '[MASK]']: - continue - _key_words.extend(jieba.cut(usage[0])) - co_keyword = Counter(_key_words).most_common(1)[0] - - return remove_local_var(locals()) diff --git a/dm/summary2023.py b/dm/summary2023.py deleted file mode 100644 index a9366b664..000000000 --- a/dm/summary2023.py +++ /dev/null @@ -1,930 +0,0 @@ -"""Generating summary data -TODO: Remove type errors -""" - -from typing import Dict, Any -from datetime import * -from collections import defaultdict, Counter - -from django.db.models import * -from django.db.models.functions import TruncDay - -from utils.models.query import * -from app.config import * -from app.models import * -from app.YQPoint_utils import get_income_expenditure -from generic.models import User, YQPointRecord -from Appointment.models import Appoint, CardCheckInfo, Room -from Appointment.utils.identity import get_participant - -# from Appointment.config import CONFIG - -SUMMARY_YEAR1 = 2023 -SUMMARY_SEMSTER1 = '秋' -SUMMARY_YEAR2 = 2022 -SUMMARY_SEMSTER2 = '春' -SUMMARY_SEM_START = datetime(2023, 2, 1) -SUMMARY_SEM_END: datetime = datetime(2024, 1, 15) - - -def remove_local_var(d: Dict[str, Any]): - keys = list(d.keys()) - for k in keys: - if k.startswith('_'): - d.pop(k) - if k == 'np': - d.pop(k) - if k == 'jieba': - d.pop(k) - return d - - -def generic_info(): - generic_info = {} - generic_info.update(cal_all_underground()) - generic_info.update(cal_all_org()) - generic_info.update(cal_all_course()) - return generic_info - - -def person_info(np: 'NaturalPerson|User'): - if isinstance(np, User): - np = NaturalPerson.objects.get_by_user(np) - person_info = dict(Sname=np.name) - # 个人的书院部分信息统计 - person_info.update(cal_login_num(np)) - person_info.update(cal_act(np)) - person_info.update(cal_course(np)) - person_info.update(cal_anual_yqpoint(np)) - person_info.update(cal_anual_academic(np)) - - # 个人的地下室部分信息统计 - person_info.update(cal_study_room(np)) - person_info.update(cal_appoint(np)) - person_info.update(cal_sharp_appoint(np)) - person_info.update(cal_co_appoint(np)) - # person_info.update(cal_early_room(np)) - # person_info.update(cal_late_room(np)) - # person_info.update(cal_appoint_kw(np)) - # person_info.update(cal_appoint_sum(np)) - - return person_info - - -def person_infos(min=0, max=10000, count=10000): - npd = {} - num = 0 - for np in NaturalPerson.objects.filter( - mq(NaturalPerson.person_id, User.id, gte=min, lte=max) - ).select_related(f(NaturalPerson.person_id)): - npd[np.person_id.username] = person_info(np) - count -= 1 - if count <= 0: - break - num += 1 - if num % 100 == 0: - print(num) - - return npd - - -# 通用统计部分从此开始 -__generics = None - - -def cal_all_underground(): - """ - - 地下室年度使用情况总览 - (1) 本年度地下室总刷卡记录 - (2) 本年度总研讨室预约次数、时长 - (3) 本年度总功能室预约次数、时长 - (4) 最受欢迎的研讨室和预约次数 - (5) 最受欢迎的功能房和预约次数 - """ - _room_reords = CardCheckInfo.objects.filter( - Q(Cardtime__gt=SUMMARY_SEM_START, Cardtime__lt=SUMMARY_SEM_END,)) - _func_rooms = Room.objects.function_rooms().values_list('Rid') - _talk_rooms = Room.objects.talk_rooms().values_list('Rid') - - # 总刷卡次数 - total_swipe_num = _room_reords.aggregate(cnt=Count('*')).get('cnt', 0) - # 研讨室刷卡总次数 - total_talk_room_num = _room_reords.filter( - Q(Cardroom__in=_talk_rooms)).aggregate(cnt=Count('*')).get('cnt', 0) - # 研讨室预约总时长 - _act_appoint = Appoint.objects.not_canceled().filter( - Astart__gt=SUMMARY_SEM_START, Astart__lt=SUMMARY_SEM_END) - _act_talk_appoint = _act_appoint.filter(Room__in=_talk_rooms) - total_discuss_appoint_hour = sum([(finish - start).seconds for start, - finish in _act_talk_appoint.values_list('Astart', 'Afinish')])//3600 - # 最受欢迎的研讨室和预约次数 - total_talk_appoint_most_name, total_talk_appoint_most_num = Counter( - _act_talk_appoint.values_list('Room__Rid')).most_common(1)[0] - total_talk_appoint_most_name = total_talk_appoint_most_name[0] - - # 功能房刷卡总次数 - total_func_room_num = _room_reords.filter( - Q(Cardroom__in=_func_rooms)).aggregate(cnt=Count('*')).get('cnt', 0) - # 功能房预约总时长 - _act_func_appoint = _act_appoint.filter(Room__in=_func_rooms) - total_func_appoint_hour = sum([(finish - start).seconds for start, - finish in _act_func_appoint.values_list('Astart', 'Afinish')])//3600 - # 最受欢迎的功能房和预约次数 - total_func_appoint_most_name, total_func_appoint_most_num = Counter( - _act_func_appoint.values_list('Room__Rid')).most_common(1)[0] - total_func_appoint_most_name = total_func_appoint_most_name[0] - - return remove_local_var(locals()) - - -def get_hottest_courses(year, semester): - """ - 根据年份和学期获取最热门的前三门课程 - """ - courses = Course.objects.filter( - year=year, semester=semester).exclude(status=Course.Status.ABORT) - - # # 计算每门课程的预选人数 - course_with_preselect_count = courses.annotate( - preselect_count=Count( - 'participant_set', - filter=Q(participant_set__status__in=[ - CourseParticipant.Status.SELECT, - CourseParticipant.Status.SUCCESS, - CourseParticipant.Status.FAILED, - ]) - ) - ) - # 计算预选人数与选课名额之比,使用ExpressionWrapper来确保结果为浮点数 - hottest_courses = course_with_preselect_count.annotate( - hotness=ExpressionWrapper( - F('preselect_count') / F('capacity'), - output_field=FloatField() - ) - ).order_by('-hotness')[:3] # 按照hotness降序排列,并取前三门课程 - hottest_courses_list = [] - for course in hottest_courses: - ele = {} - ele[course.name] = course.hotness - hottest_courses_list.append(ele) - - return hottest_courses_list - - -def cal_all_org(): - """ - - YPPF年度使用情况总览 - (1) 本年度所有小组、学生小组的总数 - (2) 本年度小组发起活动的总数量、总时长 - """ - # 所有小组的总数 - total_org_num = Organization.objects.activated().count() - - # 学生小组总数量 - total_club_num = ModifyOrganization.objects.filter( - otype__otype_name='学生小组', status=ModifyOrganization.Status.CONFIRMED).count() - - # 活动总数量,注意这里是学年制 - total_act = Activity.objects.exclude(status__in=[ - Activity.Status.REVIEWING, - Activity.Status.CANCELED, - Activity.Status.ABORT, - Activity.Status.REJECT - ]).filter(Q(year=SUMMARY_YEAR1, semester=Semester.FALL) | Q(year=SUMMARY_YEAR2, semester=Semester.SPRING)) - total_act_num = total_act.count() - # 总活动时长 - total_act_hour: timedelta = sum( - [(a.end-a.start) for a in total_act], timedelta(0)) - total_act_hour = round(total_act_hour.total_seconds() / 3600, 2) - - return dict(total_org_num=total_org_num, total_club_num=total_club_num, - total_act_num=total_act_num, total_act_hour=total_act_hour, - ) - - -def cal_all_course(): - """ - - YPPF年度使用情况总览 - (3) 书院本年度开课课程总数, 总课程活动数量 - (4) 本年度课程活动时长 - (5) 本年度参与一门课程的人数、参与三门课程的人数 - (6) 23年春季、秋季学期,最热门的三门书院课程(以预选人数和选课名额之比计算) - """ - # 书院本年度开课总数 - total_course_num = Course.objects.exclude(status=Course.Status.ABORT).filter( - Q(year=SUMMARY_YEAR1, semester=Semester.FALL) | Q(year=SUMMARY_YEAR2, semester=Semester.SPRING)).count() - - # 本年度课程活动 - course_act = Activity.objects.exclude(status__in=[ - Activity.Status.REVIEWING, - Activity.Status.CANCELED, - Activity.Status.ABORT, - Activity.Status.REJECT - ]).filter( - Q(year=SUMMARY_YEAR1, semester=Semester.FALL) | Q(year=SUMMARY_YEAR2, semester=Semester.SPRING), category=Activity.ActivityCategory.COURSE) - total_course_act_num = len(course_act) - - # 本年度课程活动时长 - total_course_act_hour: timedelta = sum( - [(a.end-a.start) for a in course_act], timedelta(0)) - total_course_act_hour = round( - total_course_act_hour.total_seconds() / 3600, 2) - - # 本年度参与一门课程的人数、参与三门课程的人数 - persons = NaturalPerson.objects.annotate(cc=Count( - 'courseparticipant', filter=Q( - courseparticipant__course__year=SUMMARY_YEAR1, - courseparticipant__course__semester=Semester.FALL, - courseparticipant__status__in=[ - CourseParticipant.Status.SELECT, - CourseParticipant.Status.SUCCESS], - ) | Q( - courseparticipant__course__year=SUMMARY_YEAR2, - courseparticipant__course__semester=Semester.SPRING, - courseparticipant__status__in=[ - CourseParticipant.Status.SELECT, - CourseParticipant.Status.SUCCESS], - ) - ) - ) - have_course_num = persons.filter(cc__gte=1).count() - have_three_course_num = persons.filter(cc__gte=3).count() - - # 23年春季、秋季学期,最热门的三门书院课程(以预选人数和选课名额之比计算) - hottest_courses_23_Fall = get_hottest_courses( - year=SUMMARY_YEAR1, semester=Semester.FALL) - hottest_courses_23_Spring = get_hottest_courses( - year=SUMMARY_YEAR2, semester=Semester.SPRING) - - return dict(total_course_num=total_course_num, - total_course_act_num=total_course_act_num, total_course_act_hour=total_course_act_hour, - have_course_num=have_course_num, have_three_course_num=have_three_course_num, - hottest_courses_23_Fall=hottest_courses_23_Fall, - hottest_courses_23_Spring=hottest_courses_23_Spring - ) - - -# 个人统计部分从此开始 -__persons = None - - -def cal_login_num(np: NaturalPerson): - """ - - 系统登录介绍 - (0) 该用户的注册日期 - (1) 该用户本年度登陆系统次数 - (2) 该用户本年度最长连续登录系统天数 - """ - # 注册日期 - date_joined = np.get_user().date_joined - # 该用户本年度登陆系统次数 - _user = np.get_user() - _day_check_kws = {} - _day_check_kws.update(time__date__gt=SUMMARY_SEM_START, - time__date__lt=SUMMARY_SEM_END) - _signin_days = set(YQPointRecord.objects.filter( - user=_user, - source_type=YQPointRecord.SourceType.CHECK_IN, - **_day_check_kws, - ).order_by('time').values_list('time__date', flat=True).distinct()) - checkin_num = len(_signin_days) - - # 计算该用户最长连续登录系统天数 - _signin_days = sorted(_signin_days) - max_consecutive_days = 0 - _current_streak = 0 - _previous_date = None - - for _current_date in _signin_days: - if _previous_date is not None and _current_date == _previous_date + timedelta(days=1): - # 如果当前日期和前一天相差一天,则增加连续天数 - _current_streak += 1 - else: - # 否则,重置连续天数 - _current_streak = 1 - # 更新最大连续天数 - max_consecutive_days = max(max_consecutive_days, _current_streak) - _previous_date = _current_date - - return {'date_joined': date_joined, 'checkin_num': checkin_num, 'max_consecutive_days': max_consecutive_days} - - -def cal_act(np: NaturalPerson): - """ - - 小组板块 - (1) 该用户参与的学生小组与书院课程小组数量 - (2) 该用户创建或担任职务的小组名称、担任职务 - (3) 该用户参与的小组活动数 - (4) 该用户参与活动的出现频率最高的三个活动关键词、活动频率最高的时间段 - """ - import jieba.analyse - from collections import defaultdict - - pos = Position.objects.activated(noncurrent=None).filter( - Q(person=np, year=SUMMARY_YEAR1, semester__in=[Semester.FALL, Semester.ANNUAL]) | Q( - person=np, year=SUMMARY_YEAR2, semester__in=[Semester.SPRING, Semester.ANNUAL]), - ) - # 参与的学生小组的数量 - club_num = pos.filter(org__otype__otype_name='学生小组').count() - # 参与的书院课程的数量 - course_org_num = pos.filter(org__otype__otype_name='书院课程').count() - # - participated_acts = Participation.objects.activated().filter( - sq(Participation.person, np), - ( - Q(activity__year=SUMMARY_YEAR1, activity__semester=Semester.FALL) | - Q(activity__year=SUMMARY_YEAR2, activity__semester=Semester.SPRING) - )) - # 参与的活动的数量 - act_num = participated_acts.count() - # 参与活动的关键词 - keyword_freq = defaultdict(int) - activity_titles = participated_acts.values_list( - 'activity__title', flat=True) - # 该用户参与活动的出现频率最高的三个活动关键词 - for text in activity_titles: - for keyword in jieba.analyse.extract_tags(text): - keyword_freq[keyword] += 1 - act_top_three_keywords = sorted( - keyword_freq, key=keyword_freq.get, reverse=True)[:3] - - # 活动次数最高的时间段,按照每小时作为时间段。 - time_periods = Activity.objects.filter(id__in=participated_acts.values_list( - 'activity_id', flat=True)).values_list('start', 'end') - if time_periods.exists(): - hour_frequencies = Counter() - for start, end in time_periods: - duration = int((end - start).total_seconds() / 3600) - for hour in range(start.hour, start.hour + duration): - hour_frequencies[hour % 24] += 1 - most_act_common_hour = hour_frequencies.most_common(1)[0][0] - else: - most_act_common_hour = None - # 担任的职务的数量 - position_num = pos.count() - # 担任负责人的小组名称 - admin_pos = pos.filter(is_admin=True) - admin_org_names = [position.org.oname for position in admin_pos] - - # 该用户是否创建小组、创建的小组名称 - orgs = ModifyOrganization.objects.filter( - pos=np.person_id, status=ModifyOrganization.Status.CONFIRMED) - IScreate = bool(orgs) - myclub_name = '' - if IScreate: - myclub_name = ','.join(orgs.values_list('oname', flat=True)) - - return dict( - IScreate=IScreate, myclub_name=myclub_name, - club_num=club_num, course_org_num=course_org_num, act_num=act_num, - position_num=position_num, admin_org_names=admin_org_names, - act_top_three_keywords=act_top_three_keywords, most_act_common_hour=most_act_common_hour - ) - - -def cal_course(np: NaturalPerson): - """ - - 书院课程板块 - (1) 该用户选修的课程总数、总学时 - (2) 该用户选修的课程在五类课程中的哪几类 - (3) 该用户投入学时最长的课程及学时时长 - (4) 用户春季学期、秋季学期选课数量 - (5) 用户春季学期、秋季学期选中书院课数量 - """ - - # 这里计算的实际参与的课程活动总数,即便学时可能无效,但是只要有学时,就算 - course_me_past = CourseRecord.objects.filter(person=np, total_hours__gt=0) - course_num = course_me_past.count() - - # 计算每个类别学时的时候,只考虑有效学时 - course_me_past = course_me_past.filter(invalid=False) - pro = [] - # 计算每个类别的学时 - for _course_type in list(Course.CourseType): # CourseType.values亦可 - t = course_me_past.filter(course__type=_course_type) - if not t: - continue - t = t.aggregate(Sum('total_hours'), Sum( - 'attend_times'), count=Count('*')) - pro.append([_course_type.label, t['total_hours__sum'] - or 0, t['attend_times__sum'] or 0, t['count'] or 0]) - - unclassified_hour = course_me_past.filter(course__isnull=True).aggregate( - Sum('total_hours'))['total_hours__sum'] or 0 - - # 个人选修课程的总学时,选修课程类别学时最多的是哪一类、多少学时 - course_hour = 0 - types = [] - max_type_info = '无', 0 - for label, hour, _, count in pro: - if count > max_type_info[1]: - max_type_info = label, count - types.append(label) - course_hour += hour - - if unclassified_hour: - types.append('其它') - course_hour += unclassified_hour - - # 该用户选修的课程在五类课程中的哪几类 - course_type = '/'.join(types) + f' {len(types)}' - type_count = len(types) - - # 该用户投入学时最长的课程、学时时长、参与次数 - if course_me_past: - most_time: CourseRecord = max( - course_me_past, key=lambda x: x.total_hours) - most_num: CourseRecord = max( - course_me_past, key=lambda x: x.attend_times) - course_most_time_name, course_most_hour = most_time.get_course_name(), most_time.total_hours - course_most_num_name, course_most_num = most_num.get_course_name(), most_num.attend_times - else: - course_most_time_name, course_most_hour = "无", 0 - course_most_num_name, course_most_num = "无", 0 - - elect_course = Course.objects.exclude( - status=Course.Status.ABORT).filter( - participant_set__person=np, - participant_set__status__in=[ - CourseParticipant.Status.SELECT, - CourseParticipant.Status.SUCCESS, - CourseParticipant.Status.FAILED, - ]) - # 该用户23秋、23春课程选课数量(包含失败的情况) - preelect_course_23fall = elect_course.filter( - year=SUMMARY_YEAR1, semester=Semester.FALL,) - preelect_course_23spring = elect_course.filter( - year=SUMMARY_YEAR2, semester=Semester.SPRING,) - preelect_course_23fall_num = preelect_course_23fall.count() - preelect_course_23spring_num = preelect_course_23spring.count() - - # 该用户23秋、23春课程选上课数量 - elected_course_23fall_num = preelect_course_23fall.filter( - participant_set__person=np, - participant_set__status__in=[ - CourseParticipant.Status.SELECT, - CourseParticipant.Status.SUCCESS, - ]).count() - elected_course_23spring_num = preelect_course_23spring.filter( - participant_set__person=np, - participant_set__status__in=[ - CourseParticipant.Status.SELECT, - CourseParticipant.Status.SUCCESS, - ]).count() - - return dict(course_num=course_num, - course_hour=course_hour, course_type=course_type, - course_most_time_name=course_most_time_name, course_most_hour=course_most_hour, - course_most_num_name=course_most_num_name, course_most_num=course_most_num, - max_type_info=max_type_info, type_count=type_count, - preelect_course_23fall_num=preelect_course_23fall_num, - preelect_course_23spring_num=preelect_course_23spring_num, - elected_course_23fall_num=elected_course_23fall_num, - elected_course_23spring_num=elected_course_23spring_num, - ) - - -def cal_anual_yqpoint(np: NaturalPerson): - """ - - 元气值板块 - (1) 获取元气值总值 - (2) 消耗元气值总值 - (3) 兑换奖品种类数量 - (4) 盲盒兑换次数、抽中次数 - """ - _user = np.get_user() - # 获取元气值总值, 消耗元气值总值 - income, expenditure = get_income_expenditure( - _user, SUMMARY_SEM_START, SUMMARY_SEM_END) - - _pool_records = PoolRecord.objects.filter( - user=_user, time__gte=SUMMARY_SEM_START, time__lt=SUMMARY_SEM_END) - _lucky_pool_records = _pool_records.filter(status__in=[ - PoolRecord.Status.UN_REDEEM, - PoolRecord.Status.REDEEMED, - ]).exclude(prize__name__contains='空盒') - - _unique_prizes = _lucky_pool_records.values_list( - 'prize', flat=True).distinct() - # 兑换奖品种类 - number_of_unique_prizes = len(_unique_prizes) - # 盲盒兑换次数 - _mystery_boxes = _pool_records.filter(pool__type=Pool.Type.RANDOM) - mystery_boxes_num = _mystery_boxes.count() - # 盲盒抽中次数 - lucky_mystery_boxes_num = _lucky_pool_records.filter( - pool__type=Pool.Type.RANDOM).count() - - return remove_local_var(locals()) - - -def cal_anual_academic(np: NaturalPerson): - """ - - 学术地图板块 - (1) 学术地图标签关键词数量 - (2) 学术地图提问次数 - """ - _academic_tags = AcademicTagEntry.objects.activated().filter(person=np) - academic_tags_num = _academic_tags.count() - - # # 获取并统计每种类型的标签数量 - # _tag_counts = _academic_tags.values('tag__atype').annotate(count=Count('tag')).order_by('tag__atype') - - # for _tag_count in _tag_counts: - # _atype = AcademicTag.Type(_tag_count['tag__atype']).label - # _count = _tag_count['count'] - - # 学术地图提问次数 - academic_QA_num = AcademicQA.objects.filter( - chat__questioner=np.get_user()).count() - - return remove_local_var(locals()) - - -def cal_study_room(np: NaturalPerson): - """ - - 自习室 - (1) 用户本年度自习室刷卡次数、天数、超越“%”的同学 - (2) 用户本年度最常去的自习室、次数 - """ - _start_time = SUMMARY_SEM_START - _end_time = SUMMARY_SEM_END - - _user = np.get_user() - _par = get_participant(_user) - if _par is None: - return {} - _study_room_record_filter = Q(Cardroom__Rtitle__contains='自习', - Cardtime__gt=_start_time, - Cardtime__lt=_end_time, - Cardstudent=_par) - - _study_room_reords = CardCheckInfo.objects.filter( - _study_room_record_filter) - - if not _study_room_reords.exists(): - return dict(study_room_num=0) - # 用户本年度自习室刷卡次数 - study_room_num = _study_room_reords.aggregate(cnt=Count('*')).get('cnt', 0) - # 用户本年度自习室刷卡天数 - study_room_day = _study_room_reords.values_list('Cardtime__date').annotate( - cnt=Count('*')).aggregate(cnt=Count('*')).get('cnt', 0) - # 个人最常去的自习室,和对应的天数 - _cnt_dict = defaultdict(int) - for r, _, _ in _study_room_reords.values_list('Cardroom__Rid', 'Cardtime__date').annotate(cnt=Count('*')): - _cnt_dict[r] += 1 - study_room_top, study_room_top_day = max( - [(r, cnt) for r, cnt in _cnt_dict.items()], key=lambda x: x[1]) - - return dict(study_room_num=study_room_num, - study_room_day=study_room_day, - study_room_top=study_room_top, - study_room_top_day=study_room_top_day, - ) - - -def cal_anual_appoint(_me_appoint: QuerySet[Appoint], _room_type: str = None): - """" - 根据不同的房间类型,获取以下内容: - (1) 用户本年度{_room_type}总预约次数、总时长 - (2) 用户本年度最多使用的{_room_type}预约理由关键词 - (3) 用户本年度最多预约的{_room_type}、次数 - (5) {_room_type}预约时长最多的日期,当日预约时长,当天的预约关键词 - """ - import jieba.analyse - if _room_type is None: - # 所有类型房间 - _room_list = Room.objects.permitted().values_list('Rid') - elif _room_type == 'Discuss': - # 研讨室 - _room_list = Room.objects.talk_rooms().values_list('Rid') - elif _room_type == 'Function': - # 功能房 - _room_list = Room.objects.function_rooms().values_list('Rid') - - _me_act_appoints = _me_appoint.filter(Room__in=_room_list) - if not _me_act_appoints.exists(): - appoint_num = appoint_hour = 0 - else: - # 用户本年度{room_type}预约次数 - appoint_num = _me_act_appoints.aggregate(cnt=Count('*'))['cnt'] - # 用户本年度{room_type}预约时长 - appoint_hour = sum([(finish - start).seconds for start, - finish in _me_act_appoints.values_list('Astart', 'Afinish')])//3600 - # 用户本年度最长预约的{room_type}、时长 - # _my_rooms = set(_me_act_appoints.values_list('Room', flat=True)) - # appoint_long_room, appoint_long_hour = max([(r, _me_act_appoint.filter(Room=r).aggregate( - # tol=Sum(F('Afinish') - F('Astart')))['tol'].total_seconds()//3600) for r in _my_rooms], key=lambda x: x[1]) - - # 用户本年度最多预约的{room_type}、次数 - appoint_most_room, appoint_most_room_num = Counter( - _me_act_appoints.values_list('Room__Rtitle', flat=True)).most_common(1)[0] - - # 预约时长最多的日期,当日预约时长,当天的预约关键词 - # 计算每个预约的时长 - _discuss_duration_appointments = _me_act_appoints.annotate( - duration=ExpressionWrapper( - F('Afinish') - F('Astart'), - output_field=DurationField() - ) - ) - - # 按天分组并计算总时长 - _daily_duration = _discuss_duration_appointments.annotate( - day=TruncDay('Astart') - ).values('day').annotate( - total_duration=Sum('duration') - ).order_by('-total_duration') - - # 获取时长最长的那一天 - _longest_day = _daily_duration.first() if _daily_duration else None - - if _longest_day: - _hours, _remainder = divmod( - _longest_day['total_duration'].seconds, 3600) - _minutes = _remainder // 60 - if _minutes == 0: - appoint_longest_duration = f"{_hours}小时" - else: - appoint_longest_duration = f"{_hours}小时{_minutes}分钟" - appoint_longest_day = _longest_day['day'] - _purposes = _me_act_appoints.filter( - Astart__date=appoint_longest_day).values_list('Ausage', flat=True) - - if len(_purposes) == 1: - _keywords = jieba.analyse.extract_tags(_purposes[0], topK=1) - appoint_longest_day_keyword = _keywords[0] if _keywords else None - else: - # 多个文本:提取频率最高的关键词 - _all_words = [] - for _text in _purposes: - _words = jieba.cut(_text) - _all_words.extend(_words) - _most_common_words = Counter(_all_words).most_common(1) - appoint_longest_day_keyword = _most_common_words[0][0] if _most_common_words else None - else: - appoint_longest_duration = None - appoint_longest_day = None - appoint_longest_day_keyword = None - - # 用户本年度最常使用的关键词 - _year_purposes = _me_act_appoints.values_list('Ausage', flat=True) - _year_all_words = [] - for _text in _year_purposes: - _year_all_words.extend(jieba.cut(_text)) - _year_most_words = Counter(_year_all_words).most_common(1) - appoint_year_keyword = _year_most_words[0][0] if _year_most_words else None - - _room_dict = remove_local_var(locals()) - _prefix_room_dict = {f"{_room_type}_" + - _key: _value for _key, _value in _room_dict.items()} - - return _prefix_room_dict - - -def cal_appoint(np: NaturalPerson): - """ - -研讨室 - (1) 用户本年度研讨室总预约次数、总时长 - (2) 用户本年度最多使用的研讨室预约理由关键词 - (3) 用户本年度最多预约的研讨室、次数 - (5) 研讨室预约时长最多的日期,当日预约时长,当天的预约关键词 - -功能房 - 内容同研讨室 - """ - _start_time = SUMMARY_SEM_START - _end_time = SUMMARY_SEM_END - - _user = np.get_user() - _par = get_participant(_user) - room_dict = {} - if _par is None: - return room_dict - _me_act_appoint = Appoint.objects.not_canceled().filter( - students=_par, Astart__gt=_start_time, Astart__lt=_end_time) - if not _me_act_appoint.exists(): - return room_dict - - func_room_dict = cal_anual_appoint(_me_act_appoint, _room_type='Function') - talk_room_dict = cal_anual_appoint(_me_act_appoint, _room_type='Discuss') - room_dict.update(func_room_dict) - room_dict.update(talk_room_dict) - - return room_dict - - -def cal_sharp_appoint(np: NaturalPerson): - """ - - 极限预约 & 违约次数 - (1) 在预约时间前30分钟内预约次数 - (2) 预约最紧的一次的时长、日期、理由、房间号 - (3) 违约次数,总计扣分 - """ - appoints = Appoint.objects.filter( - Astart__gte=SUMMARY_SEM_START, - Astart__lt=SUMMARY_SEM_END, - major_student__Sid=np.person_id) - sharp_appoints = appoints.exclude(Atype=Appoint.Type.TEMPORARY).filter( - Astart__lt=F('Atime') + timedelta(minutes=30)) - # 在预约时间前30分钟内预约次数 - sharp_appoint_num = sharp_appoints.count() - if not sharp_appoint_num: - return dict(sharp_appoint_num=sharp_appoint_num) - # 预约最紧的一次的日期 - sharp_appoint: Appoint = min( - sharp_appoints, key=lambda x: x.Astart-x.Atime) - sharp_appoint_day = sharp_appoint.Astart.strftime('%Y年%m月%d日') - sharp_appoint_reason = sharp_appoint.Ausage - sharp_appoint_min = (sharp_appoint.Astart - - sharp_appoint.Atime).total_seconds() - if sharp_appoint_min < 60: - sharp_appoint_min = f'{round(sharp_appoint_min)}秒' - else: - sharp_appoint_min = f'{round((sharp_appoint_min / 60) % 60)}分钟' - sharp_appoint_room = str(sharp_appoint.Room) - - # 违约次数 - disobey_num = appoints.filter(Astatus=Appoint.Status.VIOLATED).count() - - return dict( - sharp_appoint_num=sharp_appoint_num, - sharp_appoint_day=sharp_appoint_day, - sharp_appoint_reason=sharp_appoint_reason, - sharp_appoint_min=sharp_appoint_min, - sharp_appoint_room=sharp_appoint_room, - disobey_num=disobey_num - ) - - -def cal_co_appoint(np: NaturalPerson): - """ - - 海内存知己,天涯若比邻 - (1) 该用户本年度一起预约最多的同学 - (2) 一起预约的次数、时长 - (3) 一起预约最多的理由 - (4) 获得称号 - """ - _start_time = SUMMARY_SEM_START - _end_time = SUMMARY_SEM_END - - _user = np.get_user() - _par = get_participant(_user) - if _par is None: - return {} - - _me_act_appoint = Appoint.objects.not_canceled().filter( - students=_par, Astart__gt=_start_time, Astart__lt=_end_time) - _co_np_list = [] - for _appoint in _me_act_appoint: - for _co_np in _appoint.students.all(): - if _co_np != _par: - _co_np_list.append(_co_np) - if not _co_np_list: - return {} - # 该用户本年度一起预约最多的同学、次数 - co_mate, co_appoint_num = Counter(_co_np_list).most_common(1)[0] - _co_act_appoint = _me_act_appoint.filter(students=co_mate) - # 一起预约的时长 - co_appoint_hour = _co_act_appoint.aggregate( - tol=Sum(F('Afinish') - F('Astart')))['tol'].total_seconds()//3600 - co_mate = co_mate.name - # 获得称号 - co_title = '' - if co_appoint_hour > 30: - co_title = '莫逆之交' - elif co_appoint_hour > 15: - co_title = '形影不离' - elif co_appoint_hour > 8: - co_title = '结伴同行' - elif co_appoint_hour > 3: - co_title = '一拍即合' - # 一起预约最多的理由 - import jieba - _key_words = [] - for _usage in _co_act_appoint.values_list('Ausage'): - if _usage[0] in ['临时预约', '[MASK]']: - continue - _key_words.extend(jieba.cut(_usage[0])) - co_keyword = Counter(_key_words).most_common(1)[0][0] - - return remove_local_var(locals()) - - -# 本年度未用到以下部分 -__useless = None - - -def cal_appoint_kw(np: NaturalPerson): - """ - 计算个人,年度预约关键词(最常出现的前三名) - """ - import jieba - - _start_time = SUMMARY_SEM_START - _end_time = SUMMARY_SEM_END - - _user = np.get_user() - _par = get_participant(_user) - if _par is None: - return {} - - _talk_rooms = Room.objects.talk_rooms().values_list('Rid') - _func_rooms = Room.objects.function_rooms().values_list('Rid') - _me_act_appoint = Appoint.objects.not_canceled().filter( - students=_par, Astart__gt=_start_time, Astart__lt=_end_time).exclude(Atype=Appoint.Type.TEMPORARY) - - _key_words = [] - for _usage in _me_act_appoint.values_list('Ausage'): - _key_words.extend(jieba.cut(_usage[0])) - Skeywords = Counter(_key_words).most_common(3) - return remove_local_var(locals()) - - -def cal_appoint_sum(np: NaturalPerson): - """ - 个人总预约时长、次数 - """ - appoints = Appoint.objects.not_canceled().filter( - Astart__gte=SUMMARY_SEM_START, - Astart__lt=SUMMARY_SEM_END, - major_student__Sid=np.person_id - ) - total_time = appoints.aggregate( - time=Sum(F('Afinish')-F('Astart')))['time'] or timedelta() - appoint_hour = round(total_time.total_seconds() / 3600, 1) - appoint_num = appoints.count() - - return dict( - appoint_hour=appoint_hour, appoint_num=appoint_num, - # poem_word=?? - ) - - -def cal_early_room(np: NaturalPerson): - """ - 计算个人 上午6-8点到达地下室的次数,每年度最早到地下室的时间 - """ - _start_time = SUMMARY_SEM_START - _end_time = SUMMARY_SEM_END - - _user = np.get_user() - _par = get_participant(_user) - if _par is None: - return {} - - _record_filter = Q(Cardtime__gt=_start_time, - Cardtime__lt=_end_time, - Cardstudent=_par, - Cardtime__hour__lt=8, - Cardtime__hour__gte=6) - _room_reords = CardCheckInfo.objects.filter(_record_filter) - if not _room_reords.exists(): - return dict(early_day_num=0) - - early_day_num = _room_reords.values_list('Cardtime__date').annotate( - cnt=Count('*')).aggregate(cnt=Count('*')).get('cnt', 0) - if early_day_num: - early_room, early_room_day, early_room_time = min(list(_room_reords.values_list( - 'Cardroom__Rid', 'Cardtime__date').annotate(time=Min('Cardtime__time'))), key=lambda x: x[2]) - return remove_local_var(locals()) - - -def cal_late_room(np: NaturalPerson): - """ - 计算个人 凌晨23-5点在地下室的次数,有多少人在同一时期陪同,哪间自习室陪你到最晚 - """ - _start_time = SUMMARY_SEM_START - _end_time = SUMMARY_SEM_END - - _user = np.get_user() - _par = get_participant(_user) - if _par is None: - return {} - - _record_filter = Q(Cardtime__gt=_start_time, - Cardtime__lt=_end_time, - Cardstudent=_par) - _late_filter_night = Q(Cardtime__hour__gte=23) - _late_filter_dawn = Q(Cardtime__hour__lt=5) - _room_reords = CardCheckInfo.objects.filter(_record_filter) - late_room_num = len(list(set(_room_reords.filter( - _late_filter_night).values_list('Cardtime__date')))) - if not late_room_num: - return dict(late_room_num=0) - _dawn_records = list(_room_reords.filter(_late_filter_dawn).values_list( - 'Cardroom', 'Cardtime__date', 'Cardtime__time')) - if _dawn_records: - _latest_record = max(_dawn_records, key=lambda x: x[2]) - else: - _latest_record = max(_room_reords.filter(_late_filter_night).values_list( - 'Cardroom', 'Cardtime__date', 'Cardtime__time'), key=lambda x: x[2]) - late_room, late_room_date, late_room_time = _latest_record - _late_room_ref_date = late_room_date - if late_room_time.hour < 23: - _late_room_ref_date = late_room_date - timedelta(days=1) - late_room_people = len(list(set(CardCheckInfo.objects.filter(Cardtime__gt=_start_time, - Cardtime__lt=_end_time, - Cardtime__date=_late_room_ref_date, - Cardtime__hour__gte=22 - ).values_list('Cardstudent')))) - return remove_local_var(locals()) diff --git a/dormitory/__init__.py b/dormitory/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/dormitory/admin.py b/dormitory/admin.py deleted file mode 100644 index 94eb845d6..000000000 --- a/dormitory/admin.py +++ /dev/null @@ -1,17 +0,0 @@ -from django.contrib import admin - -from dormitory.models import * -from generic.admin import UserAdmin - -admin.site.register(Dormitory) - -@admin.register(Agreement) -class DormitoryAgreementAdmin(admin.ModelAdmin): - list_display = ['user', 'sign_time'] - search_fields = ['user__username', 'user__name'] - -@admin.register(DormitoryAssignment) -class DormitoryAssignmentAdmin(admin.ModelAdmin): - list_display = ['dormitory', 'user', 'bed_id', 'time'] - list_filter = ['bed_id', 'time'] - search_fields = ['dormitory__id', *UserAdmin.suggest_search_fields('user'), 'time'] diff --git a/dormitory/apps.py b/dormitory/apps.py deleted file mode 100644 index f1f29a1eb..000000000 --- a/dormitory/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class DormitoryConfig(AppConfig): - name = 'dormitory' - verbose_name = '4.宿舍管理' diff --git a/dormitory/management/__init__.py b/dormitory/management/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/dormitory/management/commands/__init__.py b/dormitory/management/commands/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/dormitory/management/commands/assign_dormitory.py b/dormitory/management/commands/assign_dormitory.py deleted file mode 100644 index 365e1efbb..000000000 --- a/dormitory/management/commands/assign_dormitory.py +++ /dev/null @@ -1,429 +0,0 @@ -import copy -import random -from collections import defaultdict - -import numpy as np -import pandas as pd -from django.core.management.base import BaseCommand -from tqdm import trange - -''' -有关reference文件夹的说明: -reference文件夹用于存放宿舍分配时的参考信息。 -results.xlsx是新生问卷填写结果; -info.xlsx是学院提供的含有新生姓名、学号、生源地、生源高中的表格; -dorm.xlsx是学院提供的含有空余宿舍列表的表格; -dorm_assigned.xlsx是保存宿舍分配结果的目标文件。 -''' - - -class Freshman: - def __init__(self, data): - self.data = data - - def __repr__(self): - return repr(self.data) - - -class Dormitory: - def __init__(self, id: int, remain: int, is_noisy: bool): - self.id = id - self.remain = remain - self.stu = [] - # 盥洗室和楼道口比较吵闹,此属性为 True,其他寝室为 False - self.noisy = is_noisy - - def add(self, student: Freshman): - self.stu.append(student) - - def check_must(self): - ''' - 一个宿舍必须满足的条件: - 宿舍里的同学至少来自3个不同的省份 - 来自同一省份的2人不能来自同一所高中 - ''' - origin = [s.data['origin'] for s in self.stu] - if len(set(origin)) < len(self.stu) - 1: - return False - elif len(set(origin)) == len(self.stu) - 1: - indices = {} - for i, p in enumerate(origin): - indices[p] = indices.get(p, []) + [i] - dupl = [v for k, v in indices.items() if len(v) > 1][0] - hs = [self.stu[i].data['high_school'] for i in dupl] - if hs[0] == hs[1]: - return False - return True - - def check_better(self): - ''' - 计算宿舍得分,应用于交换优化场景。 - 宿舍计分项包括: - 存在来自同一省份的同学减分,并针对北京地区特别操作 - 专业是否平均分配:2文2理 > 4文/4理 > 文理1:3 - 性格分配是否合理:尽量一个寝室不要多于两个内向 - 是否愿意和留学生/交换生同宿舍 - 衡量能接受的最低空调温度接近程度,计算方差,特别计算能否接受整夜开空调的统一程度 - 衡量起床时间、睡眠时间的接近程度,计算方差 - 睡眠困扰同学尽量远离盥洗室和楼梯口(用 Dormitory.noisy 衡量) - 宿舍环境(尽量保证一个宿舍整洁条理的有2人/随性就好的有2人) - 对室友期待(一个寝室尽量不要全部专注学习/全面发展) - 在手动分配前,尽量保证宿舍是4人或3人的 - ''' - # Just return 0 for empty dormitories. Otherwise, using np.var raises a warning - if len(self.stu) == 0: - return 0 - - score = 0 - - origin = [s.data['origin'] for s in self.stu] - if len(set(origin)) == len(self.stu) - 1: - score -= 300 - beijing = [s for s in origin if s == "北京"] - if len(beijing) >= 2: - score -= 700 - - major_score = sum([s.data['major'] for s in self.stu]) - if major_score == 2: - score += 1200 - elif major_score == 0 or major_score == 4: - score += 800 - - if len([s for s in self.stu if s.data['personality'] == 0]) > 2: - score -= 600 - - # score += 8 * np.prod([s.data['international'] for s in self.stu]) - - ac_score = 20 * np.var([s.data['ac_temp'] for s in self.stu], ddof = 0) - ac_score += (len(set([s.data['all_night_ac'] - for s in self.stu])) - 1) * 400 - score -= ac_score - - wake_score = np.var([s.data['wake'] for s in self.stu], ddof = 0) - score -= 30 * wake_score - - sleep_score = np.var([s.data['sleep'] for s in self.stu], ddof = 0) - score -= 30 * sleep_score - - if self.noisy and any(s.data['sleep_quality'] == 0 for s in self.stu): - score -= 300 - - env_score = sum(s.data['environment'] for s in self.stu) - if env_score == 2: - score += 200 - if env_score in (0, 4): - score += 100 - - if len(set(s.data['expectation'] for s in self.stu)) == 1: - score -= 200 - - stu_cnt_map = {4: 600, - 3: 400, - 2: 0, - 1: 0, - 0: 0, } - score += stu_cnt_map.get(len(self.stu)) - - return score - - -def read_info() -> list[Freshman]: - '''返回一个Freshman的list''' - freshmen = [] - - df = pd.read_excel("/workspace/dormitory/references/results.xlsx") - df2 = pd.read_excel("/workspace/dormitory/references/info.xlsx") - - for index, stu in df.iterrows(): - data = defaultdict() - - data['name'] = stu["姓名"] - data['gender'] = stu["性别"] - data['sid'] = stu["学号"] - data['origin'] = stu["生源地"] - data['high_school'] = stu["生源高中"] - data['major'] = stu["专业意向"] - data['weight'] = stu["体重"] - data['international'] = stu["是否愿意和留学生住一起"] - data['wake'] = stu["你预期的大学生活起床时间"] - data['sleep'] = stu["你预期的大学生活睡觉时间"] - data['ac_temp'] = stu["夏天能接受的最低空调温度"] - data['all_night_ac'] = stu["是否接受夏天整晚开空调"] - data['personality'] = stu["你的性格"] - data['sleep_quality'] = stu["你的睡眠质量是"] - data['environment'] = stu["你希望你的宿舍环境是"] - data['expectation'] = stu["你本人更希望大学生活是"] - - # 在info表格中,根据学号找到对应行,读取生源地和生源高中信息,保证信息准确 - try: - info_row = df2.loc[df2["学号"] == data['sid']].iloc[0] - data['origin'] = info_row["省市"] - except IndexError: - import sys - print('IndexError when consulting info.xlsx', data['name']) - sys.exit(1) - # 2024年的 info 表格不包含这个列,只能选择相信问卷里填的 - # data['high_school'] = info_row["中学"] - - # 注意此处 map 的值要和 out_as_excel() 中对应 - major_map = {"文科类": 0, - "理工类": 1, } - data['major'] = major_map.get(data['major']) - - data['weight'] = float(data['weight'].replace("kg", "")) - - international_map = {"愿意": 5, - "都可以": 1, - "不愿意": 0, } - data['international'] = international_map.get(data['international']) - - wake_map = {"7点前": 0, - "7~8点": 1, - "8~9点": 2, - "9-10点": 3, - "10-11点": 4, - "11点后": 5, } - data['wake'] = wake_map.get(data['wake']) - - sleep_map = {"23点前": 0, - "23-24点": 1, - "24-1点": 2, - "1-2点": 3, - "2点后": 4, } - data['sleep'] = sleep_map.get(data['sleep']) - - data['ac_temp'] = int(data['ac_temp'][:2]) - - ac_map = {"是": 1, - "否": 0, } - data['all_night_ac'] = ac_map.get(data['all_night_ac']) - - personality_map = {"内向型(独处时精力充沛;更封闭,更愿意在经挑选的小群体中分享个人的情况;不把兴奋说出来。)": 0, - "适中型(介于二者之间,能够在内外向之间切换,在人群中乐意与人交谈结交朋友,同时也享受独处。)": 1, - "外向型(与他人相处时精力充沛;易于“读”和了解,随意地分享个人情况;高度热情地社交。)": 2, } - data['personality'] = personality_map.get(data['personality']) - - sleep_quality_map = {"浅眠型(易受声、光影响)": 0, - "酣睡型(较少受影响,一觉到天亮)": 1, } - data['sleep_quality'] = sleep_quality_map.get(data['sleep_quality']) - - environment_map = {"整洁条理": 0, "随性就好": 1, } - data['environment'] = environment_map.get(data['environment']) - - expectation_map = {"专注学习": 0, "全面发展": 1} - data['expectation'] = expectation_map.get(data['expectation']) - - freshman_data = dict(data) - freshman = Freshman(freshman_data) - freshmen.append(freshman) - - return freshmen - - -def read_dorm() -> tuple[list[Dormitory], list[Dormitory]]: - '''返回两个Dormitory的list,分别代表男寝和女寝''' - def read_from_sheet(sheet: str) -> list[Dormitory]: - ''' Reads information from a specific sheet in the workbook. ''' - dorm = [] - - df = pd.read_excel("/workspace/dormitory/references/dorm.xlsx", sheet_name = sheet) - - for index, room in df.iterrows(): - rid = int(room["房间"]) - if len(dorm) == 0 or dorm[-1].id != rid: - if len(dorm) != 0: - assert dorm[-1].id < rid, "Expect room number to be ascending order" - # We can tell if a dormitory is noisy from its last two digits - dorm.append(Dormitory(rid, 1, (rid % 100) in (12, 25, 35, 36, 38, 39, 40, 49, 64))) - else: - dorm[-1].remain += 1 - - # 注意,只选择了剩余床位为4的作为分配目标 - return list(filter(lambda d: d.remain == 4, dorm)) - - return read_from_sheet("男生宿舍"), read_from_sheet("女生宿舍") - -def assign_dorm() -> list[Dormitory]: - ''' - 分配宿舍算法: - 执行若干次(250000次)随机交换(选取任一宿舍,选取任一床位), - 衡量交换前后两宿舍得分之和,使得总得分最大化 - ''' - freshmen = read_info() - male_dorm, female_dorm = read_dorm() - - # 初始随机分配 - # TODO: 如果运气很烂,最后剩余4人无法分到同一宿舍,可能导致算法卡死。 - for stu in freshmen: - assigned = False - while not assigned: - male_vacant = [d for d in male_dorm if d.remain > 0] - female_vacant = [d for d in female_dorm if d.remain > 0] - if stu.data['gender'] == "男": - dorm = random.choice(male_vacant) - dorm.add(stu) - if dorm.check_must(): - dorm.remain -= 1 - assigned = True - else: - dorm.stu.pop() - else: - dorm = random.choice(female_vacant) - dorm.add(stu) - if dorm.check_must(): - dorm.remain -= 1 - assigned = True - else: - dorm.stu.pop() - - # 随机交换 - print('\033[36mProcessing male dormitories...\033[0m') - epsilon = 0.3 - for episode in trange(250000): - - rid1 = random.randint(0, len(male_dorm) - 1) - rid2 = random.randint(0, len(male_dorm) - 1) - if rid1 == rid2: - continue - - room1: Dormitory = copy.deepcopy(male_dorm[rid1]) - room2: Dormitory = copy.deepcopy(male_dorm[rid2]) - if len(room1.stu) == 0 or len(room2.stu) == 0: - continue - o_score = room1.check_better() + room2.check_better() - - temp1: Dormitory = copy.deepcopy(room1) - temp2: Dormitory = copy.deepcopy(room2) - - del male_dorm[max(rid1, rid2)] - del male_dorm[min(rid1, rid2)] - - if random.random() < epsilon: - if len(room1.stu) != 4 and len(room2.stu) != 4: - temp2.add(temp1.stu.pop()) - if temp1.check_must() and temp2.check_must() and (temp1.check_better() + temp2.check_better() > o_score): - room1 = temp1 - room2 = temp2 - o_score = room1.check_better() + room2.check_better() - - if len(room1.stu) == 0 or len(room2.stu) == 0: - male_dorm.append(room1) - male_dorm.append(room2) - continue - - temp1: Dormitory = copy.deepcopy(room1) - temp2: Dormitory = copy.deepcopy(room2) - - bid1 = random.randint(0, len(room1.stu) - 1) - bid2 = random.randint(0, len(room2.stu) - 1) - - temp1.stu[bid1], temp2.stu[bid2] = temp2.stu[bid2], temp1.stu[bid1] - if temp1.check_must() and temp2.check_must() and (temp1.check_better() + temp2.check_better() > o_score): - male_dorm.append(temp1) - male_dorm.append(temp2) - else: - male_dorm.append(room1) - male_dorm.append(room2) - - print("\033[35mProcessing female dormitories...\033[0m") - for episode in trange(250000): - - rid1 = random.randint(0, len(female_dorm) - 1) - rid2 = random.randint(0, len(female_dorm) - 1) - if rid1 == rid2: - continue - - room1: Dormitory = copy.deepcopy(female_dorm[rid1]) - room2: Dormitory = copy.deepcopy(female_dorm[rid2]) - if len(room1.stu) == 0 or len(room2.stu) == 0: - continue - o_score = room1.check_better() + room2.check_better() - - temp1: Dormitory = copy.deepcopy(room1) - temp2: Dormitory = copy.deepcopy(room2) - - del female_dorm[max(rid1, rid2)] - del female_dorm[min(rid1, rid2)] - - if random.random() < epsilon: - if len(room1.stu) != 4 and len(room2.stu) != 4: - temp2.add(temp1.stu.pop()) - if temp1.check_must() and temp2.check_must() and (temp1.check_better() + temp2.check_better() > o_score): - room1 = temp1 - room2 = temp2 - o_score = room1.check_better() + room2.check_better() - - if len(room1.stu) == 0 or len(room2.stu) == 0: - female_dorm.append(room1) - female_dorm.append(room2) - continue - - temp1: Dormitory = copy.deepcopy(room1) - temp2: Dormitory = copy.deepcopy(room2) - - bid1 = random.randint(0, len(room1.stu) - 1) - bid2 = random.randint(0, len(room2.stu) - 1) - - temp1.stu[bid1], temp2.stu[bid2] = temp2.stu[bid2], temp1.stu[bid1] - if temp1.check_must() and temp2.check_must() and (temp1.check_better() + temp2.check_better() > o_score): - female_dorm.append(temp1) - female_dorm.append(temp2) - else: - female_dorm.append(room1) - female_dorm.append(room2) - - male_dorm.sort(key=lambda d: d.id) - female_dorm.sort(key=lambda d: d.id) - - dorm_result = male_dorm + female_dorm - return dorm_result - - -def out_as_excel(dorm_result: list[Dormitory]): - '''将结果导出为excel文件,存储在reference/dorm_assigned.xlsx下''' - df = pd.DataFrame() - - major_list = ["文科类", "理工类"] - international_list = ["不愿意", "都可以", "愿意"] - wake_list = ["7点前", "7~8点", "8~9点", "9-10点", "10-11点", "11点后"] - sleep_list = ["23点前", "23-24点", "24-1点", "1-2点", "2点后"] - ac_list = ["否", "是"] - personality_list = ["内向型", "适中型", "外向型"] - sleep_quality_list = ["浅眠型", "酣睡型"] - environment_list = ["整洁条理", "随性就好"] - expectation_list = ["专注学习", "全面发展"] - - for dorm in dorm_result: - for stu in dorm.stu: - data = { - "宿舍号": dorm.id, - "姓名": stu.data['name'], - "性别": stu.data['gender'], - "学号": stu.data['sid'], - "生源地": stu.data['origin'], - "生源高中": stu.data['high_school'], - "意向专业": major_list[stu.data['major']], - "体重": stu.data['weight'], - "是否愿意与留学生住在同一间宿舍?": international_list[stu.data['international'] % 3], - "起床时间": wake_list[stu.data['wake']], - "入睡时间": sleep_list[stu.data['sleep']], - "夏天能接受的最低空调温度": stu.data['ac_temp'], - "是否接受夏天整夜开空调": ac_list[stu.data['all_night_ac']], - "性格": personality_list[stu.data['personality']], - "睡眠质量": sleep_quality_list[stu.data['sleep_quality']], - "希望宿舍环境": environment_list[stu.data['environment']], - "对大学生活期待": expectation_list[stu.data['expectation']], - "得分": dorm.check_better(), - } - temp_df = pd.DataFrame(data, index=[0]) - df = pd.concat([df, temp_df], ignore_index=True) - - df.to_excel( - '/workspace/dormitory/references/dorm_assigned.xlsx', index=False) - - -class Command(BaseCommand): - help = "Assign dormitory." - - def handle(self, *args, **options): - out_as_excel(assign_dorm()) diff --git a/dormitory/management/commands/create_dormitory_questionnaire_2023.py b/dormitory/management/commands/create_dormitory_questionnaire_2023.py deleted file mode 100644 index 19b6a009d..000000000 --- a/dormitory/management/commands/create_dormitory_questionnaire_2023.py +++ /dev/null @@ -1,330 +0,0 @@ -from django.core.management.base import BaseCommand -from questionnaire.models import Survey, Question, Choice - - -# 创建调查问卷 -# 信息 1.姓名 2.性别 3.学号 4.生源地 5.生源高中 6.专业 7.身高 8.体重 9.是否愿意和留学生住一起 -# 10.起床时间 11.入睡时间 12.夏天空调温度 13.是否整夜空调 14.是否在宿舍吃外卖 15.是否接受舍友在宿舍吃外卖 16.舍友在宿舍时,会在宿舍打电话吗 17.接受舍友在宿舍打电话吗 -# 18.性格偏向 19.希望的宿舍氛围 20.对大学生活的期待 -class Command(BaseCommand): - help = "Create dormitory questionnaire." - - def handle(self, *args, **options): - # 创建一个survey - survey = Survey.objects.create( - title = "宿舍生活习惯调研-2023", - description = "根据问卷情况对宿舍进行分配", - status = Survey.Status.PUBLISHED, # 传参 - creator_id = 1, # 传参 - start_time = "2023-08-09", # 传参 - end_time = "2023-08-19", # 传参 - ) - survey.save() - - # 创建问题 - question1 = Question.objects.create( - survey = survey, - order = 1, - topic = "姓名", - type = Question.Type.TEXT, - ) - question1.save() - - question2 = Question.objects.create( - survey = survey, - order = 2, - topic = "性别", - type = Question.Type.SINGLE, - ) - question2.save() - - choice2_1 = Choice.objects.create( - question = question2, - order = 1, - text = "男", - ) - choice2_1.save() - - choice2_2 = Choice.objects.create( - question = question2, - order = 2, - text = "女", - ) - choice2_2.save() - - question3 = Question.objects.create( - survey = survey, - order = 3, - topic = "学号", - type = Question.Type.TEXT, - ) - question3.save() - - question4 = Question.objects.create( - survey = survey, - order = 4, - topic = "生源地", - type = Question.Type.TEXT, - description = "如:山东济南", - ) - question4.save() - - question5 = Question.objects.create( - survey = survey, - order = 5, - topic = "生源高中", - type = Question.Type.TEXT, - description = "请写全称", - ) - question5.save() - - question6 = Question.objects.create( - survey = survey, - order = 6, - topic = "专业意向", - type = Question.Type.SINGLE, - ) - question6.save() - - choice6_1 = Choice.objects.create( - question = question6, - order = 1, - text = "文科类", - ) - choice6_1.save() - - choice6_2 = Choice.objects.create( - question = question6, - order = 2, - text = "理工类", - ) - choice6_2.save() - - question7 = Question.objects.create( - survey = survey, - order = 7, - topic = "身高", - type = Question.Type.TEXT, - description = "单位:cm", - ) - question7.save() - - question8 = Question.objects.create( - survey = survey, - order = 8, - topic = "体重", - type = Question.Type.TEXT, - description = "单位:kg", - ) - question8.save() - - question9 = Question.objects.create( - survey = survey, - order = 9, - topic = "是否愿意和留学生住一起", - type = Question.Type.SINGLE, - ) - question9.save() - - choice9_1 = Choice.objects.create( - question = question9, - order = 1, - text = "愿意", - ) - choice9_1.save() - - choice9_2 = Choice.objects.create( - question = question9, - order = 2, - text = "都可以", - ) - choice9_2.save() - - choice9_3 = Choice.objects.create( - question = question9, - order = 3, - text = "不愿意", - ) - choice9_3.save() - - question10 = Question.objects.create( - survey = survey, - order = 10, - topic = "起床时间", - type = Question.Type.TEXT, - description = "24小时制,如:7:30", # 如何统一一下格式 - ) - question10.save() - - question11 = Question.objects.create( - survey = survey, - order = 11, - topic = "入睡时间", - type = Question.Type.TEXT, - description = "24小时制,如:23:30", - ) - question11.save() - - question12 = Question.objects.create( - survey = survey, - order = 12, - topic = "夏天空调温度", - type = Question.Type.TEXT, - description = "单位:℃", - ) - question12.save() - - question13 = Question.objects.create( - survey = survey, - order = 13, - topic = "是否整夜开空调", - type = Question.Type.SINGLE, - ) - question13.save() - - choice13_1 = Choice.objects.create( - question = question13, - order = 1, - text = "是", - ) - choice13_1.save() - - choice13_2 = Choice.objects.create( - question = question13, - order = 2, - text = "否", - ) - choice13_2.save() - - question14 = Question.objects.create( - survey = survey, - order = 14, - topic = "是否在宿舍吃外卖", - type = Question.Type.SINGLE, - ) - question14.save() - - choice14_1 = Choice.objects.create( - question = question14, - order = 1, - text = "是", - ) - choice14_1.save() - - choice14_2 = Choice.objects.create( - question = question14, - order = 2, - text = "否", - ) - choice14_2.save() - - question15 = Question.objects.create( - survey = survey, - order = 15, - topic = "是否接受舍友在宿舍吃外卖", - type = Question.Type.SINGLE, - ) - question15.save() - - choice15_1 = Choice.objects.create( - question = question15, - order = 1, - text = "是", - ) - choice15_1.save() - - choice15_2 = Choice.objects.create( - question = question15, - order = 2, - text = "否", - ) - choice15_2.save() - - question16 = Question.objects.create( - survey = survey, - order = 16, - topic = "舍友在宿舍时,是否会在宿舍视频/电话", - type = Question.Type.SINGLE, - ) - question16.save() - - choice16_1 = Choice.objects.create( - question = question16, - order = 1, - text = "是", - ) - choice16_1.save() - - choice16_2 = Choice.objects.create( - question = question16, - order = 2, - text = "否", - ) - choice16_2.save() - - question17 = Question.objects.create( - survey = survey, - order = 17, - topic = "是否接受舍友在宿舍视频/电话", - type = Question.Type.SINGLE, - ) - question17.save() - - choice17_1 = Choice.objects.create( - question = question17, - order = 1, - text = "是", - ) - choice17_1.save() - - choice17_2 = Choice.objects.create( - question = question17, - order = 2, - text = "否", - ) - choice17_2.save() - - question18 = Question.objects.create( - survey = survey, - order = 18, - topic = "性格偏向", - type = Question.Type.SINGLE, - ) - question18.save() - - choice18_1 = Choice.objects.create( - question = question18, - order = 1, - text = "外向型", - ) - choice18_1.save() - - choice18_2 = Choice.objects.create( - question = question18, - order = 2, - text = "内向型", - ) - choice18_2.save() - - choice18_3 = Choice.objects.create( - question = question18, - order = 3, - text = "适中型", - ) - choice18_3.save() - - question19 = Question.objects.create( - survey = survey, - order = 19, - topic = "希望的宿舍氛围", - type = Question.Type.TEXT, - ) - question19.save() - - question20 = Question.objects.create( - survey = survey, - order = 20, - topic = "对大学生活的期待", - type = Question.Type.TEXT, - ) - question20.save() diff --git a/dormitory/management/commands/create_dormitory_questionnaire_2024.py b/dormitory/management/commands/create_dormitory_questionnaire_2024.py deleted file mode 100644 index 62751f76a..000000000 --- a/dormitory/management/commands/create_dormitory_questionnaire_2024.py +++ /dev/null @@ -1,778 +0,0 @@ -from django.core.management.base import BaseCommand -from questionnaire.models import Survey, Question, Choice - - -# 创建调查问卷 -class Command(BaseCommand): - help = "Create dormitory questionnaire." - - def handle(self, *args, **options): - # 创建一个survey - survey = Survey.objects.create( - title = "宿舍生活习惯调研-2024", - description = "根据问卷情况对宿舍进行分配", - status = Survey.Status.PUBLISHED, # 传参 - creator_id = 1, # 传参 - start_time = "2024-08-09", # 传参 - end_time = "2024-08-19", # 传参 - ) - survey.save() - - # 创建问题 - question1 = Question.objects.create( - survey = survey, - order = 1, - topic = "姓名", - type = Question.Type.TEXT, - ) - question1.save() - - question2 = Question.objects.create( - survey = survey, - order = 2, - topic = "性别", - type = Question.Type.SINGLE, - ) - question2.save() - - choice2_1 = Choice.objects.create( - question = question2, - order = 1, - text = "男", - ) - choice2_1.save() - - choice2_2 = Choice.objects.create( - question = question2, - order = 2, - text = "女", - ) - choice2_2.save() - - question3 = Question.objects.create( - survey = survey, - order = 3, - topic = "学号", - type = Question.Type.TEXT, - ) - question3.save() - - question4 = Question.objects.create( - survey = survey, - order = 4, - topic = "生源地", - type = Question.Type.TEXT, - description = "如:山东济南", - ) - question4.save() - - question5 = Question.objects.create( - survey = survey, - order = 5, - topic = "生源高中", - type = Question.Type.TEXT, - description = "请写全称", - ) - question5.save() - - question6 = Question.objects.create( - survey = survey, - order = 6, - topic = "专业意向", - type = Question.Type.SINGLE, - ) - question6.save() - - choice6_1 = Choice.objects.create( - question = question6, - order = 1, - text = "文科类", - ) - choice6_1.save() - - choice6_2 = Choice.objects.create( - question = question6, - order = 2, - text = "理工类", - ) - choice6_2.save() - - question7 = Question.objects.create( - survey = survey, - order = 7, - topic = "具体意向专业", - type = Question.Type.TEXT - ) - question7.save() - - question8 = Question.objects.create( - survey = survey, - order = 8, - topic = "身高", - type = Question.Type.TEXT, - description = "单位:cm", - ) - question8.save() - - question9 = Question.objects.create( - survey = survey, - order = 9, - topic = "体重", - type = Question.Type.TEXT, - description = "单位:kg", - ) - question9.save() - - question10 = Question.objects.create( - survey = survey, - order = 10, - topic = "衣服尺码", - type = Question.Type.SINGLE, - ) - question10.save() - - choice10_1 = Choice.objects.create( - question = question10, - order = 1, - text = "S码", - ) - choice10_1.save() - - choice10_2 = Choice.objects.create( - question = question10, - order = 2, - text = "M码", - ) - choice10_2.save() - - choice10_3 = Choice.objects.create( - question = question10, - order = 3, - text = "L码", - ) - choice10_3.save() - - choice10_4 = Choice.objects.create( - question = question10, - order = 4, - text = "XL码", - ) - choice10_4.save() - - choice10_5 = Choice.objects.create( - question = question10, - order = 5, - text = "XXL码", - ) - choice10_5.save() - - choice10_6 = Choice.objects.create( - question = question10, - order = 6, - text = "XXXL码", - ) - choice10_6.save() - - choice10_7 = Choice.objects.create( - question = question10, - order = 7, - text = "XXXXL码", - ) - choice10_7.save() - - question11 = Question.objects.create( - survey = survey, - order = 11, - topic = "是否愿意和留学生住一起", - type = Question.Type.SINGLE, - ) - question11.save() - - choice11_1 = Choice.objects.create( - question = question11, - order = 1, - text = "愿意", - ) - choice11_1.save() - - choice11_2 = Choice.objects.create( - question = question11, - order = 2, - text = "都可以", - ) - choice11_2.save() - - choice11_3 = Choice.objects.create( - question = question11, - order = 3, - text = "不愿意", - ) - choice11_3.save() - - question12 = Question.objects.create( - survey = survey, - order = 12, - topic = "你的睡眠类型", - type = Question.Type.SINGLE, - ) - question12.save() - - choice12_1 = Choice.objects.create( - question = question12, - order = 1, - text = "早睡早起“百灵鸟型”", - ) - choice12_1.save() - - choice12_2 = Choice.objects.create( - question = question12, - order = 2, - text = "晚睡晚起“猫头鹰型”", - ) - choice12_2.save() - - question13 = Question.objects.create( - survey = survey, - order = 13, - topic = "你预期的大学生活起床时间", - type = Question.Type.SINGLE, - ) - question13.save() - - choice13_1 = Choice.objects.create( - question = question13, - order = 1, - text = "7点前", - ) - choice13_1.save() - - choice13_2 = Choice.objects.create( - question = question13, - order = 2, - text = "7~8点", - ) - choice13_2.save() - - choice13_3 = Choice.objects.create( - question = question13, - order = 3, - text = "8~9点", - ) - choice13_3.save() - - choice13_4 = Choice.objects.create( - question = question13, - order = 4, - text = "9-10点", - ) - choice13_4.save() - - choice13_5 = Choice.objects.create( - question = question13, - order = 5, - text = "10-11点", - ) - choice13_5.save() - - choice13_6 = Choice.objects.create( - question = question13, - order = 6, - text = "11点后", - ) - choice13_6.save() - - question14 = Question.objects.create( - survey = survey, - order = 14, - topic = "你预期的大学生活睡觉时间", - type = Question.Type.SINGLE, - description = "指能够躺在床上不发出大的声响的时间(指能够躺在床上不发出大的声响的时间)", - ) - question14.save() - - choice14_1 = Choice.objects.create( - question = question14, - order = 1, - text = "23点前", - ) - choice14_1.save() - - choice14_2 = Choice.objects.create( - question = question14, - order = 2, - text = "23-24点", - ) - choice14_2.save() - - choice14_3 = Choice.objects.create( - question = question14, - order = 3, - text = "24-1点", - ) - choice14_3.save() - - choice14_4 = Choice.objects.create( - question = question14, - order = 4, - text = "1-2点", - ) - choice14_4.save() - - choice14_5 = Choice.objects.create( - question = question14, - order = 5, - text = "2点后", - ) - choice14_5.save() - - question15 = Question.objects.create( - survey = survey, - order = 15, - topic = "你的睡眠质量是", - type = Question.Type.SINGLE, - ) - question15.save() - - choice15_1 = Choice.objects.create( - question = question15, - order = 1, - text = "浅眠型(易受声、光影响)", - ) - choice15_1.save() - - choice15_2 = Choice.objects.create( - question = question15, - order = 2, - text = "酣睡型(较少受影响,一觉到天亮)", - ) - choice15_2.save() - - question16 = Question.objects.create( - survey = survey, - order = 16, - topic = "你是否存在以下睡眠困扰", - type = Question.Type.MULTIPLE, - ) - question16.save() - - choice16_1 = Choice.objects.create( - question = question16, - order = 1, - text = "入睡困难", - ) - choice16_1.save() - - choice16_2 = Choice.objects.create( - question = question16, - order = 2, - text = "入睡后中间易醒", - ) - choice16_2.save() - - choice16_3 = Choice.objects.create( - question = question16, - order = 3, - text = "醒后难于再入睡", - ) - choice16_3.save() - - choice16_4 = Choice.objects.create( - question = question16, - order = 4, - text = "鼾声如雷", - ) - choice16_4.save() - - choice16_5 = Choice.objects.create( - question = question16, - order = 5, - text = "现在/曾经服用过安眠药", - ) - choice16_5.save() - - choice16_6 = Choice.objects.create( - question = question16, - order = 6, - text = "以上均无", - ) - choice16_6.save() - - question17 = Question.objects.create( - survey = survey, - order = 17, - topic = "夏天能接受的最低空调温度", - type = Question.Type.TEXT, - description = "单位:℃", - ) - question17.save() - - question18 = Question.objects.create( - survey = survey, - order = 18, - topic = "是否接受夏天整晚开空调", - type = Question.Type.SINGLE, - ) - question18.save() - - choice18_1 = Choice.objects.create( - question = question18, - order = 1, - text = "是", - ) - choice18_1.save() - - choice18_2 = Choice.objects.create( - question = question18, - order = 2, - text = "否", - ) - choice18_2.save() - - question19 = Question.objects.create( - survey = survey, - order = 19, - topic = "你的性格", - type = Question.Type.SINGLE, - ) - question19.save() - - choice19_1 = Choice.objects.create( - question = question19, - order = 1, - text = "内向型(独处时精力充沛;更封闭,更愿意在经挑选的小群体中分享个人的情况;不把兴奋说出来。)", - ) - choice19_1.save() - - choice19_2 = Choice.objects.create( - question = question19, - order = 2, - text = "适中型(介于二者之间,能够在内外向之间切换,在人群中乐意与人交谈结交朋友,同时也享受独处。)", - ) - choice19_2.save() - - choice19_3 = Choice.objects.create( - question = question19, - order = 3, - text = "外向型(与他人相处时精力充沛;易于“读”和了解,随意地分享个人情况;高度热情地社交。)", - ) - choice19_3.save() - - question20 = Question.objects.create( - survey = survey, - order = 20, - topic = "你希望室友的性格", - type = Question.Type.SINGLE, - ) - question20.save() - - choice20_1 = Choice.objects.create( - question = question20, - order = 1, - text = "内向型", - ) - choice20_1.save() - - choice20_2 = Choice.objects.create( - question = question20, - order = 2, - text = "适中型", - ) - choice20_2.save() - - choice20_3 = Choice.objects.create( - question = question20, - order = 3, - text = "外向型", - ) - choice20_3.save() - - question21 = Question.objects.create( - survey = survey, - order = 21, - topic = "你希望你的宿舍环境是", - type = Question.Type.SINGLE, - ) - question21.save() - - choice21_1 = Choice.objects.create( - question = question21, - order = 1, - text = "整洁条理", - ) - choice21_1.save() - - choice21_2 = Choice.objects.create( - question = question21, - order = 2, - text = "随性就好", - ) - choice21_2.save() - - question22 = Question.objects.create( - survey = survey, - order = 22, - topic = "你对于室友的期待是", - type = Question.Type.SINGLE, - ) - question22.save() - - choice22_1 = Choice.objects.create( - question = question22, - order = 1, - text = "专注学习", - ) - choice22_1.save() - - choice22_2 = Choice.objects.create( - question = question22, - order = 2, - text = "全面发展", - ) - choice22_2.save() - - question23 = Question.objects.create( - survey = survey, - order = 23, - topic = "你本人更希望大学生活是", - type = Question.Type.SINGLE, - ) - question23.save() - - choice23_1 = Choice.objects.create( - question = question23, - order = 1, - text = "专注学习", - ) - choice23_1.save() - - choice23_2 = Choice.objects.create( - question = question23, - order = 2, - text = "全面发展", - ) - choice23_2.save() - - question24 = Question.objects.create( - survey = survey, - order = 24, - topic = "你希望在一天结束后与室友进行学业或成长思考上的交流吗", - type = Question.Type.SINGLE, - ) - question24.save() - - choice24_1 = Choice.objects.create( - question = question24, - order = 1, - text = "希望", - ) - choice24_1.save() - - choice24_2 = Choice.objects.create( - question = question24, - order = 2, - text = "不希望", - ) - choice24_2.save() - - question25 = Question.objects.create( - survey = survey, - order = 25, - topic = "你每周与家人通话累计时长", - type = Question.Type.SINGLE, - ) - question25.save() - - choice25_1 = Choice.objects.create( - question = question25, - order = 1, - text = "低于1h", - ) - choice25_1.save() - - choice25_2 = Choice.objects.create( - question = question25, - order = 2, - text = "约1~3h", - ) - choice25_2.save() - - choice25_3 = Choice.objects.create( - question = question25, - order = 3, - text = "约3~6h", - ) - choice25_3.save() - - choice25_4 = Choice.objects.create( - question = question25, - order = 4, - text = "约6~10h", - ) - choice25_4.save() - - choice25_5 = Choice.objects.create( - question = question25, - order = 5, - text = "大于10h", - ) - choice25_5.save() - - question26 = Question.objects.create( - survey = survey, - order = 26, - topic = "是否愿意担任宿舍长,担任活跃宿舍氛围、架构沟通桥梁的作用", - type = Question.Type.SINGLE, - ) - question26.save() - - choice26_1 = Choice.objects.create( - question = question26, - order = 1, - text = "愿意", - ) - choice26_1.save() - - choice26_2 = Choice.objects.create( - question = question26, - order = 2, - text = "不愿意", - ) - choice26_2.save() - - question27 = Question.objects.create( - survey = survey, - order = 27, - topic = "是否愿意担任班干部?", - type = Question.Type.SINGLE, - ) - question27.save() - - choice27_1 = Choice.objects.create( - question = question27, - order = 1, - text = "愿意", - ) - choice27_1.save() - - choice27_2 = Choice.objects.create( - question = question27, - order = 2, - text = "不愿意", - ) - choice27_2.save() - - question28 = Question.objects.create( - survey = survey, - order = 28, - topic = "如愿意,愿意担任以下哪项职务", - type = Question.Type.MULTIPLE, - required = False - ) - question28.save() - - choice28_1 = Choice.objects.create( - question = question28, - order = 1, - text = "班长", - ) - choice28_1.save() - - choice28_2 = Choice.objects.create( - question = question28, - order = 2, - text = "团支书", - ) - choice28_2.save() - - choice28_3 = Choice.objects.create( - question = question28, - order = 3, - text = "组织委员", - ) - choice28_3.save() - - choice28_4 = Choice.objects.create( - question = question28, - order = 4, - text = "学习委员", - ) - choice28_4.save() - - choice28_5 = Choice.objects.create( - question = question28, - order = 5, - text = "心理委员", - ) - choice28_5.save() - - choice28_6 = Choice.objects.create( - question = question28, - order = 6, - text = "宣传委员", - ) - choice28_6.save() - - choice28_7 = Choice.objects.create( - question = question28, - order = 7, - text = "体育委员", - ) - choice28_7.save() - - question29 = Question.objects.create( - survey = survey, - order = 29, - topic = "如愿意,可否填写竞选理由或优势?如果可以,请填写在下方。", - type = Question.Type.TEXT, - required = False - ) - question29.save() - - question30 = Question.objects.create( - survey = survey, - order = 30, - topic = "是否愿意担任班级联络人?", - type = Question.Type.SINGLE, - ) - question30.save() - - choice30_1 = Choice.objects.create( - question = question30, - order = 1, - text = "是", - ) - choice30_1.save() - - choice30_2 = Choice.objects.create( - question = question30, - order = 2, - text = "否", - ) - choice30_2.save() - - question31 = Question.objects.create( - survey = survey, - order = 31, - topic = "你希望宿舍的氛围:", - type = Question.Type.TEXT, - required = False - ) - question31.save() - - question32 = Question.objects.create( - survey = survey, - order = 32, - topic = "你的兴趣/特长/爱好(例如乐器、剪辑、运动、唱歌跳舞等)", - type = Question.Type.TEXT, - required = False - ) - question32.save() - - question33 = Question.objects.create( - survey = survey, - order = 33, - topic = "你对于大学生活的期待:", - type = Question.Type.TEXT, - required = False - ) - question33.save() diff --git a/dormitory/management/commands/import_dormitory.py b/dormitory/management/commands/import_dormitory.py deleted file mode 100644 index 22b60c2a7..000000000 --- a/dormitory/management/commands/import_dormitory.py +++ /dev/null @@ -1,40 +0,0 @@ -import pandas as pd -from django.core.management.base import BaseCommand -from tqdm import tqdm - -from dormitory.models import Dormitory - - -# 导入宿舍信息,包括宿舍号、容量(4)、性别。 -class Command(BaseCommand): - help = 'Imports dormitory data' - - def add_arguments(self, parser): - parser.add_argument('excel_file', type=str, - help='Path to the Excel file') - - def handle(self, *args, **options): - excel_file = options['excel_file'] - - try: - df_raw = pd.read_excel(excel_file) - except Exception as e: - self.stdout.write(self.style.ERROR( - f'Error reading Excel file: {e}')) - return - - df_dorms = df_raw.groupby('宿舍号') - for dorm_id, df in tqdm(df_dorms): - - gender = pd.unique(df['性别']) - assert len(gender) == 1, len(gender) - - _, created = Dormitory.objects.get_or_create( - id=dorm_id, - capacity=4, - gender={"男": 0, "女": 1}[gender[0]] - ) - if not created: - gender_dict = {'男': 'male', '女': 'female'} - print( - f"Dormitory {dorm_id} already exists. Its capacity is 4 and it's a {gender_dict[gender[0]]} dormitory") diff --git a/dormitory/management/commands/import_dormitory_assignment.py b/dormitory/management/commands/import_dormitory_assignment.py deleted file mode 100644 index e80b2e8fa..000000000 --- a/dormitory/management/commands/import_dormitory_assignment.py +++ /dev/null @@ -1,41 +0,0 @@ -import pandas as pd -from django.core.management.base import BaseCommand -from tqdm import tqdm - -from dormitory.models import Dormitory, DormitoryAssignment -from generic.models import User - - -# 导入宿舍信息,包括宿舍号、容量(4)、性别。 -class Command(BaseCommand): - help = 'Imports dormitory data' - - def add_arguments(self, parser): - parser.add_argument('excel_file', type=str, - help='Path to the Excel file') - - def handle(self, *args, **options): - excel_file = options['excel_file'] - - try: - df_raw = pd.read_excel(excel_file) - except Exception as e: - self.stdout.write(self.style.ERROR( - f'Error reading Excel file: {e}')) - return - - df_dorms = df_raw.groupby('宿舍号') - - for dorm_id, df in tqdm(df_dorms): - dormitory = Dormitory.objects.get(id=dorm_id) - for i in range(len(df)): - user = User.objects.get(username=df.iloc[i]["学号"]) - bed_id = int(df.iloc[i]["床位"]) - _, created = DormitoryAssignment.objects.get_or_create( - dormitory=dormitory, - user=user, - bed_id=bed_id - ) - if not created: - print( - f"This dormitory assignment entity already exists. Info: Dormitory id {dormitory.id}, user {user}, bed id {bed_id}.") diff --git a/dormitory/migrations/0001_initial.py b/dormitory/migrations/0001_initial.py deleted file mode 100644 index 65aa3e7bd..000000000 --- a/dormitory/migrations/0001_initial.py +++ /dev/null @@ -1,40 +0,0 @@ -# Generated by Django 4.2.3 on 2023-10-18 18:25 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='Dormitory', - fields=[ - ('id', models.BigAutoField(primary_key=True, serialize=False, verbose_name='宿舍号')), - ('capacity', models.IntegerField(default=4, verbose_name='容量')), - ('gender', models.CharField(choices=[('M', '男'), ('F', '女')], max_length=1, verbose_name='性别')), - ], - options={ - 'verbose_name': '宿舍', - 'verbose_name_plural': '宿舍', - }, - ), - migrations.CreateModel( - name='DormitoryAssignment', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('bed_id', models.IntegerField(verbose_name='床位号')), - ('time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('dormitory', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dormitory.dormitory', verbose_name='宿舍号')), - ], - options={ - 'verbose_name': '宿舍分配信息', - 'verbose_name_plural': '宿舍分配信息', - }, - ), - ] diff --git a/dormitory/migrations/0002_initial.py b/dormitory/migrations/0002_initial.py deleted file mode 100644 index 98c23bd80..000000000 --- a/dormitory/migrations/0002_initial.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 4.2.3 on 2023-10-18 18:25 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('dormitory', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='dormitoryassignment', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='成员'), - ), - ] diff --git a/dormitory/migrations/0003_agreement.py b/dormitory/migrations/0003_agreement.py deleted file mode 100644 index 1b2794eee..000000000 --- a/dormitory/migrations/0003_agreement.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 4.2.3 on 2024-01-02 13:11 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('dormitory', '0002_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Agreement', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('sign_time', models.DateTimeField(auto_now_add=True, verbose_name='签订时间')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='用户')), - ], - options={ - 'verbose_name': '住宿协议', - 'verbose_name_plural': '住宿协议', - }, - ), - ] diff --git a/dormitory/migrations/__init__.py b/dormitory/migrations/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/dormitory/models.py b/dormitory/models.py deleted file mode 100644 index fbbc85f66..000000000 --- a/dormitory/models.py +++ /dev/null @@ -1,51 +0,0 @@ -from django.db import models - -from utils.models.descriptor import admin_only -from utils.models.choice import choice -from generic.models import User - -__all__ = [ - 'Dormitory', - 'DormitoryAssignment', - 'Agreement' -] - -class Agreement(models.Model): - - class Meta: - verbose_name = '住宿协议' - verbose_name_plural = verbose_name - - user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='用户') - sign_time = models.DateTimeField('签订时间', auto_now_add=True) - - -class Dormitory(models.Model): - class Meta: - verbose_name = '宿舍' - verbose_name_plural = verbose_name - - id = models.BigAutoField('宿舍号', primary_key=True) - capacity = models.IntegerField('容量', default=4) - - class Gender(models.TextChoices): - MALE = choice('M', '男') - FEMALE = choice('F', '女') - - gender = models.CharField('性别', max_length=1, choices=Gender.choices) - - @admin_only - def __str__(self): - return str(self.id) - - -class DormitoryAssignment(models.Model): - class Meta: - verbose_name = '宿舍分配信息' - verbose_name_plural = verbose_name - - dormitory = models.ForeignKey( - Dormitory, on_delete=models.CASCADE, verbose_name='宿舍号') - user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='成员') - bed_id = models.IntegerField('床位号') - time = models.DateTimeField('创建时间', auto_now_add=True) diff --git a/dormitory/serializers.py b/dormitory/serializers.py deleted file mode 100644 index 8509a5fee..000000000 --- a/dormitory/serializers.py +++ /dev/null @@ -1,31 +0,0 @@ -from rest_framework import serializers - -from dormitory.models import Dormitory, DormitoryAssignment, Agreement - - -class DormitorySerializer(serializers.ModelSerializer): - class Meta: - model = Dormitory - fields = '__all__' - - -class DormitoryAssignmentSerializer(serializers.ModelSerializer): - class Meta: - model = DormitoryAssignment - fields = '__all__' - - -class AgreementSerializerFixme(serializers.ModelSerializer): - class Meta: - model = Agreement - fields = ['id'] - - -class AgreementSerializer(serializers.ModelSerializer): - - user = serializers.StringRelatedField() - - class Meta: - model = Agreement - fields = ['user', 'sign_time'] - read_only_fields = ['user', 'sign_time'] diff --git a/dormitory/tests.py b/dormitory/tests.py deleted file mode 100644 index de8bdc00e..000000000 --- a/dormitory/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/dormitory/urls.py b/dormitory/urls.py deleted file mode 100644 index f7f193a4b..000000000 --- a/dormitory/urls.py +++ /dev/null @@ -1,28 +0,0 @@ -from django.urls import include, path -from rest_framework.routers import DefaultRouter - -from dormitory.views import ( - DormitoryAssignmentViewSet, DormitoryAssignResultView, - DormitoryRoutineQAView, DormitoryViewSet, - AgreementView, DormitoryAgreementViewSetFixme, DormitoryAgreementViewSet) - - -router = DefaultRouter() -router.register('dormitory', DormitoryViewSet, basename='dormitory') -router.register('dormitoryassignment', DormitoryAssignmentViewSet, - basename='dormitoryassignment') -router.register('agreement-query-fixme', DormitoryAgreementViewSetFixme, - basename='agreement-query-fixme') -router.register('agreement-query', DormitoryAgreementViewSet, - basename='agreement-query') - - -urlpatterns = [ - path('', include(router.urls)), - path('routine-QA/', DormitoryRoutineQAView.as_view(), - name='dormitory-routine-QA'), - path('assign-result/', DormitoryAssignResultView.as_view(), - name='dormitory-assign-result'), - path('agreement/', AgreementView.as_view(), - name='agreement'), -] diff --git a/dormitory/views.py b/dormitory/views.py deleted file mode 100644 index 07db6a7a2..000000000 --- a/dormitory/views.py +++ /dev/null @@ -1,123 +0,0 @@ -from django.db import transaction -from rest_framework import viewsets - -# TODO: Leaky dependency -from utils.marker import fix_me -from generic.models import User -from app.models import NaturalPerson -from app.view.base import ProfileTemplateView -from dormitory.models import Dormitory, DormitoryAssignment, Agreement -from dormitory.serializers import ( - DormitoryAssignmentSerializer, DormitorySerializer, - AgreementSerializerFixme, AgreementSerializer) -from questionnaire.models import AnswerSheet, AnswerText, Survey -from semester.api import next_semester - - -class DormitoryViewSet(viewsets.ReadOnlyModelViewSet): - queryset = Dormitory.objects.all() - serializer_class = DormitorySerializer - - -class DormitoryAssignmentViewSet(viewsets.ReadOnlyModelViewSet): - queryset = DormitoryAssignment.objects.all() - serializer_class = DormitoryAssignmentSerializer - - -class DormitoryAgreementViewSetFixme(viewsets.ReadOnlyModelViewSet): - serializer_class = AgreementSerializerFixme - def get_queryset(self): - # Only active students need to sign the agreement - require_agreement = User.objects.filter(active=True, - utype=User.Type.STUDENT).contains(self.request.user) - if require_agreement: - return Agreement.objects.filter(user=self.request.user) - # A hack to return something, so that the frontend won't redirect - official_user = User.objects.get(username='zz00000') - Agreement.objects.get_or_create(user=official_user) - return Agreement.objects.filter(user=official_user) - -class DormitoryAgreementViewSet(viewsets.ReadOnlyModelViewSet): - queryset = Agreement.objects.all() - serializer_class = AgreementSerializer - -class DormitoryRoutineQAView(ProfileTemplateView): - - template_name = 'dormitory/routine_QA.html' - page_name = '生活习惯调研' - need_prepare = False - - def get_survey(self): - return Survey.objects.get(title=f'宿舍生活习惯调研-{next_semester().year}') - - def get(self): - survey = self.get_survey() - if AnswerSheet.objects.filter(creator=self.request.user, - survey=survey).exists(): - return self.render(submitted=True) - return self.render(survey_iter=[ - (question, question.choices.order_by('order')) - for question in survey.questions.order_by('order') - ]) - - def post(self): - survey = self.get_survey() - assert not AnswerSheet.objects.filter(creator=self.request.user, - survey=survey).exists() - with transaction.atomic(): - sheet = AnswerSheet.objects.create(creator=self.request.user, - survey=survey) - for question in survey.questions.order_by('order'): - answer = self.request.POST.get(str(question.order)) - if answer is None: - assert not question.required, f"必填题{question.order}未作答" - continue - AnswerText.objects.create(question=question, - answersheet=sheet, - body=answer) - return self.render(submitted=True) - - - -class DormitoryAssignResultView(ProfileTemplateView): - - template_name = 'dormitory/assign_result.html' - page_name = '宿舍分配结果' - http_method_names = ['get'] - need_prepare = False - - def get(self): - self.show_dorm_assign() - return self.render() - - def show_dorm_assign(self): - user = self.request.user - try: - assignment = DormitoryAssignmentViewSet.queryset.get(user=user) - dorm_assignment = DormitoryAssignmentViewSet.queryset.filter( - dormitory=assignment.dormitory) - roommates = [NaturalPerson.objects.get_by_user(assign.user) - for assign in dorm_assignment.exclude(user=user)] - self.extra_context.update( - dorm_assigned=True, - name=user.get_full_name(), - dorm_id=assignment.dormitory.id, - bed_id=assignment.bed_id, - roommates=roommates, - ) - except DormitoryAssignment.DoesNotExist: - self.extra_context.update(dorm_assigned=False) - -class AgreementView(ProfileTemplateView): - template_name = 'dormitory/agreement.html' - page_name = '住宿协议' - need_prepare = False - - def get(self): - return self.render() - - @fix_me - def post(self): - Agreement.objects.get_or_create(user=self.request.user) - from django.shortcuts import redirect - return redirect("/welcome") diff --git a/feedback/__init__.py b/feedback/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/feedback/admin.py b/feedback/admin.py deleted file mode 100644 index 92795c253..000000000 --- a/feedback/admin.py +++ /dev/null @@ -1,18 +0,0 @@ -from django.contrib import admin - -from feedback.models import ( - Feedback, - FeedbackType, -) - -# Register your models here. -@admin.register(Feedback) -class FeedbackAdmin(admin.ModelAdmin): - list_display = ["type", "title", "person", "org", "feedback_time",] - search_fields = ("person__name", "org__oname",) - - -@admin.register(FeedbackType) -class FeedbackTypeAdmin(admin.ModelAdmin): - list_display = ["name", "org_type", "org",] - search_fields = ("name", "org_type__otype_name", "org__oname",) diff --git a/feedback/apps.py b/feedback/apps.py deleted file mode 100644 index d26ac3cb4..000000000 --- a/feedback/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class FeedbackConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "feedback" diff --git a/feedback/feedback_utils.py b/feedback/feedback_utils.py deleted file mode 100644 index 4d83439a4..000000000 --- a/feedback/feedback_utils.py +++ /dev/null @@ -1,274 +0,0 @@ -from django.http import HttpRequest - -from app.utils_dependency import * -from app.models import ( - NaturalPerson, - Organization, - OrganizationType, - Notification, -) -from app.notification_utils import ( - notification_create, -) -from app.extern.wechat import ( - WechatApp, - WechatMessageLevel, -) -from app.log import logger -from feedback.models import ( - FeedbackType, - Feedback, -) - - -__all__ = [ - 'check_feedback', - 'update_feedback', - 'make_relevant_notification', - 'examine_notification', - 'inform_notification', -] - - -def check_feedback(request, post_type, me): - '''返回feedback的context字典,如果是提交反馈则检查feedback参数的合法性''' - context = dict() - context["warn_code"] = 0 - - try: - type = str(request.POST.get("type")) - context["type"] = FeedbackType.objects.get(name=type) - except: - context["warn_code"] = 1 - # User can't see it. We use it for debugging. - context["warn_message"] = "数据库没有对应反馈类型,请联系管理员!" - return context - - try: - otype = request.POST.get("otype") - if otype: - context["otype"] = OrganizationType.objects.get(otype_name=otype) - except: - context["warn_code"] = 1 - # User can't see it. We use it for debugging. - context["warn_message"] = "数据库没有对应小组类型,请联系管理员!" - return context - - try: - org = request.POST.get("org") - if org: - context["org"] = Organization.objects.get(oname=org) - except: - context["warn_code"] = 1 - # User can't see it. We use it for debugging. - context["warn_message"] = "数据库没有对应小组,请联系管理员!" - return context - - title = str(request.POST["title"]) - otype = str(request.POST.get("otype")) # 接收小组类型 - org = str(request.POST.get("org")) - content = str(request.POST["content"]) - publisher_public = str(request.POST['publisher_public']) - - # 草稿不用检查标题、内容、公开的合法性,提交反馈需要检查! - if post_type in ["directly_submit", "submit_draft"]: - if len(title) >= 30: - return wrong("标题不能超过30字哦!") - if title == "": - return wrong("标题不能为空哦!") - - if otype == "": - return wrong("不能不选择接收小组的类型哦!") - - if org == "": - return wrong("不选择接收小组就没有小组收到你的反馈了哦!请选择接收小组~") - - if content == "": - return wrong("反馈内容不能为空哦!") - - if publisher_public == "": - return wrong("必须选择同意/不同意公开~") - - if OrganizationType.objects.get(otype_name=otype).incharge == me: - return wrong("老师您好,本系统暂不支持给您管理的小组发送反馈!抱歉。") - - context["title"] = title # 反馈标题 - context["person"] = request.user # 反馈发出者 - context["otype"] = str(request.POST.get("otype")) # 接收小组类型 - context["org"] = str(request.POST.get("org")) # 接收小组 - context["content"] = str(request.POST.get("content")) # 反馈内容 - context["publisher_public"] = True if request.POST.get("publisher_public")=="公开" else False - # 个人是否同意公开 - return context - - -def update_feedback(feedback, me, request: HttpRequest): - ''' - 修改反馈详情的操作函数, feedback为修改的对象,可以为None - me为操作者 - info为前端POST字典 - 返回值为context, warn_code = 1表示失败, 2表示成功; 错误信息在context["warn_message"] - 如果成功context会返回update之后的feedback, - ''' - - # 首先上锁 - with transaction.atomic(): - info = request.POST - post_type = str(info.get("post_type")) - - context = check_feedback(request, post_type, me) - if context['warn_code'] == 1: - return context - - # TODO:删除草稿的功能 - content = dict( - type=FeedbackType.objects.get(name=str(info.get('type'))), - title=str(info.get('title')), - content=str(info.get('content')), - person=me, - org_type=OrganizationType.objects.get( - otype_name=str(info.get('otype')) - ) if info.get('otype') else None, - org=Organization.objects.get( - oname=str(info.get('org')) - ) if info.get('org') else None, - publisher_public=(str(info.get('publisher_public')) == '公开'), - ) - - # 如果session中存在feedback_url,保存类型匹配时添加url并弹出 - if request.session.has_key('feedback_url') and post_type in ['save', 'directly_submit']: - feedback_type = request.session.get('feedback_type') - if content['type'].name == feedback_type: - feedback_url = request.session.pop('feedback_url') - request.session.pop('feedback_type', None) - content.update(url=feedback_url) - - if post_type == 'save': - feedback = Feedback.objects.create( - **content, - issue_status=Feedback.IssueStatus.DRAFTED, - ) - context = succeed("成功将反馈保存成草稿!") - context['feedback_id'] = feedback.id - return context - elif post_type == 'directly_submit': - feedback = Feedback.objects.create( - **content, - issue_status=Feedback.IssueStatus.ISSUED, - ) - context = succeed( - "成功提交反馈“" + str(info.get('title')) + "”!" + - "请耐心等待" + str(info.get('org')) + "处理!" - ) - context['feedback_id'] = feedback.id - return context - elif post_type == 'modify': - publisher_public = True if str(info.get('publisher_public'))=='公开' else False - if (feedback.title == str(info.get("title")) - and feedback.type == FeedbackType.objects.get(name=str(info.get('type'))) - and feedback.content == str(info.get('content')) - and feedback.publisher_public == publisher_public - and feedback.org == (Organization.objects.get(oname=str(info.get('org'))) - if str(info.get('org')) else None) - ): - return wrong("没有检测到修改!") - Feedback.objects.filter(id=feedback.id).update( - **content, - ) - context = succeed("成功修改反馈“" + str(info.get('title')) + "”!点击“提交反馈”可提交~") - context["feedback_id"] = feedback.id - return context - elif post_type == 'submit_draft': - Feedback.objects.filter(id=feedback.id).update( - **content, - issue_status=Feedback.IssueStatus.ISSUED, - ) - context = succeed( - "成功提交反馈“" + str(info.get('title')) + "”!" + - "请耐心等待" + str(info.get('org')) + "处理!" - ) - context['feedback_id'] = feedback.id - return context - - -@logger.secure_func(raise_exc=True) -def make_relevant_notification(feedback: Feedback, info, me: NaturalPerson): - ''' - 在用户提交反馈后,向对应组织发送通知 - ''' - - post_type = str(info.get("post_type")) - feasible_post = [ - "directly_submit", - "submit_draft", - ] - - # 准备创建notification需要的构件:发送方、接收方、发送内容、通知类型、通知标题、URL、关联外键 - sender = me.person_id - receiver = Organization.objects.get(oname=str(info.get('org'))).get_user() - typename = (Notification.Type.NEEDDO - if post_type == 'new_submit' - else Notification.Type.NEEDREAD) - title = f"反馈:{feedback.title}" - content = "您收到一条新的反馈~点击标题立刻查看!" - # TODO:小组看到的反馈详情呈现 - # URL = f'/modifyFeedback/?feedback_id={feedback.id}' - relate_instance = feedback - - # 正式创建notification - notification_create( - receiver=receiver, - sender=sender, - typename=typename, - title=title, - content=content, - URL=f"/viewFeedback/{feedback.id}", - relate_instance=relate_instance, - anonymous_flag=True, - to_wechat=dict(app=WechatApp.AUDIT, level=WechatMessageLevel.IMPORTANT), - ) - - -@logger.secure_func(raise_exc=True) -def examine_notification(feedback: Feedback): - examin_teacher = feedback.org.otype.incharge.person_id - notification_create( - receiver=examin_teacher, - sender=feedback.org.get_user(), - typename=Notification.Type.NEEDREAD, - title=Notification.Title.VERIFY_INFORM, - content=f"{feedback.org.oname}申请公开一条反馈信息", - URL=f"/viewFeedback/{feedback.id}", - to_wechat=dict(app=WechatApp.AUDIT, level=WechatMessageLevel.INFO), - ) - -@logger.secure_func(raise_exc=True) -def inform_notification(sender: ClassifiedUser, receiver: ClassifiedUser, - content, feedback, anonymous=None, important=False): - ''' - 根据信息创建通知并发送到微信 - - Parameters - ---------- - content : str - 消息内容 - feedback : Feedback - 只使用id用于创建URL - anonymous : bool, optional - 是否匿名,默认个人匿名 - important : bool, optional - 微信发送的等级, by default False - ''' - if anonymous is None: - anonymous = not isinstance(sender, Organization) - level = WechatMessageLevel.IMPORTANT if important else WechatMessageLevel.INFO - notification_create( - receiver=receiver.get_user(), - sender=sender.get_user(), - typename=Notification.Type.NEEDREAD, - title=Notification.Title.FEEDBACK_INFORM, - content=content, - URL=f"/viewFeedback/{feedback.id}", - anonymous_flag=anonymous, - to_wechat=dict(app=WechatApp.AUDIT, level=level), - ) diff --git a/feedback/jobs.py b/feedback/jobs.py deleted file mode 100644 index 5a64f86af..000000000 --- a/feedback/jobs.py +++ /dev/null @@ -1,42 +0,0 @@ -from datetime import datetime, timedelta - -from django.db import transaction - -from app.models import ( - User, -) -from app.config import * -from generic.models import YQPointRecord -from scheduler.periodic import periodical -from feedback.feedback_utils import inform_notification -from feedback.models import Feedback - -__all__ = [ - 'public_feedback_per_hour', -] - - -@periodical('cron', 'feedback_public_updater', minute=5) -@transaction.atomic -def public_feedback_per_hour(): - '''查找距离组织公开反馈24h内没被审核的反馈,将其公开''' - time = datetime.now() - timedelta(days=1) - feedbacks = Feedback.objects.filter( - issue_status=Feedback.IssueStatus.ISSUED, - public_status=Feedback.PublicStatus.PRIVATE, - publisher_public=True, - org_public=True, - public_time__lte=time, - ) - feedbacks.select_for_update().update( - public_status=Feedback.PublicStatus.PUBLIC) - for feedback in feedbacks: - User.objects.modify_YQPoint(feedback.person.get_user(), - CONFIG.yqpoint.per_feedback, - "问题反馈", YQPointRecord.SourceType.FEEDBACK) - inform_notification(feedback.org.otype.incharge, feedback.person, - f"您的反馈[{feedback.title}]已被公开", - feedback, anonymous=False) - inform_notification(feedback.org.otype.incharge, feedback.org, - f"您处理的反馈[{feedback.title}]已自动公开", - feedback, anonymous=False) \ No newline at end of file diff --git a/feedback/migrations/0001_initial.py b/feedback/migrations/0001_initial.py deleted file mode 100644 index 50d770fb2..000000000 --- a/feedback/migrations/0001_initial.py +++ /dev/null @@ -1,57 +0,0 @@ -# Generated by Django 4.2.3 on 2023-10-18 18:25 - -import datetime -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('app', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='FeedbackType', - fields=[ - ('id', models.SmallIntegerField(primary_key=True, serialize=False, verbose_name='反馈类型编号')), - ('name', models.CharField(max_length=20, verbose_name='反馈类型名称')), - ('flexible', models.SmallIntegerField(choices=[(0, '无默认值'), (1, '仅提供组织类型默认值'), (2, '全部提供默认值')], default=0)), - ('org', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='app.organization')), - ('org_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='app.organizationtype')), - ], - options={ - 'verbose_name': '#EX.反馈类型', - 'verbose_name_plural': '#EX.反馈类型', - }, - ), - migrations.CreateModel( - name='Feedback', - fields=[ - ('commentbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='app.commentbase')), - ('title', models.CharField(max_length=30, verbose_name='标题')), - ('content', models.TextField(verbose_name='内容')), - ('url', models.URLField(blank=True, default='', max_length=256, verbose_name='相关链接')), - ('issue_status', models.SmallIntegerField(choices=[(0, '草稿'), (1, '已发布'), (2, '已删除')], default=0, verbose_name='发布状态')), - ('read_status', models.SmallIntegerField(choices=[(0, '已读'), (1, '未读')], default=1, verbose_name='阅读情况')), - ('solve_status', models.SmallIntegerField(choices=[(0, '已解决'), (1, '解决中'), (2, '无法解决'), (3, '未标记')], default=3, verbose_name='解决进度')), - ('feedback_time', models.DateTimeField(auto_now_add=True, verbose_name='反馈时间')), - ('publisher_public', models.BooleanField(default=False, verbose_name='发布者是否公开')), - ('org_public', models.BooleanField(default=False, verbose_name='组织是否公开')), - ('public_time', models.DateTimeField(default=datetime.datetime.now, verbose_name='组织公开时间')), - ('public_status', models.SmallIntegerField(choices=[(0, '公开'), (1, '未公开'), (2, '撤销公开'), (3, '不予公开')], default=1, verbose_name='公开状态')), - ('org', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='app.organization')), - ('org_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='app.organizationtype')), - ('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.naturalperson')), - ('type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='feedback.feedbacktype')), - ], - options={ - 'verbose_name': '#EX.反馈', - 'verbose_name_plural': '#EX.反馈', - }, - bases=('app.commentbase',), - ), - ] diff --git a/feedback/migrations/__init__.py b/feedback/migrations/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/feedback/models.py b/feedback/models.py deleted file mode 100644 index 09dcb51c7..000000000 --- a/feedback/models.py +++ /dev/null @@ -1,118 +0,0 @@ -from datetime import datetime - -from django.db import models - -from app.models import ( - OrganizationType, - Organization, - CommentBase, - NaturalPerson, -) - -__all__ = [ - 'FeedbackType', - 'Feedback', -] - -class FeedbackType(models.Model): - class Meta: - verbose_name = "#EX.反馈类型" - verbose_name_plural = verbose_name - - id = models.SmallIntegerField("反馈类型编号", primary_key=True) - name = models.CharField("反馈类型名称", max_length=20) - org_type: OrganizationType = models.ForeignKey( - OrganizationType, on_delete=models.CASCADE, null=True, blank=True) - org: Organization = models.ForeignKey( - Organization, on_delete=models.CASCADE, null=True, blank=True) - - class Flexible(models.IntegerChoices): - NO_DEFAULT = (0, "无默认值") - ORG_TYPE_DEFAULT = (1, "仅提供组织类型默认值") - ALL_DEFAULT = (2, "全部提供默认值") - - flexible = models.SmallIntegerField( - choices=Flexible.choices, default=Flexible.NO_DEFAULT - ) - - def __str__(self): - return self.name - - -class Feedback(CommentBase): - class Meta: - verbose_name = "#EX.反馈" - verbose_name_plural = verbose_name - - type = models.ForeignKey(FeedbackType, on_delete=models.CASCADE) - title = models.CharField("标题", max_length=30, blank=False) - content = models.TextField("内容", blank=False) - person = models.ForeignKey(NaturalPerson, on_delete=models.CASCADE) - org_type: OrganizationType = models.ForeignKey( - OrganizationType, on_delete=models.CASCADE, null=True, blank=True) - org: Organization = models.ForeignKey( - Organization, on_delete=models.CASCADE, null=True, blank=True) - url = models.URLField("相关链接", max_length=256, default="", blank=True) - - class IssueStatus(models.IntegerChoices): - DRAFTED = (0, "草稿") - ISSUED = (1, "已发布") - DELETED = (2, "已删除") - - class ReadStatus(models.IntegerChoices): - READ = (0, "已读") - UNREAD = (1, "未读") - - class SolveStatus(models.IntegerChoices): - SOLVED = (0, "已解决") - SOLVING = (1, "解决中") - UNSOLVABLE = (2, "无法解决") - UNMARKED = (3, "未标记") - - issue_status = models.SmallIntegerField( - '发布状态', choices=IssueStatus.choices, default=IssueStatus.DRAFTED - ) - read_status = models.SmallIntegerField( - '阅读情况', choices=ReadStatus.choices, default=ReadStatus.UNREAD - ) - solve_status = models.SmallIntegerField( - '解决进度', choices=SolveStatus.choices, default=SolveStatus.UNMARKED - ) - - feedback_time = models.DateTimeField('反馈时间', auto_now_add=True) - # anonymous = models.BooleanField("发布者是否匿名", default=True) - publisher_public = models.BooleanField('发布者是否公开', default=False) - org_public = models.BooleanField('组织是否公开', default=False) - public_time = models.DateTimeField('组织公开时间', default=datetime.now) - - class PublicStatus(models.IntegerChoices): - PUBLIC = (0, '公开') - PRIVATE = (1, '未公开') - WITHDRAWAL = (2, '撤销公开') - FORCE_PRIVATE = (3, '不予公开') - - public_status = models.SmallIntegerField( - '公开状态', choices=PublicStatus.choices, default=PublicStatus.PRIVATE - ) - - def __str__(self): - return self.title - - def save(self, *args, **kwargs): - self.typename = "feedback" - super().save(*args, **kwargs) - - def get_absolute_url(self, absolute=False) -> str: - ''' - 获取显示页面网址 - - :param absolute: 是否返回绝对地址, defaults to False - :type absolute: bool, optional - :return: 显示页面的网址 - :rtype: str - ''' - if self.issue_status == Feedback.IssueStatus.DRAFTED: - url = f'/modifyFeedback/?feedback_id={self.id}' - else: - url = f'/viewFeedback/{self.id}' - return url \ No newline at end of file diff --git a/feedback/tests.py b/feedback/tests.py deleted file mode 100644 index 7ce503c2d..000000000 --- a/feedback/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/feedback/urls.py b/feedback/urls.py deleted file mode 100644 index 9b6cb02d4..000000000 --- a/feedback/urls.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.urls import path - -from feedback import views - - -urlpatterns = [ - # 反馈中心 - path("feedback/", views.feedbackWelcome, name="feadbackWelcome"), - path("modifyFeedback/", views.modifyFeedback, name="modifyFeedback"), - path("viewFeedback/", views.viewFeedback, name="viewFeedback"), -] diff --git a/feedback/views.py b/feedback/views.py deleted file mode 100644 index 949bc5e1c..000000000 --- a/feedback/views.py +++ /dev/null @@ -1,732 +0,0 @@ -from datetime import datetime - -from django.db.models import Q -from django.db import transaction - -from app.views_dependency import * -from app.models import ( - Organization, - OrganizationType, - Notification, -) -from app.utils import ( - get_person_or_org, -) -from app.comment_utils import addComment, showComment -from generic.models import YQPointRecord -from feedback.feedback_utils import ( - examine_notification, - update_feedback, - make_relevant_notification, - inform_notification, -) -from feedback.models import ( - FeedbackType, - Feedback, -) -from achievement.api import unlock_achievement - - -__all__ = [ - 'feedbackWelcome', - 'viewFeedback', - 'modifyFeedback', -] - - -@login_required(redirect_field_name="origin") -@utils.check_user_access(redirect_url="/logout/") -@logger.secure_view() -def viewFeedback(request: HttpRequest, fid): - try: - # 查找fid对应的反馈条目 - fid = int(fid) - feedback: Feedback = Feedback.objects.get(id=fid) - except: - return redirect(message_url(wrong("反馈不存在!"), '/feedback/')) - - html_display = {} - me = utils.get_person_or_org(request.user) - - # 获取前端页面中可能存在的提示 - my_messages.transfer_message_context(request.GET, html_display) - - # 添加评论和修改活动状态 - if request.method == "POST" and request.POST: - # 添加评论 - if request.POST.get("comment_submit"): - # 只有未完成反馈可以发送评论 - if feedback.solve_status not in [ - Feedback.SolveStatus.SOLVING, - Feedback.SolveStatus.UNMARKED, - ]: - return redirect(message_url(wrong("已结束的反馈不能评论!"), request.path)) - anonymous = False - # 确定通知消息的发送人,互相发送给对方 - if request.user.is_person() and feedback.person == me: - receiver = feedback.org.get_user() - anonymous = True - elif request.user.is_org() and feedback.org == me: - receiver = feedback.person.person_id - # 老师可以评论,给双方发送通知消息 - elif request.user.is_person() and me.is_teacher(): - receiver = [ - feedback.org.get_user(), - feedback.person.person_id, - ] - # 其他人没有评论权限 - else: - return redirect(message_url(wrong("没有评论权限!"), request.path)) - # 满足以上条件后可以添加评论 - addComment(request, feedback, receiver, - anonymous=anonymous, - notification_title=Notification.Title.FEEDBACK_INFORM) - return redirect(message_url(succeed("成功添加1条评论!"), request.path)) - - # 以下为调整反馈的状态 - public = request.POST.get("public_status") - read = request.POST.get("read_status", "unread") - solve = request.POST.get("solve_status", "solving") - # 成功反馈信息 - succeed_message = [] - # 一、修改已读状态 - # 只有已读条目才可以进行后续的修改 - if feedback.read_status == Feedback.ReadStatus.UNREAD: - # 只有组织可以修改已读状态 - if request.user.is_org() and feedback.org == me: - if read != "read": - return redirect(message_url(wrong("必须先设置为已读!"), request.path)) - with transaction.atomic(): - feedback = Feedback.objects.select_for_update().get(id=fid) - if read == "read": - feedback.read_status = Feedback.ReadStatus.READ - feedback.solve_status = Feedback.SolveStatus.SOLVING - # 已读条目不允许恢复为未读 - # elif read == "unread": - # feedback.read_status = Feedback.ReadStatus.UNREAD - feedback.save() - succeed_message.append("成功修改状态为【已读】!") - inform_notification(me, feedback.person, - f"您的反馈[{feedback.title}]已知悉。", - feedback, - important=True) - # 其他人没有标记已读权限 - else: - return redirect(message_url(wrong("没有修改已读状态的权限!"), request.path)) - - - # 二、修改解决状态 - feedback = Feedback.objects.get(id=fid) - # 只有已读条目才可以修改解决状态;只有已解决/无法解决的条目才可以修改后续状态 - if feedback.solve_status == Feedback.SolveStatus.SOLVING: - # 只有组织可以修改解决状态 - if request.user.is_org() and feedback.org == me: - # 只有已读条目才可以修改解决状态: - if feedback.read_status != Feedback.ReadStatus.READ: - return redirect(message_url(wrong("只有已读反馈可以修改解决状态!"), request.path)) - with transaction.atomic(): - feedback = Feedback.objects.select_for_update().get(id=fid) - # 修改为已解决 - if solve == "solve": - feedback.solve_status = Feedback.SolveStatus.SOLVED - # 修改为无法解决 - elif solve == "unsolvable": - feedback.solve_status = Feedback.SolveStatus.UNSOLVABLE - feedback.save() - if solve != "solving": - succeed_message.append( - f"成功修改解决状态为【{feedback.get_solve_status_display()}】") - inform_notification( - me, feedback.person, - f"您的反馈[{feedback.title}]已修改状态为" - +f"【{feedback.get_solve_status_display()}】。", - feedback, important=True, - ) - # 其他人没有修改解决状态权限 - # else: - # return redirect(message_url(wrong("没有修改解决状态的权限!"), request.path)) - - # 三、公开反馈信息 - feedback = Feedback.objects.get(id=fid) - if public == "public": - # 组织选择公开反馈 - if request.user.is_org() and feedback.org == me: - # 只有完成的反馈可以公开。另外组织已公开的反馈必然已完成 - if feedback.solve_status in [ - Feedback.SolveStatus.SOLVING, - Feedback.SolveStatus.UNMARKED, - ]: - return redirect(message_url( - wrong("只有已解决/无法解决的反馈才可以公开"), request.path)) - # 若老师不予公开,则不允许修改 - if feedback.public_status == Feedback.PublicStatus.FORCE_PRIVATE: - return redirect(message_url( - wrong("审核教师已设置不予公开!"), request.path)) - # 若个人不公开 - if feedback.publisher_public == False: - return redirect(message_url( - wrong("根据反馈人的设置,你无法公开这一反馈结果。" - + "如果以后遇到希望公开的反馈,请在状态为【解决中】时提醒用户调整状态为【公开】!"), - request.path)) - # 若老师没有不予公开,则修改组织公开状态 - with transaction.atomic(): - feedback = Feedback.objects.select_for_update().get(id=fid) - feedback.org_public = True - feedback.save() - succeed_message.append("成功修改组织公开状态为【公开】!通过学院审核后,该反馈将向所有人公开。") - inform_notification(me, feedback.person, - f"已申请公开您的反馈[{feedback.title}],等待老师审核。", - feedback) - # 此时若发布者也选择公开,则向老师发送通知消息,提醒审核 - if feedback.publisher_public: - examine_notification(feedback) - - # 发布者(个人)选择公开反馈 - elif request.user.is_person() and feedback.person == me: - # 若老师不予公开,则不允许修改 - if feedback.public_status == Feedback.PublicStatus.FORCE_PRIVATE: - return redirect(message_url(wrong("审核教师已设置不予公开!"), request.path)) - # 若老师没有不予公开,则修改发布者公开状态 - with transaction.atomic(): - feedback = Feedback.objects.select_for_update().get(id=fid) - feedback.publisher_public = True - feedback.save() - succeed_message.append("成功修改个人公开状态为【公开】!待小组公开并通过学院审核后,该反馈将向所有人公开。") - # 此时若组织也选择公开,则向老师发送通知消息,提醒审核 - if feedback.org_public: - examine_notification(feedback) - - # 教师选择公开反馈 - elif ( - request.user.is_person() and me.is_teacher() - ): - # 若组织或发布者有不公开的意愿,则教师不能公开 - if (feedback.publisher_public != True or feedback.org_public != True): - return redirect(message_url(wrong("小组/个人没有选择公开反馈!"), request.path)) - # 教师可以公开组织和发布者均公开的反馈 - with transaction.atomic(): - feedback = Feedback.objects.select_for_update().get(id=fid) - feedback.public_status = Feedback.PublicStatus.PUBLIC - feedback.save() - # 为提出者增加元气值 - User.objects.modify_YQPoint(feedback.person.get_user(), - CONFIG.yqpoint.per_feedback, - "问题反馈", YQPointRecord.SourceType.FEEDBACK) - succeed_message.append("成功修改反馈公开状态为【公开】!所有学生都有访问权限。") - inform_notification(me, feedback.person, f"已公开您的反馈[{feedback.title}]。", feedback, anonymous=False) - inform_notification(me, feedback.org, f"已公开您处理的反馈[{feedback.title}]。", feedback, anonymous=False) - # 其他人没有公开反馈权限 - else: - return redirect(message_url(wrong("没有公开该反馈的权限!"), request.path)) - else: - if request.user.is_org() and feedback.org == me: - # 修改组织公开状态 - with transaction.atomic(): - feedback = Feedback.objects.select_for_update().get(id=fid) - feedback.org_public = False - if feedback.public_status == Feedback.PublicStatus.PUBLIC: - feedback.public_status = Feedback.PublicStatus.PRIVATE - feedback.save() - succeed_message.append("成功修改组织公开状态为【不公开】!") - inform_notification(me, feedback.person, - f"组织设置您的反馈[{feedback.title}]状态为【不公开】。", - feedback) - # 四、隐藏反馈信息 - feedback = Feedback.objects.get(id=fid) - if public == "private": - # 小组和个人公开反馈后,暂不允许恢复隐藏状态 - # 组织选择隐藏反馈 - if request.user.is_org() and feedback.org == me: - pass - """ # 小组已公开不允许恢复隐藏,因此不采取任何操作 - with transaction.atomic(): - feedback = Feedback.objects.select_for_update().get(id=fid) - feedback.org_public = False - # 此时若老师没有不予公开,则隐藏反馈状态 - if feedback.public_status != Feedback.PublicStatus.FORCE_PRIVATE: - feedback.public_status = Feedback.PublicStatus.PRIVATE - feedback.save() - """ - # 发布者(个人)选择隐藏反馈 - elif request.user.is_person() and feedback.person == me: - pass - """ # 发布者已公开不允许恢复隐藏,因此不采取任何操作 - with transaction.atomic(): - feedback = Feedback.objects.select_for_update().get(id=fid) - feedback.publisher_public = False - # 此时若老师没有不予公开,则隐藏反馈状态 - if feedback.public_status != Feedback.PublicStatus.FORCE_PRIVATE: - feedback.public_status = Feedback.PublicStatus.PRIVATE - feedback.save() - """ - # 教师选择隐藏反馈 - elif ( - request.user.is_person() and me.is_teacher() - ): - with transaction.atomic(): - feedback = Feedback.objects.select_for_update().get(id=fid) - # 教师为不予公开 - feedback.public_status = Feedback.PublicStatus.FORCE_PRIVATE - feedback.save() - succeed_message.append("成功修改反馈状态为【不予公开】,除发布者和小组外均无访问权限。") - inform_notification(me, feedback.person, f"暂不公开您的反馈[{feedback.title}]。", feedback, anonymous=False) - inform_notification(me, feedback.org, f"暂不公开您处理的反馈[{feedback.title}]。", feedback, anonymous=False) - # 其他人没有隐藏反馈权限 - else: - return redirect(message_url(wrong("没有隐藏该反馈的权限!"), request.path)) - """撤销反馈修改为聚合页面进行,这里暂时不用 - # 五、撤销反馈 - if request.POST.get("post_type") == "cancel": - # 只有发布者可以撤销反馈 - if feedback.person == me: - # 已完成的反馈不允许撤回 - if feedback.solve_status != Feedback.SolveStatus.SOLVING: - return redirect(message_url(wrong("只有未解决的反馈才可以撤回"), request.path)) - with transaction.atomic(): - feedback = Feedback.objects.select_for_update().get(id=fid) - feedback.issue_status = Feedback.IssueStatus.DELETED - feedback.save() - succeed_message.append("成功撤销反馈!") - """ - # 如果有任何数据库操作,都需要提示操作成功 - if succeed_message: - return redirect(message_url(succeed(",".join(succeed_message)), request.path)) - - # 使用 GET 方法访问,展示页面 - # 首先确定不同用户对反馈的评论和修改权限 - read = feedback.get_read_status_display() - solve = feedback.get_solve_status_display() - public = False - is_person = feedback.person == me - commentable = False - public_editable = False - read_editable = False - solve_editable = False - cancel_editable = False # 不允许修改反馈标题和反馈内容 - form_editable = False - # 一、当前登录用户为发布者 - if request.user.is_person() and feedback.person == me: - login_identity = "publisher" - # 未结束反馈发布者可评论,可撤销 - if feedback.solve_status in (Feedback.SolveStatus.SOLVING, Feedback.SolveStatus.UNMARKED) \ - and feedback.issue_status != Feedback.IssueStatus.DELETED: - commentable = True - # 撤销反馈功能迁移到反馈聚合页面 - # cancel_editable = True - # 未公开反馈,且老师没有设置成不予公开时,发布者可修改自身公开状态 - if (not feedback.publisher_public) and feedback.public_status != Feedback.PublicStatus.FORCE_PRIVATE \ - and feedback.issue_status != Feedback.IssueStatus.DELETED: - public_editable = True - # 二、当前登录用户为老师 - elif request.user.is_person() and me.is_teacher(): - login_identity = "teacher" - # 未结束反馈可评论 - if feedback.solve_status in (Feedback.SolveStatus.SOLVING, Feedback.SolveStatus.UNMARKED) \ - and feedback.issue_status != Feedback.IssueStatus.DELETED: - commentable = True - # 所有反馈老师可修改公开状态 - if feedback.issue_status != Feedback.IssueStatus.DELETED: - public_editable = True - if feedback.public_status == Feedback.PublicStatus.PUBLIC \ - and feedback.issue_status != Feedback.IssueStatus.DELETED: - public = True - # 未结束反馈可评论 - if feedback.solve_status in (Feedback.SolveStatus.SOLVING, Feedback.SolveStatus.UNMARKED) \ - and feedback.issue_status != Feedback.IssueStatus.DELETED: - commentable = True - # 三、当前登录用户为发布者和老师以外的个人 - elif request.user.is_person(): - # 检查当前个人是否具有访问权限,只有公开反馈有访问权限 - if feedback.public_status == Feedback.PublicStatus.PUBLIC \ - and feedback.issue_status != Feedback.IssueStatus.DELETED: - login_identity = "student" - else: - # 如果是组织管理员,尝试登录 - login_context = utils.user_login_org(request, feedback.org) - if login_context["warn_code"] == SUCCEED: - return redirect(message_url(login_context, request.path)) - return redirect(message_url(wrong("该反馈尚未公开,没有访问该反馈的权限!"), '/feedback/')) - # 四、当前登录用户为受反馈小组 - elif request.user.is_org() and feedback.org == me: - login_identity = "org" - # 未读反馈可修改未为已读 - if feedback.read_status == Feedback.ReadStatus.UNREAD \ - and feedback.issue_status != Feedback.IssueStatus.DELETED: - read_editable = True - # 未结束反馈可修改为已结束,并且可以评论 - if feedback.solve_status in (Feedback.SolveStatus.SOLVING, Feedback.SolveStatus.UNMARKED) \ - and feedback.issue_status != Feedback.IssueStatus.DELETED: - solve_editable = True - commentable = True - # 个人愿意公开,老师没有设置成不予公开时,组织可修改自身公开状态 - if feedback.public_status != Feedback.PublicStatus.FORCE_PRIVATE \ - and feedback.issue_status != Feedback.IssueStatus.DELETED: - public_editable = True - # 其他用户(非受反馈小组)暂时不开放任何权限 - else: - return redirect(message_url(wrong("没有访问该反馈的权限"), '/feedback/')) - - # 撤销反馈、公开反馈、标记已读、修改解决状态需要表单操作 - # 撤销反馈迁移到反馈聚合页面 - form_editable = public_editable or read_editable or solve_editable - - bar_display = utils.get_sidebar_and_navbar(request.user, navbar_name="反馈信息") - title = feedback.title - comments = showComment(feedback, anonymous_users=[feedback.person.person_id]) - return render(request, "feedback/info.html", locals()) - - -@login_required(redirect_field_name='origin') -@utils.check_user_access(redirect_url="/logout/") -@logger.secure_view() -def feedbackWelcome(request: HttpRequest): - html_display = {} - is_person = request.user.is_person() - me = get_person_or_org(request.user) - - if 'argue' in request.GET.keys() and request.session.has_key('feedback_type'): - if not request.session.has_key('feedback_url'): - # 弹出 - feedback_type = request.session.pop('feedback_type') - else: - feedback_type = request.session['feedback_type'] - feedback_url = request.session['feedback_url'] - try: - feedback: Feedback = Feedback.objects.filter(url=feedback_url)[0] - # 弹出 - request.session.pop('feedback_type') - request.session.pop('feedback_url') - request.session.pop('feedback_content', '') - return redirect(message_url( - succeed('检测到您填写的申诉内容,已自动跳转'), - feedback.get_absolute_url() - )) - except: - pass - return redirect(f'/modifyFeedback/?type={feedback_type}') - - # 准备用户提示量 - my_messages.transfer_message_context(request.GET, html_display) - - # -----------------------------反馈记录--------------------------------- - issued_feedback = ( - Feedback.objects - .filter(issue_status=Feedback.IssueStatus.ISSUED) - .order_by("-feedback_time") - ) - - # 是否显示我的反馈 - show_feedback = True - if request.user.is_person(): - # 教师页面不显示我的反馈 - if me.is_teacher(): - show_feedback = False - my_feedback = issued_feedback.filter(person=me) - my_all_feedback = Feedback.objects.filter(person=me) - else: - my_feedback = issued_feedback.filter(org=me) - my_all_feedback = Feedback.objects.filter(org=me) - - undone_feedback = ( - my_feedback - .filter( - Q(solve_status=Feedback.SolveStatus.SOLVING) - | Q(solve_status=Feedback.SolveStatus.UNMARKED) - ) - ) - done_feedback = ( - my_all_feedback - .filter( - Q(solve_status=Feedback.SolveStatus.SOLVED) - | Q(solve_status=Feedback.SolveStatus.UNSOLVABLE) - | Q(issue_status=Feedback.IssueStatus.DELETED) - ) - ) - - # -----------------------------留言公开--------------------------------- - public_feedback = ( - Feedback.objects - .filter(public_status=Feedback.PublicStatus.PUBLIC) - .filter(issue_status=Feedback.IssueStatus.ISSUED) - .order_by("-feedback_time") - ) - - # 我已处理 - # 已公开的或者强制不公开(不予公开)的是已经处理过的 - process_feedback = ( - Feedback.objects - .filter(Q(public_status=Feedback.PublicStatus.PUBLIC) | Q(public_status=Feedback.PublicStatus.FORCE_PRIVATE)) - .filter(issue_status=Feedback.IssueStatus.ISSUED) - .order_by("-feedback_time") - ) - - # -----------------------------反馈草稿--------------------------------- - # 准备需要呈现的内容 - # 这里应该呈现:所有person为自己的feedback - draft_feedback = my_all_feedback.filter(issue_status=Feedback.IssueStatus.DRAFTED) - # -----------------------------老师审核--------------------------------- - - is_teacher = me.is_teacher() if is_person else False - my_wait_public = [] - my_public_feedback = [] - my_process_feedback = [] # 我已处理列表 - wait_public = ( - issued_feedback - .filter(publisher_public=True, org_public=True) - .filter(public_status=Feedback.PublicStatus.PRIVATE) - ) - if is_teacher: - for feedback in wait_public: - can_show = me.incharge.filter(otype_id=feedback.org.otype_id) - if can_show.exists(): - my_wait_public.append(feedback) - - for feedback in public_feedback: - can_show = me.incharge.filter(otype_id=feedback.org.otype_id) - if can_show.exists(): - my_public_feedback.append(feedback) - - # 获取我已处理列表 - for feedback in process_feedback: - can_show = me.incharge.filter(otype_id=feedback.org.otype_id) - if can_show.exists(): - my_process_feedback.append(feedback) - - if request.method == "POST": - option = request.POST.get("option") - if not is_person: - return redirect(message_url(wrong('组织不可以撤回/删除反馈!'), request.path)) - if option not in ["delete", "withdraw"]: - return redirect(message_url(wrong('无效的请求!'), request.path)) - - if option == "delete": - try: - del_feedback = Feedback.objects.get(id=request.POST["id"]) - except Exception: - return redirect(message_url(wrong("不存在这样的反馈!"), request.path)) - # 合法性检查 - if del_feedback.issue_status == Feedback.IssueStatus.ISSUED: - return redirect(message_url(wrong('不能删除已经发布的反馈!'), request.path)) - if del_feedback.issue_status == Feedback.IssueStatus.DELETED: - return redirect(message_url(wrong('这条反馈已经被删除啦!'), request.path)) - - with transaction.atomic(): - del_feedback.issue_status = Feedback.IssueStatus.DELETED - del_feedback.save() - succeed('成功删除反馈草稿!', html_display) - - elif option == "withdraw": - try: - del_feedback = Feedback.objects.get(id=request.POST["id"]) - except Exception: - return redirect(message_url(wrong("不存在这样的反馈!"), request.path)) - # 合法性检查 - if del_feedback.issue_status != Feedback.IssueStatus.ISSUED: - return redirect(message_url(wrong('不能撤回没有发布的反馈!'), request.path)) - if del_feedback.issue_status == Feedback.IssueStatus.DELETED: - return redirect(message_url(wrong('这条反馈已经被撤回啦!'), request.path)) - if del_feedback.solve_status in [ - Feedback.SolveStatus.SOLVED, - Feedback.SolveStatus.UNSOLVABLE, - ]: - return redirect(message_url(wrong('不能撤回已经发布的反馈!'), request.path)) - - with transaction.atomic(): - del_feedback.issue_status = Feedback.IssueStatus.DELETED - del_feedback.save() - succeed('成功撤回反馈!', html_display) - - bar_display = utils.get_sidebar_and_navbar(request.user, navbar_name="反馈中心") - - return render(request, "feedback/welcome.html", locals()) - - -@login_required(redirect_field_name='origin') -@utils.check_user_access(redirect_url="/logout/") -@logger.secure_view() -def modifyFeedback(request: HttpRequest): - ''' - 反馈表单填写、修改与提交的视图函数 - ''' - html_display = {} - me = get_person_or_org(request.user) - - # 设置feedback为None, 如果非None则自动覆盖 - feedback = None - - # 根据是否有newid来判断是否是第一次 - feedback_id = request.GET.get("feedback_id") - - # 获取前端页面中可能存在的提示 - my_messages.transfer_message_context(request.GET, html_display) - - if feedback_id is not None: # 如果存在对应反馈 - try: # 尝试读取已有的Feedback存档 - feedback = Feedback.objects.get(id=feedback_id) - # 接下来检查是否有权限check这个条目,应该是本人/对应组织 - assert (feedback.person == me) or (feedback.org == me) - except: #恶意跳转 - return redirect(message_url(wrong("您没有权限访问该网址!"))) - is_new_feedback = False # 前端使用量, 表示是已有的反馈还是新的 - - else: - # 如果不存在id, 是一个新建反馈页面。 - feedback = None - is_new_feedback = True - - ''' - 至此,如果是新反馈那么feedback为None,否则为对应反馈 - feedback = None只有在个人新建反馈的时候才可能出现,对应为is_new_feedback - 接下来POST - ''' - - if request.method == "POST": - context = update_feedback(feedback, me, request) - - if context["warn_code"] == 2: # 成功修改 - feedback: Feedback = Feedback.objects.get(id=context["feedback_id"]) - is_new_application = False # 状态变更 - unlock_achievement(request.user, "使用一次反馈中心") # 解锁成就-使用一次反馈中心 - # 处理通知相关的操作 - try: - feasible_post = [ - "directly_submit", - "submit_draft", - ] - if request.POST.get('post_type') in feasible_post: - make_relevant_notification(feedback, request.POST, me) - except: - return redirect(message_url( - wrong("返回了未知类型的post_type,请注意检查!"), - request.path)) - - elif context["warn_code"] != 1: # 没有返回操作提示 - return redirect(message_url( - wrong("在处理反馈中出现了未预见状态,请联系管理员处理!"), - request.path)) - - # 准备用户提示量 - my_messages.transfer_message_context(context, html_display) - - # 为了保证稳定性,完成POST操作后同意全体回调函数,进入GET状态 - if feedback is None: - return redirect(message_url(context, '/modifyFeedback/')) - else: - return redirect(message_url(context, feedback.get_absolute_url())) - - # ———————— 完成Post操作, 接下来开始准备前端呈现 ———————— - - # 首先是写死的前端量 - feedback_type_list = { - fbtype.name:{ - 'value' : fbtype.name, - 'display' : fbtype.name, # 前端呈现的使用量 - 'disabled' : False, # 是否禁止选择这个量 - 'selected' : False # 是否默认选中这个量 - } - for fbtype in FeedbackType.objects.all() - } - - org_type_list = { - otype.otype_name:{ - 'value' : otype.otype_name, - 'display' : otype.otype_name, # 前端呈现的使用量 - 'disabled' : False, # 是否禁止选择这个量 - 'selected' : False # 是否默认选中这个量 - } - for otype in OrganizationType.objects.all() - } - - org_list = { - org.oname:{ - 'value' : org.oname, - 'display' : org.oname, # 前端呈现的使用量 - 'disabled' : False, # 是否禁止选择这个量 - 'selected' : False # 是否默认选中这个量 - } - for org in Organization.objects.all() - } - - org_type_list[''] = { - 'value': '', 'display': '', 'disabled': False, 'selected': False, - } - org_list[''] = { - 'value': '', 'display': '', 'disabled': False, 'selected': False, - } - - # 用户写表格? - if (is_new_feedback or (feedback.person == me and feedback.issue_status == Feedback.IssueStatus.DRAFTED)): - allow_form_edit = True - else: - allow_form_edit = False - - # 用于前端展示 - feedback_person = me if is_new_feedback else feedback.person - app_avatar_path = feedback_person.get_user_ava() - all_org_types = [otype.otype_name for otype in OrganizationType.objects.all()] - all_org_list = [] - for otype in all_org_types: - all_org_list.append(([otype,] + - [org.oname for org in Organization.objects.filter( - otype=OrganizationType.objects.get(otype_name=otype) - )]) if otype != '' else ([otype,] + [ - org.oname for org in Organization.objects.all() - ]) - ) - fbtype_to_org = [] #存有多个列表,每个列表为[fbtype, orgtype, org],用于前端变换下拉选项 - for fbtype in feedback_type_list.keys(): - fbtype_obj = FeedbackType.objects.get(name=fbtype) - fbtype_to_org.append([fbtype,] + ([ - fbtype_obj.org_type.otype_name, - ] if fbtype_obj.org_type else ['',]) + ([ - fbtype_obj.org.oname, - ] if fbtype_obj.org else ['',]) - ) - if not is_new_feedback: - feedback_type_list[feedback.type.name]['selected'] = True - if feedback.org_type is not None: - org_type_list[feedback.org_type.otype_name]['selected'] = True - for org in Organization.objects.exclude( - otype=OrganizationType.objects.get( - otype_name=feedback.org_type.otype_name) - ): - org_list[org.oname]['disabled'] = True - else: - org_type_list['']['selected'] = True - for org in org_list.keys(): - org_list[org]['disabled'] = True - if feedback.org is not None: - org_list[feedback.org.oname]['selected'] = True - else: - org_list['']['selected'] = True - else: - # feedback_type默认选中的反馈类型通过GET获取,如未获取到则默认选中第一项。 - try: - feedback_type = request.GET['type'] - FeedbackType.objects.get(name=feedback_type) - except: # 有可能出现需要的反馈类型数据库不存在的情况,此时默认选中第一项 - feedback_type = list(feedback_type_list.keys())[0] - feedback_type_list[feedback_type]['selected'] = True - selected_feedback = FeedbackType.objects.get(name=feedback_type) - - if selected_feedback.org_type is not None: - org_type_list[selected_feedback.org_type.otype_name]['selected'] = True - for org in Organization.objects.exclude( - otype=OrganizationType.objects.get( - otype_name=selected_feedback.org_type.otype_name) - ): - org_list[org.oname]['disabled'] = True - else: - org_type_list['']['selected'] = True - for org in org_list.keys(): - org_list[org]['disabled'] = True - if selected_feedback.org is not None: - org_list[selected_feedback.org.oname]['selected'] = True - else: - org_list['']['selected'] = True - # 从session弹出默认反馈内容 - default_content = request.session.pop('feedback_content', '') - bar_display = utils.get_sidebar_and_navbar( - request.user, navbar_name="填写反馈" if is_new_feedback else "反馈详情" - ) - return render(request, "feedback/modify.html", locals()) diff --git a/questionnaire/__init__.py b/questionnaire/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/questionnaire/admin.py b/questionnaire/admin.py deleted file mode 100644 index 4ae1a278c..000000000 --- a/questionnaire/admin.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.contrib import admin - -from questionnaire.models import * - -admin.site.register(Survey) -admin.site.register(Question) -admin.site.register(Choice) -admin.site.register(AnswerSheet) -admin.site.register(AnswerText) diff --git a/questionnaire/apps.py b/questionnaire/apps.py deleted file mode 100644 index e9902832b..000000000 --- a/questionnaire/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class QuestionnaireConfig(AppConfig): - name = 'questionnaire' - verbose_name = '3. 问卷系统' diff --git a/questionnaire/migrations/0001_initial.py b/questionnaire/migrations/0001_initial.py deleted file mode 100644 index 2b34a2042..000000000 --- a/questionnaire/migrations/0001_initial.py +++ /dev/null @@ -1,96 +0,0 @@ -# Generated by Django 4.2.3 on 2023-10-18 18:25 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='AnswerSheet', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='填写时间')), - ('status', models.SmallIntegerField(choices=[(0, '存为草稿'), (1, '提交')], default=0, verbose_name='状态')), - ('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='答卷人')), - ], - options={ - 'verbose_name': '答卷', - 'verbose_name_plural': '答卷', - }, - ), - migrations.CreateModel( - name='Survey', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=50, verbose_name='标题')), - ('description', models.TextField(blank=True, verbose_name='描述')), - ('status', models.SmallIntegerField(choices=[(0, '审核中'), (1, '发布中'), (2, '已结束'), (3, '草稿')], default=0, verbose_name='状态')), - ('start_time', models.DateTimeField(verbose_name='起始时间')), - ('end_time', models.DateTimeField(verbose_name='截止时间')), - ('time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='创建人')), - ], - options={ - 'verbose_name': '问卷', - 'verbose_name_plural': '问卷', - }, - ), - migrations.CreateModel( - name='Question', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('order', models.IntegerField(verbose_name='序号')), - ('topic', models.CharField(max_length=50, verbose_name='简介')), - ('description', models.TextField(blank=True, verbose_name='题目描述')), - ('type', models.CharField(choices=[('TEXT', '填空题'), ('SINGLE', '单选题'), ('MULTIPLE', '多选题'), ('RANKING', '排序题')], default='SINGLE', max_length=10, verbose_name='类型')), - ('required', models.BooleanField(default=True, verbose_name='必填')), - ('survey', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='questionnaire.survey', verbose_name='所属问卷')), - ], - options={ - 'verbose_name': '题目', - 'verbose_name_plural': '题目', - 'ordering': ['survey', 'order'], - }, - ), - migrations.CreateModel( - name='Choice', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('order', models.IntegerField(verbose_name='序号')), - ('text', models.TextField(verbose_name='内容')), - ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='questionnaire.question', verbose_name='问题')), - ], - options={ - 'verbose_name': '选项', - 'verbose_name_plural': '选项', - 'ordering': ['question', 'order'], - }, - ), - migrations.CreateModel( - name='AnswerText', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('body', models.TextField(verbose_name='内容')), - ('answersheet', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='questionnaire.answersheet', verbose_name='所属答卷')), - ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='questionnaire.question', verbose_name='问题')), - ], - options={ - 'verbose_name': '回答', - 'verbose_name_plural': '回答', - }, - ), - migrations.AddField( - model_name='answersheet', - name='survey', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='questionnaire.survey', verbose_name='对应问卷'), - ), - ] diff --git a/questionnaire/migrations/__init__.py b/questionnaire/migrations/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/questionnaire/models.py b/questionnaire/models.py deleted file mode 100644 index feeb55dfd..000000000 --- a/questionnaire/models.py +++ /dev/null @@ -1,134 +0,0 @@ -from django.db import models - -from generic.models import User - -from utils.models.choice import choice -from utils.models.descriptor import admin_only - - -__all__ = [ - 'Survey', - 'AnswerSheet', - 'Question', - 'Choice', - 'AnswerText', -] - - -class Survey(models.Model): - class Meta: - verbose_name = "问卷" - verbose_name_plural = verbose_name - - class Status(models.IntegerChoices): - REVIEWING = choice(0, "审核中") - PUBLISHED = choice(1, "发布中") - ENDED = choice(2, "已结束") - DRAFT = choice(3, "草稿") - - title = models.CharField("标题", max_length=50, blank=False, null=False) - description = models.TextField("描述", blank=True) - creator = models.ForeignKey( - User, on_delete=models.CASCADE, verbose_name="创建人") - status = models.SmallIntegerField( - "状态", choices=Status.choices, default=Status.REVIEWING) - start_time = models.DateTimeField("起始时间") - end_time = models.DateTimeField("截止时间") - time = models.DateTimeField("创建时间", auto_now_add=True) - - questions: models.manager.BaseManager['Question'] - - @admin_only - def __str__(self): - return self.title - - -class AnswerSheet(models.Model): - class Meta: - verbose_name = "答卷" - verbose_name_plural = verbose_name - - class Status(models.IntegerChoices): - DRAFT = choice(0, "存为草稿") - SUBMITTED = choice(1, "提交") - - survey = models.ForeignKey( - Survey, on_delete=models.CASCADE, verbose_name="对应问卷") - creator = models.ForeignKey( - User, on_delete=models.CASCADE, verbose_name="答卷人") - create_time = models.DateTimeField("填写时间", auto_now_add=True) - status = models.SmallIntegerField( - "状态", choices=Status.choices, default=Status.DRAFT) - - @admin_only - def __str__(self): - # TODO: 关联查询太多时会很慢,主要用于后台显示(如答案和问卷),暂未优化 - return self.survey.title + " - " + self.creator.username + "的答卷" - - -class Question(models.Model): - class Meta: - verbose_name = "题目" - verbose_name_plural = verbose_name - ordering = ["survey", "order"] - - class Type(models.TextChoices): - TEXT = choice("TEXT", "填空题") - SINGLE = choice("SINGLE", "单选题") - MULTIPLE = choice("MULTIPLE", "多选题") - RANKING = choice("RANKING", "排序题") - - @classmethod - def WithChoice(cls) -> list['Question.Type']: - return [cls.SINGLE, cls.MULTIPLE, cls.RANKING] - - survey = models.ForeignKey(Survey, on_delete=models.CASCADE, - related_name="questions", - verbose_name="所属问卷") - order = models.IntegerField("序号") - topic = models.CharField("简介", max_length=50) - description = models.TextField("题目描述", blank=True) - type = models.CharField("类型", max_length=10, - choices=Type.choices, default=Type.SINGLE) - required = models.BooleanField("必填", default=True) - - choices: models.manager.BaseManager['Choice'] - - def have_choice(self): - return self.type in self.Type.WithChoice() - - @admin_only - def __str__(self): - return self.topic - - -class Choice(models.Model): - class Meta: - verbose_name = "选项" - verbose_name_plural = verbose_name - ordering = ["question", "order"] - - question = models.ForeignKey( - Question, on_delete=models.CASCADE, related_name="choices", verbose_name="问题") - order = models.IntegerField("序号") - text = models.TextField("内容") - - @admin_only - def __str__(self): - return self.text - - -class AnswerText(models.Model): - ''' - 回答,按字符串形式储存 - ''' - class Meta: - verbose_name = "回答" - verbose_name_plural = verbose_name - - question = models.ForeignKey( - Question, on_delete=models.CASCADE, verbose_name="问题") - # TODO: 后台显示有潜在的性能问题 - answersheet = models.ForeignKey( - AnswerSheet, on_delete=models.CASCADE, verbose_name="所属答卷") - body = models.TextField("内容") diff --git a/questionnaire/permissions.py b/questionnaire/permissions.py deleted file mode 100644 index ddfff7149..000000000 --- a/questionnaire/permissions.py +++ /dev/null @@ -1,51 +0,0 @@ -from rest_framework import permissions - -__all__ = [ - 'IsTextOwnerOrAsker', - 'IsSheetOwnerOrAsker', - 'IsSurveyOwnerOrReadOnly', - 'IsQuestionOwnerOrReadOnly', - 'IsChoiceOwnerOrReadOnly', -] - - -def check_owner_or_asker(request, owner, asker): - return request.user.is_staff or request.user == owner or request.user == asker - - -class IsTextOwnerOrAsker(permissions.BasePermission): - def has_object_permission(self, request, view, obj): - owner = obj.answersheet.creator - asker = obj.question.survey.creator - return check_owner_or_asker(request, owner, asker) - - -class IsSheetOwnerOrAsker(permissions.BasePermission): - def has_object_permission(self, request, view, obj): - owner = obj.creator - asker = obj.survey.creator - return check_owner_or_asker(request, owner, asker) - - -def check_owner_or_read_only(request, owner): - return (request.user.is_staff - or request.method in permissions.SAFE_METHODS - or request.user == owner) - - -class IsSurveyOwnerOrReadOnly(permissions.BasePermission): - def has_object_permission(self, request, view, obj): - owner = obj.creator - return check_owner_or_read_only(request, owner) - - -class IsQuestionOwnerOrReadOnly(permissions.BasePermission): - def has_object_permission(self, request, view, obj): - owner = obj.survey.creator - return check_owner_or_read_only(request, owner) - - -class IsChoiceOwnerOrReadOnly(permissions.BasePermission): - def has_object_permission(self, request, view, obj): - owner = obj.question.survey.creator - return check_owner_or_read_only(request, owner) diff --git a/questionnaire/serializers.py b/questionnaire/serializers.py deleted file mode 100644 index 4a9831ca9..000000000 --- a/questionnaire/serializers.py +++ /dev/null @@ -1,55 +0,0 @@ -from rest_framework import serializers - -from questionnaire.models import Survey, Question, Choice, AnswerText, AnswerSheet - -__all__ = [ - 'ChoiceSerializer', - 'QuestionSerializer', - 'SurveySerializer', - 'AnswerSheetSerializer', - 'AnswerTextSerializer', -] - - -class ChoiceSerializer(serializers.ModelSerializer): - class Meta: - model = Choice - fields = '__all__' - - -class QuestionSerializer(serializers.ModelSerializer): - class Meta: - model = Question - fields = '__all__' - - -class SurveySerializer(serializers.ModelSerializer): - creator = serializers.HiddenField(default=serializers.CurrentUserDefault()) - - class Meta: - model = Survey - fields = '__all__' - - def validate(self, attrs): - if attrs['start_time'] >= attrs['end_time']: - raise serializers.ValidationError("起始时间不得晚于终止时间!") - return attrs - - -class AnswerSheetSerializer(serializers.ModelSerializer): - creator = serializers.HiddenField(default=serializers.CurrentUserDefault()) - - class Meta: - model = AnswerSheet - fields = '__all__' - - -class AnswerTextSerializer(serializers.ModelSerializer): - class Meta: - model = AnswerText - fields = '__all__' - - def validate(self, attrs): - if attrs['question'].survey != attrs['answersheet'].survey: - raise serializers.ValidationError("问题与答卷不属于同一问卷!") - return attrs diff --git a/questionnaire/tests.py b/questionnaire/tests.py deleted file mode 100644 index 7ce503c2d..000000000 --- a/questionnaire/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/questionnaire/urls.py b/questionnaire/urls.py deleted file mode 100644 index 7b2e1d741..000000000 --- a/questionnaire/urls.py +++ /dev/null @@ -1,18 +0,0 @@ -from django.urls import path, include -from rest_framework.routers import DefaultRouter - -from questionnaire.views import ( - QuestionViewSet, ChoiceViewSet, SurveyViewSet, - AnswerSheetViewSet, AnswerTextViewSet -) - -router = DefaultRouter() -router.register('survey', SurveyViewSet, basename="survey") -router.register('answersheet', AnswerSheetViewSet, basename="answersheet") -router.register('answertext', AnswerTextViewSet, basename="answertext") -router.register('question', QuestionViewSet, basename="question") -router.register('choice', ChoiceViewSet, basename="choice") - -urlpatterns = [ - path('', include(router.urls)), -] diff --git a/questionnaire/views.py b/questionnaire/views.py deleted file mode 100644 index b5b2d666c..000000000 --- a/questionnaire/views.py +++ /dev/null @@ -1,170 +0,0 @@ -from django.db.models import Q -from rest_framework import viewsets -from rest_framework.authentication import SessionAuthentication -from rest_framework.permissions import IsAuthenticated -from rest_framework.decorators import action -from rest_framework.response import Response - -from questionnaire.models import * -from questionnaire.serializers import * -from questionnaire.permissions import * - - -# 用viewsets -class SurveyViewSet(viewsets.ModelViewSet): - authentication_classes = [SessionAuthentication] - permission_classes = [IsAuthenticated, IsSurveyOwnerOrReadOnly] - serializer_class = SurveySerializer - - def get_queryset(self): - if self.request.user.is_staff: - return Survey.objects.all() - else: # 根据发布状态和发布时间来筛选 - return Survey.objects.filter(Q(status=Survey.Status.PUBLISHED) | Q(creator=self.request.user)) - - -class QuestionViewSet(viewsets.ModelViewSet): - authentication_classes = [SessionAuthentication] - permission_classes = [IsAuthenticated, IsQuestionOwnerOrReadOnly] - serializer_class = QuestionSerializer - - def get_queryset(self): - if self.request.user.is_staff: - return Question.objects.all() - else: - return Question.objects.filter(Q(survey__status=Survey.Status.PUBLISHED) | Q(survey__creator=self.request.user)) - - # 只有问卷创始人能创建问题 - def perform_create(self, serializer: QuestionSerializer): - survey = serializer.validated_data['survey'] - if survey.creator == self.request.user: - serializer.save() - else: - raise PermissionError("只有问卷创始人能添加问题!") - - def perform_update(self, serializer: QuestionSerializer): - survey = serializer.instance.survey - if survey != serializer.validated_data['survey']: - raise PermissionError("禁止修改问题所属问卷!请通过删除后新建完成操作。") - serializer.save() - - -class ChoiceViewSet(viewsets.ModelViewSet): - authentication_classes = [SessionAuthentication] - permission_classes = [IsAuthenticated, IsChoiceOwnerOrReadOnly] - serializer_class = ChoiceSerializer - - def get_queryset(self): - if self.request.user.is_staff: - return Choice.objects.all() - else: # TODO:当数据量大的时候会很慢,考虑优化或者直接删除 - return Choice.objects.filter(Q(question__survey__status=Survey.Status.PUBLISHED) | Q(question__survey__creator=self.request.user)) - - # 只有问卷创始人能创建选项,而且只有选择题才能创建选项 - def perform_create(self, serializer: ChoiceSerializer): - question: Question = serializer.validated_data['question'] - if not question.have_choice(): - raise TypeError("当前问题不能设置选项!") - elif question.survey.creator != self.request.user: - raise PermissionError("只有问卷创始人能添加选项!") - else: - serializer.save() - - def perform_update(self, serializer: ChoiceSerializer): - question = serializer.instance.question - if question != serializer.validated_data['question']: - raise PermissionError("禁止修改选项所属问题!请通过删除后新建完成操作。") - serializer.save() - - -class AnswerTextViewSet(viewsets.ModelViewSet): - queryset = AnswerText.objects.all() - authentication_classes = [SessionAuthentication] - permission_classes = [IsAuthenticated, IsTextOwnerOrAsker] - serializer_class = AnswerTextSerializer - - # debug时可以注释掉 - def list(self, request, *args, **kwargs): - raise PermissionError("禁止直接查看所有答案!") - - def perform_create(self, serializer: AnswerTextSerializer): - answersheet = serializer.validated_data['answersheet'] - question = serializer.validated_data['question'] - if answersheet.creator != self.request.user: - raise PermissionError("只有答卷创始人才能添加答案!") - elif AnswerText.objects.filter(answersheet=answersheet, question=question).exists(): - raise PermissionError("禁止重复提交答案!") - elif answersheet.survey.status != Survey.Status.PUBLISHED: - raise PermissionError("只能创建已发布问卷的答案!") - else: - serializer.save() - - def perform_update(self, serializer: AnswerTextSerializer): - answersheet = serializer.instance.answersheet - question = serializer.instance.question - if answersheet.status == AnswerSheet.Status.DRAFT: - if answersheet.creator != self.request.user: - raise PermissionError("只有答卷创始人才能修改答案!") - if answersheet != serializer.validated_data['answersheet']: - raise PermissionError("禁止修改答案所属答卷!") - if question != serializer.validated_data['question']: - raise PermissionError("禁止修改答案所属问题!") - serializer.save() - else: - raise PermissionError("禁止修改答案!") - - @action(detail=False, methods=['GET']) - def answer_owner(self, request): - text = AnswerText.objects.filter(answersheet__creator=request.user) - serializer = AnswerTextSerializer(text, many=True) - return Response(serializer.data) - - @action(detail=False, methods=['GET']) - def survey_owner(self, request): - text = AnswerText.objects.filter( - question__survey__creator=request.user) - serializer = AnswerTextSerializer(text, many=True) - return Response(serializer.data) - - -class AnswerSheetViewSet(viewsets.ModelViewSet): - queryset = AnswerSheet.objects.all() - authentication_classes = [SessionAuthentication] - permission_classes = [IsAuthenticated, IsSheetOwnerOrAsker] - serializer_class = AnswerSheetSerializer - - # debug时可以注释掉 - def list(self, request, *args, **kwargs): - raise PermissionError("禁止直接查看所有答卷!") - - def perform_create(self, serializer: AnswerSheetSerializer): - creator = serializer.validated_data['creator'] - survey = serializer.validated_data['survey'] - if survey.status != Survey.Status.PUBLISHED: # 问卷必须处于发布状态才能创建答卷 - raise PermissionError("只能创建已发布问卷的答案!") - elif AnswerSheet.objects.filter(creator=creator, survey=survey).exists(): - raise PermissionError("禁止重复创建答卷!") - else: - serializer.save() - - def perform_update(self, serializer: AnswerSheetSerializer): - sheet_status = serializer.instance.status - if sheet_status == AnswerSheet.Status.DRAFT: - survey = serializer.instance.survey - if survey != serializer.validated_data['survey']: - raise PermissionError("禁止修改答卷所属问卷!") - serializer.save() # 此部分中只能修改提交状态 - else: - raise PermissionError("禁止修改答卷!") - - @action(detail=False, methods=['GET']) - def answer_owner(self, request): - sheet = AnswerSheet.objects.filter(creator=request.user) - serializer = AnswerSheetSerializer(sheet, many=True) - return Response(serializer.data) - - @action(detail=False, methods=['GET']) - def survey_owner(self, request): - sheet = AnswerSheet.objects.filter(survey__creator=request.user) - serializer = AnswerSheetSerializer(sheet, many=True) - return Response(serializer.data) diff --git a/templates/Appointment/admin-credit.html b/templates/Appointment/admin-credit.html index 1db4493b2..b4e7bcb6c 100644 --- a/templates/Appointment/admin-credit.html +++ b/templates/Appointment/admin-credit.html @@ -204,14 +204,14 @@
违约原因
-
+ diff --git a/templates/Appointment/admin-index.html b/templates/Appointment/admin-index.html index f0904ebe3..efde17f3f 100644 --- a/templates/Appointment/admin-index.html +++ b/templates/Appointment/admin-index.html @@ -435,7 +435,7 @@
-
+
{% endfor %} diff --git a/templates/Appointment/summary.html b/templates/Appointment/summary.html deleted file mode 100755 index ecfecc025..000000000 --- a/templates/Appointment/summary.html +++ /dev/null @@ -1,569 +0,0 @@ -{% load static %} - - - - - - - 我的地下室年度回忆 - - - - - - - - - - - - - - - - - - -
- - - - - - -
-
-
-
-

-

-
-
Hi ,{{ Sname }}
- - 立即
查看
-
-
- - - - - -
-
-
-

这一年里

-

地下室预约系统一共发起了

-

{{all_appoint_num}}条预约

-

总预约时长达到{{all_appoint_len}}小时

-

相当于{{all_appoint_len_day}}个整天

-
-
- - - - - -
-
-
-
-
- - - - -
-
- - -
- -
-
-
-
- {% if appoint_make_num %} -

其中,你一共发起了{{appoint_make_num}}条预约

- -

在所有使用者中排前{{appoint_make_num_pct}}%

-

总预约时长为{{appoint_make_hour}}小时

-

在所有使用者中排前{{appoint_make_hour_pct}}%

- {% else %} -

其中,你一共参加了{{appoint_attend_num}}条预约

- - -

参加的预约总时长为{{appoint_attend_hour}}小时

- - {% endif %} -
-
- - - - - - -
-
- - - -
-

这一年

-

{{hottest_room_1.0}} {{hottest_room_1.1}}

-

{{hottest_room_1.2}}次的预约数

-

成为最抢手的功能房

-

{{hottest_room_2.0}} {{hottest_room_2.1}}{{hottest_room_3.0}} {{hottest_room_3.1}}

-

分别以{{hottest_room_2.2}}次{{hottest_room_3.2}}次的预约数

-

成为第二、第三热门的功能房

-
-
- - - - {% if Sfav_room_id %} - - -
-
-
-
- -
- - -
-
-

在这一年里

-

你最喜欢使用的房间是{{Sfav_room_id}} {{Sfav_room_name}}

-

一共使用了{{Sfav_room_freq}}

-

不知道在这个房间里

-

有没有留下属于你的独家记忆

-
-
- - {% endif %} - - - -
-
-
-
-
-
-
-
-
-
-
-
- - {% if appoint_make_num %} -

你最常在{{Smake_time_most}}:00-{{Smake_time_most|add:1}}:00

-

发起预约

-

而在{{Suse_time_most}}:00-{{Suse_time_most|add:1}}:00

-

使用房间最为频繁

- {% elif appoint_attend_num %} -

你在{{Suse_time_most}}:00-{{Suse_time_most|add:1}}:00

-

使用房间最为频繁

- {% endif %} -

在忙碌的同时

-

不要忘了好好休息

-
-
- - -
- - - - {% if appoint_make_num %} - -
-
-
-
-
-
- -
- -
-

{{Sfirst_appoint.0}}{{Sfirst_appoint.1}}{{Sfirst_appoint.2}}日 -

-

因为{{Sfirst_appoint.3}}

-

你在{{Sfirst_appoint.4}} {{Sfirst_appoint.5}}

-

发起了自己的第一条预约

-

与新地下室的初见

-

有没有带给你惊喜呢

-
-
- - {% endif %} - - - - {% if Skeywords_len %} - -
-
-
-
-
-
-
-
这些
-

是你本学年的预约关键词

- -
-
-
-
-
-
-
-
-
    -
  • {{Skeywords.0}}
  • -
  • {{Skeywords.1}}
  • -
  • {{Skeywords.2}}
  • -
  • {{Skeywords.3}}
  • -
  • {{Skeywords.4}}
  • -
  • {{Skeywords.5}}
  • -
  • {{Skeywords.6}}
  • -
  • {{Skeywords.7}}
  • -
  • {{Skeywords.8}}
  • -
  • {{Skeywords.9}}
  • -
-
- - {% endif %} - - - {% if Sfriend %} - -
-
-
-
-
-
-
-
-
-
-
-
-
-

这一年

-

最常与你一起预约的小伙伴是

-

{{Sfriend.0}} {{Sfriend.1}} {{Sfriend.2}}

-

化孤独为共同

-

我们践行着我们的初心

-
-
- - {% endif %} - - - - {% if aygj %} - - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-

你曾经在2:00-5:00发起预约

-

最晚的一次

-

是在{{aygj.0}}{{aygj.1}}{{aygj.2}}日的{{aygj.3}}

-

解锁成就熬夜冠军

-

本年度有{{aygj_num}}人获此殊荣

-
-
- -
- {% endif %} - - - - - - {% if zqgj %} - - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-

你曾经在6:00-8:00间进行预约

-

最早的一次发生在{{zqgj.0}}{{zqgj.1}}{{zqgj.2}}{{zqgj.3}}

-

{{zqgj.5}} {{zqgj.6}}{{zqgj.4}}

-

解锁成就早起冠军

-

{{zqgj_num}}人获此殊荣

-
-
- -
- {% endif %} - - - {% if wycm %} - - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-

在你发起的所有预约中

-

有超过5条提前一周以上预约

-

解锁成就未雨绸缪

-

本年度仅有{{wycm_num}}人获此殊荣

-
-
- -
- {% endif %} - - {% if jxcz %} - - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-

你曾在开始时间前2分钟内才进行预约

-

解锁成就极限操作

-

其中,{{jxcz.0}}{{jxcz.1}}{{jxcz.2}}

-

你在{{jxcz.3}}预约了{{jxcz.4}}{{jxcz.5}} {{jxcz.6}}

-

{{jxcz_num}}人获此殊荣

-
-
- -
- {% endif %} - - - {% if ypgw %} - - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-

本学年

-

你因为迟到而违规的次数达到{{ypgw}}

-

解锁成就元培鸽王

-

本年度仅有{{ypgw_num}}人获此殊荣

-

分享到朋友圈,寻找身边志同道合的鸽子

-
-
-
欸,有彩蛋吗?滑动看下~
-
- {% endif %} - - - - - - - -
-
- - - -
-

邀 请 函

-

 

-

亲爱的{{Sname}}:

-

       你好!

-

       2021年秋季,Yuanpei Profile(元培智慧校园系统)即将上线,在这座“看不见的城市”里,你会解锁以下角落:

- -

       元培门户:生活展示窗口

-

       成长档案:一串成长足迹

-

       订阅服务:消息提醒助手

-

       事务平台:事务一键解决

-

       通知信箱:你的事务管家

-

       元气积分:意愿点与动力

- -

       在这里,你能够享受活动举办的全线上操作服务,代码替你奔跑,只需动动手指,你就能获知学院所有的活动消息。

-

       在这里,你可以有多种存在方式:安利音乐、设置个性签名、保存成长节点,你表达,故你在。

-

       这里还会有许多有意思的故事,详情可关注后续推文,也欢迎你在以后的使用中自己不断探索体验。

-

       Yuanpei Profile——共同生活的线上家园,期待并欢迎你的入住!

- -

 

-

智慧校园项目组

-
-
-
- -
-

制作|wxy pht

-

文案|wxy zcy lzq

-

感谢JohnieXu在Github的开源项目

- -
-
- -
- - - - - - - - - - - \ No newline at end of file diff --git a/templates/Appointment/summary2021.html b/templates/Appointment/summary2021.html deleted file mode 100644 index 55fe48370..000000000 --- a/templates/Appointment/summary2021.html +++ /dev/null @@ -1,696 +0,0 @@ -{% load static %} - - - - - - - 我的地下室年度回忆 - - - - - - - - - - - - - - - - - - - - -
- - -
- -
- - - -
-
-
-
-
-
-
-
-

(点击右上角打开音乐体验更佳)

-
-
- - - - - -
-
-
-

好,{{ Sname }}

-

这是我们的初遇

-

还是重逢呢

-
-
-

那么

-

迈下阶梯

-

去看看你与地下空间

-

在过去一年的共同记忆吧

-
-
- - - -
-
-
-

是否曾与我对视呢?

-
-
-

你好,

-

我是地下室入口的那面凸面镜~

-

-

我记得我们的每一次相遇,

-

也记得你的每一次来去匆匆的模样。

-
-
- - - {% if study_room_num > 0 %} -
-
-
-

咔哒,咔哒

-

这一年中

-

自习室的门扉同你问过{{study_room_num}}次好

-

也记得你这一年在自习室中有{{study_room_day}}天的共同时光

-

其中

-

{{study_room_top}}与你共同见证{{study_room_top_day}}天的时间滴答

-

你知道,在地下室的某一角

-

多少属于你的忙碌沉淀于此

-

静静生长

-
-
- {% endif %} - - - {% if early_day_num > 0 %} -
-
-
-

{{early_room_date}}

-

告别眷恋的床

-

和初生的朝阳问好

-

你早早开启了美好的一天

-

你在{{early_room_time}}来到了{{early_room}}{{early_room_type}}

-

这一年中有{{early_day_num}}

-

你在8:00前的清晨便来到了地下空间

-

早安,愿你在每一个自律的清晨元气满满

-
-
- {% endif %} - - - - {% if late_room_num > 0 %} -
-
-
-

{{late_room_date}}

-

{{late_room}}陪伴你直到{{late_room_time}}

-

那一天,有{{late_room_people}}人也在地下室的另一角研磨属于自己的夜

-

你们是否在地下空间相遇

-

共同见证了地球上最后的夜晚呢

-

也许你甚至能够见到新一天的朝阳

-

时针走过23点

-

像这样在地下空间等待午夜的钟声敲响之时,你一共经历了{{late_room_num}}

-

无论如何,记得好好休息

-

梦中的星空总会为你留出最佳观赏位置

-
-
- {% endif %} - - - -
-
-
-

Hi~你好!

-

我是新入住这里的咖啡角~

-

我想我已经记住你最喜欢的口味啦!

-

哦等等,我这里也有一些记录,

-

希望能唤醒你的一些记忆!

-
-
- - {% if discuss_appoint_num > 0 %} -
-
-
-

过去的一年中,

-
-
-
-

你总共预约了{{discuss_appoint_num}}次研讨室

-

总预约时长为{{discuss_appoint_hour}}小时

-

超过了{{discuss_appoint_pct}}%的同学

-

其中,你在{{discuss_appoint_long_room}}

-

共经历了{{discuss_appoint_long_hour}}小时的激烈讨论

-
-
-

那些流转的时间

-

到底见证了多少灵感的碰撞呢?

-
-
- {% endif %} - - - {% if appoint_num >= 3 %} -
-
-
-

一年,你的年度预约关键词为

-
-
-
- {{Skeywords.0.0}}  {{Skeywords.0.1}}次 -
-
-
- {{Skeywords.1.0}}  {{Skeywords.1.1}}次 -
-
-
- {{Skeywords.2.0}}  {{Skeywords.2.1}}次 -
-
-

欢乐、疲惫、热爱、专注…

-

无论是学习工作,

-

还是轻松的欢聚...

-

这里都由你记录,由你定义。

-
-
- {% endif %} - - - {% if appoint_num >= 3 %} -
-
-
-

记得{{appiont_most_day}}吗?

-

这一天中,你共有{{appoint_most_num}}条预约

-

那一天,整个地下空间都记得你忙碌奔走的模样吧。

-

你留下了什么特别的记忆呢?

-
-
- {% endif %} - - - -
-
-
-

(不知是哪里传来了窸窸窣窣的声音...)

-
-
-

要下楼吗?

-

我们是住在绿植墙上的每一抹绿色!

-

从每一扇窗户中安静地注视着,

-

每一个特别的灵魂、

-

每一种特别的生活...

-

你也想跳舞吗?

-
-
- -
-
-
-

过去的一年中,

-
-
-
-

你预约了{{func_appoint_num}}次功能室

-

总预约时长为{{func_appoint_hour}}小时

-

超过了{{func_appoint_pct}}%的元培er

- {% if appoint_num > 0 %} -

其中,你最常去{{func_appoint_most}}

- {% endif %} -
-

你一定有一份特别的热爱!

- -
- - - {% if co_appoint_num > 3%} -
-
-
-

茫茫世界中,

-

在碌碌时光中,

-

相遇、相知、相陪,也许是最大的幸运。

-

我们在这片海洋中

-

为你找到了另一片共鸣的浪花:

-

{{co_mate}}

-
-
- -
-
-
-

去的一年

-

你们在地下空间共同经历了{{co_appoint_hour}}小时的时光

-

预约次数超{{co_appoint_num}}

-

你们的常用预约关键词为

-

{{co_keyword.0}}

-

在这一年,你们是

-

{{co_title}}

-

你们的匹配度超过了{{co_pct}}%的同学!

-

致陪伴!致共同!

-
-
- {% endif %} - - - {% if sharp_appoint_num > 0%} -
-
-
-

的极限是什么?

-

至少在地下空间

-

你总共极限预约{{sharp_appoint_num}}

-

其中,在{{sharp_appoint_day}},你为{{sharp_appoint_reason}}

-

在预约时间前{{sharp_appoint_min}}成功预约了{{sharp_appoint_room}}

-

恭喜你在人生的二十岁实现了光速入住自由!

-

而在过去的一年

-

你留下了{{disobey_num}}条违规记录

- {% if disobey_num > 0%} -

嗯...怎么不算在违规的极限边缘反复试探呢?

-

温馨提示:要好好遵守地下空间使用规则哦~

- {% else %} -

恭喜你达成了“地下空间极限守序记录保持者”成就!

-

接下来也要继续保持呀~

- {% endif %} -
-
- {% endif %} - - - -
-
-
-

们的地下空间溯回旅行即将到达终点站

-

这一年中

-

你在地下空间总使用时长为{{appoint_hour}}小时,

-

总使用次数为{{appoint_num}}

-

当然,

-

也许你更偏好在公共讨论区,

-

在书房,在绿植墙下....

-

在地下空间的每一个角落、每一种生活中,

-

拉近灵魂与灵魂之间的距离,

-

或是消磨属于自己的时光。

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

溯完有关地下物理空间的记忆

-

让我们再去YPers共同的线上家园看看吧

-
-
- -
-
-
-

一年里

-

YPers一共创建起

-

{{total_club_num}}个学生小组与{{total_courseorg_num}}个书院课程小组

-

总计发起了{{total_act_num}}次活动

-

总时长为{{total_act_hour}}个小时

-
-
-

“嘤其鸣矣,求其友声”

-

大家因相似的兴趣爱好聚集在一起

-

交流、陪伴、成长……

-

化孤独为共同

-
-
- -
-
-
-

一年中:

-
-
-
- {% if IScreate %} -

你创建了{{myclub_name}}

- {% endif %} -

参加了{{club_num}}个学生小组与

-

{{course_org_num}}个书院课程小组

-

参与了{{act_num}}个由小组发起的活动

-
-
-

新的学期

-

也请多多参与线上家园的营建哦~

-
-
- - - {% if act_num == 0 %} -
-
-
-

恭喜你解锁成就:

-

“潜水冠军”

-
-
-
-
-

评选标准

-

未参加过小组发起的活动

-

to社恐:减少焦虑,重在参与

-

to卷王:停止内卷,速来玩耍

-
-
- {% endif %} - - - {% if club_num >= 2 or act_num >= 5 %} -
-
-
-

恭喜你解锁成就:

-

“社牛”

-
-
-
-
-

评选标准

-

参加小组数>=2个

-

或参与活动数>=5次

-

心有多大,舞台就有多大

-
-
- {% endif %} - - - {% if position_num >= 5 %} -
-
-
-

恭喜你解锁成就:

-

“我全都要”

-
-
-
-
-

评选标准

-

参与小组数>=5个

-

……小孩子才做选择题!

-
-
- {% endif %} - -
-
-
-

丰富的线上社区之外

-

多元的书院课程

-

同样濡染着YPers的共同生活

-
-
-

在这一年里,学院共开设了{{total_course_num}}门书院课

-

总计开展课程活动{{total_course_act_num}}

-

总时长为{{total_course_act_hour}}个小时

-

{{have_course_num}}位同学至少选修了一门书院课

-

{{have_three_course_num}}位同学选修了三门及以上的书院课

-
-
- - {% if course_num > 0 %} -
-
-
-
-

这一年里

-

你选修了{{course_num}}门课程

-

共计{{course_hour}}个学时

-

分布在{{course_type}}

-
- {% if course_most_hour > 0 and course_most_num > 0%} -
-
-

其中,你在{{course_most_time_name}}投入的时间最长,共计{{course_most_hour}}个学时

-

参与{{course_most_num_name}}的次数最多,共计{{course_most_num}}

-
- {% endif %} -
- {% endif %} - - {% if max_type_info.1 >= 2 %} -
-
-
-

恭喜你解锁成就:

-

“垂直深耕”

-
-
-
-
-

评选标准

-

选修同类(德智体美劳)

-

书院课程>=2门

-

砥砺深耕,笃行致远

-

“一万小时定律”在向你招手!

-
-
- {% endif %} - - {% if type_count >= 3 %} -
-
-
-

恭喜你解锁成就:

-

“通识先锋”

-
-
-
-
-

评选标准

-

选修的书院课程

-

类别(德智体美劳)>=3类

-

通识教育的先锋

-

我辈学子的楷模

-
-
- {% endif %} - -
-
-
-

忆起上个学期初紧张刺激的选课战况

-

你依然记忆犹新……

-
-
-
-

你或许是非酋体质,选的全掉

-

又或许是欧皇再世,选课都中

-
-
-
-

不论如何,都衷心祝愿你

-

新学期顺利

-
-
-
- -
-
-
-

们的旅途还未结束

-

也许未来

-

还有满载美好的相遇

-

还有令人欣喜的忙碌

-

还有无限的灵感与热爱

-

也许元培地下早已成为你生活的一部分

-

如果,你仍然将在这里度过未来的几年

-

那么请在这里

-

继续书写你的故事吧

- {% comment %}

那么,从这里

-

开启新的旅程吧

{% endcomment %} -

又如果,你已经从元培毕业

-

走上了新的人生道路

-

也欢迎常回来看看

-
-
- - -
-
-
- {% if logged_in %} -

来加入我们一起建设更好的元培!

-

欢迎扫码咨询智慧校园招新

-
-
- {% else %} -

元培智慧书院系统致力为高校院系提供综合化、信息化、智能化的管理成长平台

-

目前已在元培学院有了长足的积累与发展

-

如果您对智慧书院有兴趣

-

欢迎扫码进一步联系

- -
-
- {% endif %} - -
-

文案|智慧书院产品组

-

图片|元培学院设计组

-

制作|智慧书院开发组

-

感谢JohnieXu在Github的开源项目

-
- - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/templates/Appointment/summary2023.html b/templates/Appointment/summary2023.html deleted file mode 100644 index d984baaaa..000000000 --- a/templates/Appointment/summary2023.html +++ /dev/null @@ -1,656 +0,0 @@ -{% load static %} - - - - - - - 35楼来信:这一年的我和你 - - - - - - - - - - - - - - - - - - - - -
- - -
- -
- - - -
-
-
-
-
@ {{ home_Sname }}
- - -
-
-
-

智慧书院2023年度回忆授权协议

-

感谢您阅读《智慧书院2023年度回忆授权协议》!在正式使用智慧书院2023年度回忆功能之前,您应仔细阅读并充分理解本协议中的全部内容,并通过本页面点击确认的方式同意使用该功能,如您不同意本协议中的任何条款,请勿点击确认授权。您使用智慧书院2023年度回忆功能的行为将被视为已经仔细阅读、充分理解并毫无保留地接受本协议所有条款。

-
    -
  1. 1. 为了生成您的2023年度回忆,您同意授权智慧校园项目组查询您的姓名、年级等个人基本信息以及在2023自然年度(2023.2.1-2024.1.15)的智慧书院使用数据(包括系统登录记录、地下室使用记录、加入小组记录、书院课程参与记录、活动参与记录、元气值使用记录、学术地图使用记录等)并进行汇总统计分析,以用于年度回忆页面向您进行个性化专属展示。
  2. -
  3. 2. 您的2023年度回忆为根据算法自动生成,可能与实际情况存在偏差,敬请理解。
  4. -
  5. 3. 请您确认,您的2023年度回忆包含您的个人信息,您有权自行处理您的个人信息,包括但不限于使用、保存及对外共享给他人等行为,您将承担由您的操作或行为导致的个人信息安全风险。
  6. -
  7. 4. 除以上声明的目的外,智慧校园项目组不会对本次查询的信息以及产生的分析结果作其他任何处理。
  8. -
  9. 5. 如您对本次2023年度回忆有任何疑问,您可通过YPPF反馈中心联系智慧校园项目组。
  10. -
- -
-
- - -
- - -
-
-
-

好,{{ Sname }}

- -

我是燕园35号楼

-

与你相识已有 {{ days_passed }}

-

很高兴认识你!

- -

不知不觉,到了该说新年快乐的时候

-

不过在此之前

-

我想给你看一些东西

-

来吧,与我一同走进我所见证的这一年

-
- - - -
-
-
-

一年人来人往

-

地下室的各个房间

-

迎来了 {{ total_swipe_num }} 次推门而入

-

有多少是远去的熟面孔

-

又有多少新朋友在这里渐渐相识?

- -

听,研讨室又传来愉悦的讨论声

-

{{ total_talk_room_num }} 次预约,{{ total_discuss_appoint_hour }} 小时

-

思维的火花中,诞生了多少奇妙的灵感

- -

最受欢迎的研讨室似乎是 {{ total_talk_appoint_most_name }}

-

被预约 {{ total_talk_appoint_most_num }}

-

你也喜欢在这里讲述想法吗?

-
-
- - -
-
-
-

进B2

-

西南角有时传来琴声

-

有时也可以分辨出跑步机上节奏清晰的脚踏声

-

台球室的碰击声传不到安静的地下影院

-

对面舞蹈室却朗灯明镜

-

你也曾在这里起舞吗?

- -

这一年

-

功能室共被预约 {{ total_func_room_num }} 次,{{ total_func_appoint_hour }} 小时

-

其中被预约最多的是 {{ total_func_appoint_most_name }},共 {{ total_func_appoint_most_num }}

-

或许这里是大家最爱的灵魂徜徉地

-
-
- - -
-
-
-

问我可曾记录过你吗?

-

当然啦,让我想想···

- -

今年你曾 {{ study_room_num }} 次走进自习室

-

超越 {{ study_room_num_rank }} %的同学

-

祝愿你的努力都有好的回报!

- - {% if study_room_num > 0 %} -

其中你最常去的自习室是 {{ study_room_top }},去过 {{ study_room_top_day }}

-

或许这个房间对你有种神秘的吸引力

-

那里有你偏爱的座位吗?

- {% endif %} -
-
- - -
-
-
-

一年

-

你预约了 {{ Discuss_appoint_num }} 次研讨室,共 {{ Discuss_appoint_hour }} 小时

- {% if Discuss_appoint_num > 0 %} -

最常用的预约关键词是“ {{ Discuss_appoint_year_keyword|default:'' }}

- {% endif %} -

还记得你和同伴的每一次讨论吗?

-

那一定是特别的

- - {% if Discuss_appoint_num > 0 %} -

我想你最熟悉的研讨室是 {{ Discuss_appoint_most_room }}

-

你在这里预约了 {{ Discuss_appoint_most_room_num }} 次讨论

-

这个房间应该也记住你啦!

- -

{{ Discuss_appoint_longest_day }} 这一天

-

你预约了长达 {{ Discuss_appoint_longest_day_hours }} 小时

-

使用的关键词是“ {{ Discuss_appoint_longest_day_keyword }}

-

这天你收获了多少有趣的想法和观点呢?

- {% endif %} -
-
- - -
-
-
-

在这一年中

-

预约了 {{ Function_appoint_num }} 次功能室,共 {{ Function_appoint_hour }} 小时

-

或许这里让你感受到热爱的自由

- {% if Function_appoint_num > 0 %} -

{{ Function_appoint_year_keyword|default:'' }} ”是你最常使用的预约关键词

-

很有你的风格!

- -

我好像经常在 {{ Function_appoint_most_room }} 看到你?

-

这个房间你预约了 {{ Function_appoint_most_room_num }}

-

它说很高兴陪你一起坚持

- -

{{ Function_appoint_longest_day }},你预约了 {{ Function_appoint_longest_day_hours }} 小时

-

预约理由是“ {{ Discuss_appoint_longest_day_keyword }}

-

沉浸式的体验一定很特别!

- {% endif %} -
-
- -
-
-
-

曾在开始前30分钟才匆忙预约房间吗?

-

这一年你极限预约了 {{ sharp_appoint_num }}

- {% if sharp_appoint_num > 0 %} -

元培有自己的急急国王!

- {% else %} -

主打一个稳如泰山!

- {% endif %} - -

Warning!

-

突击检查一下你有多少违规记录!

-

{{ disobey_num }} 次违规,扣除了 {{ disobey_num }} 信用分

- {% if disobey_num == 0 %} -

太棒了,我愿称你为“地下室良好室民”

-

新的一年也要继续保持呀~

- {% else %} -

删了吧,我有一个朋友……

-

答应我

-

新的一年要好好遵守地下室使用规则哦!

- {% endif %} -
-
- - - {% if co_appoint_num > 0 %} -
-
-
-

独似乎是大学的常态

-

不过,有一个同学好像经常和你一起预约

-

{{ co_mate }},是你的“地下室搭子”吗?

- -

这一年你们一起预约过 {{ co_appoint_num }}

-

使用最多的理由是“ {{ co_keyword }}

-

你们在地下室共处的时间超过 {{ co_appoint_hour }} 小时

-

恭喜你们获得系统认证的“ {{ co_title }} ”称号!

- -

在流动不居的时光中

-

多么幸运,能够遇见同频共振的人

-

愿你爱孤独,也爱共同

-
-
- {% endif %} - - -
-
-
-

记得曾经和伙伴们桌游厮杀的场景吗

-

又或者是在书院课上“偷得浮生半日闲”?

-

每一个你在35楼生活的一点一滴

-

都构筑成一个崭新的赛博家园

- -

你好呀,我是智慧书院!

-
-
- -
-
-
-

许你还不知道

-

看似平静无奇的35楼里

-

有过多少相见恨晚、狂歌竞夜

-

过去的一年里

-

智慧校园注册小组数量已经达到 {{ total_org_num }}

-

其中是否有你的热爱和珍藏?

- -

这一年

-

学生小组累计发起 {{ total_act_num }} 次活动

-

累计活动时长 {{ total_act_hour }} 小时

- -

我们还共同创造了 {{ total_course_num }} 门书院课程

-

你有没有找到属于自己的“绝世好课”?

-
-
- -
-
-
-

季学期里

-

YPers最爱的书院课TOP3花落

-

{{ hottest_course_names_23_fall|linebreaksbr }}

-

不知道你是不是那个百里挑一的幸运儿?

-
- -
-

季学期

-

{{ hottest_course_names_23_spring|linebreaksbr }}

-

则更受到大家的青睐

-

看来诗人所说的“秋日胜春朝”

-

至少从YPers的选课热情来看

-

确实不错

-
-
- -
-
-
-

么,你呢?

-

在这个琳琅纷繁的网络家园里

-

又是什么装饰了你的空间?

- -

这一年里

-

你曾经 {{ checkin_num }} 次登录智慧书院系统

-

最长连续 {{ max_consecutive_days }} 天回到这里

-

超过 {{ max_consecutive_days_rank }} %的智慧书院用户

-

你是使用YPPF的小狮子里 {{ consecutive_days_name }} 的一员

-
-
- -
-
-
-

去一年中

-

你参与了 {{ club_course_num }} 个学生小组和书院课程小组

- {% if myclub_name != '' %} -

其中你创建的有

-

{{ myclub_name }}

- {% endif %} - {% if admin_org_names_str != '' %} -

你还是这些组织中不可缺少的中坚力量

-

{{ admin_org_names_str }}

- {% endif %} -

怎么样,有没有什么别样的收获能与我们分享?

- -

这一年里,你参与了 {{ act_num }} 场小组活动

- {% if act_num > 0 and act_top_three_keywords_str != '' %} -

出现最多的关键词是 {{ act_top_three_keywords_str }}

- {% endif %} -

和伙伴们的相遇相知

-

有没有给你的生活注入能量?

- - {% if act_num > 0 and most_act_common_hour != None %} -

这一年里

-

你最喜欢在 {{ most_act_common_hour }} 点参加活动

-

{{ most_act_common_hour_name }}

- {% endif %} -
-
- -
-
-
-

2023年结束

-

你已经选修了 {{ course_num }} 门书院课程

-

累计学时 {{ course_hour }} 小时

-

在所有类别的课程中

-

你已经选修了 {{ course_type }} 类课程

-

恭喜你达成 {{ type_count_name }} 成就

- -

在尝试过的课程里

-

你似乎最爱 {{ course_most_time_name }}

-

与它相伴的 {{ course_most_hour }} 个课时里

-

哪些瞬间让你至今难忘?

- -

选课开盘的瞬间,怎么不算一种惊心动魄呢?

-

在过去的两个学期里

-

你平均每次选课 {{ avg_preelect_num }} 门,选中课程 {{ avg_elected_num }}

-

有没有哪一门课,让你愿意投入一万年的爱?

-
-
- -
-
-
-

日子缺少元气

-

或许可以来这里逛逛

-

这一年,智慧书院为你提供了 {{ income }} 点元气值

-

未来也要继续元气满满呀!

- -

这一年,你在元气值商城共花费 {{ expenditure }} 点元气值

-

成功兑换了 {{ number_of_unique_prizes }} 类奖品

-

那些属于“元培人”的自豪与归属

-

都凝结在这里,被具象地诠释和珍藏

- -

我想你一定也同意

-

生活需要一些未知作为调剂

-

过去一年,你在元气商城参与过 {{ mystery_boxes_num }} 次盲盒抽奖

-

其中有 {{ lucky_mystery_boxes_num }} 次成功抽中大礼

- {% if lucky_mystery_boxes_num > 0 %} -

恭喜你达成 {{ mystery_boxes_name|default:'' }} 成就

- {% endif %} -
-
- -
-
-
-

果校园里的生活是一场寻宝之旅

-

独属于你的藏宝图又记载了怎样的风景?

- {% if academic_tags_num > 0 %} -

过去的一年里

-

你在学术地图中记录了 {{ academic_tags_num }} 个高光时刻

-

在园子里的探索

-

也多了些值得骄傲的注脚

- {% else %} -

在学术地图里

-

你的每一次尝试都值得被记录!

- {% endif %} - - {% if academic_QA_num == 0 %} -

漂泊的远行者

-

能否看见远方闪烁的航标?

-

那些挣扎困顿时

-

偶然萦绕脑海的迷思

-

曾经敲下又匆匆删去的疑惑

-

都可以在学术地图中寻找答案

- {% else %} -

过去的一年里

-

你在学术地图上向他人提出过 {{ academic_QA_num }} 次问题

-

不知道你的勇敢可曾驱散生活里偶然的乌云?

- {% endif %} -
-
- -
-
-
-

到这里

-

时光的旅行似乎就要结束了

-

不论你与我初次会面

-

还是早已成为旧交

-

一年又一年

-

我就在这里

-

与你一路相陪

-

那么你呢?

-

你将如何继续这场与我同行的时光之旅?

-
-
- - -
- - - - - - - - - - - diff --git a/templates/academic/audit.html b/templates/academic/audit.html deleted file mode 100644 index 87836d6ad..000000000 --- a/templates/academic/audit.html +++ /dev/null @@ -1,78 +0,0 @@ -{% extends "base.html" %} - - -{% block mainpage %} - - - -
- - {% if warn_code == 1 %} -
{{ warn_message }}
- {% elif warn_code == 2 %} -
{{ warn_message }}
- {% endif %} - - -
-
- - {% if bar_display.help_paragraphs %} - {% include 'help.html' %} - {% endif %} - -
-
-
-
-

待审核的学术地图

-
- -
-
- - {% if student_list|length != 0 %} - - - {% for person in student_list %} - - - - - {% endfor %} - - {% else %} - - - - - - - {% endif %} -
- - - 大头照{{ person.name }} - - - - - - - - -
没有待审核的学术地图~
-
-
-
-
-
-
-
-
- - - -{% endblock %} diff --git a/templates/academic/modify.html b/templates/academic/modify.html deleted file mode 100644 index fa94a7e7d..000000000 --- a/templates/academic/modify.html +++ /dev/null @@ -1,591 +0,0 @@ -{% extends "base.html" %} - -{% load static %} - - -{% block mainpage %} - - -
- - - - - - - -
- {% if warn_code == "2" %} -
{{warn_message}}
- {% elif warn_code == "1" %} -
{{warn_message}}
- {% endif %} -
- -
-
-
-
- - -
-
-
-

-
-

编辑我的学术地图

-
-
-
-
- - - -
-
-
-
-
-
- - - -
- - -
-
- -
- - - -
-
- -
- -
-
- -
- - - -
-
- -
- -
-
- -
- - - -
-
- -
- -
-
- -
- - - -
-
- -
- -
-
- -
- - - -
-
-
- {% for scientific_research in scientific_research_list %} - -
- {% endfor %} - - -
- - - -
- -
-
- -
- - - -
-
-
- {% for challenge_cup in challenge_cup_list %} - -
- {% endfor %} - - -
- - - -
- -
-
- -
- - - -
-
-
- {% for internship in internship_list %} - -
- {% endfor %} - - -
- - - -
- -
-
- -
- - - -
-
-
- {% for scientific_direction in scientific_direction_list %} - -
- {% endfor %} - - -
- - - -
- -
-
- -
- - - -
-
-
- {% for graduation in graduation_list %} - -
- {% endfor %} - - -
- - - -
- -
- -
- - - -
-
- -
-
- -
- - -
- - -
-
-
-
-
-
- -
- -{% endblock %} - - -{% block add_js_file %} - - - - -{% endblock %} diff --git a/templates/academic/showChats.html b/templates/academic/showChats.html deleted file mode 100644 index 5be9b0bcd..000000000 --- a/templates/academic/showChats.html +++ /dev/null @@ -1,655 +0,0 @@ -{% extends "base.html" %} - -{% load static %} - -{% block add_css_file %} - -{% endblock %} - -{% block mainpage %} - - - -
- - - - - - - {% if warn_code == 1 %} -
{{ warn_message }}
- {% elif warn_code == 2 %} -
{{ warn_message }}
- {% endif %} - - -
-
- - {% if bar_display.help_paragraphs %} - {% include 'help.html' %} - {% endif %} - -
-
-
-
-

问答中心

- -
- - - - - - - -
- -
-
-
-
- -
- - - - - - {% if sent_chats.progressing|length == 0 and sent_chats.not_progressing|length == 0 %} -
-

您还没有进行过任何提问.

-
- {% endif %} - - {% for chat in sent_chats.progressing %} -
-
-
-
-
- {{chat.title}} - {{chat.status}}
-
-
- -
-
-
- - -
-
-
-
- -

- - - {% if not chat.respondent_anonymous %} - {% if chat.questioner_anonymous %} - [匿名] - {% endif %} - 发给  - {{chat.respondent_name}} - - {% else %} - 非定向提问 - {% endif %} - -

- -

- - 上次更新于{{chat.last_modification_time}} -

- -

- - 发起于{{chat.start_time}} -

- -

- - 共{{chat.message_count}}条信息 -

-
-
- {% endfor %} - - - - {% for chat in sent_chats.not_progressing %} -
-
-
-
-
- {{chat.title}} - {% if chat.status == "已关闭" %} - {{chat.status}} - {% else %} - {{chat.status}} - {% endif %} -
-
- -
-
-
- - -
-
-
-
- -

- - - {% if not chat.respondent_anonymous %} - {% if chat.questioner_anonymous %} - [匿名] - {% endif %} - 发给  - {{chat.respondent_name}} - - {% else %} - 非定向提问 - {% endif %} - -

- -

- - 上次更新于{{chat.last_modification_time}} -

- -

- - 发起于{{chat.start_time}} -

- -

- - 共{{chat.message_count}}条信息 -

-
-
- {% endfor %} - -
-
-
- - - -
- -
-
- - {% for chat in received_chats.progressing %} -
-
-
-
-
- {{chat.title}} - {{chat.status}}
-
-
- -
-
-
- - -
-
-
-
- -

- - - - {% if chat.questioner_anonymous %} - 来自 匿名用户 - {% else %} - 来自 {{chat.questioner_name}} - {% endif %} - - -

- -

- - 上次更新于{{chat.last_modification_time}} -

- -

- - 发起于{{chat.start_time}} -

- -

- - 共{{chat.message_count}}条信息 -

-
-
- {% endfor %} - - - - {% for chat in received_chats.not_progressing %} -
-
-
-
-
- {{chat.title}} - {% if chat.status == "已关闭" %} - {{chat.status}} - {% else %} - {{chat.status}} - {% endif %} -
-
- -
-
-
- - -
-
-
-
- -

- - - 来自  - {% if chat.academic_url != "" %} - {{chat.questioner_name}} - {% else %} - {{chat.questioner_name}} - {% endif %} - -

- -

- - 上次更新于{{chat.last_modification_time}} -

- -

- - 发起于{{chat.start_time}} -

- -

- - 共{{chat.message_count}}条信息 -

-
-
- {% endfor %} - -
-
-
- -
- -
-
-
-
-
-
- -{% endblock %} - -{% block add_js_file %} - -{% comment %} 提供本项目的用户搜索函数matchUser {% endcomment %} - - - - - - - - -{% endblock %} diff --git a/templates/academic/viewChat.html b/templates/academic/viewChat.html deleted file mode 100644 index 2a9fc2484..000000000 --- a/templates/academic/viewChat.html +++ /dev/null @@ -1,477 +0,0 @@ -{% extends "base.html" %} - -{% load static %} - -#TODO: -{% block mainpage %} - - - - -
-
- - - - {% if warn_code == 2 %} -
{{warn_message}}
- {% elif warn_code == 1%} -
{{warn_message}}
- {% endif %} - - -
- - {% if bar_display.help_paragraphs %} - {% include 'help.html' %} - {% endif %} - -
-
-
- -
-

- {% if is_questioner %} - {% if not respondent_anonymous %} - 发给{{respondent_name}}的提问:{{title}}  - {% else %} - 非定向提问:{{title}}  - {% for tag in respondent_tags %} - {% if forloop.counter == 1 %} - - {{tag}} - - {% elif forloop.counter == 2 %} - - {{tag}} - - {% elif forloop.counter == 3 %} - - {{tag}} - - {% endif %} - {% endfor %} - {% endif %} - {% else %} - {% if not questioner_anonymous %} - 来自{{questioner_name}}的提问:{{title}}  - {% else %} - 匿名提问:{{title}}  - {% endif %} - {% endif %} -

-

- {% for tag in tags %} - {% if forloop.counter == 1 %} - - {{tag}} - - {% elif forloop.counter == 2 %} - - {{tag}} - - {% elif forloop.counter == 3 %} - - {{tag}} - - {% endif %} - {% endfor %} -

-
- - - -
-
-
-
- - {% if messages|length != 0 %} - {% for comment in messages %} -
-
- - {% if my_name == comment.commentator.real_name %} -
-

- {{ comment.time }}

-
-
- avatar - {% if comment.commentator.URL %} - - {{ comment.commentator.name }} - - {% else %} - {{ comment.commentator.name }} - {% endif %} -
- - - - {% else %} -
- avatar - {% if comment.commentator.URL %} - - {{ comment.commentator.name }} - - {% else %} - {{ comment.commentator.name }} - {% endif %} -
-
-

- {{ comment.time }}

-
- - {% endif %} -
- - {% if comment.text %} - {% if comment.commentator.name == my_name %} - - - {% else %} - - - {% endif %} - {% endif %} - {% if comment.photos %} - {% if comment.text %}
{% endif %} -
- {% for image in comment.photos %} - - {% endfor %} -
- {% endif %} -
-
- {% endfor %} - - {% else %} - - - - - - - {% endif %} -
{{ not_found_message }}
-
-
-
-
- - - - {% if commentable %} -
-
-
-
- -
-
- -
-
-
-
-
- {% if is_questioner and questioner_anonymous or not is_questioner and respondent_anonymous %} -
- - -
- {% endif %} - - -
-
-
-
-
-
-
-
- {% endif %} - - - - {% if is_questioner and answered %} -
-
-
-
-

这个回答对您有帮助吗?

-
- - -
-
- - -
-
- - -
-
- {% if rating == 0 %} - - {% else %} - - {% endif %} - -
- -
- -
-
-
- {% endif %} - -
-
-
-
-
-
-{% endblock %} - - -{% block add_js_file %} - - -{% endblock %} diff --git a/templates/activity/center.html b/templates/activity/center.html index e551b82e5..d7165bb67 100644 --- a/templates/activity/center.html +++ b/templates/activity/center.html @@ -29,9 +29,6 @@ {% elif html_display.warn_code == 2 %}
{{ html_display.warn_message }}
{% endif %} - {% if YQPoint_Source_Org %} -
元培元气值中心仅负责元气值发放,请使用其他账号发布活动。
- {% endif %}
diff --git a/templates/activity/info.html b/templates/activity/info.html index 6039eb58f..0fc5df770 100644 --- a/templates/activity/info.html +++ b/templates/activity/info.html @@ -313,14 +313,6 @@

- {% if price %} -

- - - {{ price }} 元气值 - -

- {% endif %}

- -{% endblock %} - -{% block add_js_file %} - - -{% endblock %} diff --git a/templates/course/course_record.html b/templates/course/course_record.html deleted file mode 100644 index 130b2cd35..000000000 --- a/templates/course/course_record.html +++ /dev/null @@ -1,99 +0,0 @@ -{% extends "base.html" %} - -{% block mainpage %} - -
- {% if messages.warn_code == 1 %} -
{{ messages.warn_message }}
- {% elif messages.warn_code == 2 %} -
{{ messages.warn_message }}
- {% endif %} -
-
- - {% if bar_display.help_paragraphs %} -
- {% include 'help.html' %} -

- {% endif %} - -
-

- {{course_info.year}}年{{course_info.semester}}{{course_info.course}}学时记录 -

- - {% if editable %} -
-
- -
-
- {% endif %} -
- - -
-
-
-
- -
-
-
- - - - - - - {% if editable %} - - {% endif %} - - - - {% for record in records_list %} - - - - - {% if editable %} - - - - {% endif %} - - {% endfor %} - - -
姓名年级次数学时
- avatar - {{record.name}} - {{record.grade}}{{record.times}} - -
- {% if not records_list %} -

没有任何记录!

- {% endif %} -
-
- {% if editable %} - - {% else %} - - {% endif %} -
-
-
-
- -{% endblock %} diff --git a/templates/course/lesson_add.html b/templates/course/lesson_add.html deleted file mode 100644 index f8d091ff4..000000000 --- a/templates/course/lesson_add.html +++ /dev/null @@ -1,363 +0,0 @@ -{% extends "base.html" %} - -{% load static %} - -{% block add_css_file %} - - - - - - - -{% endblock %} - -{% block mainpage %} -
-
- {% if html_display.warn_code == 2 %} -
{{ html_display.warn_message }}
- {% else %} - - {% endif %} - {% if html_display.warn_code == 1 %} -
{{ html_display.warn_message }}
- {% else %} - - {% endif %} -
-
-
-
-
-
- {% if edit %} -

修改课程

- {% else %} -

新增单次课程

- {% endif %} -
- -
- -
-
-
-
-

- 基本信息 - {% if edit and course_time_tag %} - - {% endif %} -

-
- - {% if not edit %} - - {% else %} - - {% endif %} -
- -
-
-

- 详细信息 -

- -
-
-
- - {% if not edit %} - - {% else %} - - {% endif%} -
-
- -
-
- {% if not edit %} - - {% else %} - - {% endif %} -
-
- {% if not edit %} - - {% else %} - - {% endif %} -
-
-
-
-
-
- -
-
- - -
-
-
-
- -
-
- - - - - -
-
-
-
- {% if edit %} - {%if activity.course_time%} - -
- - - - - - - -
-
- {% else %} - - {% endif %} - {% else %} - - {% endif %} -
-
-
-
-
-
-
-{% endblock %} - -{% block add_js_file %} - - - - - - -{% endblock %} diff --git a/templates/course/register_course.html b/templates/course/register_course.html deleted file mode 100644 index 9f4723c95..000000000 --- a/templates/course/register_course.html +++ /dev/null @@ -1,546 +0,0 @@ -{% extends "base.html" %} - -{% load static %} - -{% block add_css_file %} - - - - - - - -{% endblock %} - -{% block mainpage %} - -
-
- - {% if html_display.warn_code == 2 %} -
{{ html_display.warn_message }}
- {% else %} - - {% endif %} - {% if html_display.warn_code == 1 %} -
{{ html_display.warn_message }}
- {% else %} - - {% endif %} - - -
- {% if bar_display.help_paragraphs %} - {% include 'help_with_table.html' %} - {% endif %} -
-
-
-
- - -
-
-
- {% if edit %} -

编辑开课信息

- {% else %} -

设置开课信息

- {% endif %} -
- -
- -
- - -
- -
- -
- - - - - - - -
- - -
- - - - - - -
-
-
-
- - - - - - -
-
-
- -
- - -
- -
- - -
- - -
- - -
- - -
- - - - - - -
-
- -
-
- -
-
-
-
- -
-
- -
-
-
-
- -
-
- -
-
-
-
- -
-
- - -
-
-
-
- -
-
- - - - -
-
-
-
-

课程宣传信息 - - - - - -

- -
- - -
-
- - -
-
- - -
-
-
-
- - -
- {% if QRcode %} -
- - -
- {%endif%} -
- - {%if not edit%} -
-
-
- - -
- -
- -
- -
-
- - {% for pic in defaultpics %} -
- - -
- {% endfor %} -
-
- - -
- - - {%endif%} - - - {% if edit and editable %} - - {% elif edit %} - - {% else %} - - {% endif %} - -
-
-
-
-
-
-
- - -
-
- - - - -{% endblock %} - -{% block add_js_file %} - - - - - - - -{% if edit %} - -{% endif %} - -{% endblock %} \ No newline at end of file diff --git a/templates/course/select_course.html b/templates/course/select_course.html deleted file mode 100644 index 4fae30b49..000000000 --- a/templates/course/select_course.html +++ /dev/null @@ -1,390 +0,0 @@ -{% extends "base.html" %} - -{% block mainpage %} - - -
- - {% if html_display.warn_code == 2 %} -
{{html_display.warn_message}}
- {% elif html_display.warn_code == 1 %} -
{{html_display.warn_message}}
- {% endif %} - -
-
-

- {{html_display.current_year}}学年{{html_display.semester}}季学期书院课程选课 - {% if html_display.status %} - - {{html_display.status}} - {% endif %} -

- {% comment %} 从后端获取学期数据 {% endcomment %} -
-
- -
-

预选时间:{{html_display.yx_election_start}}至{{html_display.yx_election_end}}

-

公布抽签结果:{{html_display.publish_time}}

-

补退选时间:{{html_display.btx_election_start}}至{{html_display.btx_election_end}}

-
- -
-

注:

-

1. 选课各阶段均最多选择6门书院课程,不可同时选择两门时间冲突的课程。

-

2. 原则上同一门书院课程不能重复计算时长,如有特殊情况,需向学院申请。

-

3. 自2023级本科生起,个人宿舍卫生情况将纳入劳动课学时认定的前提条件。在每月卫生检查中,对于宿舍卫生检查不合格的同学,可给予一次整改机会。如果卫生检查经整改仍不合格,所获得的书院劳动育人类课程时长无效。

-
-
- -
- - - -
- -
- {% if is_drawing == True %} -
-
-
-

抽签进行中,暂不支持选课!

-
-
- {% elif is_end == True %} -
-
-
-

选课已经全部结束,请前往”我的课程“查看选课结果!

-
-
- - {% elif unselected_display|length == 0 %} -
-
-
-

暂时没有课程哦!

-
-
- - {% else %} - -
-
-
- - - - -
-
- - - - - - - - - - - - - - - - {% for course in unselected_display %} - - - - - {% comment %} 注意这里的status是课程的,除了已撤销的四个都在这儿 {% endcomment %} - - - - - {% endfor %} - - -
课程名称上课时间类型已选/限数操作
- {% comment %} 课程小组头像路径(后端调用get_user_ava获得){% endcomment %} - avatar - - {{course.name}} - - - {% for time in course.time_set %} - {{time}}
- {% endfor %} - {% if course.time_set|length == 0 %} - 时间待定 - {% endif %} -
{{course.type}}{{course.current_participants}}/{{course.capacity}} -
- - - {% if course.status != "预选" and course.status != "补退选" or is_student == False %} - - {% else %} - - {% comment %} {% endcomment %} - {% endif %} -
- -
-
- - - {% for course_type_name, typed_courses in courses.items %} - {% if typed_courses|length == 0 %} - - - {% else %} - - - - {% endif %} - - {% endfor %} - -
- -
-
-
- - {% endif %} - - - {% if selected_display|length == 0 %} -
-
-
-

你还没有选课哦!

-
-
- - {% else %} - -
-
-
- - - - - - - - - - - - - - - - {% for course in selected_display %} - - - - - - - - - - - {% endfor %} - - - -
课程名称上课时间类型状态已选/限数操作
- avatar - - {{course.name}} - - - {% for time in course.time_set %} - {{time}}
- {% endfor %} - {% if course.time_set|length == 0 %} - 时间待定 - {% endif %} -
{{course.type}} - {% if is_drawing == True %} - 抽签中 - {% elif course.student_status == "已选课" %} - {{course.student_status}} - {% elif course.student_status == "选课成功" %} - {{course.student_status}} - {% elif course.student_status == "选课失败" %} - {{course.student_status}} - {% endif %} - {{course.current_participants}}/{{course.capacity}} -
- - - {% if course.status != "预选" and course.status != "补退选" or course.student_status == "选课失败" %} - - {% else %} - - {% endif %} - {% comment %} {% endcomment %} -
- -
-
-
- -
- - {% endif %} - -
- -
- -{% endblock %} diff --git a/templates/course/show_course_activity.html b/templates/course/show_course_activity.html deleted file mode 100644 index 4a04d03c3..000000000 --- a/templates/course/show_course_activity.html +++ /dev/null @@ -1,297 +0,0 @@ -{% extends "base.html" %} - -{% block mainpage %} - - - -{% with title='我的课程' link1='/addSingleCourseActivity' btn_name1='增加课时' link2='/addCourse' btn_name2='发起选课' link3='/showCourseRecord' btn_name3='课程结项' link4='/outputSelectInfo' btn_name4='导出名单' link5='/sendMessage/?receiver_type=小组成员' btn_name5='发送通知'%} - - -
- {% if html_display.warn_code == 1 %} -
{{ html_display.warn_message }}
- {% elif html_display.warn_code == 2 %} -
{{ html_display.warn_message }}
- {% endif %} - - -
-
-
-
-
- -
-

{{title}}

-
- - -
-
- - - -
-
- - -
-
-
- {% if future_activity_list|length == 0 %} -

没有未开始的课程活动!

- {% endif %} -
- {% for act in future_activity_list %} - -
-
-
-
-
- {% if act.status == "等待中" %} - - 等待开始 - {% else %} - {% if act.status == "审核中" %} - - {% elif act.status == "未过审" or act.status == "已取消" or act.status == "已撤销" or act.status == "已结束" %} - - {% elif act.status == "报名中" %} - - {% elif act.status == "进行中" %} - - {% elif act.status == "待发布" %} - - {% endif %} - {{ act.status }} - {% endif %} - - - {{ act.title }} - -
-
-
- -
- - - -
- - -

- - 开始时间:{{ act.start }}  -

- -

- - 发布时间:{{ act.publish_time }}  -

- -

- - {{ act.location }}  -

-
-
- - - {% endfor %} -
-
- -
- -
-
- {% if finished_activity_list|length == 0 %} -

没有已结束的课程活动!

- {% endif %} -
- {% for act in finished_activity_list %} - -
-
-
-
-
- {% if act.status == "等待中" %} - - 等待开始 - {% else %} - {% if act.status == "审核中" %} - - {% elif act.status == "未过审" or act.status == "已取消" or act.status == "已撤销" or act.status == "已结束" %} - - {% elif act.status == "报名中" %} - - {% elif act.status == "进行中" %} - - {% endif %} - {{ act.status }} - {% endif %} - - - - {{ act.title }} - -
-
-
- {% if act.status == "已取消" %} - - {% else %} - - {% endif %} -
- -
- - -

- - 结束时间:{{ act.end }}  -

- -

- - {{ act.location }}  -

-
-
- - {% endfor %} -
-
-
-
- -
- -
-
- - -
-
-
-
- -
- - -{% endwith %} - -{% endblock %} diff --git a/templates/dormitory/agreement.html b/templates/dormitory/agreement.html deleted file mode 100644 index 806e709ad..000000000 --- a/templates/dormitory/agreement.html +++ /dev/null @@ -1,96 +0,0 @@ -{% extends "base.html" %} -{% comment %} -# XXX: Vars {{ I(bar_display) }} -# XXX: Request {{ I(session) }} -# XXX: Depends {% base %} -{% endcomment %} -{% load static %} - -{% block add_css_file %} - - - - -{% endblock %} - -{% block mainpage %} -
-
- -
-
-
- -
- -
- -
-
-
-

元培学院学生公寓住宿公约

-
-
-
- - -
-

一、自觉遵守国家法律法规、《北京大学学生公寓管理办法》及学校、学院的其他相关规定,包括但不限于公寓管理、安全等。

-
-

二、服从学校及学院的住宿安排,不擅自更换、占用床位和房间;因特殊情况确有需要调换床位或宿舍的,应向元培学院提出申请,经批准后方可进行调换。

-
-

三、爱护和正常合理使用公寓楼内的设备和家具等生活服务设施,自觉维护公寓楼内公共秩序,不得留宿他人,不得出租、私自出借学生公寓床位。自觉遵守熄灯规定,适时关闭宿舍内有关电器。不得有任何影响或可能影响公共安全和他人身心健康的行为。

-
-

四、积极发挥“自我教育、自我管理、自我服务”的作用,主动参与学生公寓管理服务与文化建设活动。住宿学生应互相尊重,团结友爱,养成健康的学习生活习惯。住宿学生应当尊重公寓工作人员劳动,保持楼梯间、走廊无垃圾;节约水电,杜绝浪费;自觉爱护公寓区的绿地和景观等。

-
-

五、未经学院同意,不占用公共空间存放物品;违反学校、学院规定存放物品,经公告30日仍不收回、清理的,视为同意学院清理该物品。

-
-

六、学院每月进行卫生检查,检查不合格且经提醒后仍不整改的,取消评奖评优资格,学院有权请学生搬离35号楼。

-
-

七、35楼宿舍为学生租借使用,管理权归属学校公寓中心与学院。学生租借床位期间,应按规定使用分配设备,不得私自占用他人或空置床位及设施、设备。

-
-
-
-
-

我已认真阅读上述内容,承诺遵守上述公约及其他学校和学院的规定,如有违反,我接受宿舍调整安排及学校和学院的其他决定。

- -
- -
-
- -
-
- - -
-
-
-
-
- -{% endblock %} - -{% block add_js_file %} - - - - - -{% endblock %} \ No newline at end of file diff --git a/templates/dormitory/assign_result.html b/templates/dormitory/assign_result.html deleted file mode 100644 index d494327f3..000000000 --- a/templates/dormitory/assign_result.html +++ /dev/null @@ -1,95 +0,0 @@ -{% extends "base.html" %} - -{% load static %} - -{% block add_css_file %} - - - - - - - -{% endblock %} - -{% block mainpage %} - -
-
- - - {% if html_display.warn_code == 1 %} -
{{ html_display.warn_message }}
- {% endif %} - -
- {% if bar_display.help_paragraphs %} - {% include 'help_with_table.html' %} - {% endif %} -
-
-
-
- - -
-
-
-

宿舍分配结果

-
-
-
- - -
- {% if dorm_assigned %} -
- 此处是你的宿舍分配结果
-
- - - - - - - - - {% for roommate in roommates %} - - {% if forloop.first %} - - - - {% endif %} - - - {% endfor %} -
-
姓名 宿舍号 床号 你的室友
{{name}}{{dorm_id}}{{bed_id}}  {{roommate.name}}
-
- {% else %} -
- 系统中尚未查询到你的宿舍分配结果,待结果载入后将通知
- {% endif %} -
-
-
-
-
-
- - - - -
- -
- - -{% endblock %} \ No newline at end of file diff --git a/templates/dormitory/routine_QA.html b/templates/dormitory/routine_QA.html deleted file mode 100644 index 37e1f42f4..000000000 --- a/templates/dormitory/routine_QA.html +++ /dev/null @@ -1,142 +0,0 @@ -{% extends "base.html" %} - -{% load static %} - -{% block add_css_file %} - - - -{% endblock add_css_file %} - -{% block mainpage %} - - -
-
- {% if html_display.warn_code == 1 %} -
{{ html_display.warn_message }}
- {% endif %} -
- {% if bar_display.help_paragraphs %} - {% include 'help_with_table.html' %} - {% endif %} -
-
-
-
-
- -
-
-
-

宿舍生活习惯调研

-
-
-
- -
-
-

*的为必填问题,其他问题可填“空”。

-
-
-
- {% for question, choices in survey_iter %} -
-
- {{ question.order }}. {{ question.topic }} - {% if question.required %}*{% endif %} -
- {% if question.type == 'TEXT' %} - - {% elif question.type == 'SINGLE' %} - {% for choice in choices %} -
- - -
- {% endfor %} - {% elif question.type == 'MULTIPLE' %} - {% for choice in choices %} -
- - -
- {% endfor %} - {% endif %} -
- {% endfor %} - {% if submitted %} -
- 问卷已成功提交,宿舍分配结果将稍后进行通知 -
- {% else %} -
-
- -
- {% endif %} -
-
-
-
-
-
-
-
-
- -
-{% endblock mainpage %} - -{% block add_js_file %} - - -{% endblock add_js_file %} diff --git a/templates/myYQPoint.html b/templates/myYQPoint.html deleted file mode 100644 index a967a7e90..000000000 --- a/templates/myYQPoint.html +++ /dev/null @@ -1,176 +0,0 @@ -{% extends "base.html" %} - -{% block mainpage %} - - -
- {% if html_display.warn_code == 1 %} -
{{ html_display.warn_message }}
- {% elif html_display.warn_code == 2 %} -
{{ html_display.warn_message }}
- {% endif %} -
-
- -
-
-
-
- Card image -
- - 当前元气值 - {{YQPoint}} -
-
-
-
-
-
- {% if bar_display.help_paragraphs %} - {% include 'help.html' %} - {% endif %} -
-
-
-

元气值记录

- - - -
-
- {% if not received_set %} -
-

-

没有收入记录.

-
- {% else %} - -
-
- {% for record in received_set %} -
- -
- - -
-
-
- {{record.source}} -
-
-
-
- 元气值 +{{record.delta}} -
-
-
-
-
-

- {{record.time}}

-
-
-
-
- {% endfor %} -
-
- {% endif %} -
-
- {% if not send_set %} -
-

-

没有支出记录.

-
- {% else %} - -
-
- {% for record in send_set %} -
- -
-
-
-
- {{record.source}} -
-
-
-
- 元气值 {{record.delta}} -
-
-
-
-
-

- {{record.time}}

-
-
- -
-
- {% endfor %} -
-
- {% endif %} -
-
-
-
-
-
-
- -
- - - - - - -{% endblock %} diff --git a/templates/org_left_navbar.html b/templates/org_left_navbar.html index 5290929fe..399273aea 100644 --- a/templates/org_left_navbar.html +++ b/templates/org_left_navbar.html @@ -35,10 +35,9 @@ - + --> - - - {% endif %} - - + --> - {% if bar_display.user_active %} - - {% endif %} - {% if bar_display.person_type == 1 %} - - {% endif %} {% if bar_display.person_type == 0 %} {% endif %} - - {% endif %} {% if bar_display.is_auditor %} @@ -282,21 +177,6 @@ {% endif %} - {% if bar_display.person_type == 0 %} - - {% endif %} {% if request.session.Incharge %} {% endif %} -