问卷项目-admin.py

# survey/admin.py
from django.contrib import admin
from django.utils.html import format_html
from django.urls import reverse
from django.template.response import TemplateResponse
from django.http import HttpResponseRedirect, HttpResponse
from django.utils.translation import gettext_lazy as _
from django.db.models import Q, Count
from django import forms
from django.shortcuts import render, redirect
import csv
import os
import re
import random
import string
import uuid

from .models import Survey, Question, Response, Answer, QRCode, Option, SurveyQuestion, Category


# 设置站点标题、标题头等
admin.site.site_header = _('问卷调查管理系统')
admin.site.site_title = _('问卷调查后台')
admin.site.index_title = _('系统管理')



# ==================== 内联表单类 ====================

class OptionInline(admin.TabularInline):
    """问题选项内联表单"""
    model = Option
    extra = 1
    ordering = ['order']
    fields = ['value', 'label', 'order']
    verbose_name = '选项'
    verbose_name_plural = '选项'
    
    def formfield_for_dbfield(self, db_field, **kwargs):
        """优化表单字段显示"""
        field = super().formfield_for_dbfield(db_field, **kwargs)
        if db_field.name == 'label':
            field.widget.attrs['placeholder'] = '请输入选项显示文本'
        elif db_field.name == 'value':
            field.widget.attrs['placeholder'] = '自动生成,可自定义'
        return field


class SurveyQuestionInline(admin.TabularInline):
    """问卷问题关联内联表单"""
    model = SurveyQuestion
    extra = 1
    ordering = ['order']
    fields = ['question', 'order', 'is_required']
    verbose_name = '问卷问题'
    verbose_name_plural = '问卷问题'
    
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        """优化问题选择框"""
        if db_field.name == 'question':
            # 预加载相关数据以提高性能,只显示激活分类的问题
            kwargs['queryset'] = Question.objects.filter(
                (Q(is_public=True) | Q(created_by=request.user)),
                (Q(category__is_active=True) | Q(category__isnull=True))
            ).select_related('category').order_by('category__name', 'text')
        
        # 先调用父类方法获取表单字段
        form_field = super().formfield_for_foreignkey(db_field, request, **kwargs)
        
        # 然后设置自定义的label_from_instance
        if db_field.name == 'question':
            def label_from_instance(obj):
                category_name = obj.category.name if obj.category else '未分类'
                type_display = obj.get_question_type_display()
                text_preview = obj.text[:50] + '...' if len(obj.text) > 50 else obj.text
                return f"[{category_name}] {text_preview} ({type_display})"
            
            form_field.label_from_instance = label_from_instance
        
        return form_field


# ==================== 模型管理类 ====================

@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    """问题分类管理"""
    list_display = ['name', 'slug', 'is_active', 'created_at', 'question_count']
    list_filter = ['is_active', 'created_at']
    search_fields = ['name', 'description']
    prepopulated_fields = {'slug': ('name',)}
    fields = ['name', 'slug', 'description', 'is_active']
    ordering = ['-created_at']
    actions = ['make_active', 'make_inactive']
    
    def get_queryset(self, request):
        """优化查询集,预加载相关问题计数"""
        return super().get_queryset(request).annotate(
            _question_count=Count('questions')
        )
    
    def question_count(self, obj):
        """统计该分类下的问题数量"""
        return obj._question_count if hasattr(obj, '_question_count') else obj.questions.count()
    question_count.short_description = '问题数量'
    question_count.admin_order_field = '_question_count'
    
    def make_active(self, request, queryset):
        """批量激活选中的分类"""
        queryset.update(is_active=True)
    make_active.short_description = '批量激活分类'
    
    def make_inactive(self, request, queryset):
        """批量停用选中的分类"""
        queryset.update(is_active=False)
    make_inactive.short_description = '批量停用分类'


