diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 158fc71..9668fb5 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 10e79dd..cfc3e5a 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index fd109e1..8923acc 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..22aece2 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,49 @@ +# Generated by Django 5.2.7 on 2026-02-18 01:18 + +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='AdminAccess', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('private_key', models.CharField(default=uuid.uuid4, max_length=255, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name='Lottery', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(choices=[('mega_sena', 'Mega-Sena'), ('quina', 'Quina'), ('dupla_sena', 'Dupla Sena'), ('lotomania', 'Lotomania'), ('lotofacil', 'Lotofácil')], max_length=50, unique=True)), + ('min_number', models.IntegerField(default=1)), + ('max_number', models.IntegerField()), + ('numbers_to_draw', models.IntegerField()), + ('annulled_numbers', models.TextField(blank=True, default='')), + ], + ), + migrations.CreateModel( + name='DrawResult', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('draw_number', models.IntegerField()), + ('draw_date', models.DateField()), + ('numbers', models.CharField(max_length=255)), + ('lottery', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='draws', to='core.lottery')), + ], + options={ + 'ordering': ['-draw_date'], + 'unique_together': {('lottery', 'draw_number')}, + }, + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..4cbd3af Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a8362..4b08aec 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,44 @@ from django.db import models +import uuid -# Create your models here. +class AdminAccess(models.Model): + """Armazena a chave privada única para acesso ao painel.""" + private_key = models.CharField(max_length=255, unique=True, default=uuid.uuid4) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"Key created on {self.created_at}" + +class Lottery(models.Model): + """Configurações específicas de cada tipo de loteria.""" + LOTTERY_TYPES = [ + ('mega_sena', 'Mega-Sena'), + ('quina', 'Quina'), + ('dupla_sena', 'Dupla Sena'), + ('lotomania', 'Lotomania'), + ('lotofacil', 'Lotofácil'), + ] + name = models.CharField(max_length=50, choices=LOTTERY_TYPES, unique=True) + min_number = models.IntegerField(default=1) + max_number = models.IntegerField() + numbers_to_draw = models.IntegerField() + + # Lista de números anulados manualmente (armazenado como string separada por vírgula) + annulled_numbers = models.TextField(default="", blank=True) + + def __str__(self): + return self.get_name_display() + +class DrawResult(models.Model): + """Resultados reais dos sorteios da Caixa.""" + lottery = models.ForeignKey(Lottery, on_delete=models.CASCADE, related_name='draws') + draw_number = models.IntegerField() + draw_date = models.DateField() + numbers = models.CharField(max_length=255) # Ex: "05,12,34,45,56,59" + + class Meta: + unique_together = ('lottery', 'draw_number') + ordering = ['-draw_date'] + + def __str__(self): + return f"{self.lottery.name} - Concurso {self.draw_number}" diff --git a/core/templates/core/admin_dashboard.html b/core/templates/core/admin_dashboard.html new file mode 100644 index 0000000..1465dd6 --- /dev/null +++ b/core/templates/core/admin_dashboard.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} +{% block content %} +
+
+

Painel Administrativo

+ Sair +
+
+ {% for lottery in loterias %} +
+
+

{{ lottery.get_name_display }}

+

Números: 1 a {{ lottery.max_number }}

+
+ Editar Números +
+
+ {% endfor %} +
+
+{% endblock %} diff --git a/core/templates/core/admin_login.html b/core/templates/core/admin_login.html new file mode 100644 index 0000000..1730ba4 --- /dev/null +++ b/core/templates/core/admin_login.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} +{% block content %} +
+
+
+
+

Acesso Administrativo

+

Insira sua Private Key para acessar as configurações.

+
+ {% csrf_token %} +
+ +
+ +
+ {% if messages %} + {% for message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} +
+
+
+
+{% endblock %} diff --git a/core/templates/core/edit_lottery.html b/core/templates/core/edit_lottery.html new file mode 100644 index 0000000..73147b0 --- /dev/null +++ b/core/templates/core/edit_lottery.html @@ -0,0 +1,70 @@ +{% extends "base.html" %} +{% block content %} +
+ + +
+
+

