Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

书院课选课权重 #870

Open
wants to merge 11 commits into
base: develop
Choose a base branch
from
1 change: 1 addition & 0 deletions app/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ def get_normal_fields(self, request, obj: NaturalPerson = None):
f(_m.identity), f(_m.status),
f(_m.wechat_receive_level),
f(_m.accept_promote), f(_m.active_score),
f(_m.course_priority)
])
return fields

Expand Down
64 changes: 49 additions & 15 deletions app/course_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
process_time: 把datetime对象转换成人类可读的时间表示
check_course_time_conflict: 检查当前选择的课是否与已选的课上课时间冲突
"""
from collections import Counter, defaultdict
from typing import Callable

from app.utils_dependency import *
from app.models import (
User,
Expand Down Expand Up @@ -46,7 +49,7 @@

import openpyxl
import openpyxl.worksheet.worksheet
from random import sample
from numpy.random import choice
from urllib.parse import quote
from collections import Counter
from datetime import datetime, timedelta
Expand Down Expand Up @@ -608,16 +611,13 @@ def registration_status_change(course_id: int, user: NaturalPerson,
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:
Expand Down Expand Up @@ -681,15 +681,11 @@ def registration_status_change(course_id: int, user: NaturalPerson,
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)
CourseParticipant.objects.update_or_create(
course_id = course_id,
person = user,
defaults = {"status": to_status}
)
succeed("选课成功!", context)
except:
return context
Expand Down Expand Up @@ -816,7 +812,11 @@ def draw_lots():
if participants_num <= 0:
continue

participants_id = list(participants.values_list("id", flat=True))
participants_info = participants.values_list("id", "person__course_priority")
participants_id, priority = map(list, zip(*participants_info))
# Turn priority into a probability distribution
sum_priority = sum(priority)
priority = [i / sum_priority for i in priority]
capacity = course.capacity

if participants_num <= capacity:
Expand All @@ -828,7 +828,7 @@ def draw_lots():
current_participants=participants_num)
else:
# 抽签;可能实现得有一些麻烦
lucky_ones = sample(participants_id, capacity)
lucky_ones = choice(participants_id, capacity, replace=False, p=priority)
unlucky_ones = list(
set(participants_id).difference(set(lucky_ones)))
# 不确定是否要加悲观锁
Expand Down Expand Up @@ -1489,3 +1489,37 @@ def download_select_info(single_course: Course | None = None):
file_name = f'{year}{semester}选课名单汇总-{ctime}'
# 保存并返回
return _excel_response(wb, file_name)


def update_course_priority(year: int, semester: str, priority_func: Callable[[int], float]):
"""
根据指定学期的学时表,计算新的书院课选课权重。

:param year: 用于查找学时表 table, 一个可能值示例为2024
:param semester: 用于查找学时表 table。semester的值应为'Fall'或'Spring'
:param priority_func: 一个函数,它接受一个int,表示过去学期学时无效的书院课门数,返回新学期选课优先级(0~1)
:return: None
"""
if semester not in ('Fall', 'Spring'):
raise ValueError('semester must have value of "Fall" or "Spring"!')
with transaction.atomic():
# First, all set to 1.0
NaturalPerson.objects.update(course_priority=1.0)
# Invalid records in the given semester
invalid_records = CourseRecord.objects.filter(
year = year, invalid = True, semester = semester
).values_list(
'person__id', flat = True
)
# Counts the number of occurrences of a person's ID in invalid list
invalid_counter: Counter[int] = Counter(invalid_records)
# a[k] is the list of id of people who have k invalid records
a = defaultdict(list)
# Length of the current continuous segment
for person_id in invalid_counter:
cnt = invalid_counter[person_id]
a[cnt].append(person_id)
for i in a:
# There are len(a[i]) people with i invalid records
p = priority_func(i)
NaturalPerson.objects.filter(id__in = a[i]).update(course_priority = p)
21 changes: 21 additions & 0 deletions app/management/commands/update_course_priority.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from django.core.management.base import BaseCommand
Deophius marked this conversation as resolved.
Show resolved Hide resolved

from app.course_utils import update_course_priority

class Command(BaseCommand):
# This command should be run before new semester starts
help = "Updates course_priority for all NaturalPerson"

def add_arguments(self, parser):
parser.add_argument('year', type=int, help='year of the course records to check (required)')
parser.add_argument('semester', type=str, help='semester of the course records to check. Valid values are: Fall, Spring (required)')

def handle(self, *args, **options):
Deophius marked this conversation as resolved.
Show resolved Hide resolved
try:
# 修改这里的这个lambda,可以实现不同的权重策略
update_course_priority(options['year'], options['semester'],
lambda x: 1 - 0.05 * x)
except Exception as e:
print('Error:', e.args)
else:
print('Success!')
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.0.9 on 2025-01-17 20:52

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("app", "0008_pool_activity_alter_poolitem_exchange_attributes_and_more"),
]

operations = [
migrations.AddConstraint(
model_name="courseparticipant",
constraint=models.UniqueConstraint(
fields=("course", "person"), name="Unique course selection record"
),
),
]
17 changes: 17 additions & 0 deletions app/migrations/0010_naturalperson_course_priority.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 5.0.9 on 2025-01-17 21:27

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("app", "0009_courseparticipant_unique_course_selection_record"),
]

operations = [
migrations.AddField(
model_name="naturalperson",
name="course_priority",
field=models.FloatField(default=1.0, verbose_name="书院课选课权重"),
),
]
4 changes: 4 additions & 0 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ class ReceiveLevel(models.IntegerChoices):

accept_promote = models.BooleanField(default=True) # 是否接受推广消息
active_score = models.FloatField("活跃度", default=0) # 用户活跃度
course_priority = models.FloatField("书院课选课权重", default=1.0) # 书院课预选抽签权重

def __str__(self):
return str(self.name)
Expand Down Expand Up @@ -1580,6 +1581,9 @@ class CourseParticipant(models.Model):
class Meta:
verbose_name = "4.课程报名情况"
verbose_name_plural = verbose_name
constraints = [
Deophius marked this conversation as resolved.
Show resolved Hide resolved
models.UniqueConstraint(fields = ['course', 'person'], name='Unique course selection record')
]

course = models.ForeignKey(Course, on_delete=models.CASCADE,
related_name="participant_set")
Expand Down
Loading