@admin.register(Question)
class QuestionAdmin(admin.ModelAdmin):
    """问题库管理"""
    list_display = ['text_preview', 'question_type_display', 'category', 'created_by', 
                    'is_public', 'created_at', 'option_count', 'survey_usage_count']
    list_filter = ['question_type', 'category', 'is_public', 'created_at']
    search_fields = ['text', 'category__name']
    list_select_related = ['category', 'created_by']
    ordering = ['category__name', '-created_at']
    inlines = [OptionInline]
    fields = ['text', 'question_type', 'category', 'created_by', 'is_public']
    change_list_template = 'admin/survey/question/change_list.html'
    actions = ['make_public', 'make_private', 'change_category', 'export_questions']
    list_editable = ['is_public', 'category']
    list_per_page = 20
    
    # 自定义表单字段
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == 'created_by' and not request.user.is_superuser:
            kwargs['initial'] = request.user.id
            kwargs['disabled'] = True
        return super().formfield_for_foreignkey(db_field, request, **kwargs)
    
    # 自定义查询集
    def get_queryset(self, request):
        qs = super().get_queryset(request).prefetch_related('options', 'survey_questions')
        if request.user.is_superuser:
            return qs
        return qs.filter(Q(is_public=True) | Q(created_by=request.user))
    
    # 自定义显示字段
    def text_preview(self, obj):
        """问题文本预览"""
        return obj.text[:80] + '...' if len(obj.text) > 80 else obj.text
    text_preview.short_description = '问题文本'
    
    def question_type_display(self, obj):
        """显示中文问题类型"""
        return obj.get_question_type_display()
    question_type_display.short_description = '问题类型'
    
    def option_count(self, obj):
        """选项数量"""
        return obj.options.count()
    option_count.short_description = '选项数'
    
    def survey_usage_count(self, obj):
        """统计问题在问卷中被使用的次数"""
        return obj.survey_questions.count()
    survey_usage_count.short_description = '使用次数'
    
    # 批量操作
    def make_public(self, request, queryset):
        """批量设为公开"""
        updated = queryset.update(is_public=True)
        self.message_user(request, f'成功将 {updated} 个问题设为公开')
    make_public.short_description = '设为公开'
    
    def make_private(self, request, queryset):
        """批量设为私有"""
        updated = queryset.update(is_public=False)
        self.message_user(request, f'成功将 {updated} 个问题设为私有')
    make_private.short_description = '设为私有'
    
    def change_category(self, request, queryset):
        """批量更改分类"""
        selected_ids = list(queryset.values_list('pk', flat=True))
        if selected_ids:
            url = reverse('admin:survey_question_change_category')
            url += f'?ids={",".join(map(str, selected_ids))}'
            return HttpResponseRedirect(url)
    change_category.short_description = '更改分类'
    
    def export_questions(self, request, queryset):
        """导出选中的问题"""
        export_format = request.GET.get('format', 'csv')
        
        if export_format == 'excel':
            return self._export_excel(queryset)
        else:
            return self._export_csv(queryset)
    export_questions.short_description = '导出问题'
    
    # 导出辅助方法
    def _export_csv(self, queryset):
        """导出为CSV格式"""
        response = HttpResponse(content_type='text/csv; charset=utf-8-sig')
        response['Content-Disposition'] = 'attachment; filename="questions_export.csv"'
        
        writer = csv.writer(response)
        # 写入表头
        writer.writerow(['ID', '问题文本', '问题类型', '分类', '创建者', '是否公开', '创建时间', '选项(格式: 标签1;标签2;标签3)'])
        
        # 写入数据
        for question in queryset.select_related('category', 'created_by').prefetch_related('options'):
            options_str = ''
            if question.question_type in ['single_choice', 'multiple_choice']:
                options = [option.label for option in question.options.order_by('order')]
                options_str = ';'.join(options)
            
            writer.writerow([
                question.id,
                question.text,
                question.get_question_type_display(),
                question.category.name if question.category else '未分类',
                question.created_by.username,
                '是' if question.is_public else '否',
                question.created_at.strftime('%Y-%m-%d %H:%M:%S'),
                options_str
            ])
        
        return response
    
    def _export_excel(self, queryset):
        """导出为Excel格式"""
        try:
            from openpyxl import Workbook
            from openpyxl.styles import Font, Alignment
        except ImportError:
            return HttpResponse('请先安装 openpyxl 库:pip install openpyxl', status=500)
        
        wb = Workbook()
        ws = wb.active
        ws.title = "问题导出"
        
        # 设置表头样式
        header_font = Font(bold=True)
        header_alignment = Alignment(horizontal="center", vertical="center")
        
        # 写入表头
        headers = ['ID', '问题文本', '问题类型', '分类', '创建者', '是否公开', '创建时间', '选项(格式: 标签1;标签2;标签3)']
        for col_idx, header in enumerate(headers, 1):
            cell = ws.cell(row=1, column=col_idx, value=header)
            cell.font = header_font
            cell.alignment = header_alignment
        
        # 写入数据
        for row_idx, question in enumerate(queryset.select_related('category', 'created_by').prefetch_related('options'), 2):
            options_str = ''
            if question.question_type in ['single_choice', 'multiple_choice']:
                options = [option.label for option in question.options.order_by('order')]
                options_str = ';'.join(options)
            
            ws.cell(row=row_idx, column=1, value=question.id)
            ws.cell(row=row_idx, column=2, value=question.text)
            ws.cell(row=row_idx, column=3, value=question.get_question_type_display())
            ws.cell(row=row_idx, column=4, value=question.category.name if question.category else '未分类')
            ws.cell(row=row_idx, column=5, value=question.created_by.username)
            ws.cell(row=row_idx, column=6, value='是' if question.is_public else '否')
            ws.cell(row=row_idx, column=7, value=question.created_at.strftime('%Y-%m-%d %H:%M:%S'))
            ws.cell(row=row_idx, column=8, value=options_str)
        
        # 自动调整列宽
        for column in ws.columns:
            max_length = 0
            column_letter = column[0].column_letter
            for cell in column:
                try:
                    if len(str(cell.value)) > max_length:
                        max_length = len(str(cell.value))
                except:
                    pass
            adjusted_width = min(max_length + 2, 50)
            ws.column_dimensions[column_letter].width = adjusted_width
        
        response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
        response['Content-Disposition'] = 'attachment; filename="questions_export.xlsx"'
        wb.save(response)
        return response
    
    # 自定义URL和视图
    def get_urls(self):
        from django.urls import path
        urls = super().get_urls()
        custom_urls = [
            path('change_category/', self.admin_site.admin_view(self.change_category_view), 
                 name='survey_question_change_category'),
            path('import_questions/', self.admin_site.admin_view(self.import_questions_view), 
                 name='survey_question_import_questions'),
            path('export_template/', self.admin_site.admin_view(self.export_template_view), 
                 name='survey_question_export_template'),
        ]
        return custom_urls + urls
    
    def change_category_view(self, request):
        """批量更改分类的视图"""
        class CategoryChangeForm(forms.Form):
            category = forms.ModelChoiceField(
                queryset=Category.objects.all(), 
                label='新分类',
                empty_label="请选择分类"
            )
            ids = forms.CharField(widget=forms.HiddenInput())
        
        if request.method == 'POST':
            form = CategoryChangeForm(request.POST)
            if form.is_valid():
                category = form.cleaned_data['category']
                ids = form.cleaned_data['ids'].split(',')
                Question.objects.filter(id__in=ids).update(category=category)
                self.message_user(request, f'成功更新 {len(ids)} 个问题的分类')
                return redirect(reverse('admin:survey_question_changelist'))
        else:
            ids = request.GET.get('ids', '')
            form = CategoryChangeForm(initial={'ids': ids})
        
        return render(request, 'admin/survey/change_category.html', {
            'form': form,
            'title': '批量更改问题分类',
            'opts': self.model._meta,
        })
    
    def import_questions_view(self, request):
        """导入问题的视图"""
        class ImportQuestionsForm(forms.Form):
            file = forms.FileField(
                label='文件 (支持CSV和Excel格式)',
                help_text='请上传包含问题的CSV或Excel文件'
            )
            is_public = forms.BooleanField(
                label='设为公开', 
                initial=True, 
                required=False,
                help_text='导入的问题是否公开可见'
            )
            
            def clean_file(self):
                file = self.cleaned_data['file']
                allowed_types = [
                    'text/csv',
                    'application/vnd.ms-excel',
                    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
                ]
                allowed_extensions = ['.csv', '.xls', '.xlsx']
                
                # 检查文件类型
                if file.content_type not in allowed_types:
                    ext = os.path.splitext(file.name)[1].lower()
                    if ext not in allowed_extensions:
                        raise forms.ValidationError('只支持CSV和Excel文件格式')
                
                # 检查文件大小(限制为5MB)
                max_size = 5 * 1024 * 1024
                if file.size > max_size:
                    raise forms.ValidationError(f'文件大小不能超过{max_size//1024//1024}MB')
                
                return file
        
        if request.method == 'POST':
            form = ImportQuestionsForm(request.POST, request.FILES)
            if form.is_valid():
                try:
                    file = request.FILES['file']
                    is_public = form.cleaned_data['is_public']
                    rows = self._read_import_file(file)
                    
                    created_count = 0
                    error_count = 0
                    error_messages = []
                    
                    for i, row in enumerate(rows, 2):  # 从第2行开始计数(表头是第1行)
                        try:
                            # 处理分类
                            category_name = row.get('分类', '').strip()
                            category = None
                            if category_name:
                                category, _ = Category.objects.get_or_create(
                                    name=category_name, 
                                    defaults={'slug': category_name}
                                )
                            
                            # 处理问题类型
                            question_type = self._parse_question_type(row.get('问题类型', 'text').strip())
                            
                            # 创建问题
                            question = Question.objects.create(
                                text=row.get('问题文本', '').strip(),
                                question_type=question_type,
                                category=category,
                                created_by=request.user,
                                is_public=is_public
                            )
                            created_count += 1
                            
                            # 处理选项
                            options_str = self._get_options_string(row)
                            if options_str and question_type in ['single_choice', 'multiple_choice']:
                                self._create_options(question, options_str)
                                
                        except Exception as e:
                            error_count += 1
                            error_messages.append(f'第{i}行导入失败: {str(e)}')
                    
                    # 显示结果消息
                    if created_count > 0:
                        self.message_user(request, f'成功导入 {created_count} 个问题')
                    if error_count > 0:
                        self.message_user(request, f'有 {error_count} 个问题导入失败: {"; ".join(error_messages[:5])}', 'warning')
                    
                    return redirect(reverse('admin:survey_question_changelist'))
                    
                except Exception as e:
                    form.add_error(None, f'文件处理失败: {str(e)}')
        else:
            form = ImportQuestionsForm()
        
        return render(request, 'admin/survey/import_questions.html', {
            'form': form,
            'title': '导入问题',
            'opts': self.model._meta,
        })
    
    def _read_import_file(self, file):
        """读取导入文件"""
        ext = os.path.splitext(file.name)[1].lower()
        
        if ext == '.csv':
            # 读取CSV文件
            decoded_file = file.read().decode('utf-8').splitlines()
            reader = csv.DictReader(decoded_file)
            return list(reader)
        else:
            # 读取Excel文件
            try:
                from openpyxl import load_workbook
            except ImportError:
                raise Exception('请安装 openpyxl 库以支持Excel导入')
            
            wb = load_workbook(file)
            ws = wb.active
            
            # 获取表头
            header_row = []
            for cell in ws[1]:
                header_row.append(str(cell.value).strip())
            
            # 读取数据行
            rows = []
            for row in ws.iter_rows(min_row=2, values_only=True):
                if all(cell is None for cell in row):
                    continue
                
                row_dict = {}
                for i, cell_value in enumerate(row):
                    if i < len(header_row):
                        row_dict[header_row[i]] = str(cell_value).strip() if cell_value is not None else ''
                rows.append(row_dict)
            
            return rows
    
    def _parse_question_type(self, question_type):
        """解析问题类型"""
        question_type_mapping = {
            '文本题': 'text',
            '单选题': 'single_choice',
            '多选题': 'multiple_choice',
            '评分题': 'rating',
            '日期题': 'date',
            'text': 'text',
            'single_choice': 'single_choice',
            'multiple_choice': 'multiple_choice',
            'rating': 'rating',
            'date': 'date'
        }
        
        return question_type_mapping.get(question_type, 'text')
    
    def _get_options_string(self, row):
        """从行数据中获取选项字符串"""
        possible_fields = ['选项', '选项(格式: 值|标签;值|标签)', 
                          '选项(格式: 值|标签)', '选项(格式: 标签1;标签2;标签3)']
        for field in possible_fields:
            if field in row:
                return row[field].strip()
        return ''
    
    def _create_options(self, question, options_str):
        """创建问题选项"""
        options = options_str.split(';')
        for i, option in enumerate(options):
            if '|' in option:
                # 格式: 值|标签
                value, label = option.split('|', 1)
                value = value.strip()
                label = label.strip()
            else:
                # 格式: 标签
                label = option.strip()
                # 生成值:移除特殊字符,转为小写,空格替换为下划线
                value = re.sub(r'[^\w\s]', '', label)
                value = value.lower().replace(' ', '_')
                if not value:
                    value = f'option_{i+1}'
            
            if value and label:
                Option.objects.create(
                    question=question,
                    value=value,
                    label=label,
                    order=i
                )
    
    def export_template_view(self, request):
        """导出问题模板"""
        export_format = request.GET.get('format', 'csv')
        
        template_data = [
            ['问题文本', '问题类型', '分类', '是否必填', '选项(格式: 标签1;标签2;标签3)'],
            ['您对我们的产品整体满意度如何?', '评分题', '用户体验', '是', ''],
            ['您是通过什么渠道知道我们的?', '单选题', '用户信息', '是', '朋友推荐;广告;搜索引擎;社交媒体;其他'],
            ['您喜欢我们产品的哪些方面?', '多选题', '产品反馈', '否', '产品设计;产品质量;价格合理;客户服务;功能实用'],
            ['您有什么建议或意见?', '文本题', '产品反馈', '否', ''],
            ['您的生日是哪一天?', '日期题', '个人信息', '是', ''],
            ['您对我们的服务评价如何?', '评分题', '服务评价', '是', ''],
            ['您使用过我们的哪些产品?', '多选题', '产品使用', '是', '产品1;产品2;产品3;产品4'],
            ['您希望我们添加哪些功能?', '文本题', '产品建议', '否', ''],
            ['您是在哪里购买我们的产品的?', '单选题', '购买渠道', '否', '线上;线下;其他']
        ]
        
        if export_format == 'excel':
            return self._export_template_excel(template_data)
        else:
            return self._export_template_csv(template_data)
    
    def _export_template_csv(self, template_data):
        """导出CSV模板"""
        response = HttpResponse(content_type='text/csv; charset=utf-8-sig')
        response['Content-Disposition'] = 'attachment; filename="question_template.csv"'
        
        writer = csv.writer(response)
        for row in template_data:
            writer.writerow(row)
        
        return response
    
    def _export_template_excel(self, template_data):
        """导出Excel模板"""
        try:
            from openpyxl import Workbook
            from openpyxl.styles import Font, Alignment
        except ImportError:
            return HttpResponse('请先安装 openpyxl 库:pip install openpyxl', status=500)
        
        wb = Workbook()
        ws = wb.active
        ws.title = "问题模板"
        
        # 设置表头样式
        header_font = Font(bold=True)
        header_alignment = Alignment(horizontal="center", vertical="center")
        
        # 写入数据
        for row_idx, row_data in enumerate(template_data, 1):
            for col_idx, cell_value in enumerate(row_data, 1):
                cell = ws.cell(row=row_idx, column=col_idx, value=cell_value)
                if row_idx == 1:
                    cell.font = header_font
                    cell.alignment = header_alignment
        
        # 自动调整列宽
        for column in ws.columns:
            max_length = 0
            column_letter = column[0].column_letter
            for cell in column:
                try:
                    if len(str(cell.value)) > max_length:
                        max_length = len(str(cell.value))
                except:
                    pass
            adjusted_width = min(max_length + 2, 50)
            ws.column_dimensions[column_letter].width = adjusted_width
        
        response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
        response['Content-Disposition'] = 'attachment; filename="question_template.xlsx"'
        wb.save(response)
        return response


