refactor(db): initial, incomplete work to update model and re-normalize fields
This commit is contained in:
parent
30ce126a07
commit
4b9e4f651e
4 changed files with 850 additions and 50 deletions
|
|
@ -1,7 +1,481 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin, messages
|
||||||
from .models import Deck, Card, DeckNameTranslation, CardNameTranslation
|
from django.urls import path
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
from parler.admin import TranslatableAdmin
|
||||||
|
from .models import (
|
||||||
|
CardSet_New, Pack_New, Energy_New, Attack_New, Ability_New,
|
||||||
|
Rarity_New, CardType_New, Card_New, AttackCost_New, RarityMapping
|
||||||
|
)
|
||||||
|
|
||||||
admin.site.register(Deck)
|
import json
|
||||||
admin.site.register(Card)
|
import os
|
||||||
admin.site.register(DeckNameTranslation)
|
import re # For parsing set name and ID
|
||||||
admin.site.register(CardNameTranslation)
|
from django.conf import settings
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.db import transaction
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
def parse_set_details(set_string):
|
||||||
|
match = re.match(r'^(.*?)\s*\(([A-Za-z0-9]+)\)$', set_string)
|
||||||
|
if match:
|
||||||
|
name = match.group(1).strip()
|
||||||
|
set_id = match.group(2)
|
||||||
|
return name, set_id
|
||||||
|
match = re.match(r'^Promo-(.*?)$', set_string)
|
||||||
|
if match:
|
||||||
|
name = set_string
|
||||||
|
set_id = 'P-' + match.group(1)
|
||||||
|
return name, set_id
|
||||||
|
return set_string, None
|
||||||
|
|
||||||
|
def calculate_card_checksum(card_data):
|
||||||
|
"""
|
||||||
|
Calculates a SHA256 checksum for a card's data.
|
||||||
|
The data is first normalized by sorting lists and dictionary keys
|
||||||
|
to ensure consistent checksums for semantically identical cards.
|
||||||
|
"""
|
||||||
|
# Select and normalize fields that define the card's state
|
||||||
|
# Order of keys in `data_to_hash` and sorting of lists are important for consistency
|
||||||
|
data_to_hash = {
|
||||||
|
'id': card_data.get('id'),
|
||||||
|
'name': card_data.get('name'),
|
||||||
|
'type': card_data.get('type'),
|
||||||
|
'subtype': card_data.get('subtype'),
|
||||||
|
'rarity': card_data.get('rarity'), # Rarity name from JSON
|
||||||
|
'health': card_data.get('health'),
|
||||||
|
'evolvesFrom': card_data.get('evolvesFrom'),
|
||||||
|
'retreatCost': card_data.get('retreatCost'),
|
||||||
|
'element': card_data.get('element'), # Element name from JSON
|
||||||
|
'weakness': card_data.get('weakness'), # Weakness name from JSON
|
||||||
|
'pack': card_data.get('pack'), # Pack name from JSON
|
||||||
|
# For abilities and attacks, ensure stable order and content
|
||||||
|
'abilities': sorted([
|
||||||
|
{'name': a.get('name'), 'effect': a.get('effect')}
|
||||||
|
for a in card_data.get('abilities', []) if a and a.get('name') # ensure ability itself and name exist
|
||||||
|
], key=lambda x: x['name'] if x and x.get('name') else ''),
|
||||||
|
'attacks': sorted([
|
||||||
|
{
|
||||||
|
'name': atk.get('name'),
|
||||||
|
'effect': atk.get('effect', ''),
|
||||||
|
'damage': atk.get('damage', ''),
|
||||||
|
'cost': sorted(atk.get('cost', []) if atk.get('cost') else []) # Sort energy costs
|
||||||
|
}
|
||||||
|
for atk in card_data.get('attacks', []) if atk and atk.get('name') # ensure attack itself and name exist
|
||||||
|
], key=lambda x: x['name'] if x and x.get('name') else ''),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Serialize to a canonical JSON string (sort keys, no indent, compact)
|
||||||
|
canonical_json = json.dumps(data_to_hash, sort_keys=True, separators=(',', ':'))
|
||||||
|
|
||||||
|
sha256_hash = hashlib.sha256(canonical_json.encode('utf-8')).hexdigest()
|
||||||
|
return sha256_hash
|
||||||
|
|
||||||
|
def _get_or_create_card_type(card_data):
|
||||||
|
card_type_obj, created = CardType_New.objects.language('en').get_or_create(
|
||||||
|
translations__name=card_data['type'],
|
||||||
|
translations__subtype=card_data.get('subtype', ''),
|
||||||
|
defaults={'name': card_data['type'], 'subtype': card_data.get('subtype', '')}
|
||||||
|
)
|
||||||
|
if not created:
|
||||||
|
current_subtype = card_data.get('subtype')
|
||||||
|
if current_subtype is not None and card_type_obj.subtype != current_subtype:
|
||||||
|
card_type_obj.set_current_language('en')
|
||||||
|
card_type_obj.subtype = current_subtype
|
||||||
|
card_type_obj.save()
|
||||||
|
return card_type_obj
|
||||||
|
|
||||||
|
def _get_or_create_rarity(card_data, rarity_mappings_dict):
|
||||||
|
original_rarity_name_from_json = card_data.get('rarity')
|
||||||
|
|
||||||
|
# Attempt to find a mapping for the original rarity name
|
||||||
|
mapping = rarity_mappings_dict.get(original_rarity_name_from_json)
|
||||||
|
|
||||||
|
if mapping:
|
||||||
|
# Use mapped values
|
||||||
|
target_rarity_name = mapping.mapped_name
|
||||||
|
target_icon = mapping.icon
|
||||||
|
target_level = mapping.level
|
||||||
|
elif original_rarity_name_from_json:
|
||||||
|
# No mapping found, use the original name from JSON, default icon/level
|
||||||
|
target_rarity_name = original_rarity_name_from_json
|
||||||
|
target_icon = 'x' # Default icon if no mapping
|
||||||
|
target_level = 0 # Default level if no mapping
|
||||||
|
else:
|
||||||
|
# Rarity is None or empty in JSON, treat as 'Promo'
|
||||||
|
target_rarity_name = 'Promo'
|
||||||
|
# Check if 'Promo' itself has a mapping
|
||||||
|
promo_mapping = rarity_mappings_dict.get('Promo')
|
||||||
|
if promo_mapping:
|
||||||
|
target_icon = promo_mapping.icon
|
||||||
|
target_level = promo_mapping.level
|
||||||
|
else:
|
||||||
|
target_icon = 'x' # Default icon for 'Promo' if no mapping for 'Promo'
|
||||||
|
target_level = 0 # Default level for 'Promo' if no mapping for 'Promo'
|
||||||
|
|
||||||
|
# Get or create the Rarity_New object using the (potentially mapped) values
|
||||||
|
rarity_obj, created = Rarity_New.objects.language('en').get_or_create(
|
||||||
|
translations__name=target_rarity_name,
|
||||||
|
defaults={'name': target_rarity_name, 'icon': target_icon, 'level': target_level}
|
||||||
|
)
|
||||||
|
|
||||||
|
# If the rarity already existed, check if its icon or level needs updating based on the mapping
|
||||||
|
if not created:
|
||||||
|
updated_fields = False
|
||||||
|
if rarity_obj.icon != target_icon:
|
||||||
|
rarity_obj.icon = target_icon
|
||||||
|
updated_fields = True
|
||||||
|
if rarity_obj.level != target_level:
|
||||||
|
rarity_obj.level = target_level
|
||||||
|
updated_fields = True
|
||||||
|
|
||||||
|
if updated_fields:
|
||||||
|
rarity_obj.save()
|
||||||
|
|
||||||
|
return rarity_obj
|
||||||
|
|
||||||
|
def _get_or_create_energy(energy_name):
|
||||||
|
if not energy_name:
|
||||||
|
return None
|
||||||
|
energy_obj, _ = Energy_New.objects.language('en').get_or_create(
|
||||||
|
translations__name=energy_name,
|
||||||
|
defaults={'name': energy_name}
|
||||||
|
)
|
||||||
|
return energy_obj
|
||||||
|
|
||||||
|
def _update_card_packs(card_obj, card_data, card_set):
|
||||||
|
card_obj.packs.clear()
|
||||||
|
pack_name_from_json = card_data.get('pack')
|
||||||
|
if pack_name_from_json:
|
||||||
|
card_set.set_current_language('en')
|
||||||
|
pack_full_name = f"{card_set.name}: {pack_name_from_json}"
|
||||||
|
|
||||||
|
pack_obj, _ = Pack_New.objects.language('en').get_or_create(
|
||||||
|
translations__name=pack_name_from_json,
|
||||||
|
cardset=card_set,
|
||||||
|
defaults={
|
||||||
|
'name': pack_name_from_json,
|
||||||
|
'full_name': pack_full_name,
|
||||||
|
'hex_color': '#FFFFFF'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
card_obj.packs.add(pack_obj)
|
||||||
|
else:
|
||||||
|
all_packs_in_set = Pack_New.objects.filter(cardset=card_set)
|
||||||
|
if all_packs_in_set.exists():
|
||||||
|
card_obj.packs.add(*all_packs_in_set)
|
||||||
|
|
||||||
|
def _update_card_abilities(card_obj, card_data):
|
||||||
|
card_obj.abilities.clear()
|
||||||
|
for ability_data in card_data.get('abilities', []):
|
||||||
|
ability_obj, created = Ability_New.objects.language('en').get_or_create(
|
||||||
|
translations__name=ability_data['name'],
|
||||||
|
defaults={'name': ability_data['name'], 'effect': ability_data['effect']}
|
||||||
|
)
|
||||||
|
if not created and ability_obj.effect != ability_data['effect']:
|
||||||
|
ability_obj.set_current_language('en')
|
||||||
|
ability_obj.effect = ability_data['effect']
|
||||||
|
ability_obj.save()
|
||||||
|
card_obj.abilities.add(ability_obj)
|
||||||
|
|
||||||
|
def _update_card_attacks_and_costs(card_obj, card_data):
|
||||||
|
card_obj.attacks.clear()
|
||||||
|
for attack_data in card_data.get('attacks', []):
|
||||||
|
attack_obj, created = Attack_New.objects.language('en').get_or_create(
|
||||||
|
translations__name=attack_data['name'],
|
||||||
|
defaults={
|
||||||
|
'name': attack_data['name'],
|
||||||
|
'effect': attack_data.get('effect', ''),
|
||||||
|
'damage': attack_data.get('damage', '')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
needs_save = False
|
||||||
|
if not created:
|
||||||
|
json_effect = attack_data.get('effect', '')
|
||||||
|
if attack_obj.effect != json_effect:
|
||||||
|
attack_obj.set_current_language('en')
|
||||||
|
attack_obj.effect = json_effect
|
||||||
|
needs_save = True
|
||||||
|
|
||||||
|
json_damage = attack_data.get('damage', '')
|
||||||
|
if attack_obj.damage != json_damage:
|
||||||
|
attack_obj.damage = json_damage
|
||||||
|
needs_save = True
|
||||||
|
|
||||||
|
if created or needs_save:
|
||||||
|
attack_obj.save()
|
||||||
|
|
||||||
|
card_obj.attacks.add(attack_obj)
|
||||||
|
|
||||||
|
attack_obj.energy_cost.clear()
|
||||||
|
energy_counts = {}
|
||||||
|
for cost_energy_name in attack_data.get('cost', []):
|
||||||
|
energy_counts[cost_energy_name] = energy_counts.get(cost_energy_name, 0) + 1
|
||||||
|
|
||||||
|
for energy_name, quantity in energy_counts.items():
|
||||||
|
energy_obj = _get_or_create_energy(energy_name)
|
||||||
|
if energy_obj:
|
||||||
|
AttackCost_New.objects.update_or_create(
|
||||||
|
attack=attack_obj,
|
||||||
|
energy=energy_obj,
|
||||||
|
defaults={'quantity': quantity}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _process_single_card_data(card_data, card_set, stats_accumulator, error_tracking, rarity_mappings_dict):
|
||||||
|
"""
|
||||||
|
Processes a single card's data from the JSON.
|
||||||
|
Updates stats_accumulator with newly_imported_count, updated_count, or skipped_count.
|
||||||
|
error_tracking is a dict {'file_name': ..., 'card_id': ...} for precise error reporting.
|
||||||
|
"""
|
||||||
|
card_id = card_data['id']
|
||||||
|
incoming_checksum = calculate_card_checksum(card_data)
|
||||||
|
error_tracking['card_id'] = card_id
|
||||||
|
|
||||||
|
try:
|
||||||
|
existing_card = Card_New.objects.language('en').get(id=card_id)
|
||||||
|
if existing_card.checksum == incoming_checksum:
|
||||||
|
stats_accumulator['skipped_count'] += 1
|
||||||
|
return
|
||||||
|
except Card_New.DoesNotExist:
|
||||||
|
existing_card = None
|
||||||
|
|
||||||
|
card_type_obj = _get_or_create_card_type(card_data)
|
||||||
|
rarity_obj = _get_or_create_rarity(card_data, rarity_mappings_dict)
|
||||||
|
pkmn_type_obj = _get_or_create_energy(card_data.get('element'))
|
||||||
|
weakness_type_obj = _get_or_create_energy(card_data.get('weakness'))
|
||||||
|
|
||||||
|
card_defaults = {
|
||||||
|
'name': card_data['name'],
|
||||||
|
'cardset': card_set,
|
||||||
|
'card_type': card_type_obj,
|
||||||
|
'rarity': rarity_obj,
|
||||||
|
'health': card_data.get('health'),
|
||||||
|
'evolves_from_name': card_data.get('evolvesFrom'),
|
||||||
|
'retreat_cost': card_data.get('retreatCost'),
|
||||||
|
'pkmn_type': pkmn_type_obj,
|
||||||
|
'weakness_type': weakness_type_obj,
|
||||||
|
'checksum': incoming_checksum
|
||||||
|
}
|
||||||
|
|
||||||
|
card_obj, card_created = Card_New.objects.language('en').update_or_create(
|
||||||
|
id=card_id,
|
||||||
|
defaults=card_defaults
|
||||||
|
)
|
||||||
|
|
||||||
|
if card_created:
|
||||||
|
stats_accumulator['newly_imported_count'] += 1
|
||||||
|
elif existing_card:
|
||||||
|
stats_accumulator['updated_count'] +=1
|
||||||
|
# If not created and checksum differs, it's an update, which is handled by updated_count.
|
||||||
|
# update_or_create takes care of setting the new checksum via defaults.
|
||||||
|
|
||||||
|
_update_card_packs(card_obj, card_data, card_set)
|
||||||
|
_update_card_abilities(card_obj, card_data)
|
||||||
|
_update_card_attacks_and_costs(card_obj, card_data)
|
||||||
|
|
||||||
|
# The checksum is based on the incoming card_data. If the update_* functions
|
||||||
|
# modify the card_obj in a way that would change its representation
|
||||||
|
# based on the original card_data fields, the checksum logic is fine.
|
||||||
|
# If those functions derive new data that *should* be part of the checksum,
|
||||||
|
# the checksum calculation would need to happen *after* them, using the card_obj state.
|
||||||
|
# However, for skipping based on *incoming JSON data*, this approach is correct.
|
||||||
|
# The `update_or_create` will ensure the `checksum` field (which is part of `card_defaults`) is saved.
|
||||||
|
|
||||||
|
def perform_card_import_logic():
|
||||||
|
"""
|
||||||
|
Main importer logic. Iterates through JSON files and processes them.
|
||||||
|
Halts and rolls back on any error.
|
||||||
|
"""
|
||||||
|
print("Card import process started.")
|
||||||
|
base_path = os.path.join(settings.BASE_DIR, 'REMOTE_GIT_REPOS', 'pokemon-tcg-pocket-card-database', 'cards', 'en')
|
||||||
|
|
||||||
|
stats = {'newly_imported_count': 0, 'updated_count': 0, 'skipped_count': 0, 'files_processed_count': 0}
|
||||||
|
error_tracking = {'file_name': "N/A", 'card_id': "N/A"}
|
||||||
|
|
||||||
|
# Fetch all rarity mappings once
|
||||||
|
rarity_mappings = RarityMapping.objects.all()
|
||||||
|
rarity_mappings_dict = {mapping.original_name: mapping for mapping in rarity_mappings}
|
||||||
|
print(f"Loaded {len(rarity_mappings_dict)} rarity mappings.")
|
||||||
|
|
||||||
|
if not os.path.isdir(base_path):
|
||||||
|
message = f"Source directory not found: {base_path}. Import halted."
|
||||||
|
print(message)
|
||||||
|
return 0, 0, True, message, 0, 0
|
||||||
|
|
||||||
|
json_files = [f for f in os.listdir(base_path) if f.endswith('.json')]
|
||||||
|
json_files.sort()
|
||||||
|
if not json_files:
|
||||||
|
message = "No JSON files found in the source directory to import."
|
||||||
|
print(message)
|
||||||
|
return 0, 0, False, message, 0, 0
|
||||||
|
|
||||||
|
print(f"Found {len(json_files)} JSON files to process.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
|
for idx, file_name in enumerate(json_files):
|
||||||
|
error_tracking['file_name'] = file_name
|
||||||
|
error_tracking['card_id'] = "N/A"
|
||||||
|
file_path = os.path.join(base_path, file_name)
|
||||||
|
|
||||||
|
print(f"Processing file: {file_name} ({idx + 1}/{len(json_files)})")
|
||||||
|
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
raise ValueError(f"JSON file {file_name} is empty or contains no data.")
|
||||||
|
|
||||||
|
stats['files_processed_count'] += 1
|
||||||
|
|
||||||
|
first_card_data = data[0]
|
||||||
|
set_info_str = first_card_data.get('set')
|
||||||
|
if not set_info_str:
|
||||||
|
raise ValueError(f"Could not determine set information from first card in {file_name}.")
|
||||||
|
|
||||||
|
parsed_set_name, parsed_set_id = parse_set_details(set_info_str)
|
||||||
|
if not parsed_set_id:
|
||||||
|
raise ValueError(f"Could not parse set ID from '{set_info_str}' in {file_name}.")
|
||||||
|
|
||||||
|
card_set_defaults = {
|
||||||
|
'name': parsed_set_name,
|
||||||
|
'file_name': file_name
|
||||||
|
}
|
||||||
|
card_set, _ = CardSet_New.objects.language('en').update_or_create(
|
||||||
|
id=parsed_set_id,
|
||||||
|
defaults=card_set_defaults
|
||||||
|
)
|
||||||
|
|
||||||
|
for card_data_item in data:
|
||||||
|
print("Processing card: ", card_data_item['id'])
|
||||||
|
_process_single_card_data(card_data_item, card_set, stats, error_tracking, rarity_mappings_dict)
|
||||||
|
|
||||||
|
print(f"Finished processing file: {file_name}")
|
||||||
|
|
||||||
|
success_message = (
|
||||||
|
f"Import completed successfully. Processed {stats['files_processed_count']} files. "
|
||||||
|
f"Imported {stats['newly_imported_count']} new cards. "
|
||||||
|
f"Updated {stats['updated_count']} existing cards. "
|
||||||
|
f"Skipped {stats['skipped_count']} unchanged cards."
|
||||||
|
)
|
||||||
|
print("Committing transaction.")
|
||||||
|
transaction.on_commit(lambda: print(success_message))
|
||||||
|
return stats['newly_imported_count'], stats['updated_count'], False, success_message, stats['files_processed_count'], stats['skipped_count']
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Any exception during the process will cause the transaction to roll back.
|
||||||
|
error_detail = f"Error during import (file: {error_tracking['file_name']}, card: {error_tracking['card_id']}): {str(e)}"
|
||||||
|
halt_message = f"Import HALTED. All changes rolled back. Reason: {error_detail}"
|
||||||
|
print(halt_message)
|
||||||
|
# Return 0 for counts as the transaction is rolled back
|
||||||
|
return 0, 0, True, halt_message, stats['files_processed_count'], stats.get('skipped_count', 0)
|
||||||
|
|
||||||
|
|
||||||
|
if admin.site.is_registered(CardSet_New): admin.site.unregister(CardSet_New)
|
||||||
|
if admin.site.is_registered(Pack_New): admin.site.unregister(Pack_New)
|
||||||
|
if admin.site.is_registered(Energy_New): admin.site.unregister(Energy_New)
|
||||||
|
if admin.site.is_registered(Attack_New): admin.site.unregister(Attack_New)
|
||||||
|
if admin.site.is_registered(Ability_New): admin.site.unregister(Ability_New)
|
||||||
|
if admin.site.is_registered(Rarity_New): admin.site.unregister(Rarity_New)
|
||||||
|
if admin.site.is_registered(CardType_New): admin.site.unregister(CardType_New)
|
||||||
|
if admin.site.is_registered(Card_New): admin.site.unregister(Card_New)
|
||||||
|
if admin.site.is_registered(AttackCost_New): admin.site.unregister(AttackCost_New)
|
||||||
|
if admin.site.is_registered(RarityMapping): admin.site.unregister(RarityMapping)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(CardSet_New)
|
||||||
|
class CardSetAdmin(TranslatableAdmin):
|
||||||
|
list_display = ('id', 'name', 'file_name')
|
||||||
|
readonly_fields = ('id', 'file_name', 'created_at', 'updated_at')
|
||||||
|
search_fields = ('translations__name',)
|
||||||
|
readonly_fields = ('id', 'created_at', 'updated_at', 'deleted_at')
|
||||||
|
|
||||||
|
@admin.register(Pack_New)
|
||||||
|
class PackAdmin(TranslatableAdmin):
|
||||||
|
list_display = ('id', 'full_name', 'name', 'cardset', 'hex_color')
|
||||||
|
list_filter = ('cardset',)
|
||||||
|
search_fields = ('translations__name', 'translations__full_name')
|
||||||
|
readonly_fields = ('id', 'created_at', 'updated_at')
|
||||||
|
|
||||||
|
@admin.register(Energy_New)
|
||||||
|
class EnergyAdmin(TranslatableAdmin):
|
||||||
|
list_display = ('id', 'name')
|
||||||
|
search_fields = ('translations__name',)
|
||||||
|
readonly_fields = ('id', 'created_at', 'updated_at', 'deleted_at')
|
||||||
|
|
||||||
|
@admin.register(Attack_New)
|
||||||
|
class AttackAdmin(TranslatableAdmin):
|
||||||
|
list_display = ('id', 'name', 'damage', 'effect')
|
||||||
|
search_fields = ('translations__name',)
|
||||||
|
readonly_fields = ('id', 'created_at', 'updated_at', 'deleted_at')
|
||||||
|
|
||||||
|
@admin.register(Ability_New)
|
||||||
|
class AbilityAdmin(TranslatableAdmin):
|
||||||
|
list_display = ('id', 'name', 'effect')
|
||||||
|
search_fields = ('translations__name',)
|
||||||
|
readonly_fields = ('id', 'created_at', 'updated_at')
|
||||||
|
|
||||||
|
@admin.register(Rarity_New)
|
||||||
|
class RarityAdmin(TranslatableAdmin):
|
||||||
|
list_display = ('id', 'name', 'icon', 'level')
|
||||||
|
search_fields = ('translations__name',)
|
||||||
|
readonly_fields = ('id', 'created_at', 'updated_at', 'deleted_at')
|
||||||
|
|
||||||
|
@admin.register(CardType_New)
|
||||||
|
class CardTypeAdmin(TranslatableAdmin):
|
||||||
|
list_display = ('id', 'name', 'subtype')
|
||||||
|
search_fields = ('translations__name', 'translations__subtype')
|
||||||
|
readonly_fields = ('id', 'created_at', 'updated_at', 'deleted_at')
|
||||||
|
|
||||||
|
@admin.register(Card_New)
|
||||||
|
class CardAdmin(TranslatableAdmin):
|
||||||
|
list_display = ('id', 'cardnum', 'name', 'cardset', 'card_type', 'rarity', 'health', 'pkmn_type')
|
||||||
|
list_filter = ('cardset', 'card_type', 'rarity', 'pkmn_type', 'packs')
|
||||||
|
search_fields = ('id', 'translations__name', 'cardset__translations__name', 'packs__translations__name')
|
||||||
|
filter_horizontal = ('packs', 'abilities', 'attacks')
|
||||||
|
readonly_fields = ('id', 'cardnum', 'created_at', 'updated_at', 'deleted_at')
|
||||||
|
|
||||||
|
admin.site.register(AttackCost_New)
|
||||||
|
|
||||||
|
@admin.register(RarityMapping)
|
||||||
|
class RarityMappingAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('original_name', 'mapped_name', 'icon', 'level', 'created_at', 'updated_at', 'deleted_at')
|
||||||
|
search_fields = ('original_name', 'mapped_name')
|
||||||
|
list_filter = ('level',)
|
||||||
|
readonly_fields = ('created_at', 'updated_at', 'deleted_at')
|
||||||
|
|
||||||
|
def get_admin_urls(urls):
|
||||||
|
def importer_view(request):
|
||||||
|
context = {
|
||||||
|
'title': 'Card Importer',
|
||||||
|
'site_header': admin.site.site_header,
|
||||||
|
'site_title': admin.site.site_title,
|
||||||
|
'index_title': admin.site.index_title,
|
||||||
|
'has_permission': admin.site.has_permission(request),
|
||||||
|
'app_label': 'cards',
|
||||||
|
}
|
||||||
|
if request.method == 'POST':
|
||||||
|
new, updated, has_error, message_text, files_processed, skipped = perform_card_import_logic()
|
||||||
|
|
||||||
|
if has_error:
|
||||||
|
messages.error(request, message_text + f" Files attempted before halt: {files_processed}.")
|
||||||
|
else:
|
||||||
|
messages.success(request, message_text)
|
||||||
|
|
||||||
|
return HttpResponseRedirect(request.path_info)
|
||||||
|
|
||||||
|
return render(request, 'admin/cards/importer_status.html', context)
|
||||||
|
|
||||||
|
custom_urls = [
|
||||||
|
path('cards/import/', admin.site.admin_view(importer_view), name='cards_full_importer'),
|
||||||
|
]
|
||||||
|
return custom_urls + urls
|
||||||
|
|
||||||
|
original_get_urls = admin.site.get_urls
|
||||||
|
|
||||||
|
def new_get_urls():
|
||||||
|
urls = original_get_urls()
|
||||||
|
return get_admin_urls(urls)
|
||||||
|
|
||||||
|
admin.site.get_urls = new_get_urls
|
||||||
|
|
@ -1,53 +1,319 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Prefetch
|
from parler.models import TranslatableModel, TranslatedFields
|
||||||
from django.apps import apps
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
class DeckNameTranslation(models.Model):
|
|
||||||
id = models.AutoField(primary_key=True)
|
|
||||||
name = models.CharField(max_length=64)
|
|
||||||
deck = models.ForeignKey("Deck", on_delete=models.PROTECT, related_name='name_translations')
|
|
||||||
language = models.CharField(max_length=64)
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
class CardSet(TranslatableModel):
|
||||||
return self.name
|
"""
|
||||||
|
Represents a single JSON file from the repository, considered a 'cardset', e.g., "Genetic Apex (A1)",
|
||||||
|
or collection of cards. Each cardset file belongs to a specific CardSet and language.
|
||||||
|
"""
|
||||||
|
|
||||||
class CardNameTranslation(models.Model):
|
translations = TranslatedFields(
|
||||||
id = models.AutoField(primary_key=True)
|
name=models.CharField(
|
||||||
name = models.CharField(max_length=64)
|
max_length=32,
|
||||||
card = models.ForeignKey("Card", on_delete=models.PROTECT, related_name='name_translations')
|
help_text=_("The full name of the set, e.g., 'Genetic Apex'."),
|
||||||
language = models.CharField(max_length=64)
|
)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
id = models.CharField(
|
||||||
|
max_length=3,
|
||||||
|
primary_key=True,
|
||||||
|
help_text=_("The ID for the set, e.g., 'A1', 'A1a'."),
|
||||||
|
)
|
||||||
|
file_name = models.CharField(
|
||||||
|
max_length=32,
|
||||||
|
help_text=_("Original name of the JSON file, e.g., 'a1-genetic-apex.json'."),
|
||||||
|
)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
deleted_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
def __str__(self):
|
class Meta:
|
||||||
return self.name
|
verbose_name = _("Card Set (New)")
|
||||||
class Deck(models.Model):
|
verbose_name_plural = _("Card Sets (New)")
|
||||||
id = models.AutoField(primary_key=True)
|
|
||||||
name = models.CharField(max_length=64)
|
|
||||||
hex_color = models.CharField(max_length=9)
|
|
||||||
cardset = models.CharField(max_length=8)
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return f"{self.id} - {self.name}"
|
||||||
|
|
||||||
class Card(models.Model):
|
|
||||||
id = models.AutoField(primary_key=True)
|
|
||||||
name = models.CharField(max_length=64)
|
|
||||||
decks = models.ManyToManyField("Deck")
|
|
||||||
cardset = models.CharField(max_length=32)
|
|
||||||
cardnum = models.IntegerField()
|
|
||||||
style = models.CharField(max_length=128)
|
|
||||||
rarity_icon = models.CharField(max_length=12)
|
|
||||||
rarity_level = models.IntegerField()
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Pack(TranslatableModel):
|
||||||
unique_together = ('cardset', 'cardnum')
|
"""
|
||||||
|
Represents a single pack that is part of a cardset. E.g., "Genetic Apex: Mewtwo"
|
||||||
|
"""
|
||||||
|
|
||||||
def __str__(self):
|
translations = TranslatedFields(
|
||||||
return f"{self.name} ({self.cardset} #{self.cardnum})"
|
full_name=models.CharField(
|
||||||
|
max_length=32,
|
||||||
|
help_text=_("The full name of the pack, e.g., 'Genetic Apex: Mewtwo'."),
|
||||||
|
),
|
||||||
|
name=models.CharField(
|
||||||
|
max_length=32, help_text=_("The pack name itself, e.g., 'Mewtwo'.")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
id = models.AutoField(primary_key=True)
|
||||||
|
hex_color = models.CharField(max_length=9)
|
||||||
|
cardset = models.ForeignKey(CardSet, on_delete=models.CASCADE, related_name="packs")
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
deleted_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Pack (New)")
|
||||||
|
verbose_name_plural = _("Packs (New)")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.full_name}"
|
||||||
|
|
||||||
|
|
||||||
|
class Energy(TranslatableModel):
|
||||||
|
"""
|
||||||
|
A type a Pokémon card can have.
|
||||||
|
"""
|
||||||
|
|
||||||
|
translations = TranslatedFields(
|
||||||
|
name=models.CharField(max_length=32, help_text=_("The name of the energy."))
|
||||||
|
)
|
||||||
|
id = models.AutoField(primary_key=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
deleted_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Energy (New)")
|
||||||
|
verbose_name_plural = _("Energies (New)")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name}"
|
||||||
|
|
||||||
|
|
||||||
|
class AttackCost(models.Model):
|
||||||
|
"""
|
||||||
|
Intermediary model to store the quantity of each energy type for an attack's cost.
|
||||||
|
"""
|
||||||
|
|
||||||
|
attack = models.ForeignKey("Attack", on_delete=models.CASCADE)
|
||||||
|
energy = models.ForeignKey(Energy, on_delete=models.CASCADE)
|
||||||
|
quantity = models.PositiveIntegerField(
|
||||||
|
default=1, help_text=_("Quantity of this energy type required for the attack.")
|
||||||
|
)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
deleted_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Attack Cost (New)")
|
||||||
|
verbose_name_plural = _("Attack Costs (New)")
|
||||||
|
unique_together = ("attack", "energy")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.attack.name} {_("requires")} {self.quantity} {self.energy.name}"
|
||||||
|
|
||||||
|
|
||||||
|
class Attack(TranslatableModel):
|
||||||
|
"""
|
||||||
|
An attack a Pokémon card can have.
|
||||||
|
"""
|
||||||
|
|
||||||
|
translations = TranslatedFields(
|
||||||
|
name=models.CharField(max_length=32, help_text=_("The name of the attack.")),
|
||||||
|
effect=models.TextField(help_text=_("Description of the attack's effect.")),
|
||||||
|
)
|
||||||
|
id = models.AutoField(primary_key=True)
|
||||||
|
damage = models.CharField(
|
||||||
|
max_length=10,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text=_("Damage string, e.g., '40', '20x', '80+'."),
|
||||||
|
)
|
||||||
|
energy_cost = models.ManyToManyField(
|
||||||
|
Energy, through=AttackCost, related_name="attacks"
|
||||||
|
)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
deleted_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Attack (New)")
|
||||||
|
verbose_name_plural = _("Attacks (New)")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name}"
|
||||||
|
|
||||||
|
|
||||||
|
class Ability(TranslatableModel):
|
||||||
|
"""
|
||||||
|
An ability a Pokémon card can have.
|
||||||
|
"""
|
||||||
|
|
||||||
|
translations = TranslatedFields(
|
||||||
|
name=models.CharField(max_length=32, help_text=_("The name of the ability.")),
|
||||||
|
effect=models.TextField(help_text=_("Description of the ability's effect.")),
|
||||||
|
)
|
||||||
|
id = models.AutoField(primary_key=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
deleted_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Ability (New)")
|
||||||
|
verbose_name_plural = _("Abilities (New)")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name}"
|
||||||
|
|
||||||
|
|
||||||
|
class Rarity(TranslatableModel):
|
||||||
|
"""
|
||||||
|
A rarity a Pokémon card can have.
|
||||||
|
"""
|
||||||
|
|
||||||
|
translations = TranslatedFields(
|
||||||
|
name=models.CharField(max_length=32, help_text=_("The name of the rarity."))
|
||||||
|
)
|
||||||
|
id = models.AutoField(primary_key=True)
|
||||||
|
icon = models.CharField(max_length=12)
|
||||||
|
level = models.PositiveIntegerField()
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
deleted_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Rarity (New)")
|
||||||
|
verbose_name_plural = _("Rarities (New)")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name}"
|
||||||
|
|
||||||
|
|
||||||
|
class CardType(TranslatableModel):
|
||||||
|
"""
|
||||||
|
A type a Pokémon card can have.
|
||||||
|
"""
|
||||||
|
|
||||||
|
translations = TranslatedFields(
|
||||||
|
name=models.CharField(max_length=32, help_text=_("The name of the card type.")),
|
||||||
|
subtype=models.CharField(
|
||||||
|
max_length=32,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text=_("The subtype of the card type."),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
id = models.AutoField(primary_key=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
deleted_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Card Type (New)")
|
||||||
|
verbose_name_plural = _("Card Types (New)")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name}"
|
||||||
|
|
||||||
|
|
||||||
|
class Card(TranslatableModel):
|
||||||
|
"""
|
||||||
|
Represents a single, unique digital printing of a Pokémon card.
|
||||||
|
"""
|
||||||
|
|
||||||
|
translations = TranslatedFields(
|
||||||
|
name=models.CharField(max_length=32, help_text=_("The name of the card.")),
|
||||||
|
evolves_from_name=models.CharField(
|
||||||
|
max_length=32,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text=_("Name of the Pokémon this card evolves from."),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
cardnum = models.AutoField(primary_key=True)
|
||||||
|
id = models.CharField(
|
||||||
|
max_length=10,
|
||||||
|
db_index=True,
|
||||||
|
help_text=_(
|
||||||
|
"The unique ID from the JSON source, cardset-cardnum (e.g., 'a1-001')."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
checksum = models.CharField(
|
||||||
|
max_length=64,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text=_("SHA256 checksum of the card data."),
|
||||||
|
db_index=True,
|
||||||
|
)
|
||||||
|
health = models.PositiveIntegerField(
|
||||||
|
null=True, blank=True, help_text=_("HP of the Pokémon.")
|
||||||
|
)
|
||||||
|
|
||||||
|
retreat_cost = models.PositiveIntegerField(
|
||||||
|
null=True, blank=True, help_text=_("The number of retreat cost for the card.")
|
||||||
|
)
|
||||||
|
weakness_type = models.ForeignKey(
|
||||||
|
Energy,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="cards_weakness_type",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
pkmn_type = models.ForeignKey(
|
||||||
|
Energy,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="cards_pkmn_type",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
card_type = models.ForeignKey(
|
||||||
|
CardType, on_delete=models.CASCADE, related_name="cards"
|
||||||
|
)
|
||||||
|
packs = models.ManyToManyField(Pack, related_name="cards")
|
||||||
|
cardset = models.ForeignKey(CardSet, on_delete=models.CASCADE, related_name="cards")
|
||||||
|
abilities = models.ManyToManyField(Ability, blank=True, related_name="cards")
|
||||||
|
attacks = models.ManyToManyField(Attack, related_name="cards")
|
||||||
|
rarity = models.ForeignKey(Rarity, on_delete=models.CASCADE, related_name="cards")
|
||||||
|
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
deleted_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Card (New)")
|
||||||
|
verbose_name_plural = _("Cards (New)")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.id} {self.name}"
|
||||||
|
|
||||||
|
|
||||||
|
class RarityMapping(models.Model):
|
||||||
|
"""
|
||||||
|
Maps an original rarity name from the import source to a standardized
|
||||||
|
rarity name, icon, and level to be used in the system.
|
||||||
|
"""
|
||||||
|
|
||||||
|
id = models.AutoField(primary_key=True)
|
||||||
|
original_name = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
unique=True,
|
||||||
|
help_text=_(
|
||||||
|
"The rarity name as it appears in the import source (e.g., JSON file)."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
mapped_name = models.CharField(
|
||||||
|
max_length=32, help_text=_("The standardized rarity name to use in the system.")
|
||||||
|
)
|
||||||
|
icon = models.CharField(
|
||||||
|
max_length=12, help_text=_("The icon associated with this rarity.")
|
||||||
|
)
|
||||||
|
level = models.PositiveIntegerField(
|
||||||
|
help_text=_("The level or order of this rarity.")
|
||||||
|
)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
deleted_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Rarity Mapping")
|
||||||
|
verbose_name_plural = _("Rarity Mappings")
|
||||||
|
ordering = ["original_name"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"'{self.original_name}' -> '{self.mapped_name}' (L{self.level}, {self.icon})"
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ env = environ.Env(
|
||||||
DJANGO_EMAIL_PASSWORD=(str, ''),
|
DJANGO_EMAIL_PASSWORD=(str, ''),
|
||||||
DJANGO_EMAIL_USE_TLS=(bool, True),
|
DJANGO_EMAIL_USE_TLS=(bool, True),
|
||||||
DJANGO_DEFAULT_FROM_EMAIL=(str, ''),
|
DJANGO_DEFAULT_FROM_EMAIL=(str, ''),
|
||||||
|
DJANGO_EMAIL_SUBJECT_PREFIX=(str, ''),
|
||||||
SECRET_KEY=(str, '0000000000000000000000000000000000000000000000000000000000000000'),
|
SECRET_KEY=(str, '0000000000000000000000000000000000000000000000000000000000000000'),
|
||||||
ALLOWED_HOSTS=(str, 'localhost,127.0.0.1'),
|
ALLOWED_HOSTS=(str, 'localhost,127.0.0.1'),
|
||||||
PUBLIC_HOST=(str, 'localhost'),
|
PUBLIC_HOST=(str, 'localhost'),
|
||||||
|
|
@ -112,6 +113,9 @@ except Exception:
|
||||||
|
|
||||||
CSRF_TRUSTED_ORIGINS = [f"{SCHEME}://{PUBLIC_HOST}"]
|
CSRF_TRUSTED_ORIGINS = [f"{SCHEME}://{PUBLIC_HOST}"]
|
||||||
|
|
||||||
|
SHORTHAND_DATETIME_FORMAT = 'Y-m-d P'
|
||||||
|
SHORTHAND_DATE_FORMAT = 'Y-m-d'
|
||||||
|
|
||||||
FIRST_PARTY_APPS = [
|
FIRST_PARTY_APPS = [
|
||||||
'pkmntrade_club.accounts',
|
'pkmntrade_club.accounts',
|
||||||
'pkmntrade_club.cards',
|
'pkmntrade_club.cards',
|
||||||
|
|
@ -151,6 +155,7 @@ INSTALLED_APPS = [
|
||||||
'health_check.contrib.psutil',
|
'health_check.contrib.psutil',
|
||||||
'health_check.contrib.redis',
|
'health_check.contrib.redis',
|
||||||
"meta",
|
"meta",
|
||||||
|
"parler",
|
||||||
] + FIRST_PARTY_APPS
|
] + FIRST_PARTY_APPS
|
||||||
|
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
|
|
@ -251,6 +256,10 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#language-code
|
# https://docs.djangoproject.com/en/dev/ref/settings/#language-code
|
||||||
LANGUAGE_CODE = "en-us"
|
LANGUAGE_CODE = "en-us"
|
||||||
|
|
||||||
|
LANGUAGES = (
|
||||||
|
('en', _("English")),
|
||||||
|
)
|
||||||
|
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#time-zone
|
# https://docs.djangoproject.com/en/dev/ref/settings/#time-zone
|
||||||
TIME_ZONE = env('TIME_ZONE')
|
TIME_ZONE = env('TIME_ZONE')
|
||||||
|
|
||||||
|
|
@ -310,6 +319,7 @@ EMAIL_PORT = env('DJANGO_EMAIL_PORT')
|
||||||
EMAIL_HOST_USER = env('DJANGO_EMAIL_USER')
|
EMAIL_HOST_USER = env('DJANGO_EMAIL_USER')
|
||||||
EMAIL_HOST_PASSWORD = env('DJANGO_EMAIL_PASSWORD')
|
EMAIL_HOST_PASSWORD = env('DJANGO_EMAIL_PASSWORD')
|
||||||
EMAIL_USE_TLS = env('DJANGO_EMAIL_USE_TLS')
|
EMAIL_USE_TLS = env('DJANGO_EMAIL_USE_TLS')
|
||||||
|
EMAIL_SUBJECT_PREFIX = env('DJANGO_EMAIL_SUBJECT_PREFIX')
|
||||||
|
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#default-from-email
|
# https://docs.djangoproject.com/en/dev/ref/settings/#default-from-email
|
||||||
DEFAULT_FROM_EMAIL = env('DJANGO_DEFAULT_FROM_EMAIL')
|
DEFAULT_FROM_EMAIL = env('DJANGO_DEFAULT_FROM_EMAIL')
|
||||||
|
|
@ -334,6 +344,17 @@ AUTH_USER_MODEL = "accounts.CustomUser"
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#site-id
|
# https://docs.djangoproject.com/en/dev/ref/settings/#site-id
|
||||||
SITE_ID = 1
|
SITE_ID = 1
|
||||||
|
|
||||||
|
PARLER_LANGUAGES = {
|
||||||
|
SITE_ID: (
|
||||||
|
{'code': 'en'},
|
||||||
|
),
|
||||||
|
'default': {
|
||||||
|
'fallbacks': ['en'],
|
||||||
|
'hide_untranslated': False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#login-redirect-url
|
# https://docs.djangoproject.com/en/dev/ref/settings/#login-redirect-url
|
||||||
LOGIN_REDIRECT_URL = "home"
|
LOGIN_REDIRECT_URL = "home"
|
||||||
|
|
||||||
|
|
@ -374,6 +395,7 @@ SOCIALACCOUNT_ONLY = False
|
||||||
|
|
||||||
SESSION_ENGINE = "django.contrib.sessions.backends.signed_cookies"
|
SESSION_ENGINE = "django.contrib.sessions.backends.signed_cookies"
|
||||||
SESSION_COOKIE_HTTPONLY = True
|
SESSION_COOKIE_HTTPONLY = True
|
||||||
|
SESSION_COOKIE_SECURE = PUBLIC_HOST != 'localhost' or PUBLIC_HOST != '127.0.0.1'
|
||||||
|
|
||||||
# auto-detection doesn't work properly sometimes, so we'll just use the DEBUG setting
|
# auto-detection doesn't work properly sometimes, so we'll just use the DEBUG setting
|
||||||
DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": lambda request: DEBUG}
|
DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": lambda request: DEBUG}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
{% extends "admin/base_site.html" %}
|
||||||
|
{% load i18n static %}
|
||||||
|
|
||||||
|
{% block extrastyle %}{{ block.super }}
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static "admin/css/dashboard.css" %}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block coltype %}colM{% endblock %}
|
||||||
|
|
||||||
|
{% block bodyclass %}{{ block.super }} dashboard{% endblock %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
<div class="breadcrumbs">
|
||||||
|
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
|
||||||
|
› <a href="{% url 'admin:app_list' app_label='cards' %}">Cards</a>
|
||||||
|
› {% translate 'Full Card Importer' %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{% translate 'Full Card Set Importer' %}</h1>
|
||||||
|
<p>{% translate 'Click the button below to import all card data from the configured JSON file directory.' %}</p>
|
||||||
|
<p>{% translate 'This process will scan all .json files, create or update card sets, and then import or update all individual cards and their related data (packs, abilities, attacks, etc.).' %}</p>
|
||||||
|
|
||||||
|
<form method="POST" action="">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="button btn-success">{% translate 'Start Full Import' %}</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if messages %}
|
||||||
|
<h2>{% translate 'Import Status:' %}</h2>
|
||||||
|
<ul class="messagelist">
|
||||||
|
{% for message in messages %}
|
||||||
|
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue