├── .python-version ├── backend ├── src │ ├── __init__.py │ └── doritostats │ │ ├── __init__.py │ │ ├── exceptions.py │ │ ├── PseudoTeam.py │ │ ├── PseudoPlayer.py │ │ ├── PseudoMatchup.py │ │ ├── filter_utils.py │ │ ├── pick_value.csv │ │ ├── scrape_player_stats.py │ │ └── draft_utils.py ├── django_errors.log ├── tests │ ├── __init__.py │ ├── test_luck_index.py │ └── test_fetch_utils.py ├── doritostats │ ├── __init__.py │ ├── asgi.py │ ├── wsgi.py │ ├── urls.py │ └── settings.py └── fantasy_stats │ ├── __init__.py │ ├── migrations │ ├── __init__.py │ ├── 0001_initial.py │ ├── 0005_leagueinfo_deleted.py │ ├── 0004_alter_leagueinfo_swid.py │ ├── 0003_leagueinfo_created_date_alter_leagueinfo_espn_s2_and_more.py │ └── 0002_leagueinfo.py │ ├── templatetags │ ├── __init__.py │ └── filters.py │ ├── admin.py │ ├── apps.py │ ├── tests.py │ ├── errors │ ├── error_codes.py │ ├── email.py │ └── error_middleware.py │ ├── sitemaps.py │ ├── styles │ └── LeagueSimulationPage.module.css │ ├── models.py │ └── urls.py ├── frontend ├── src │ ├── styles.css │ ├── components │ │ ├── styles │ │ │ ├── leagueRecords.css │ │ │ ├── logo.css │ │ │ ├── buttonStyles.css │ │ │ ├── loadingRow.css │ │ │ ├── spinner.css │ │ │ ├── common.css │ │ │ ├── mobileStyles.css │ │ │ ├── modalStyles.css │ │ │ ├── tables.css │ │ │ ├── league.css │ │ │ └── tableStyles.css │ │ ├── Layout.js │ │ ├── Header.js │ │ ├── WeekSelector.css │ │ ├── Button.module.css │ │ ├── DoritoStatsLogo.js │ │ ├── LeagueRecordsButton.js │ │ ├── ReturnToLeaguePageButton.js │ │ ├── GitHubIcon.js │ │ ├── LinkedInIcon.js │ │ ├── LoadingRow.js │ │ ├── BuyMeACoffeeImage.js │ │ ├── SimulatePlayoffOddsButton.js │ │ ├── SeasonSelection.js │ │ ├── Footer.js │ │ ├── ReturnToHomePageButton.js │ │ ├── Footer.module.css │ │ ├── PendingDataNotice.js │ │ ├── SimulationSelector.js │ │ ├── CookiesInstructionsBox.js │ │ ├── AwardsReact.js │ │ ├── WeekSelector.js │ │ ├── LeagueSelector.js │ │ ├── Awards.js │ │ ├── NaughtyList.js │ │ ├── SeasonTeamRecordsTable.js │ │ ├── ReturningLeagueSelector.js │ │ ├── SeasonPositionalRecordsTable.js │ │ ├── BoxScoresTable.js │ │ ├── PlayoffOddsTable.js │ │ ├── StandingsTable.js │ │ ├── SeedingOutcomesTable.js │ │ ├── LuckIndexTable.js │ │ ├── WeeklyAwardsTable.js │ │ ├── PowerRankingsTable.js │ │ ├── RankDistributionTable.js │ │ ├── NewLeagueForm.js │ │ └── RemainingStrengthOfScheduleTable.js │ ├── assets │ │ └── img │ │ │ ├── logo.png │ │ │ ├── logo1.png │ │ │ ├── logo2.png │ │ │ ├── favicon.png │ │ │ ├── linkedin-icon.svg │ │ │ └── github-icon.svg │ ├── setupTests.js │ ├── App.test.js │ ├── index.css │ ├── reportWebVitals.js │ ├── utils │ │ ├── csrf.js │ │ ├── google_analytics.js │ │ └── api.js │ ├── pages │ │ ├── LeagueRecordsPage.css │ │ ├── NotFoundPage.js │ │ ├── InvalidLeaguePage.js │ │ ├── UhOhTooEarlyPage.module.css │ │ ├── UhOhTooEarlyPage.js │ │ ├── HomePage.js │ │ ├── LeagueRecordsPage.js │ │ └── TestErrorPage.js │ ├── index.js │ ├── App.css │ ├── logo.svg │ ├── App.js │ └── ErrorBoundary.js ├── public │ └── index.html ├── .gitignore ├── package.json └── README.md ├── runtime.txt ├── db.sqlite3 ├── railway.json ├── .github ├── workflows │ ├── linting.yml │ └── pytest.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── .gitignore ├── dependabot.yaml ├── manage.py ├── LICENSE ├── pyproject.toml └── CODE_OF_CONDUCT.md /.python-version: -------------------------------------------------------------------------------- 1 | 3.12 -------------------------------------------------------------------------------- /backend/src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/styles.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.12 -------------------------------------------------------------------------------- /backend/django_errors.log: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/doritostats/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/fantasy_stats/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/src/doritostats/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/fantasy_stats/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/fantasy_stats/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/components/styles/leagueRecords.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DesiPilla/espn-api-v3/HEAD/db.sqlite3 -------------------------------------------------------------------------------- /frontend/src/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DesiPilla/espn-api-v3/HEAD/frontend/src/assets/img/logo.png -------------------------------------------------------------------------------- /frontend/src/assets/img/logo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DesiPilla/espn-api-v3/HEAD/frontend/src/assets/img/logo1.png -------------------------------------------------------------------------------- /frontend/src/assets/img/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DesiPilla/espn-api-v3/HEAD/frontend/src/assets/img/logo2.png -------------------------------------------------------------------------------- /frontend/src/assets/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DesiPilla/espn-api-v3/HEAD/frontend/src/assets/img/favicon.png -------------------------------------------------------------------------------- /backend/src/doritostats/exceptions.py: -------------------------------------------------------------------------------- 1 | class InactiveLeagueError(Exception): 2 | """Exception raised when a league is inactive.""" 3 | 4 | pass 5 | -------------------------------------------------------------------------------- /backend/fantasy_stats/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import LeagueInfo 4 | 5 | # Register your models here. 6 | admin.site.register(LeagueInfo) 7 | -------------------------------------------------------------------------------- /frontend/src/components/Layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Layout = ({ children }) => { 4 | return
{children}
; 5 | }; 6 | 7 | export default Layout; 8 | -------------------------------------------------------------------------------- /backend/fantasy_stats/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class FantasyStatsConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "backend.fantasy_stats" 7 | -------------------------------------------------------------------------------- /backend/fantasy_stats/tests.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from django.test import TestCase 3 | 4 | # Create your tests here. 5 | import django 6 | 7 | logger = logging.getLogger(__name__) 8 | print(django.get_version()) 9 | -------------------------------------------------------------------------------- /railway.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "buildCommand": "./build.sh" 4 | }, 5 | "deploy": { 6 | "startCommand": "PYTHONPATH=backend gunicorn doritostats.wsgi:application --bind 0.0.0.0:$PORT" 7 | } 8 | } -------------------------------------------------------------------------------- /frontend/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Header = () => ( 4 |
5 |