@admin.register(Survey)
class SurveyAdmin(admin.ModelAdmin):
    """问卷管理"""
    list_display = ['title', 'created_by', 'created_at', 'is_active', 'response_count', 'view_statistics']
    list_filter = ['is_active', 'created_at']
    search_fields = ['title', 'description']
    readonly_fields = ['created_at', 'updated_at', 'statistics']
    inlines = [SurveyQuestionInline]
    list_select_related = ['created_by']
    
    # 优化查询集
    def get_queryset(self, request):
        return super().get_queryset(request).prefetch_related(
            'responses', 'survey_questions__question'
        ).annotate(response_count=Count('responses'))
    
    # 自定义字段
    def response_count(self, obj):
        return obj.response_count if hasattr(obj, 'response_count') else obj.responses.count()
    response_count.short_description = '回答数量'
    response_count.admin_order_field = 'response_count'
    
    def view_statistics(self, obj):
        """查看详细统计的链接"""
        url = reverse('admin:survey_statistics', args=[obj.pk])
        return format_html(
            '<a href="{}" class="button" target="_blank">查看统计</a>',
            url
        )
    view_statistics.short_description = '详细统计'
    
    def statistics(self, obj):
        """显示问卷统计信息"""
        total_responses = obj.responses.count()
        html = f"<h3>问卷统计</h3>"
        html += f"<p><strong>总回答数:</strong>{total_responses}</p>"
        
        survey_questions = obj.survey_questions.select_related('question').all()
        if survey_questions:
            html += "<h4>问题详情:</h4><ul>"
            for sq in survey_questions:
                answer_count = sq.question.answers.count()
                html += f"<li><strong>{sq.question.text}</strong> ({sq.question.get_question_type_display()}):{answer_count} 个回答</li>"
            html += "</ul>"
        
        return format_html(html)
    statistics.short_description = '统计信息'
    
    # 自定义URL和视图
    def get_urls(self):
        from django.urls import path
        urls = super().get_urls()
        custom_urls = [
            path('<uuid:pk>/statistics/', self.admin_site.admin_view(self.statistics_view), 
                 name='survey_statistics'),
        ]
        return custom_urls + urls
    
    def statistics_view(self, request, pk):
        """详细统计视图"""
        survey = self.get_object(request, pk)
        if not survey:
            self.message_user(request, '问卷不存在', 'error')
            return redirect(reverse('admin:survey_survey_changelist'))
        
        # 计算统计数据
        total_responses = survey.responses.count()
        survey_questions = survey.survey_questions.select_related('question').prefetch_related(
            'question__options', 'question__answers'
        ).all().order_by('order')
        
        questions_stats = []
        for sq in survey_questions:
            question = sq.question
            answers = question.answers.all()
            stats = self._calculate_question_stats(question, answers)
            questions_stats.append(stats)
        
        context = {
            **self.admin_site.each_context(request),
            'title': f'{survey.title} - 统计信息',
            'survey': survey,
            'total_responses': total_responses,
            'questions': questions_stats,
            'opts': self.model._meta,
        }
        
        return TemplateResponse(request, 'admin/survey/statistics.html', context)
    
    def _calculate_question_stats(self, question, answers):
        """计算问题统计数据"""
        stats = {
            'question': question,
            'answer_count': answers.count(),
            'type': question.question_type,
            'data': {},
            'options': []
        }
        
        if question.question_type in ['single_choice', 'multiple_choice']:
            # 选择题统计
            option_stats = {}
            for option in question.options.all():
                option_stats[option.value] = {
                    'label': option.label,
                    'count': 0,
                    'percentage': 0.0
                }
            
            for answer in answers:
                choices = answer.answer_choice
                if isinstance(choices, list):
                    for choice in choices:
                        if choice in option_stats:
                            option_stats[choice]['count'] += 1
                elif isinstance(choices, str) and choices in option_stats:
                    option_stats[choices]['count'] += 1
            
            total = answers.count() or 1
            for option_data in option_stats.values():
                option_data['percentage'] = (option_data['count'] / total) * 100
            
            stats['data'] = option_stats
            stats['options'] = list(question.options.all().values('value', 'label'))
            
        elif question.question_type == 'rating':
            # 评分题统计
            ratings = {}
            for i in range(1, 6):
                ratings[str(i)] = {'count': 0, 'percentage': 0.0}
            
            for answer in answers:
                rating = answer.answer_choice
                if isinstance(rating, list) and rating:
                    rating = rating[0]
                if isinstance(rating, str) and rating in ratings:
                    ratings[rating]['count'] += 1
            
            total = answers.count() or 1
            for rating_data in ratings.values():
                rating_data['percentage'] = (rating_data['count'] / total) * 100
            
            stats['data'] = ratings
            
        elif question.question_type == 'text':
            # 文本题统计
            text_answers = []
            for answer in answers[:10]:
                text = answer.answer_text[:100] + ('...' if len(answer.answer_text) > 100 else '')
                text_answers.append(text)
            stats['data'] = text_answers
        
        return stats


