<think>为了在Django Admin的试卷管理下的试卷列表栏增加一个“随机组题”的按钮,我们需要执行以下步骤:
1. 在试卷的Admin类中自定义一个操作(action)。
2. 在该操作中,为选中的试卷随机添加一定数量的题目。
3. 考虑到模型结构:一个试卷(Paper)包含多个PaperQuestion(试卷-题目关联),每个PaperQuestion记录了一个题目在试卷中的顺序。
假设我们有一个Paper模型和一个Question模型,并且通过PaperQuestion模型建立多对多关系(带有额外的顺序字段)。
思路:
- 在PaperAdmin中定义一个名为`random_select_questions`的自定义操作。
- 操作中,我们需要为每个选中的试卷随机选择若干题目(比如,用户输入一个数量N,然后从题库中随机选择N个题目添加到试卷中)。
- 为了避免重复添加相同的题目,我们需要检查题目是否已经在试卷中。
步骤:
1. 在PaperAdmin的actions列表中添加自定义操作。
2. 在自定义操作函数中,首先获取用户输入的题目数量(可以使用Django的messages框架来获取用户输入,但通常自定义操作不支持直接输入参数。因此,我们可以使用中间视图或者使用Django-admin的另一个方法:通过重写ModelAdmin的get_actions和添加一个表单来获取参数。但这里为了简便,我们可以假设每次随机选择固定数量(比如10题)或者使用一个中间页面让用户输入数量。这里我们采用中间页面的方式,因为需要用户指定数量)。
然而,Django Admin的自定义操作默认不支持在操作执行过程中获取额外参数(除非使用一些技巧)。这里我们使用一个常见的方法:重定向到一个自定义视图来获取参数。
但为了简化,我们先实现一个固定数量(比如10题)的随机组题。然后,再考虑如何让用户输入数量。
这里我们分两个版本:
版本1:固定随机选择10题(不考虑用户输入)。
版本2:通过中间页面让用户输入数量。
由于问题描述中要求的是增加一个操作按钮,我们首先实现版本1,然后给出版本2的思路和部分代码。
### 版本1:固定数量随机组题
在PaperAdmin中添加一个action,为选中的每个试卷随机添加10个题目(注意:如果试卷中已有题目,则避免重复)。
```python
# 在admin.py中
from django.contrib import admin
from .models import Paper, PaperQuestion, Question
class PaperAdmin(admin.ModelAdmin):
list_display = ['name', ...] # 根据实际模型调整
actions = ['random_select_questions']
def random_select_questions(self, request, queryset):
# 固定数量为10
num_questions = 10
for paper in queryset:
# 获取该试卷已有的题目ID列表,避免重复
existing_question_ids = PaperQuestion.objects.filter(paper=paper).values_list('question_id', flat=True)
# 随机选择题目,排除已经存在的
available_questions = Question.objects.exclude(id__in=existing_question_ids)
if available_questions.count() < num_questions:
self.message_user(request, f"试卷 {paper.name} 可用的题目不足 {num_questions} 道,只添加了 {available_questions.count()} 道题目。", messages.WARNING)
selected_questions = available_questions
else:
selected_questions = available_questions.order_by('?')[:num_questions] # 随机排序取前num_questions
# 创建PaperQuestion记录
for i, question in enumerate(selected_questions, start=1):
PaperQuestion.objects.create(
paper=paper,
question=question,
order=i # 顺序从1开始
)
self.message_user(request, f"试卷 {paper.name} 成功添加了 {len(selected_questions)} 道题目。", messages.SUCCESS)
random_select_questions.short_description = "随机组题(10道)"
admin.site.register(Paper, PaperAdmin)
```
### 版本2:允许用户输入数量
为了实现用户输入数量,我们需要:
1. 自定义一个中间表单页面,用于输入数量。
2. 在action中,如果选择了多个试卷,我们可以让用户输入一个数量,然后为每个试卷都随机添加这个数量的题目。
步骤:
- 在action方法中,如果request的POST方法中有我们自定义的参数(比如'_selected_action'和'action'),则重定向到我们自定义的视图。
- 自定义视图接收用户输入的数量,然后执行随机组题操作。
由于在admin中实现自定义操作带参数相对复杂,这里提供一个简化的方法:使用Django的`action`装饰器,并利用`HttpResponseRedirect`跳转到自定义视图。
由于代码较长,我们简述步骤:
1. 在PaperAdmin中定义action函数,该函数返回一个重定向到自定义视图的响应,并将选中的试卷id传递过去。
2. 自定义一个视图,该视图显示一个表单让用户输入题目数量,然后处理表单,为每个试卷随机添加指定数量的题目。
示例代码:
**admin.py**
```python
from django.contrib import admin
from django.http import HttpResponseRedirect
from django.urls import reverse
from .models import Paper, PaperQuestion, Question
class PaperAdmin(admin.ModelAdmin):
list_display = ['name', ...]
actions = ['random_select_questions_custom']
def random_select_questions_custom(self, request, queryset):
# 将选中的试卷id用逗号分隔,作为参数传递给自定义视图
selected = queryset.values_list('id', flat=True)
selected_ids = ','.join(str(id) for id in selected)
# 重定向到自定义的视图,该视图会要求输入数量
return HttpResponseRedirect(reverse('random_select_questions') + f'?ids={selected_ids}')
random_select_questions_custom.short_description = "随机组题(自定义数量)"
admin.site.register(Paper, PaperAdmin)
```
**urls.py**
```python
from django.urls import path
from . import views
urlpatterns = [
# ... 其他路由
path('admin/random_select_questions/', views.random_select_questions_view, name='random_select_questions'),
]
```
**views.py**
```python
from django.shortcuts import render, redirect
from django.contrib import messages
from django.contrib.admin.views.decorators import staff_member_required
from .models import Paper, PaperQuestion, Question
@staff_member_required
def random_select_questions_view(request):
if request.method == 'POST':
paper_ids = request.POST.get('paper_ids').split(',')
num_questions = int(request.POST.get('num_questions'))
# 处理每个试卷
for paper_id in paper_ids:
try:
paper = Paper.objects.get(id=int(paper_id))
existing_question_ids = PaperQuestion.objects.filter(paper=paper).values_list('question_id', flat=True)
available_questions = Question.objects.exclude(id__in=existing_question_ids)
if available_questions.count() < num_questions:
# 如果题目不足,使用所有可用题目
selected_questions = available_questions
else:
selected_questions = available_questions.order_by('?')[:num_questions]
# 添加到试卷,注意顺序
for i, question in enumerate(selected_questions, start=1):
PaperQuestion.objects.get_or_create(
paper=paper,
question=question,
defaults={'order': i} # 如果已经存在则不会重复创建
)
messages.success(request, f"试卷 {paper.name} 成功添加了 {len(selected_questions)} 道题目。")
except Paper.DoesNotExist:
messages.error(request, f"试卷ID {paper_id} 不存在。")
return redirect('admin:app_paper_changelist') # 修改为你的试卷changelist的URL name
# GET请求,显示表单
paper_ids = request.GET.get('ids', '')
return render(request, 'admin/random_select_questions.html', {'paper_ids': paper_ids})
```
**模板文件:templates/admin/random_select_questions.html**
```html
<!DOCTYPE html>
<html>
<head>
<title>随机组题</title>
</head>
<body>
<form method="post">
{% csrf_token %}
<input type="hidden" name="paper_ids" value="{{ paper_ids }}">
<label for="num_questions">题目数量:</label>
<input type="number" id="num_questions" name="num_questions" min="1" required>
<button type="submit">提交</button>
</form>
</body>
</html>
```
### 解释:
- 版本1简单直接,但数量固定。适用于需求固定的场景。
- 版本2通过中间页面让用户输入数量,更加灵活,但实现相对复杂。
- 在版本2中,我们创建了一个自定义视图,只有staff成员可以访问。视图处理GET请求时显示一个简单的表单,POST请求时处理随机组题。
- 注意:在版本2中,我们使用了`PaperQuestion.objects.get_or_create`来避免重复添加同一个题目到同一个试卷(但之前已经排除了,所以这里用create也可以,不过为了保险使用get_or_create)。
### 注意事项:
1. 随机排序(`order_by('?')`)在数据量大时可能效率低,如果题目数量很多,考虑使用更高效的随机抽样方法(比如使用`random.sample`在Python层面抽样,但注意数据库负载)。
2. 题目添加到试卷时,顺序(`order`字段)我们按添加顺序递增。如果需要真正的随机顺序,可以在创建时随机一个顺序值,但注意顺序不要重复(通常试卷中顺序唯一即可,这里假设顺序从1递增,不重复)。
3. 避免重复添加:在版本1和版本2中,我们都通过排除试卷中已有题目来避免重复。
### 相关问题:
根据要求,最后提出相关问题。
</think>在 Django Admin 中为试卷管理添加"随机组题"操作按钮,需要创建自定义 Admin Action。以下是完整实现方案和相关解释:
### 解决方案代码
```python
# admin.py
from django.contrib import admin, messages
from django.db.models import Count
from .models import Paper, PaperQuestion, Question
class PaperQuestionInline(admin.TabularInline):
model = PaperQuestion
extra = 0
@admin.register(Paper)
class PaperAdmin(admin.ModelAdmin):
inlines = [PaperQuestionInline]
actions = ['random_add_questions'] # 添加自定义操作
def random_add_questions(self, request, queryset):
# 步骤1:获取用户输入的数量
question_count = int(request.POST.get('question_count', 10))
for paper in queryset:
# 步骤2:获取试卷已有题目ID
existing_ids = paper.questions.values_list('id', flat=True)
# 步骤3:随机选择新题目(排除已有题目)
new_questions = Question.objects.exclude(id__in=existing_ids).order_by('?')[:question_count]
if not new_questions.exists():
self.message_user(request, f"试卷 '{paper.name}' 没有可用的新题目", messages.WARNING)
continue
# 步骤4:创建试卷-题目关联
last_order = PaperQuestion.objects.filter(paper=paper).aggregate(
max_order=Count('order')
)['max_order'] or 0
# 步骤5:批量创建关联记录
PaperQuestion.objects.bulk_create([
PaperQuestion(
paper=paper,
question=question,
order=last_order + idx + 1 # 保持顺序递增
)
for idx, question in enumerate(new_questions)
])
self.message_user(request, f"试卷 '{paper.name}' 成功添加 {len(new_questions)} 道随机题目", messages.SUCCESS)
random_add_questions.short_description = "随机组题"
# 可选:添加数量选择表单
def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
if 'actions' in request.POST and request.POST['action'] == 'random_add_questions':
form.base_fields['question_count'] = forms.IntegerField(
label='题目数量',
min_value=1,
initial=10,
help_text='选择要随机添加的题目数量'
)
return form
```
### 代码功能解释
1. **Admin Action 注册**:
- `actions = ['random_add_questions']` 将自定义操作添加到试卷管理界面
- `short_description` 设置操作按钮的显示文本
2. **随机选题逻辑**:
- `order_by('?')` 使用数据库随机排序功能
- `exclude(id__in=existing_ids)` 排除试卷中已存在的题目
- `question_count` 参数控制添加题目数量(默认为10)
3. **关联记录创建**:
- `bulk_create()` 高效批量创建关联记录
- `order` 字段自动计算,确保题目顺序连续递增
- 使用聚合函数(`aggregate`)获取当前最大序号
4. **用户反馈**:
- `message_user()` 显示操作结果通知
- 处理无可用题目时的警告场景
### 使用说明
1. 在试卷列表页选中一个或多个试卷
2. 从操作下拉菜单中选择"随机组题"
3. 系统会自动为每个试卷添加指定数量的随机题目
4. 结果将在顶部显示操作通知
### 优化建议
1. **添加数量选择表单**:
取消代码中`get_form`方法的注释,可在操作时弹出题目数量输入框
2. **题目类型过滤**:
如需按题型筛选,可修改查询逻辑:
```python
# 只选择选择题
new_questions = Question.objects.filter(type='choice').exclude(id__in=existing_ids).order_by('?')[:question_count]
```
3. **性能优化**:
- 对于大题量题库,使用`random.sample()`代替`order_by('?')`
- 添加`select_related`优化查询效率