在Django Admin中限制/过滤外键选项:竞赛题关联需求
Got it, let's tackle this problem to make sure each ContestProblem is linked to exactly one Contest, while letting a Contest have multiple problems. We'll handle this both at the Django Admin level (for smooth user experience) and database level (for rock-solid data integrity).
Step 1: Add a Unique Constraint to the Intermediate Table
First, we need to enforce that a ContestProblem can't be linked to more than one Contest at the database level. This acts as a safety net to prevent duplicates even if someone uses the Django shell or an API to create entries.
Update your ProblemsInContest model:
class ProblemsInContest(CreateUpdateDateModel): contest = models.ForeignKey(Contest, on_delete=models.CASCADE) problem = models.ForeignKey(ContestProblem, on_delete=models.CASCADE) class Meta: verbose_name = "Problem in Contest" verbose_name_plural = "Problems in Contest" # Add a unique constraint to ensure one problem per contest constraints = [ models.UniqueConstraint( fields=['problem'], name='unique_problem_contest_link' ) ] def __str__(self): return f"{self.contest} - {self.problem}"
Step 2: Filter Foreign Key Options in the Admin
Next, we'll modify the Django Admin for ProblemsInContest to only show unused ContestProblem options when creating or editing entries. This removes confusion and prevents users from selecting already-linked problems.
Create or update your admin.py file:
from django.contrib import admin from .models import Contest, ContestProblem, ProblemsInContest class ProblemsInContestAdmin(admin.ModelAdmin): list_display = ('contest', 'problem', 'created_at') # Adjust fields to match your model's actual attributes def formfield_for_foreignkey(self, db_field, request, **kwargs): if db_field.name == 'problem': # Get the ID of the current entry being edited (if we're on an edit page) current_entry_id = request.resolver_match.kwargs.get('object_id') # Exclude problems already linked to other contests (but keep the current entry's problem if editing) used_problem_ids = ProblemsInContest.objects.exclude(id=current_entry_id).values_list('problem_id', flat=True) kwargs['queryset'] = ContestProblem.objects.exclude(id__in=used_problem_ids) return super().formfield_for_foreignkey(db_field, request, **kwargs) admin.site.register(ProblemsInContest, ProblemsInContestAdmin)
Step 3: (Optional) Improve UX with Inline Admin for Contests
For a more intuitive workflow, you can add an inline to the Contest Admin so users can manage linked problems directly from the Contest edit page, instead of navigating to the intermediate table separately.
Add this to your admin.py:
class ProblemsInContestInline(admin.TabularInline): model = ProblemsInContest extra = 1 # Show one empty row by default for adding new problems def formfield_for_foreignkey(self, db_field, request, **kwargs): if db_field.name == 'problem': # Exclude problems already used by other contests current_contest_id = request.resolver_match.kwargs.get('object_id') used_problem_ids = ProblemsInContest.objects.exclude(contest_id=current_contest_id).values_list('problem_id', flat=True) kwargs['queryset'] = ContestProblem.objects.exclude(id__in=used_problem_ids) return super().formfield_for_foreignkey(db_field, request, **kwargs) class ContestAdmin(admin.ModelAdmin): inlines = [ProblemsInContestInline] # Add other Contest admin configurations here (like list_display, fields, etc.) admin.site.register(Contest, ContestAdmin)
Why This Works
- Database Constraint: The unique constraint ensures that even if someone bypasses the Admin interface, the database will reject duplicate links between a
ContestProblemand multipleContests. - Admin Filtering: Users won't see already-used problems in the dropdown, so they can't accidentally select a problem that's already linked to another contest.
- Inline Admin: Makes managing contest-problem associations more seamless by keeping everything within the Contest edit page.
内容的提问来源于stack exchange,提问作者inquilabee