@admin.register(Response)
class ResponseAdmin(admin.ModelAdmin):
    """回答记录管理"""
    list_display = ['survey', 'submit_time', 'wechat_nickname', 'completion_time', 'answer_count']
    list_filter = ['submit_time', 'survey']
    search_fields = ['wechat_nickname', 'wechat_openid', 'survey__title']
    readonly_fields = ['submit_time']
    list_select_related = ['survey']
    
    def has_add_permission(self, request):
        return False
    
    def answer_count(self, obj):
        return obj.answers.count()
    answer_count.short_description = '答案数量'


@admin.register(Answer)
class AnswerAdmin(admin.ModelAdmin):
    """答案管理"""
    list_display = ['question_preview', 'survey_preview', 'response', 'answer_preview']
    list_filter = ['response__survey', 'question__question_type']
    search_fields = ['answer_text', 'question__text', 'response__wechat_nickname']
    list_select_related = ['question', 'response__survey']
    
    def question_preview(self, obj):
        return obj.question.text[:50] + '...' if len(obj.question.text) > 50 else obj.question.text
    question_preview.short_description = '问题'
    
    def survey_preview(self, obj):
        return obj.response.survey.title
    survey_preview.short_description = '问卷'
    
    def answer_preview(self, obj):
        """答案预览,处理各种类型的答案"""
        if obj.answer_text:
            return obj.answer_text[:50] + '...' if len(obj.answer_text) > 50 else obj.answer_text
        elif obj.answer_choice:
            if obj.question.question_type in ['single_choice', 'multiple_choice']:
                choices = obj.answer_choice if isinstance(obj.answer_choice, list) else [obj.answer_choice]
                
                # 创建选项映射
                option_map = {option.value: option.label for option in obj.question.options.all()}
                
                # 转换值为标签
                labels = [option_map.get(choice, choice) for choice in choices]
                return ', '.join(labels)
            return str(obj.answer_choice)
        return '-'
    answer_preview.short_description = '答案'