Editor: {{ lottery.get_name_display }}

+

Selecione os números para ANULAR no próximo sorteio.

+
+ +
+ {% csrf_token %} +
+ {% for n in numbers %} +
+ + +
+ {% endfor %} +
+ +
+ +
+
+
+
+ + +{% endblock %} diff --git a/core/templates/core/index.html b/core/templates/core/index.html index 35b56fc..0db060f 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -18,7 +18,11 @@ - + {% if is_admin %} + + {% else %} + + {% endif %} @@ -126,19 +130,22 @@
-
Quentes no recorte
+
Probabilidades Quentes (Verde)
{% for number in result.hot_numbers %} - {{ number }} + {{ number|stringformat:"02d" }} {% endfor %}
-
-
Frias no recorte
+
+
Números Anulados (Vermelho)
- {% for number in result.cold_numbers %} - {{ number }} + {% for number in result.annulled_numbers %} + {{ number|stringformat:"02d" }} {% endfor %} + {% if not result.annulled_numbers %} + Nenhum numero anulado pelo admin. + {% endif %}
diff --git a/core/urls.py b/core/urls.py index 6299e3d..4f93d0c 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,10 @@ from django.urls import path - -from .views import home +from . import views urlpatterns = [ - path("", home, name="home"), + path('', views.home, name='home'), + path('admin-loto/', views.admin_login, name='admin_login'), + path('admin-loto/logout/', views.admin_logout, name='admin_logout'), + path('admin-loto/dashboard/', views.admin_dashboard, name='admin_dashboard'), + path('admin-loto/edit//', views.edit_lottery, name='edit_lottery'), ] diff --git a/core/views.py b/core/views.py index bd09fd5..5e0dbed 100644 --- a/core/views.py +++ b/core/views.py @@ -5,121 +5,95 @@ import random from collections import Counter from django import get_version as django_version -from django.shortcuts import render +from django.shortcuts import render, redirect, get_object_or_404 from django.utils import timezone +from django.contrib import messages from .forms import LotterySimulatorForm +from .models import Lottery, DrawResult, AdminAccess +def check_admin(request): + """Verifica se a chave privada na sessão é válida.""" + key = request.session.get('admin_key') + return AdminAccess.objects.filter(private_key=key).exists() -LOTTERY_CONFIGS = { - "mega_sena": { - "label": "Mega-Sena", - "range_max": 60, - "picks": 6, - "sample_draws": 15, - "seed": 1982, - "tagline": "6 dezenas entre 60", - }, - "quina": { - "label": "Quina", - "range_max": 80, - "picks": 5, - "sample_draws": 15, - "seed": 1971, - "tagline": "5 dezenas entre 80", - }, - "dupla_sena": { - "label": "Dupla Sena", - "range_max": 50, - "picks": 6, - "sample_draws": 15, - "seed": 1990, - "tagline": "6 dezenas entre 50", - }, - "lotomania": { - "label": "Lotomania", - "range_max": 100, - "picks": 50, - "sample_draws": 12, - "seed": 1999, - "tagline": "50 dezenas entre 100", - }, - "lotofacil": { - "label": "Lotofacil", - "range_max": 25, - "picks": 15, - "sample_draws": 20, - "seed": 1994, - "tagline": "15 dezenas entre 25", - }, -} +def admin_login(request): + """Tela de login simples para a Chave Privada.""" + if request.method == 'POST': + key = request.POST.get('private_key') + if AdminAccess.objects.filter(private_key=key).exists(): + request.session['admin_key'] = key + return redirect('admin_dashboard') + else: + messages.error(request, "Chave Privada Inválida!") + return render(request, 'core/admin_login.html') +def admin_logout(request): + request.session.flush() + return redirect('home') -def _generate_sample_draws(config): - rng = random.Random(config["seed"]) - draws = [] - population = list(range(1, config["range_max"] + 1)) - for _ in range(config["sample_draws"]): - draws.append(sorted(rng.sample(population, config["picks"]))) - return draws +def admin_dashboard(request): + """Painel principal do administrador.""" + if not check_admin(request): + return redirect('admin_login') + + loterias = Lottery.objects.all() + return render(request, 'core/admin_dashboard.html', {'loterias': loterias}) +def edit_lottery(request, lottery_id): + """Editor específico para cada jogo (anular números).""" + if not check_admin(request): + return redirect('admin_login') + + lottery = get_object_or_404(Lottery, id=lottery_id) + # Ajuste para Lotomania (0-99 se necessário, mas mantendo 1-100 por padrão) + numbers = range(1, lottery.max_number + 1) + annulled = [int(n) for n in lottery.annulled_numbers.split(',') if n] -def _weighted_unique_sample(numbers, weights, picks, rng): - available = list(zip(numbers, weights)) - selection = [] - for _ in range(picks): - total_weight = sum(weight for _, weight in available) - if total_weight <= 0: - choice = rng.choice(available) - selection.append(choice[0]) - available.remove(choice) - continue - pick = rng.uniform(0, total_weight) - cumulative = 0 - for index, (number, weight) in enumerate(available): - cumulative += weight - if cumulative >= pick: - selection.append(number) - del available[index] - break - return selection + if request.method == 'POST': + selected_numbers = request.POST.getlist('numbers') + lottery.annulled_numbers = ",".join(selected_numbers) + lottery.save() + messages.success(request, f"Configurações da {lottery.get_name_display()} salvas!") + return redirect('admin_dashboard') + return render(request, 'core/edit_lottery.html', { + 'lottery': lottery, + 'numbers': numbers, + 'annulled': annulled + }) def _format_odds(total_combinations): if total_combinations >= 1_000_000_000_000: return f"1 em {total_combinations:.2e}" return f"1 em {total_combinations:,}".replace(",", ".") - def _format_percent(odds): percent = 100 / odds if percent < 0.000001: return "<0,000001%" return f"{percent:.6f}%".replace(".", ",") - def home(request): """Render the landing screen with lottery simulator and insights.""" host_name = request.get_host().lower() agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic" now = timezone.now() - lottery_choices = [ - (key, config["label"]) for key, config in LOTTERY_CONFIGS.items() - ] + loterias_db = Lottery.objects.all() + lottery_choices = [(l.name, l.get_name_display()) for l in loterias_db] + lottery_cards = [] - for key, config in LOTTERY_CONFIGS.items(): - total_combinations = math.comb(config["range_max"], config["picks"]) - lottery_cards.append( - { - "key": key, - "label": config["label"], - "tagline": config["tagline"], - "range_max": config["range_max"], - "picks": config["picks"], - "odds": _format_odds(total_combinations), - } - ) + for l in loterias_db: + total_combinations = math.comb(l.max_number, l.numbers_to_draw) + lottery_cards.append({ + "key": l.name, + "label": l.get_name_display(), + "tagline": f"{l.numbers_to_draw} dezenas entre {l.max_number}", + "range_max": l.max_number, + "picks": l.numbers_to_draw, + "odds": _format_odds(total_combinations), + }) form = LotterySimulatorForm( request.POST or None, @@ -131,64 +105,75 @@ def home(request): lottery_key = form.cleaned_data["lottery_type"] draws_to_consider = form.cleaned_data["draws_to_consider"] games_to_generate = form.cleaned_data["games_to_generate"] - config = LOTTERY_CONFIGS[lottery_key] - draws = _generate_sample_draws(config) - draws_to_consider = min(draws_to_consider, len(draws)) - recent_draws = draws[-draws_to_consider:] - frequency = Counter( - number for draw in recent_draws for number in draw - ) - numbers = list(range(1, config["range_max"] + 1)) + + lottery_obj = Lottery.objects.get(name=lottery_key) + annulled = [int(n) for n in lottery_obj.annulled_numbers.split(',') if n] + + # Busca sorteios reais no banco + draws_db = DrawResult.objects.filter(lottery=lottery_obj)[:draws_to_consider] + draw_lists = [] + for d in draws_db: + draw_lists.append([int(n) for n in d.numbers.split(',')]) + + # Se não houver sorteios reais, usa aleatórios para manter a app funcionando + if not draw_lists: + rng_mock = random.Random(42) + population = list(range(1, lottery_obj.max_number + 1)) + for _ in range(draws_to_consider): + draw_lists.append(rng_mock.sample(population, lottery_obj.numbers_to_draw)) + + frequency = Counter(number for draw in draw_lists for number in draw) + numbers = [n for n in range(1, lottery_obj.max_number + 1) if n not in annulled] + + # Números Quentes (Verde) e Frios (Vermelho) + # Quentes: Maior frequência e não anulados + hot_candidates = frequency.most_common(15) + hot_numbers = [n for n, c in hot_candidates if n not in annulled][:8] + + # Pesos para geração baseados na frequência weights = [frequency.get(number, 0) + 1 for number in numbers] rng = random.Random(f"{lottery_key}-{draws_to_consider}-{games_to_generate}") suggestions = [] for _ in range(games_to_generate): - suggestion = _weighted_unique_sample( - numbers, weights, config["picks"], rng - ) - suggestions.append(sorted(suggestion)) + if len(numbers) >= lottery_obj.numbers_to_draw: + # Simulação ponderada simples + indices = list(range(len(numbers))) + ws = [frequency.get(numbers[i], 0) + 1 for i in indices] + selected_indices = random.choices(indices, weights=ws, k=lottery_obj.numbers_to_draw) + # Garante que não repete números no mesmo jogo + game = [] + temp_indices = indices.copy() + for _p in range(lottery_obj.numbers_to_draw): + ws_temp = [frequency.get(numbers[i], 0) + 1 for i in temp_indices] + idx = random.choices(range(len(temp_indices)), weights=ws_temp, k=1)[0] + game.append(numbers[temp_indices[idx]]) + del temp_indices[idx] + suggestions.append(sorted(game)) - total_combinations = math.comb(config["range_max"], config["picks"]) - hot_numbers = [ - number for number, _ in frequency.most_common(8) - ] - cold_numbers = [ - number - for number, _ in sorted( - frequency.items(), key=lambda item: item[1] - ) - ] - missing_numbers = [ - number for number in numbers if number not in frequency - ] - cold_numbers = (missing_numbers + cold_numbers)[:8] + total_combinations = math.comb(lottery_obj.max_number, lottery_obj.numbers_to_draw) + result = { - "lottery": config["label"], - "draws_used": draws_to_consider, + "lottery": lottery_obj.get_name_display(), + "draws_used": len(draw_lists), "total_combinations": f"{total_combinations:,}".replace(",", "."), "odds": _format_odds(total_combinations), "percent": _format_percent(total_combinations), "suggestions": suggestions, "hot_numbers": hot_numbers, - "cold_numbers": cold_numbers, + "annulled_numbers": annulled, } context = { "project_name": "LotoPulse", - "project_description": ( - "Analise loterias brasileiras, gere jogos e acompanhe " - "probabilidades com base nos sorteios mais recentes." - ), + "project_description": "Análise matemática e editor de probabilidades para Loterias Caixa.", "agent_brand": agent_brand, "django_version": django_version(), "python_version": platform.python_version(), "current_time": now, - "host_name": host_name, - "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), - "deployment_timestamp": now.strftime("%Y%m%d%H%M%S"), "form": form, "result": result, "lottery_cards": lottery_cards, + "is_admin": check_admin(request), } return render(request, "core/index.html", context)