Welcome to Dorito Stats!

6 |
7 |
8 | ); 9 | 10 | export default Header; 11 | -------------------------------------------------------------------------------- /frontend/src/components/WeekSelector.css: -------------------------------------------------------------------------------- 1 | /* App.css */ 2 | .select { 3 | margin-top: 1rem; 4 | } 5 | 6 | .select select { 7 | padding: 0.5rem; 8 | font-size: 1rem; 9 | border-radius: 6px; 10 | background-color: white; 11 | border: 1px solid #ccc; 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/components/styles/logo.css: -------------------------------------------------------------------------------- 1 | .logo-container { 2 | text-align: center; /* Center the content horizontally */ 3 | } 4 | 5 | .logo { 6 | width: 100%; 7 | max-width: 250px; /* Set a maximum width to ensure it doesn't get too large */ 8 | margin: 0 auto; 9 | } -------------------------------------------------------------------------------- /frontend/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /frontend/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dorito Stats 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /backend/fantasy_stats/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.7 on 2021-09-09 00:43 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | initial = True 9 | 10 | dependencies = [] 11 | 12 | operations = [] 13 | -------------------------------------------------------------------------------- /frontend/src/components/Button.module.css: -------------------------------------------------------------------------------- 1 | .btn { 2 | background-color: #007bff; 3 | color: #fff; 4 | padding: 10px 20px; 5 | margin: 10px; 6 | border: none; 7 | border-radius: 5px; 8 | text-decoration: none; 9 | display: inline-block; 10 | cursor: pointer; 11 | } 12 | 13 | .btn:hover { 14 | background-color: #0056b3; 15 | } -------------------------------------------------------------------------------- /frontend/src/components/DoritoStatsLogo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import doritoStatsLogo from '../assets/img/logo2.png'; 3 | import styles from './styles/logo.css'; 4 | 5 | const DoritoStatsLogo = () => ( 6 | Dorito Stats Logo 11 | ); 12 | 13 | export default DoritoStatsLogo; 14 | -------------------------------------------------------------------------------- /.github/workflows/linting.yml: -------------------------------------------------------------------------------- 1 | name: linting 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | linter_name: 11 | name: runner / black formatter 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: rickstaa/action-black@v1 16 | with: 17 | black_args: ". --check --diff" -------------------------------------------------------------------------------- /frontend/src/components/styles/buttonStyles.css: -------------------------------------------------------------------------------- 1 | .copy-btn:hover { 2 | background: #e6e8ed; 3 | transform: translateY(-1px); 4 | box-shadow: 0 2px 4px rgba(0,0,0,0.15); 5 | } 6 | 7 | .copy-btn:active { 8 | transform: translateY(0); 9 | box-shadow: 0 1px 2px rgba(0,0,0,0.1); 10 | } 11 | 12 | .copy-btn.copy-success { 13 | background-color: #34d399; 14 | color: white; 15 | border-color: #10b981; 16 | } -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /backend/fantasy_stats/templatetags/filters.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | @register.filter(name="add") 7 | def multiply(value, arg): 8 | return value + arg 9 | 10 | 11 | @register.filter(name="multiply") 12 | def multiply(value, arg): 13 | return value * arg 14 | 15 | 16 | @register.filter(name="range") 17 | def filter_range(start, end): 18 | return range(start, end) 19 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /backend/fantasy_stats/errors/error_codes.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class JsonErrorCodes(Enum): 5 | TOO_SOON_LEAGUE = "too_soon_league" 6 | TOO_SOON_SIMULATIONS = "too_soon_simulations" 7 | LEAGUE_SIGNUP_FAILURE = "invalid_league" 8 | UNKNOWN_ERROR = "unknown_error" 9 | 10 | 11 | class InvalidLeagueError(Exception): 12 | """Raised when a league is not found, inactive, or otherwise invalid.""" 13 | 14 | pass 15 | -------------------------------------------------------------------------------- /frontend/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Distribution / packaging 2 | .env 3 | *__pycache__* 4 | *.DS_Store 5 | .vscode* 6 | dist/* 7 | node_modules/* 8 | staticfiles/* 9 | static/* 10 | 11 | # Notebooks 12 | *.ipynb_checkpoints* 13 | *new espn.ipynb 14 | src/new espn.ipynb 15 | website.ipynb 16 | dev.ipynb 17 | 18 | src/tmp.csv 19 | all_league_creds.csv 20 | draft_history.csv 21 | *historical_stats.csv 22 | historical_stats_all_teams.csv 23 | league_info_backup.csv 24 | *copy.csv 25 | tmp.csv 26 | -------------------------------------------------------------------------------- /backend/doritostats/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for doritostats project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "doritostats.settings") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /backend/doritostats/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for doritostats project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "doritostats.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /backend/src/doritostats/PseudoTeam.py: -------------------------------------------------------------------------------- 1 | class PseudoTeam: 2 | """A skeleton of the Team class""" 3 | 4 | def __init__(self, team_id): 5 | self.team_id = team_id 6 | 7 | def __repr__(self): 8 | return "Team(Bye)" 9 | 10 | def __hash__(self): 11 | return hash((self.team_id)) 12 | 13 | def __eq__(self, other): 14 | if not isinstance(other, type(self)): 15 | return NotImplemented 16 | return self.team_id == other.team_id 17 | -------------------------------------------------------------------------------- /frontend/src/utils/csrf.js: -------------------------------------------------------------------------------- 1 | // frontend/src/utils/csrf.js 2 | export function getCookie(name) { 3 | let cookieValue = null; 4 | if (document.cookie && document.cookie !== "") { 5 | const cookies = document.cookie.split(";"); 6 | for (let cookie of cookies) { 7 | cookie = cookie.trim(); 8 | if (cookie.startsWith(name + "=")) { 9 | cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); 10 | break; 11 | } 12 | } 13 | } 14 | return cookieValue; 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/components/LeagueRecordsButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import styles from './Button.module.css'; 4 | 5 | const LeagueRecordsButton = ({ leagueYear, leagueId }) => { 6 | return ( 7 | 11 | View League Records 12 | 13 | ); 14 | }; 15 | 16 | export default LeagueRecordsButton; 17 | -------------------------------------------------------------------------------- /frontend/src/components/styles/loadingRow.css: -------------------------------------------------------------------------------- 1 | .loading-container { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | gap: 10px; 6 | } 7 | 8 | .spinner { 9 | width: 20px; 10 | height: 20px; 11 | border: 3px solid #f3f3f3; 12 | border-top: 3px solid #3498db; 13 | border-radius: 50%; 14 | animation: spin 1s linear infinite; 15 | } 16 | 17 | @keyframes spin { 18 | 0% { 19 | transform: rotate(0deg); 20 | } 21 | 100% { 22 | transform: rotate(360deg); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /backend/fantasy_stats/migrations/0005_leagueinfo_deleted.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.7 on 2025-09-29 03:00 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("fantasy_stats", "0004_alter_leagueinfo_swid"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="leagueinfo", 14 | name="deleted", 15 | field=models.BooleanField(default=False), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /frontend/src/components/ReturnToLeaguePageButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import styles from './Button.module.css'; 4 | 5 | const ReturnToLeaguePageButton = ({ leagueYear, leagueId }) => { 6 | return ( 7 | 11 | Return to Your League 12 | 13 | ); 14 | }; 15 | 16 | export default ReturnToLeaguePageButton; 17 | -------------------------------------------------------------------------------- /backend/src/doritostats/PseudoPlayer.py: -------------------------------------------------------------------------------- 1 | class PseudoPlayer: 2 | """A skeleton of the Player class""" 3 | 4 | def __init__(self): 5 | self.name = "Fake Player" 6 | self.stats = {} 7 | self.points = 0 8 | 9 | def __repr__(self): 10 | return "Player(Bye)" 11 | 12 | def __hash__(self): 13 | return hash((self.name)) 14 | 15 | def __eq__(self, other): 16 | if not isinstance(other, type(self)): 17 | return NotImplemented 18 | return self.name == other.name 19 | -------------------------------------------------------------------------------- /frontend/src/components/GitHubIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import githubIcon from '../assets/img/github-icon.svg'; 3 | import styles from './Footer.module.css'; 4 | 5 | const GitHubIcon = () => ( 6 | 12 | GitHub 17 | 18 | ); 19 | 20 | export default GitHubIcon; 21 | -------------------------------------------------------------------------------- /dependabot.yaml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /frontend/src/components/LinkedInIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import linkedinIcon from '../assets/img/linkedin-icon.svg'; 3 | import styles from './Footer.module.css'; 4 | 5 | const LinkedInIcon = () => ( 6 | 12 | LinkedIn 17 | 18 | ); 19 | 20 | export default LinkedInIcon; 21 | -------------------------------------------------------------------------------- /frontend/src/components/LoadingRow.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./styles/loadingRow.css"; // Create this CSS file for spinner styles 3 | 4 | const LoadingRow = ({ text, colSpan }) => { 5 | return ( 6 | 7 | 8 |
9 |
10 | {text} 11 |
12 | 13 | 14 | ); 15 | }; 16 | 17 | export default LoadingRow; 18 | -------------------------------------------------------------------------------- /frontend/src/components/BuyMeACoffeeImage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './Footer.module.css'; 3 | 4 | const BuyMeACoffeeImage = () => ( 5 | 11 | Buy Me A Coffee 16 | 17 | ); 18 | 19 | export default BuyMeACoffeeImage; 20 | -------------------------------------------------------------------------------- /frontend/src/components/styles/spinner.css: -------------------------------------------------------------------------------- 1 | .spinner-container { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | margin-top: 20px; 7 | } 8 | 9 | .spinner { 10 | border: 2px solid rgba(0, 0, 0, 0.1); /* Reduced border size */ 11 | border-left-color: #4caf50; /* Green color */ 12 | border-radius: 50%; 13 | width: 20px; /* Reduced width */ 14 | height: 20px; /* Reduced height */ 15 | animation: spin 1s linear infinite; 16 | } 17 | 18 | @keyframes spin { 19 | to { 20 | transform: rotate(360deg); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/components/SimulatePlayoffOddsButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import styles from './Button.module.css'; 4 | 5 | const SimulatePlayoffOddsButton = ({ leagueYear, leagueId, n_simulations = 100 }) => { 6 | return ( 7 |
8 | 12 | Simulate Playoff Odds 13 | 14 |
15 | ); 16 | }; 17 | 18 | export default SimulatePlayoffOddsButton; 19 | -------------------------------------------------------------------------------- /backend/fantasy_stats/migrations/0004_alter_leagueinfo_swid.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.7 on 2025-09-09 04:15 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ( 9 | "fantasy_stats", 10 | "0003_leagueinfo_created_date_alter_leagueinfo_espn_s2_and_more", 11 | ), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name="leagueinfo", 17 | name="swid", 18 | field=models.CharField(max_length=100), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /frontend/src/assets/img/linkedin-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/components/SeasonSelection.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const SeasonSelection = () => ( 4 |
5 |

Select your league

6 |
7 | 18 |
19 |
20 | ); 21 | 22 | export default SeasonSelection; 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /backend/src/doritostats/PseudoMatchup.py: -------------------------------------------------------------------------------- 1 | class PseudoMatchup: 2 | """A skeleton of the Matchup class""" 3 | 4 | def __init__(self, home_team, away_team): 5 | self.home_team = home_team 6 | self.away_team = away_team 7 | 8 | def __repr__(self): 9 | return f"Matchup({self.home_team}, {self.away_team})" 10 | 11 | def __hash__(self): 12 | return hash((self.home_team, self.away_team)) 13 | 14 | def __eq__(self, other): 15 | if not isinstance(other, type(self)): 16 | return NotImplemented 17 | return (self.home_team.team_id, self.away_team.team_id) == ( 18 | other.home_team.team_id, 19 | other.away_team.team_id, 20 | ) 21 | -------------------------------------------------------------------------------- /frontend/src/pages/LeagueRecordsPage.css: -------------------------------------------------------------------------------- 1 | .league-records-page { 2 | margin: 0 auto; 3 | padding: 2rem; 4 | max-width: 1200px; 5 | } 6 | 7 | .league-info { 8 | display: flex; 9 | gap: 2rem; 10 | margin-bottom: 1.5rem; 11 | } 12 | 13 | .button-container { 14 | display: flex; 15 | flex-wrap: wrap; 16 | gap: 1rem; 17 | margin-bottom: 2rem; 18 | } 19 | 20 | .records-tables-container { 21 | display: flex; 22 | flex-direction: column; 23 | gap: 2rem; 24 | } 25 | 26 | .table-spacer { 27 | height: 1rem; 28 | } 29 | 30 | @media (max-width: 768px) { 31 | .league-info { 32 | flex-direction: column; 33 | gap: 0.5rem; 34 | } 35 | 36 | .records-tables-container { 37 | gap: 3rem; 38 | } 39 | } -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import reportWebVitals from './reportWebVitals'; 5 | import { BrowserRouter } from 'react-router-dom'; 6 | import App from './App'; // Import your HomePage component 7 | 8 | const root = ReactDOM.createRoot(document.getElementById("root")); 9 | root.render( 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | // If you want to start measuring performance in your app, pass a function 17 | // to log results (for example: reportWebVitals(console.log)) 18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 19 | reportWebVitals(); -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.doritostats.settings") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /frontend/src/utils/google_analytics.js: -------------------------------------------------------------------------------- 1 | export const initGoogleAnalytics = () => { 2 | if ( 3 | window.location.hostname != "localhost" && 4 | window.location.hostname != "127.0.0.1" 5 | ) { 6 | const script1 = document.createElement("script"); 7 | script1.async = true; 8 | script1.src = "https://www.googletagmanager.com/gtag/js?id=G-45P4BQ6XJR"; 9 | document.head.appendChild(script1); 10 | 11 | const script2 = document.createElement("script"); 12 | script2.innerHTML = ` 13 | window.dataLayer = window.dataLayer || []; 14 | function gtag(){dataLayer.push(arguments);} 15 | gtag('js', new Date()); 16 | gtag('config', 'G-45P4BQ6XJR'); 17 | `; 18 | document.head.appendChild(script2); 19 | } 20 | 21 | 22 | } 23 | -------------------------------------------------------------------------------- /backend/fantasy_stats/sitemaps.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from django.contrib.sitemaps import Sitemap 3 | from .models import LeagueInfo 4 | 5 | 6 | class StaticViewSitemap(Sitemap): 7 | def items(self): 8 | return [ 9 | "/", 10 | "/fantasy_stats/", 11 | "/all_leagues/", 12 | "/fantasy_stats/all_leagues/", 13 | ] 14 | 15 | def location(self, item): 16 | return item 17 | 18 | 19 | class LeagueHomeSitemap(Sitemap): 20 | def items(self): 21 | return LeagueInfo.objects.all() 22 | 23 | 24 | class LeagueSimulationsSitemap(Sitemap): 25 | def items(self): 26 | return LeagueInfo.objects.filter() 27 | 28 | def location(self, obj): 29 | return f"/fantasy_stats/simulation/{obj.league_year}/{obj.league_id}/" 30 | -------------------------------------------------------------------------------- /frontend/src/components/styles/common.css: -------------------------------------------------------------------------------- 1 | .league-select-button { 2 | background-color: #007bff; 3 | color: white; 4 | padding: 10px 20px; 5 | border: none; 6 | border-radius: 5px; 7 | cursor: pointer; 8 | font-size: 16px; 9 | } 10 | 11 | .league-select-button:hover { 12 | background-color: #0056b3; 13 | } 14 | 15 | .league-dropdown { 16 | width: 100%; 17 | padding: 10px; 18 | font-size: 16px; 19 | border: 1px solid #ccc; 20 | border-radius: 5px; 21 | background-color: #f9f9f9; 22 | } 23 | 24 | .league-dropdown-container { 25 | display: flex; 26 | flex-direction: column; 27 | gap: 0px; /* Add spacing between dropdowns */ 28 | align-items: stretch; /* Ensure dropdowns stretch to full width */ 29 | } 30 | 31 | .league-dropdown:focus { 32 | border-color: #007bff; 33 | outline: none; 34 | } 35 | -------------------------------------------------------------------------------- /frontend/src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './Footer.module.css'; 3 | import GitHubIcon from './GitHubIcon'; 4 | import LinkedInIcon from './LinkedInIcon'; 5 | import BuyMeACoffeeImage from './BuyMeACoffeeImage'; 6 | 7 | const Footer = () => ( 8 | 22 | ); 23 | 24 | export default Footer; 25 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | /* background-color: #282c34; */ 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | 40 | .page-container { 41 | margin: 0 auto; 42 | padding: 2rem; 43 | max-width: 1200px; 44 | } 45 | -------------------------------------------------------------------------------- /frontend/src/components/ReturnToHomePageButton.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | import styles from "./Button.module.css"; 4 | 5 | /** 6 | * Props: 7 | * - insideErrorBoundary (boolean) — true if used inside an ErrorBoundary 8 | */ 9 | const ReturnToHomePageButton = ({ insideErrorBoundary = false }) => { 10 | const navigate = useNavigate(); 11 | 12 | const handleClick = () => { 13 | if (insideErrorBoundary) { 14 | // Force a full page reload through Django 15 | window.location.assign(window.location.origin + "/"); 16 | } else { 17 | // Normal client-side navigation 18 | navigate("/"); 19 | } 20 | }; 21 | 22 | return ( 23 |
24 | 27 |
28 | ); 29 | }; 30 | 31 | export default ReturnToHomePageButton; 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /backend/fantasy_stats/styles/LeagueSimulationPage.module.css: -------------------------------------------------------------------------------- 1 | .pageContainer { 2 | padding: 20px; 3 | /* background-color: #e8f5e9; Light green background */ 4 | max-width: 1200px; 5 | margin: 0 auto; 6 | border-radius: 8px; 7 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 8 | } 9 | 10 | .pageTitle { 11 | font-size: 2rem; 12 | color: #2e7d32; /* Dark green text */ 13 | margin-bottom: 20px; 14 | } 15 | 16 | .selectorContainer { 17 | display: flex; 18 | gap: 20px; 19 | margin-bottom: 20px; 20 | } 21 | 22 | .buttonContainer { 23 | display: flex; 24 | gap: 10px; 25 | margin-bottom: 20px; 26 | } 27 | 28 | .playoffOddsSection { 29 | margin-top: 30px; 30 | } 31 | 32 | .btn { 33 | padding: 10px 20px; 34 | background-color: #1976d2; /* Blue button */ 35 | color: white; 36 | text-decoration: none; 37 | border-radius: 4px; 38 | font-size: 1rem; 39 | text-align: center; 40 | } 41 | 42 | .btn:hover { 43 | background-color: #1565c0; /* Darker blue on hover */ 44 | } 45 | -------------------------------------------------------------------------------- /backend/fantasy_stats/migrations/0003_leagueinfo_created_date_alter_leagueinfo_espn_s2_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2024-07-25 00:12 2 | 3 | import django.utils.timezone 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("fantasy_stats", "0002_leagueinfo"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="leagueinfo", 15 | name="created_date", 16 | field=models.DateTimeField( 17 | auto_now_add=True, default=django.utils.timezone.now 18 | ), 19 | preserve_default=False, 20 | ), 21 | migrations.AlterField( 22 | model_name="leagueinfo", 23 | name="espn_s2", 24 | field=models.CharField(max_length=500), 25 | ), 26 | migrations.AlterField( 27 | model_name="leagueinfo", 28 | name="swid", 29 | field=models.CharField(max_length=36), 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /frontend/src/components/Footer.module.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | background-color: #f8f9fa; /* Light background color */ 3 | padding: 20px; 4 | text-align: center; 5 | } 6 | 7 | .container { 8 | max-width: 1200px; 9 | margin: 0 auto; 10 | } 11 | 12 | .footerContent { 13 | display: flex; 14 | flex-direction: column; 15 | align-items: center; 16 | justify-content: center; 17 | } 18 | 19 | .footerText p { 20 | font-size: 14px; 21 | color: #6c757d; 22 | margin-bottom: 10px; 23 | } 24 | 25 | .footerIcons { 26 | display: flex; 27 | justify-content: center; 28 | align-items: center; 29 | margin-bottom: 10px; 30 | } 31 | 32 | .iconLink { 33 | margin: 0 10px; /* Space out the icons */ 34 | } 35 | 36 | .icon { 37 | width: 32px; /* Adjust the icon size */ 38 | height: 32px; 39 | object-fit: contain; /* Ensure icons retain aspect ratio */ 40 | } 41 | 42 | .buyMeACoffee { 43 | margin-top: 10px; 44 | } 45 | 46 | .buyMeACoffeeImage { 47 | height: 41px; 48 | width: 174px; 49 | box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5); 50 | } 51 | -------------------------------------------------------------------------------- /frontend/src/components/PendingDataNotice.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | /** 5 | * A reusable component that displays a notification when data is not yet finalized 6 | * 7 | * @param {Object} props 8 | * @param {string} props.dataType - The type of data that is likely to change (e.g., "playoff odds", "Power Rankings") 9 | * @param {boolean} props.isPending - Whether to display the notice or not 10 | */ 11 | const PendingDataNotice = ({ dataType, isPending }) => { 12 | if (!isPending) return null; 13 | 14 | return ( 15 |

16 | 17 | Note that scores have not yet been finalized for this 18 | week and the {dataType} {dataType.toLowerCase().includes('list') ? 'are' : 'is'} likely to change. 19 |
20 | Please check back on Tuesday morning for the final 21 | results. 22 |
23 |

24 | ); 25 | }; 26 | 27 | PendingDataNotice.propTypes = { 28 | dataType: PropTypes.string.isRequired, 29 | isPending: PropTypes.bool.isRequired 30 | }; 31 | 32 | export default PendingDataNotice; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Desi Pilla 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /backend/fantasy_stats/migrations/0002_leagueinfo.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.7 on 2021-09-18 19:06 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("fantasy_stats", "0001_initial"), 9 | ] 10 | 11 | operations = [ 12 | migrations.CreateModel( 13 | name="LeagueInfo", 14 | fields=[ 15 | ( 16 | "id", 17 | models.BigAutoField( 18 | auto_created=True, 19 | primary_key=True, 20 | serialize=False, 21 | verbose_name="ID", 22 | ), 23 | ), 24 | ("league_id", models.IntegerField()), 25 | ("league_year", models.IntegerField()), 26 | ("swid", models.CharField(max_length=50)), 27 | ("espn_s2", models.CharField(max_length=300)), 28 | ( 29 | "league_name", 30 | models.CharField(default="", max_length=50), 31 | ), 32 | ], 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /frontend/src/components/SimulationSelector.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const SimulationSelector = ({ nSimulations, setNSimulations }) => { 4 | const handleChange = (event) => { 5 | const value = event.target.value; 6 | setNSimulations(value === "--" ? null : parseInt(value, 10)); // Set to null if "--" is selected 7 | }; 8 | 9 | return ( 10 |
11 | Playoff odds are calculated by generating {nSimulations || "--"} Monte Carlo simulations and comparing the results. 12 |
13 | You can choose a different n by selecting one of these options: 14 | 26 |
27 | You can also edit the n_simulations= value in the URL. However, the more simulations that are run, the longer the page will take to load. 28 |
29 | ); 30 | }; 31 | 32 | export default SimulationSelector; 33 | -------------------------------------------------------------------------------- /frontend/src/components/styles/mobileStyles.css: -------------------------------------------------------------------------------- 1 | /* Additional styles for mobile devices */ 2 | @media (max-width: 768px) { 3 | .table, 4 | .table-with-bottom-caption { 5 | font-size: 0.9rem; 6 | min-width: auto; 7 | width: 100%; 8 | } 9 | 10 | .table th, 11 | .table-with-bottom-caption th, 12 | .table td, 13 | .table-with-bottom-caption td { 14 | padding: 0.5rem; 15 | } 16 | 17 | .header-controls-row { 18 | flex-direction: column; 19 | align-items: flex-start; 20 | width: 100%; 21 | } 22 | 23 | .header-controls-row h2 { 24 | margin-bottom: 0.5rem; 25 | } 26 | 27 | .copy-btn { 28 | margin-bottom: 0.5rem; 29 | align-self: flex-end; 30 | width: auto; 31 | text-align: center; 32 | font-size: 0.9rem; 33 | padding: 6px 10px; 34 | } 35 | 36 | .wrapper-wide { 37 | padding: 8px; 38 | } 39 | } 40 | 41 | @media (max-width: 576px) { 42 | .table, 43 | .table-with-bottom-caption { 44 | font-size: 0.85rem; 45 | } 46 | 47 | .table-container { 48 | overflow-x: auto; 49 | width: 100%; 50 | } 51 | 52 | .wrapper-wide { 53 | padding: 5px; 54 | } 55 | } -------------------------------------------------------------------------------- /.github/workflows/pytest.yml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | max-parallel: 1 14 | matrix: 15 | python-version: ["3.12"] 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v3 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | 25 | - name: Install Poetry 26 | run: | 27 | python -m pip install --upgrade pip 28 | pip install poetry 29 | 30 | - name: Install Dependencies 31 | run: poetry install 32 | 33 | - name: Run Pytest 34 | env: 35 | PYTHONPATH: ./backend 36 | DJANGO_SECRET_KEY: ${{ secrets.DJANGO_SECRET_KEY }} 37 | DATABASE_USERNAME: ${{ secrets.DATABASE_USERNAME }} 38 | DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} 39 | DATABASE_URL: ${{ secrets.DATABASE_URL }} 40 | SECRET_KEY: ${{ secrets.SECRET_KEY }} 41 | LEAGUE_ID: ${{ secrets.LEAGUE_ID }} 42 | SWID: ${{ secrets.SWID }} 43 | ESPN_S2: ${{ secrets.ESPN_S2 }} 44 | run: poetry run pytest 45 | -------------------------------------------------------------------------------- /frontend/src/components/CookiesInstructionsBox.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const CookiesInstructionsBox = () => { 4 | return ( 5 |
6 |

Don't know your SWID or espn_s2? (instructions for Mac / PC)

7 |
    8 |
  1. Log into your espn fantasy football account at https://www.espn.com/fantasy/football/.
  2. 9 |
  3. Right click anywhere on the screen (Chrome browser only) and click Inspect.
  4. 10 |
  5. In the window that appears on the right, click Application on the top bar (you may have to click the dropdown arrow next to Elements, Console, Sources...).
  6. 11 |
  7. On the left, navigate to Storage > Cookies > http://fantasy.espn.com.
  8. 12 |
  9. Scroll down in the table to the right until you find SWID. Copy & paste the alphanumeric string in the Value column (without the curly brackets).
    It should look something like: 43B70875-0C4B-428L-B608-759A4BB28FA1
  10. 13 |
  11. Next, keep scrolling until you find espn_s2. Again, copy and paste the alphanumeric string in the Value column. This code will be much longer and won't have curly brackets in it.
  12. 14 |
15 |
16 | ); 17 | }; 18 | 19 | export default CookiesInstructionsBox; 20 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "doritostats" 3 | version = "4.0.0" 4 | description = "This project aims to make ESPN Fantasy Football statistics easily available. With the introduction of version 3 of the ESPN's API, this structure creates leagues, teams, and player classes that allow for advanced data analytics and the potential for many new features to be added." 5 | authors = ["Desi Pilla "] 6 | license = "https://github.com/DesiPilla/espn-api-v3/blob/master/LICENSE" 7 | readme = "README.md" 8 | packages = [{include = "backend"}, { from = "backend/src", include = "doritostats" }] 9 | 10 | [tool.poetry.dependencies] 11 | python = ">=3.12,<3.13" 12 | espn-api = "0.44.1" 13 | numpy = "^2.1.2" 14 | pandas = "^2.2.2" 15 | pytest = "^8.3.3" 16 | python-dotenv = "^0.21.0" 17 | requests = "^2.32.3" 18 | scikit-learn = "^1.5.0" 19 | 20 | black = "24.3.0" 21 | certifi = "^2024.8.30" 22 | dj-database-url = "^2.2.0" 23 | gunicorn = "^23.0.0" 24 | joblib = "^1.4.2" 25 | scipy = "^1.14.1" 26 | setuptools = "^75.2.0" 27 | sqlalchemy = "^2.0.36" 28 | urllib3 = "^2.2.3" 29 | whitenoise = { extras = ["brotli"], version = "^6.7.0" } 30 | Django = "^5.1.2" 31 | psycopg2-binary = "^2.9.10" 32 | pytz = "^2024.2" 33 | djangorestframework = "^3.15.2" 34 | django-cors-headers = "^4.7.0" 35 | resend = "^2.13.1" 36 | 37 | [build-system] 38 | requires = ["poetry-core"] 39 | build-backend = "poetry.core.masonry.api" 40 | -------------------------------------------------------------------------------- /frontend/src/components/AwardsReact.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {allAwardsData} from './Awards'; 3 | 4 | 5 | 6 | function MakeRow(awardRow){ 7 | return ( 8 | 9 | {awardRow.description} 10 | {awardRow.value} 11 | {awardRow.owner} 12 | 13 | ) 14 | } 15 | 16 | function AwardsTable() { 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {allAwardsData["bestMatchAwards"].map(MakeRow)} 28 | {allAwardsData["worstMatchAwards"].map(MakeRow)} 29 | {allAwardsData["bestSeasonAwards"].map(MakeRow)} 30 | {allAwardsData["worstSeasonAwards"].map(MakeRow)} 31 | 32 |
DescriptionValueOwner
33 | ) 34 | } 35 | 36 | function Tabs() { 37 | return ( 38 |
39 | 40 | 41 | 42 | 43 |
44 | ) 45 | } 46 | 47 | export default function AwardsPage() { 48 | return ( 49 |
50 |

Awards

51 | 52 | 53 |
54 | ); 55 | } -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "proxy": "http://localhost:8001", 6 | "react-router-dom": "^6.18.0", 7 | "dependencies": { 8 | "@testing-library/dom": "^10.4.0", 9 | "@testing-library/jest-dom": "^6.6.3", 10 | "@testing-library/react": "^16.2.0", 11 | "@testing-library/user-event": "^13.5.0", 12 | "axios": "^1.8.3", 13 | "html-to-image": "^1.11.13", 14 | "react": "^19.0.0", 15 | "react-dom": "^19.0.0", 16 | "react-router-dom": "^6.30.0", 17 | "react-scripts": "^3.0.1", 18 | "web-vitals": "^2.1.4" 19 | }, 20 | "scripts": { 21 | "start": "NODE_OPTIONS=--openssl-legacy-provider react-scripts start", 22 | "build": "NODE_OPTIONS=--openssl-legacy-provider react-scripts build", 23 | "test": "react-scripts test", 24 | "eject": "react-scripts eject" 25 | }, 26 | "eslintConfig": { 27 | "extends": [ 28 | "react-app", 29 | "react-app/jest" 30 | ] 31 | }, 32 | "babel": { 33 | "presets": [ 34 | [ 35 | "@babel/preset-react", 36 | { 37 | "runtime": "automatic" 38 | } 39 | ] 40 | ] 41 | }, 42 | "browserslist": { 43 | "production": [ 44 | ">0.2%", 45 | "not dead", 46 | "not op_mini all" 47 | ], 48 | "development": [ 49 | "last 1 chrome version", 50 | "last 1 firefox version", 51 | "last 1 safari version" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /backend/tests/test_luck_index.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import src.doritostats.luck_index as luck # The code to test 3 | from typing import List 4 | 5 | 6 | # luck.calculate_scheduling_factor -- TODO 7 | # luck.get_injury_bye_factor -- TODO 8 | # luck.get_performance_vs_projection_factor -- TODO 9 | # luck.get_optimal_vs_actual_factor -- TODO 10 | # luck.get_optimal_vs_optimal_factor -- TODO 11 | 12 | 13 | @pytest.mark.parametrize( 14 | "team_score, team_scores, result", 15 | [ 16 | (100, [105, 110, 90, 95], 0), 17 | (90, [105, 110, 90, 95], -0.63), 18 | (110, [105, 110, 90, 95], 0.63), 19 | (80, [105, 110, 90, 95], -1), 20 | (120, [105, 110, 90, 95], 1), 21 | ], 22 | ) 23 | def test_calculate_performance_vs_historical_average( 24 | team_score: float, team_scores: List[float], result: float 25 | ): 26 | assert luck.calculate_performance_vs_historical_average( 27 | team_score, team_scores 28 | ) == pytest.approx(result, 0.01) 29 | 30 | 31 | @pytest.mark.parametrize( 32 | "team_score, opp_score, result", 33 | [ 34 | (100, 110, -0.5), 35 | (110, 100, 0.5), 36 | (100, 50, 0), 37 | (50, 100, 0), 38 | (100, 101, -0.95), 39 | (101, 100, 0.95), 40 | ], 41 | ) 42 | def test_calculate_margin_of_victory_factor( 43 | team_score: float, opp_score: float, result: float 44 | ): 45 | assert luck.calculate_margin_of_victory_factor(team_score, opp_score) == result 46 | -------------------------------------------------------------------------------- /frontend/src/pages/NotFoundPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Header from '../components/Header'; 3 | import Footer from '../components/Footer'; 4 | import DoritoStatsLogo from '../components/DoritoStatsLogo'; 5 | import ReturnToHomePageButton from '../components/ReturnToHomePageButton'; 6 | import styles from './UhOhTooEarlyPage.module.css'; 7 | 8 | const NotFoundPage = () => { 9 | return ( 10 | <> 11 |
12 |
13 |
14 |
15 | 16 |
17 |
18 |

19 | Page Not Found{' '} 20 | 21 | ⚠️ 22 | 23 |

24 |
25 |
26 | 27 |
28 |

29 | We're sorry, but the page you are looking for does not exist. 30 | Please check the URL or return to the homepage. 31 |

32 |

Thank you for your understanding.

33 |
34 | 35 |
36 |
37 |
38 |
39 |