@admin.register(QRCode)
class QRCodeAdmin(admin.ModelAdmin):
    """二维码管理"""
    list_display = ['name', 'survey', 'short_code', 'scan_count', 'created_at', 'qr_code_preview']
    list_filter = ['survey', 'created_at']
    search_fields = ['name', 'short_code', 'survey__title']
    readonly_fields = ['scan_count', 'created_at', 'qr_code_preview', 'download_qrcode']
    list_select_related = ['survey']
    
    fieldsets = (
        (None, {
            'fields': ('survey', 'name', 'short_code')
        }),
        ('统计信息', {
            'fields': ('scan_count', 'created_at'),
            'classes': ('collapse',)
        }),
        ('二维码', {
            'fields': ('qr_code_preview', 'download_qrcode'),
        }),
    )
    
    def get_form(self, request, obj=None, **kwargs):
        """处理GET参数,自动填充survey字段"""
        form = super().get_form(request, obj, **kwargs)
        survey_id = request.GET.get('survey')
        if survey_id and not obj:
            try:
                from .models import Survey
                Survey.objects.get(pk=survey_id)  # 验证survey存在
                form.base_fields['survey'].initial = survey_id
                form.base_fields['survey'].widget.attrs['readonly'] = True
                form.base_fields['survey'].disabled = True
            except Exception:
                pass
        return form
    
    def save_model(self, request, obj, form, change):
        """保存模型时自动生成短代码"""
        if not change and request.GET.get('survey'):
            try:
                from .models import Survey
                survey = Survey.objects.get(pk=request.GET.get('survey'))
                obj.survey = survey
            except Survey.DoesNotExist:
                pass
        
        # 生成唯一的短代码
        if not obj.short_code:
            obj.short_code = self._generate_unique_short_code()
        
        super().save_model(request, obj, form, change)
    
    def _generate_unique_short_code(self, length=8):
        """生成唯一的短代码"""
        max_attempts = 10
        for _ in range(max_attempts):
            code = ''.join(random.choices(string.ascii_lowercase + string.digits, k=length))
            if not QRCode.objects.filter(short_code=code).exists():
                return code
        # 如果多次尝试失败,使用UUID
        return str(uuid.uuid4())[:8]
    
    def qr_code_preview(self, obj):
        """二维码预览"""
        if not obj.short_code:
            return '请先保存生成二维码'
        
        return format_html('''
            <div style="margin: 10px 0;">
                <strong>二维码预览:</strong><br>
                <img src="/qrcode/{}/image/" alt="二维码" style="width: 200px; height: 200px; margin: 10px 0; border: 1px solid #ddd; padding: 5px;"><br>
                <a href="/qrcode/{}/image/" target="_blank" style="margin-right: 10px;">查看大图</a>
            </div>
        ''', obj.short_code, obj.short_code)
    qr_code_preview.short_description = '二维码预览'
    
    def download_qrcode(self, obj):
        """下载二维码"""
        if not obj.short_code:
            return ''
        
        return format_html('''
            <div style="margin: 10px 0;">
                <a href="/qrcode/{}/image/" download="qrcode_{}_{}.png" class="button" style="background-color: #4CAF50; color: white; padding: 8px 16px; text-decoration: none; border-radius: 4px;">
                    📥 下载二维码
                </a>
            </div>
        ''', obj.short_code, obj.short_code, obj.name)
    download_qrcode.short_description = '下载'

