diff --git a/backend/spellbook/admin.py b/backend/spellbook/admin.py
deleted file mode 100644
index a58c5d1f..00000000
--- a/backend/spellbook/admin.py
+++ /dev/null
@@ -1,337 +0,0 @@
-from django.contrib import admin, messages
-from django.urls import path
-from .utils import launch_job_command
-from .models import Card, Template, Feature, Combo, CardInCombo, TemplateInCombo, Variant, CardInVariant, TemplateInVariant, Job
-from django.contrib.admin.models import LogEntry, DELETION
-from django.forms import ModelForm
-from django.utils import timezone
-from django.http import HttpRequest
-from django.shortcuts import redirect
-from django.db.models import Count
-from .variants.combo_graph import MAX_CARDS_IN_COMBO
-from django.urls import reverse
-from django.utils.html import format_html
-from .variants.variant_data import RestoreData
-from .variants.variants_generator import restore_variant
-
-
-# Admin configuration
-admin.site.site_header = 'Spellbook Admin Panel'
-admin.site.site_title = 'Spellbook Admin'
-admin.site.index_title = 'Spellbook Admin Index'
-
-
-@admin.register(Card)
-class CardAdmin(admin.ModelAdmin):
- fieldsets = [
- ('Spellbook', {'fields': ['name', 'features']}),
- ('Scryfall', {'fields': ['oracle_id', 'identity', 'legal']}),
- ]
- # inlines = [FeatureInline]
- list_filter = ['identity', 'legal']
- search_fields = ['name', 'features__name']
- autocomplete_fields = ['features']
- list_display = ['name', 'identity', 'id']
-
-
-class CardInFeatureAdminInline(admin.StackedInline):
- model = Feature.cards.through
- extra = 1
- autocomplete_fields = ['card']
- verbose_name = 'Produced by card'
- verbose_name_plural = 'Produced by cards'
-
-
-@admin.register(Feature)
-class FeatureAdmin(admin.ModelAdmin):
- fieldsets = [
- (None, {'fields': ['name', 'utility', 'description']}),
- ]
- inlines = [CardInFeatureAdminInline]
- search_fields = ['name', 'cards__name']
- list_display = ['name', 'utility', 'id']
- list_filter = ['utility']
-
-
-@admin.action(description='Mark selected variants as RESTORE')
-def set_restore(modeladmin, request, queryset):
- queryset.update(status=Variant.Status.RESTORE)
-
-
-@admin.action(description='Mark selected variants as DRAFT')
-def set_draft(modeladmin, request, queryset):
- queryset.update(status=Variant.Status.DRAFT)
-
-
-@admin.action(description='Mark selected variants as NEW')
-def set_new(modeladmin, request, queryset):
- queryset.update(status=Variant.Status.NEW)
-
-
-@admin.action(description='Mark selected variants as NOT WORKING')
-def set_not_working(modeladmin, request, queryset):
- queryset.update(status=Variant.Status.NOT_WORKING)
-
-
-class VariantForm(ModelForm):
- def clean_mana_needed(self):
- return self.cleaned_data['mana_needed'].upper() if self.cleaned_data['mana_needed'] else self.cleaned_data['mana_needed']
-
-
-class CardsCountListFilter(admin.SimpleListFilter):
- title = 'cards count'
- parameter_name = 'cards_count'
-
- def lookups(self, request, model_admin):
- return [(i, str(i)) for i in range(2, MAX_CARDS_IN_COMBO + 1)]
-
- def queryset(self, request, queryset):
- if self.value() is not None:
- value = int(self.value())
- return queryset.annotate(cards_count=Count('uses', distinct=True) + Count('requires', distinct=True)).filter(cards_count=value)
- return queryset
-
-
-class ComboForm(ModelForm):
- def clean_mana_needed(self):
- return self.cleaned_data['mana_needed'].upper() if self.cleaned_data['mana_needed'] else self.cleaned_data['mana_needed']
-
-
-class IngredientInComboForm(ModelForm):
- def clean(self):
- if hasattr(self.cleaned_data['combo'], 'ingredient_count'):
- self.cleaned_data['combo'].ingredient_count += 1
- else:
- self.cleaned_data['combo'].ingredient_count = 1
- self.instance.order = self.cleaned_data['combo'].ingredient_count
- return super().clean()
-
-
-class CardInComboAdminInline(admin.TabularInline):
- fields = ['card', 'zone_location', 'card_state']
- form = IngredientInComboForm
- model = CardInCombo
- extra = 0
- verbose_name = 'Card'
- verbose_name_plural = 'Required Cards'
- autocomplete_fields = ['card']
- max_num = MAX_CARDS_IN_COMBO
-
-
-class TemplateInComboAdminInline(admin.TabularInline):
- fields = ['template', 'zone_location', 'card_state']
- form = IngredientInComboForm
- model = TemplateInCombo
- extra = 0
- verbose_name = 'Template'
- verbose_name_plural = 'Required Templates'
- autocomplete_fields = ['template']
- max_num = MAX_CARDS_IN_COMBO
-
-
-class FeatureInComboAdminInline(admin.TabularInline):
- model = Combo.needs.through
- extra = 0
- verbose_name = 'Feature'
- verbose_name_plural = 'Required Features'
- autocomplete_fields = ['feature']
- max_num = MAX_CARDS_IN_COMBO
-
-
-@admin.register(Combo)
-class ComboAdmin(admin.ModelAdmin):
- form = ComboForm
- save_as = True
- readonly_fields = ['scryfall_link']
- fieldsets = [
- ('Generated', {'fields': ['scryfall_link']}),
- ('More Requirements', {'fields': [
- 'mana_needed',
- 'other_prerequisites']}),
- ('Features', {'fields': ['produces', 'removes']}),
- ('Description', {'fields': ['generator', 'description']}),
- ]
- inlines = [CardInComboAdminInline, TemplateInComboAdminInline, FeatureInComboAdminInline]
- filter_horizontal = ['uses', 'produces', 'needs', 'removes']
- list_filter = ['generator']
- search_fields = ['uses__name', 'requires__name', 'produces__name', 'needs__name']
- list_display = ['__str__', 'generator', 'id']
-
- def get_queryset(self, request):
- return super().get_queryset(request).prefetch_related('uses', 'requires', 'produces', 'needs', 'removes')
-
- def save_related(self, request, form, formsets, change):
- super().save_related(request, form, formsets, change)
- if change:
- query = form.instance.variants.filter(status__in=[Variant.Status.NEW, Variant.Status.RESTORE])
- count = query.count()
- if count <= 0:
- return
- if count >= 1000:
- messages.warning(request, f'{count} "New" or "Restore" variants are too many to update for this combo: no automatic update was done.')
- return
- variants_to_update = list[Variant]()
- card_in_variants_to_update = list[CardInVariant]()
- template_in_variants_to_update = list[TemplateInVariant]()
- data = RestoreData()
- for variant in list[Variant](query):
- uses_set, requires_set = restore_variant(
- variant,
- list(variant.includes.all()),
- list(variant.of.all()),
- list(variant.cardinvariant_set.all()),
- list(variant.templateinvariant_set.all()),
- data=data)
- card_in_variants_to_update.extend(uses_set)
- template_in_variants_to_update.extend(requires_set)
- update_fields = ['status', 'mana_needed', 'other_prerequisites', 'description', 'legal', 'identity']
- Variant.objects.bulk_update(variants_to_update, update_fields)
- update_fields = ['zone_location', 'card_state', 'order']
- CardInVariant.objects.bulk_update(card_in_variants_to_update, update_fields)
- TemplateInVariant.objects.bulk_update(template_in_variants_to_update, update_fields)
-
-
-class CardInVariantAdminInline(admin.TabularInline):
- readonly_fields = ['card_name']
- fields = ['card_name', 'zone_location', 'card_state']
- model = CardInVariant
- extra = 0
- verbose_name = 'Card'
- verbose_name_plural = 'Cards'
- can_delete = False
-
- def has_add_permission(self, request, obj) -> bool:
- return False
-
- def has_delete_permission(self, request, obj) -> bool:
- return False
-
- def card_name(self, instance):
- card = instance.card
- html = '{}'
- return format_html(html, reverse('admin:spellbook_card_change', args=(card.id,)), card.name)
-
-
-class TemplateInVariantAdminInline(admin.TabularInline):
- readonly_fields = ['template']
- fields = ['template', 'zone_location', 'card_state']
- model = TemplateInVariant
- extra = 0
- verbose_name = 'Template'
- verbose_name_plural = 'Templates'
- can_delete = False
-
- def has_add_permission(self, request, obj) -> bool:
- return False
-
- def has_delete_permission(self, request, obj) -> bool:
- return False
-
-
-@admin.register(Variant)
-class VariantAdmin(admin.ModelAdmin):
- form = VariantForm
- inlines = [CardInVariantAdminInline, TemplateInVariantAdminInline]
- readonly_fields = ['produces', 'of', 'includes', 'unique_id', 'identity', 'legal', 'scryfall_link']
- fieldsets = [
- ('Generated', {'fields': [
- 'unique_id',
- 'produces',
- 'of',
- 'includes',
- 'identity',
- 'legal',
- 'scryfall_link']}),
- ('Editable', {'fields': [
- 'status',
- 'mana_needed',
- 'other_prerequisites',
- 'description',
- 'frozen']})
- ]
- list_filter = ['status', CardsCountListFilter, 'identity', 'legal']
- list_display = ['__str__', 'status', 'id', 'identity']
- search_fields = ['id', 'uses__name', 'produces__name', 'requires__name', 'unique_id', 'identity']
- actions = [set_restore, set_draft, set_new, set_not_working]
-
- def generate(self, request: HttpRequest):
- if request.method == 'POST' and request.user.is_authenticated:
- if (launch_job_command('generate_variants', timezone.timedelta(minutes=30), request.user)):
- messages.info(request, 'Variant generation job started.')
- else:
- messages.warning(request, 'Variant generation is already running.')
- return redirect('admin:spellbook_job_changelist')
-
- def export(self, request: HttpRequest):
- if request.method == 'POST' and request.user.is_authenticated:
- if (launch_job_command('export_variants', timezone.timedelta(minutes=1), request.user)):
- messages.info(request, 'Variant exporting job started.')
- else:
- messages.warning(request, 'Variant exporting is already running.')
- return redirect('admin:spellbook_job_changelist')
-
- def get_urls(self):
- return [path('generate/',
- self.admin_site.admin_view(view=self.generate, cacheable=False),
- name='spellbook_variant_generate'),
- path('export/',
- self.admin_site.admin_view(view=self.export, cacheable=False),
- name='spellbook_variant_export')] + super().get_urls()
-
- def has_add_permission(self, request):
- return False
-
- def has_delete_permission(self, request, obj=None):
- return False
-
- def get_queryset(self, request):
- return super().get_queryset(request) \
- .prefetch_related('uses', 'requires', 'produces', 'of', 'includes')
-
-
-@admin.register(Job)
-class JobAdmin(admin.ModelAdmin):
- fields = ['id', 'name', 'status', 'created', 'expected_termination', 'termination', 'message', 'started_by']
- list_display = ['id', 'name', 'status', 'created', 'expected_termination', 'termination']
-
- def has_add_permission(self, request):
- return False
-
- def has_change_permission(self, request, obj=None):
- return False
-
- def has_delete_permission(self, request, obj=None):
- return False
-
-
-@admin.register(Template)
-class TemplateAdmin(admin.ModelAdmin):
- readonly_fields = ['scryfall_link']
- fields = ['name', 'scryfall_query', 'scryfall_link']
- list_display = ['name', 'scryfall_query', 'id']
- search_fields = ['name', 'scryfall_query']
-
-
-@admin.register(LogEntry)
-class LogEntryAdmin(admin.ModelAdmin):
- date_hierarchy = 'action_time'
- list_filter = ['user', 'content_type', 'action_flag']
- search_fields = ['object_repr', 'change_message']
- list_display = ['action_time', 'user', 'content_type', 'object_link', 'action_flag']
-
- def has_add_permission(self, request: HttpRequest) -> bool:
- return False
-
- def has_change_permission(self, request: HttpRequest, obj=None) -> bool:
- return False
-
- def has_delete_permission(self, request: HttpRequest, obj=None) -> bool:
- return False
-
- @admin.display(ordering='object_repr', description='object')
- def object_link(self, obj: LogEntry) -> str:
- if obj.action_flag == DELETION:
- return format_html('{}', obj.object_repr)
- if obj.get_admin_url():
- return format_html('{}', obj.get_admin_url(), obj.object_repr)
- return format_html('{}', obj.object_repr)
diff --git a/backend/spellbook/admin/__init__.py b/backend/spellbook/admin/__init__.py
new file mode 100644
index 00000000..f06787bc
--- /dev/null
+++ b/backend/spellbook/admin/__init__.py
@@ -0,0 +1,12 @@
+from .card_admin import CardAdmin
+from .template_admin import TemplateAdmin
+from .feature_admin import FeatureAdmin
+from .combo_admin import ComboAdmin
+from .variant_admin import VariantAdmin
+from .job_admin import JobAdmin
+from .log_admin import LogEntryAdmin
+
+from django.contrib import admin
+admin.site.site_header = 'Spellbook Admin Panel'
+admin.site.site_title = 'Spellbook Admin'
+admin.site.index_title = 'Spellbook Admin Index'
diff --git a/backend/spellbook/admin/card_admin.py b/backend/spellbook/admin/card_admin.py
new file mode 100644
index 00000000..ef5db157
--- /dev/null
+++ b/backend/spellbook/admin/card_admin.py
@@ -0,0 +1,15 @@
+from django.contrib import admin
+from ..models import Card
+
+
+@admin.register(Card)
+class CardAdmin(admin.ModelAdmin):
+ fieldsets = [
+ ('Spellbook', {'fields': ['name', 'features']}),
+ ('Scryfall', {'fields': ['oracle_id', 'identity', 'legal']}),
+ ]
+ # inlines = [FeatureInline]
+ list_filter = ['identity', 'legal']
+ search_fields = ['name', 'features__name']
+ autocomplete_fields = ['features']
+ list_display = ['name', 'identity', 'id']
diff --git a/backend/spellbook/admin/combo_admin.py b/backend/spellbook/admin/combo_admin.py
new file mode 100644
index 00000000..f9f73fda
--- /dev/null
+++ b/backend/spellbook/admin/combo_admin.py
@@ -0,0 +1,107 @@
+from django.contrib import admin, messages
+from django.forms import ModelForm
+from ..models import Combo, CardInCombo, TemplateInCombo, Variant, CardInVariant, TemplateInVariant
+from ..variants.combo_graph import MAX_CARDS_IN_COMBO
+from ..variants.variant_data import RestoreData
+from ..variants.variants_generator import restore_variant
+from .mixins import SearchMultipleRelatedMixin
+
+
+class ComboForm(ModelForm):
+ def clean_mana_needed(self):
+ return self.cleaned_data['mana_needed'].upper() if self.cleaned_data['mana_needed'] else self.cleaned_data['mana_needed']
+
+
+class IngredientInComboForm(ModelForm):
+ def clean(self):
+ if hasattr(self.cleaned_data['combo'], 'ingredient_count'):
+ self.cleaned_data['combo'].ingredient_count += 1
+ else:
+ self.cleaned_data['combo'].ingredient_count = 1
+ self.instance.order = self.cleaned_data['combo'].ingredient_count
+ return super().clean()
+
+
+class CardInComboAdminInline(admin.TabularInline):
+ fields = ['card', 'zone_location', 'card_state']
+ form = IngredientInComboForm
+ model = CardInCombo
+ extra = 0
+ verbose_name = 'Card'
+ verbose_name_plural = 'Required Cards'
+ autocomplete_fields = ['card']
+ max_num = MAX_CARDS_IN_COMBO
+
+
+class TemplateInComboAdminInline(admin.TabularInline):
+ fields = ['template', 'zone_location', 'card_state']
+ form = IngredientInComboForm
+ model = TemplateInCombo
+ extra = 0
+ verbose_name = 'Template'
+ verbose_name_plural = 'Required Templates'
+ autocomplete_fields = ['template']
+ max_num = MAX_CARDS_IN_COMBO
+
+
+class FeatureInComboAdminInline(admin.TabularInline):
+ model = Combo.needs.through
+ extra = 0
+ verbose_name = 'Feature'
+ verbose_name_plural = 'Required Features'
+ autocomplete_fields = ['feature']
+ max_num = MAX_CARDS_IN_COMBO
+
+
+@admin.register(Combo)
+class ComboAdmin(SearchMultipleRelatedMixin, admin.ModelAdmin):
+ form = ComboForm
+ save_as = True
+ readonly_fields = ['scryfall_link']
+ fieldsets = [
+ ('Generated', {'fields': ['scryfall_link']}),
+ ('More Requirements', {'fields': [
+ 'mana_needed',
+ 'other_prerequisites']}),
+ ('Features', {'fields': ['produces', 'removes']}),
+ ('Description', {'fields': ['generator', 'description']}),
+ ]
+ inlines = [CardInComboAdminInline, TemplateInComboAdminInline, FeatureInComboAdminInline]
+ filter_horizontal = ['uses', 'produces', 'needs', 'removes']
+ list_filter = ['generator']
+ search_fields = ['uses__name', 'requires__name', 'produces__name', 'needs__name']
+ list_display = ['__str__', 'generator', 'id']
+
+ def get_queryset(self, request):
+ return super().get_queryset(request).prefetch_related('uses', 'requires', 'produces', 'needs', 'removes')
+
+ def save_related(self, request, form, formsets, change):
+ super().save_related(request, form, formsets, change)
+ if change:
+ query = form.instance.variants.filter(status__in=[Variant.Status.NEW, Variant.Status.RESTORE])
+ count = query.count()
+ if count <= 0:
+ return
+ if count >= 1000:
+ messages.warning(request, f'{count} "New" or "Restore" variants are too many to update for this combo: no automatic update was done.')
+ return
+ variants_to_update = list[Variant]()
+ card_in_variants_to_update = list[CardInVariant]()
+ template_in_variants_to_update = list[TemplateInVariant]()
+ data = RestoreData()
+ for variant in list[Variant](query):
+ uses_set, requires_set = restore_variant(
+ variant,
+ list(variant.includes.all()),
+ list(variant.of.all()),
+ list(variant.cardinvariant_set.all()),
+ list(variant.templateinvariant_set.all()),
+ data=data)
+ card_in_variants_to_update.extend(uses_set)
+ template_in_variants_to_update.extend(requires_set)
+ update_fields = ['status', 'mana_needed', 'other_prerequisites', 'description', 'legal', 'identity']
+ Variant.objects.bulk_update(variants_to_update, update_fields)
+ update_fields = ['zone_location', 'card_state', 'order']
+ CardInVariant.objects.bulk_update(card_in_variants_to_update, update_fields)
+ TemplateInVariant.objects.bulk_update(template_in_variants_to_update, update_fields)
+ messages.info(request, f'{count} "New" or "Restore" variants were updated for this combo.')
diff --git a/backend/spellbook/admin/feature_admin.py b/backend/spellbook/admin/feature_admin.py
new file mode 100644
index 00000000..5f58e6b2
--- /dev/null
+++ b/backend/spellbook/admin/feature_admin.py
@@ -0,0 +1,21 @@
+from django.contrib import admin
+from ..models import Feature
+
+
+class CardInFeatureAdminInline(admin.StackedInline):
+ model = Feature.cards.through
+ extra = 1
+ autocomplete_fields = ['card']
+ verbose_name = 'Produced by card'
+ verbose_name_plural = 'Produced by cards'
+
+
+@admin.register(Feature)
+class FeatureAdmin(admin.ModelAdmin):
+ fieldsets = [
+ (None, {'fields': ['name', 'utility', 'description']}),
+ ]
+ inlines = [CardInFeatureAdminInline]
+ search_fields = ['name', 'cards__name']
+ list_display = ['name', 'utility', 'id']
+ list_filter = ['utility']
diff --git a/backend/spellbook/admin/job_admin.py b/backend/spellbook/admin/job_admin.py
new file mode 100644
index 00000000..28267525
--- /dev/null
+++ b/backend/spellbook/admin/job_admin.py
@@ -0,0 +1,17 @@
+from django.contrib import admin
+from ..models import Job
+
+
+@admin.register(Job)
+class JobAdmin(admin.ModelAdmin):
+ fields = ['id', 'name', 'status', 'created', 'expected_termination', 'termination', 'message', 'started_by']
+ list_display = ['id', 'name', 'status', 'created', 'expected_termination', 'termination']
+
+ def has_add_permission(self, request):
+ return False
+
+ def has_change_permission(self, request, obj=None):
+ return False
+
+ def has_delete_permission(self, request, obj=None):
+ return False
diff --git a/backend/spellbook/admin/log_admin.py b/backend/spellbook/admin/log_admin.py
new file mode 100644
index 00000000..7adafd13
--- /dev/null
+++ b/backend/spellbook/admin/log_admin.py
@@ -0,0 +1,28 @@
+from django.contrib import admin
+from django.contrib.admin.models import LogEntry, DELETION
+from django.utils.html import format_html
+
+
+@admin.register(LogEntry)
+class LogEntryAdmin(admin.ModelAdmin):
+ date_hierarchy = 'action_time'
+ list_filter = ['user', 'content_type', 'action_flag']
+ search_fields = ['object_repr', 'change_message']
+ list_display = ['action_time', 'user', 'content_type', 'object_link', 'action_flag']
+
+ def has_add_permission(self, request) -> bool:
+ return False
+
+ def has_change_permission(self, request, obj=None) -> bool:
+ return False
+
+ def has_delete_permission(self, request, obj=None) -> bool:
+ return False
+
+ @admin.display(ordering='object_repr', description='object')
+ def object_link(self, obj: LogEntry) -> str:
+ if obj.action_flag == DELETION:
+ return format_html('{}', obj.object_repr)
+ if obj.get_admin_url():
+ return format_html('{}', obj.get_admin_url(), obj.object_repr)
+ return format_html('{}', obj.object_repr)
diff --git a/backend/spellbook/admin/mixins.py b/backend/spellbook/admin/mixins.py
new file mode 100644
index 00000000..816a91b8
--- /dev/null
+++ b/backend/spellbook/admin/mixins.py
@@ -0,0 +1,10 @@
+class SearchMultipleRelatedMixin:
+ def get_search_results(self, request, queryset, search_term: str):
+ result = queryset
+ may_have_duplicates = False
+ for sub_term in search_term.split(' + '):
+ sub_term = sub_term.strip()
+ if sub_term:
+ result, d = super().get_search_results(request, result, sub_term)
+ may_have_duplicates |= d
+ return result, may_have_duplicates
diff --git a/backend/spellbook/admin/template_admin.py b/backend/spellbook/admin/template_admin.py
new file mode 100644
index 00000000..bc13a3d3
--- /dev/null
+++ b/backend/spellbook/admin/template_admin.py
@@ -0,0 +1,10 @@
+from django.contrib import admin
+from ..models import Template
+
+
+@admin.register(Template)
+class TemplateAdmin(admin.ModelAdmin):
+ readonly_fields = ['scryfall_link']
+ fields = ['name', 'scryfall_query', 'scryfall_link']
+ list_display = ['name', 'scryfall_query', 'id']
+ search_fields = ['name', 'scryfall_query']
diff --git a/backend/spellbook/admin/variant_admin.py b/backend/spellbook/admin/variant_admin.py
new file mode 100644
index 00000000..4d3005cc
--- /dev/null
+++ b/backend/spellbook/admin/variant_admin.py
@@ -0,0 +1,149 @@
+from django.contrib import admin, messages
+from django.utils.html import format_html
+from django.urls import reverse, path
+from django.db.models import Count
+from django.forms import ModelForm
+from django.http import HttpRequest
+from django.shortcuts import redirect
+from django.utils import timezone
+from ..models import Variant, CardInVariant, TemplateInVariant
+from ..variants.combo_graph import MAX_CARDS_IN_COMBO
+from ..utils import launch_job_command
+from .mixins import SearchMultipleRelatedMixin
+
+
+class CardInVariantAdminInline(admin.TabularInline):
+ readonly_fields = ['card_name']
+ fields = ['card_name', 'zone_location', 'card_state']
+ model = CardInVariant
+ extra = 0
+ verbose_name = 'Card'
+ verbose_name_plural = 'Cards'
+ can_delete = False
+
+ def has_add_permission(self, request, obj) -> bool:
+ return False
+
+ def has_delete_permission(self, request, obj) -> bool:
+ return False
+
+ def card_name(self, instance):
+ card = instance.card
+ html = '{}'
+ return format_html(html, reverse('admin:spellbook_card_change', args=(card.id,)), card.name)
+
+
+class TemplateInVariantAdminInline(admin.TabularInline):
+ readonly_fields = ['template']
+ fields = ['template', 'zone_location', 'card_state']
+ model = TemplateInVariant
+ extra = 0
+ verbose_name = 'Template'
+ verbose_name_plural = 'Templates'
+ can_delete = False
+
+ def has_add_permission(self, request, obj) -> bool:
+ return False
+
+ def has_delete_permission(self, request, obj) -> bool:
+ return False
+
+
+class CardsCountListFilter(admin.SimpleListFilter):
+ title = 'cards count'
+ parameter_name = 'cards_count'
+
+ def lookups(self, request, model_admin):
+ return [(i, str(i)) for i in range(2, MAX_CARDS_IN_COMBO + 1)]
+
+ def queryset(self, request, queryset):
+ if self.value() is not None:
+ value = int(self.value())
+ return queryset.annotate(cards_count=Count('uses', distinct=True) + Count('requires', distinct=True)).filter(cards_count=value)
+ return queryset
+
+
+@admin.action(description='Mark selected variants as RESTORE')
+def set_restore(modeladmin, request, queryset):
+ queryset.update(status=Variant.Status.RESTORE)
+
+
+@admin.action(description='Mark selected variants as DRAFT')
+def set_draft(modeladmin, request, queryset):
+ queryset.update(status=Variant.Status.DRAFT)
+
+
+@admin.action(description='Mark selected variants as NEW')
+def set_new(modeladmin, request, queryset):
+ queryset.update(status=Variant.Status.NEW)
+
+
+@admin.action(description='Mark selected variants as NOT WORKING')
+def set_not_working(modeladmin, request, queryset):
+ queryset.update(status=Variant.Status.NOT_WORKING)
+
+
+class VariantForm(ModelForm):
+ def clean_mana_needed(self):
+ return self.cleaned_data['mana_needed'].upper() if self.cleaned_data['mana_needed'] else self.cleaned_data['mana_needed']
+
+
+@admin.register(Variant)
+class VariantAdmin(SearchMultipleRelatedMixin, admin.ModelAdmin):
+ form = VariantForm
+ inlines = [CardInVariantAdminInline, TemplateInVariantAdminInline]
+ readonly_fields = ['produces', 'of', 'includes', 'unique_id', 'identity', 'legal', 'scryfall_link']
+ fieldsets = [
+ ('Generated', {'fields': [
+ 'unique_id',
+ 'produces',
+ 'of',
+ 'includes',
+ 'identity',
+ 'legal',
+ 'scryfall_link']}),
+ ('Editable', {'fields': [
+ 'status',
+ 'mana_needed',
+ 'other_prerequisites',
+ 'description',
+ 'frozen']})
+ ]
+ list_filter = ['status', CardsCountListFilter, 'identity', 'legal']
+ list_display = ['__str__', 'status', 'id', 'identity']
+ search_fields = ['=id', 'uses__name', 'produces__name', 'requires__name', '=unique_id']
+ actions = [set_restore, set_draft, set_new, set_not_working]
+
+ def generate(self, request: HttpRequest):
+ if request.method == 'POST' and request.user.is_authenticated:
+ if (launch_job_command('generate_variants', timezone.timedelta(minutes=30), request.user)):
+ messages.info(request, 'Variant generation job started.')
+ else:
+ messages.warning(request, 'Variant generation is already running.')
+ return redirect('admin:spellbook_job_changelist')
+
+ def export(self, request: HttpRequest):
+ if request.method == 'POST' and request.user.is_authenticated:
+ if (launch_job_command('export_variants', timezone.timedelta(minutes=1), request.user)):
+ messages.info(request, 'Variant exporting job started.')
+ else:
+ messages.warning(request, 'Variant exporting is already running.')
+ return redirect('admin:spellbook_job_changelist')
+
+ def get_urls(self):
+ return [path('generate/',
+ self.admin_site.admin_view(view=self.generate, cacheable=False),
+ name='spellbook_variant_generate'),
+ path('export/',
+ self.admin_site.admin_view(view=self.export, cacheable=False),
+ name='spellbook_variant_export')] + super().get_urls()
+
+ def has_add_permission(self, request):
+ return False
+
+ def has_delete_permission(self, request, obj=None):
+ return False
+
+ def get_queryset(self, request):
+ return super().get_queryset(request) \
+ .prefetch_related('uses', 'requires', 'produces', 'of', 'includes')