Internal Server Error: /admin/papers/paper/2/change/ Traceback (most recent call last): File "C:\Python310\lib\site-packages\django\core\handlers\exception.py", line 55, in inner response = get_response(request) File "C:\Python310\lib\site-packages\django\core\handlers\base.py", line 197, in _get_response response = wrapped_callback(request, *callback_args, **callback_kwargs) File "C:\Python310\lib\site-packages\django\contrib\admin\options.py", line 719, in wrapper return self.admin_site.admin_view(view)(*args, **kwargs) File "C:\Python310\lib\site-packages\django\utils\decorators.py", line 192, in _view_wrapper result = _process_exception(request, e) File "C:\Python310\lib\site-packages\django\utils\decorators.py", line 190, in _view_wrapper response = view_func(request, *args, **kwargs) File "C:\Python310\lib\site-packages\django\views\decorators\cache.py", line 80, in _view_wrapper response = view_func(request, *args, **kwargs) File "C:\Python310\lib\site-packages\django\contrib\admin\sites.py", line 246, in inner return view(request, *args, **kwargs) File "C:\Python310\lib\site-packages\django\contrib\admin\options.py", line 1987, in change_view return self.changeform_view(request, object_id, form_url, extra_context) File "C:\Python310\lib\site-packages\django\utils\decorators.py", line 48, in _wrapper return bound_method(*args, **kwargs) File "C:\Python310\lib\site-packages\django\utils\decorators.py", line 192, in _view_wrapper result = _process_exception(request, e) File "C:\Python310\lib\site-packages\django\utils\decorators.py", line 190, in _view_wrapper response = view_func(request, *args, **kwargs) File "C:\Python310\lib\site-packages\django\contrib\admin\options.py", line 1843, in changeform_view return self._changeform_view(request, object_id, form_url, extra_context) File "C:\Python310\lib\site-packages\django\contrib\admin\options.py", line 1895, in _changeform_view self.save_related(request, form, formsets, not add) File "C:\Python310\lib\site-packages\django\contrib\admin\options.py", line 1342, in save_related self.save_formset(request, form, formset, change=change) File "C:\Python310\lib\site-packages\django\contrib\admin\options.py", line 1330, in save_formset formset.save() File "C:\Python310\lib\site-packages\django\forms\models.py", line 796, in save return self.save_existing_objects(commit) + self.save_new_objects(commit) File "C:\Python310\lib\site-packages\django\forms\models.py", line 959, in save_new_objects self.new_objects.append(self.save_new(form, commit=commit)) File "D:\pythonSpace\hitest\apps\papers\admin.py", line 21, in save_new max_order = instance.order.aggregate( AttributeError: 'int' object has no attribute 'aggregate'
10-23
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值