From 7c0c407f1c16294222beb25ccf2a9393fa5f137d Mon Sep 17 00:00:00 2001 From: Nick Bottone Date: Thu, 28 May 2026 00:49:07 -0400 Subject: [PATCH 1/5] Reset Django migrations baseline --- .gitignore | 1 - discordoauth2/migrations/0001_initial.py | 53 +++++++++++ discordoauth2/migrations/__init__.py | 1 + events/migrations/0001_initial.py | 110 +++++++++++++++++++++++ events/migrations/__init__.py | 1 + highscores/migrations/0001_initial.py | 80 +++++++++++++++++ highscores/migrations/__init__.py | 0 home/migrations/0001_initial.py | 38 ++++++++ home/migrations/__init__.py | 1 + ladder/migrations/__init__.py | 0 ranked/migrations/0001_initial.py | 67 ++++++++++++++ ranked/migrations/__init__.py | 1 + teamleague/migrations/__init__.py | 0 13 files changed, 352 insertions(+), 1 deletion(-) create mode 100644 discordoauth2/migrations/0001_initial.py create mode 100644 discordoauth2/migrations/__init__.py create mode 100644 events/migrations/0001_initial.py create mode 100644 events/migrations/__init__.py create mode 100644 highscores/migrations/0001_initial.py create mode 100644 highscores/migrations/__init__.py create mode 100644 home/migrations/0001_initial.py create mode 100644 home/migrations/__init__.py create mode 100644 ladder/migrations/__init__.py create mode 100644 ranked/migrations/0001_initial.py create mode 100644 ranked/migrations/__init__.py create mode 100644 teamleague/migrations/__init__.py diff --git a/.gitignore b/.gitignore index c3b2e73..646d662 100644 --- a/.gitignore +++ b/.gitignore @@ -132,7 +132,6 @@ dmypy.json # Database *.sqlite3 -migrations/ .vscode/ .DS_Store diff --git a/discordoauth2/migrations/0001_initial.py b/discordoauth2/migrations/0001_initial.py new file mode 100644 index 0000000..11d0b77 --- /dev/null +++ b/discordoauth2/migrations/0001_initial.py @@ -0,0 +1,53 @@ +# Generated by Django 3.2.25 on 2026-05-28 04:36 + +import discordoauth2.managers +import django.contrib.auth.validators +import django.core.validators +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('display_name', models.CharField(max_length=25, null=True, validators=[django.contrib.auth.validators.ASCIIUsernameValidator(), django.core.validators.MinLengthValidator(4)], verbose_name='display name')), + ('id', models.BigIntegerField(primary_key=True, serialize=False, verbose_name='user id')), + ('username', models.CharField(max_length=100, verbose_name='username')), + ('discriminator', models.CharField(max_length=4, verbose_name='discriminator')), + ('avatar', models.URLField(verbose_name='avatar')), + ('public_flags', models.IntegerField(verbose_name='public flags')), + ('flags', models.IntegerField(verbose_name='flags')), + ('locale', models.CharField(max_length=100, verbose_name='locale')), + ('mfa_enabled', models.BooleanField(verbose_name='multi-factor authentication enabled')), + ('email', models.EmailField(max_length=254, verbose_name='email address')), + ('verified', models.BooleanField(help_text='Whether the user has verified their email address.', verbose_name='email verified')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', discordoauth2.managers.DiscordUserOAuth2Manager()), + ], + ), + ] diff --git a/discordoauth2/migrations/__init__.py b/discordoauth2/migrations/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/discordoauth2/migrations/__init__.py @@ -0,0 +1 @@ + diff --git a/events/migrations/0001_initial.py b/events/migrations/0001_initial.py new file mode 100644 index 0000000..8953f1a --- /dev/null +++ b/events/migrations/0001_initial.py @@ -0,0 +1,110 @@ +# Generated by Django 3.2.25 on 2026-05-28 04:36 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='ChampionshipPoints', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('player_name', models.CharField(max_length=25)), + ('event_1', models.IntegerField(default=0)), + ('event_2', models.IntegerField(default=0)), + ], + ), + migrations.CreateModel( + name='Event', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=25)), + ('start_time', models.DateTimeField()), + ('end_time', models.DateTimeField()), + ], + ), + migrations.CreateModel( + name='Ranking', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('player_name', models.CharField(max_length=25)), + ('ranking_points', models.IntegerField()), + ('time_set', models.DateTimeField(blank=True, null=True)), + ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.event')), + ], + ), + migrations.CreateModel( + name='Player', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('player_name', models.CharField(max_length=25)), + ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.event')), + ], + ), + migrations.CreateModel( + name='Match', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('match_number', models.IntegerField(blank=True, null=True)), + ('match_type', models.CharField(choices=[('q', 'Qualifications'), ('qf', 'Quarterfinals'), ('sf', 'Semifinals'), ('f', 'Finals')], default='q', max_length=2)), + ('red1', models.CharField(max_length=25)), + ('red2', models.CharField(max_length=25)), + ('red3', models.CharField(max_length=25)), + ('blue1', models.CharField(max_length=25)), + ('blue2', models.CharField(max_length=25)), + ('blue3', models.CharField(max_length=25)), + ('red1_redcard', models.BooleanField(default=False)), + ('red2_redcard', models.BooleanField(default=False)), + ('red3_redcard', models.BooleanField(default=False)), + ('blue1_redcard', models.BooleanField(default=False)), + ('blue2_redcard', models.BooleanField(default=False)), + ('blue3_redcard', models.BooleanField(default=False)), + ('red1_surrogate', models.BooleanField(default=False)), + ('red2_surrogate', models.BooleanField(default=False)), + ('red3_surrogate', models.BooleanField(default=False)), + ('blue1_surrogate', models.BooleanField(default=False)), + ('blue2_surrogate', models.BooleanField(default=False)), + ('blue3_surrogate', models.BooleanField(default=False)), + ('red1_contribution', models.IntegerField(blank=True, null=True)), + ('red2_contribution', models.IntegerField(blank=True, null=True)), + ('red3_contribution', models.IntegerField(blank=True, null=True)), + ('blue1_contribution', models.IntegerField(blank=True, null=True)), + ('blue2_contribution', models.IntegerField(blank=True, null=True)), + ('blue3_contribution', models.IntegerField(blank=True, null=True)), + ('red_score', models.IntegerField(blank=True, null=True)), + ('red_climb_rp', models.BooleanField(blank=True, null=True)), + ('red_wheel_rp', models.BooleanField(blank=True, null=True)), + ('red_auto_points', models.IntegerField(blank=True, null=True)), + ('red_teleop_points', models.IntegerField(blank=True, null=True)), + ('red_endgame_points', models.IntegerField(blank=True, null=True)), + ('red_power_cells', models.IntegerField(blank=True, null=True)), + ('blue_score', models.IntegerField(blank=True, null=True)), + ('blue_climb_rp', models.BooleanField(blank=True, null=True)), + ('blue_wheel_rp', models.BooleanField(blank=True, null=True)), + ('blue_auto_points', models.IntegerField(blank=True, null=True)), + ('blue_teleop_points', models.IntegerField(blank=True, null=True)), + ('blue_endgame_points', models.IntegerField(blank=True, null=True)), + ('blue_power_cells', models.IntegerField(blank=True, null=True)), + ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.event')), + ], + ), + migrations.CreateModel( + name='ElimsAlliance', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('alliance_number', models.IntegerField(blank=True, null=True)), + ('player1', models.CharField(max_length=25)), + ('player2', models.CharField(max_length=25)), + ('player3', models.CharField(max_length=25)), + ('advancement', models.CharField(choices=[('qf', 'QF'), ('sf', 'SF'), ('f', 'F'), ('w', 'W')], default='qf', max_length=25)), + ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.event')), + ], + ), + ] diff --git a/events/migrations/__init__.py b/events/migrations/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/events/migrations/__init__.py @@ -0,0 +1 @@ + diff --git a/highscores/migrations/0001_initial.py b/highscores/migrations/0001_initial.py new file mode 100644 index 0000000..8a57cd6 --- /dev/null +++ b/highscores/migrations/0001_initial.py @@ -0,0 +1,80 @@ +# Generated by Django 3.2.25 on 2026-05-28 04:36 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='ExemptedIP', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('ip', models.CharField(max_length=45)), + ('reason', models.CharField(blank=True, max_length=200, null=True)), + ], + ), + migrations.CreateModel( + name='Leaderboard', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=25)), + ('robot', models.CharField(max_length=25)), + ('game', models.CharField(max_length=25)), + ('game_slug', models.CharField(max_length=5)), + ('auto_or_teleop', models.CharField(default='TELE', max_length=4)), + ('message', models.CharField(blank=True, max_length=200, null=True)), + ], + ), + migrations.CreateModel( + name='Score', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('score', models.IntegerField()), + ('time_set', models.DateTimeField(auto_now_add=True)), + ('source', models.URLField()), + ('approved', models.BooleanField(default=False)), + ('clean_code', models.CharField(max_length=4000)), + ('decrypted_code', models.CharField(blank=True, max_length=2000, null=True)), + ('client_version', models.CharField(blank=True, max_length=20, null=True)), + ('time_of_score', models.CharField(blank=True, max_length=30, null=True)), + ('robot_position', models.CharField(blank=True, max_length=20, null=True)), + ('time_data', models.TextField(blank=True, null=True)), + ('ip', models.CharField(blank=True, max_length=45, null=True)), + ('leaderboard', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='highscores.leaderboard')), + ('player', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='CleanCodeSubmission', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('clean_code', models.CharField(max_length=600)), + ('score', models.IntegerField()), + ('time_set', models.DateTimeField(auto_now_add=True)), + ('ip', models.CharField(blank=True, max_length=45, null=True)), + ('leaderboard', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='highscores.leaderboard')), + ('player', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AddIndex( + model_name='score', + index=models.Index(fields=['leaderboard'], name='highscores__leaderb_ad4364_idx'), + ), + migrations.AddIndex( + model_name='score', + index=models.Index(fields=['player'], name='highscores__player__3776b0_idx'), + ), + migrations.AddIndex( + model_name='score', + index=models.Index(fields=['approved'], name='highscores__approve_054a97_idx'), + ), + ] diff --git a/highscores/migrations/__init__.py b/highscores/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/home/migrations/0001_initial.py b/home/migrations/0001_initial.py new file mode 100644 index 0000000..b5f9d0a --- /dev/null +++ b/home/migrations/0001_initial.py @@ -0,0 +1,38 @@ +# Generated by Django 3.2.25 on 2026-05-28 04:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='HistoricEvent', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50)), + ('date', models.DateField()), + ('youtube_url', models.URLField(blank=True, null=True)), + ('first_place', models.CharField(max_length=100)), + ('second_place', models.CharField(blank=True, max_length=100, null=True)), + ], + ), + migrations.CreateModel( + name='Staff', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50)), + ('title', models.CharField(max_length=50)), + ('image_url', models.URLField()), + ('bio', models.TextField()), + ('linkedin_url', models.URLField(blank=True, null=True)), + ('github_url', models.URLField(blank=True, null=True)), + ('email', models.EmailField(max_length=254)), + ], + ), + ] diff --git a/home/migrations/__init__.py b/home/migrations/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/home/migrations/__init__.py @@ -0,0 +1 @@ + diff --git a/ladder/migrations/__init__.py b/ladder/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ranked/migrations/0001_initial.py b/ranked/migrations/0001_initial.py new file mode 100644 index 0000000..cea68c6 --- /dev/null +++ b/ranked/migrations/0001_initial.py @@ -0,0 +1,67 @@ +# Generated by Django 3.2.25 on 2026-05-28 04:36 + +from django.conf import settings +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='GameMode', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=25, unique=True)), + ('game', models.CharField(max_length=25)), + ('players_per_alliance', models.IntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(3)])), + ('short_code', models.CharField(max_length=10, unique=True)), + ], + ), + migrations.CreateModel( + name='PlayerElo', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('elo', models.FloatField(default=1200)), + ('matches_played', models.IntegerField(default=0)), + ('matches_won', models.IntegerField(default=0)), + ('matches_lost', models.IntegerField(default=0)), + ('matches_drawn', models.IntegerField(default=0)), + ('last_match_played_time', models.DateTimeField(blank=True, null=True)), + ('last_match_played_number', models.IntegerField(blank=True, null=True)), + ('total_score', models.IntegerField(default=0)), + ('game_mode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ranked.gamemode')), + ('player', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Match', + fields=[ + ('match_number', models.AutoField(primary_key=True, serialize=False)), + ('time', models.DateTimeField(auto_now_add=True)), + ('red_score', models.IntegerField()), + ('blue_score', models.IntegerField()), + ('red_starting_elo', models.FloatField()), + ('blue_starting_elo', models.FloatField()), + ('blue_alliance', models.ManyToManyField(related_name='blue_alliance', to=settings.AUTH_USER_MODEL)), + ('game_mode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ranked.gamemode')), + ('red_alliance', models.ManyToManyField(related_name='red_alliance', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='EloHistory', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('match_number', models.IntegerField()), + ('elo', models.FloatField()), + ('player_elo', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ranked.playerelo')), + ], + ), + ] diff --git a/ranked/migrations/__init__.py b/ranked/migrations/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/ranked/migrations/__init__.py @@ -0,0 +1 @@ + diff --git a/teamleague/migrations/__init__.py b/teamleague/migrations/__init__.py new file mode 100644 index 0000000..e69de29 From b420d4299e4881eef50fba400c05ea4286fbe045 Mon Sep 17 00:00:00 2001 From: Nick Bottone Date: Thu, 28 May 2026 00:49:30 -0400 Subject: [PATCH 2/5] Add premium subscriptions and casual server orchestration --- SRCweb/settings.py | 16 +- SRCweb/urls.py | 2 + home/templates/home/base.html | 9 + subscriptions/__init__.py | 1 + subscriptions/admin.py | 66 ++ subscriptions/api/__init__.py | 1 + subscriptions/api/urls.py | 15 + subscriptions/api/views.py | 164 ++++ subscriptions/apps.py | 6 + subscriptions/management/__init__.py | 1 + subscriptions/management/commands/__init__.py | 1 + .../commands/expire_premium_sessions.py | 19 + subscriptions/migrations/0001_initial.py | 189 +++++ .../migrations/0002_seed_default_plans.py | 47 ++ subscriptions/migrations/__init__.py | 1 + subscriptions/models.py | 252 +++++++ subscriptions/orchestrator.py | 87 +++ subscriptions/services.py | 707 ++++++++++++++++++ .../templates/subscriptions/home.html | 180 +++++ subscriptions/tests.py | 403 ++++++++++ subscriptions/urls.py | 11 + subscriptions/views.py | 135 ++++ 22 files changed, 2312 insertions(+), 1 deletion(-) create mode 100644 subscriptions/__init__.py create mode 100644 subscriptions/admin.py create mode 100644 subscriptions/api/__init__.py create mode 100644 subscriptions/api/urls.py create mode 100644 subscriptions/api/views.py create mode 100644 subscriptions/apps.py create mode 100644 subscriptions/management/__init__.py create mode 100644 subscriptions/management/commands/__init__.py create mode 100644 subscriptions/management/commands/expire_premium_sessions.py create mode 100644 subscriptions/migrations/0001_initial.py create mode 100644 subscriptions/migrations/0002_seed_default_plans.py create mode 100644 subscriptions/migrations/__init__.py create mode 100644 subscriptions/models.py create mode 100644 subscriptions/orchestrator.py create mode 100644 subscriptions/services.py create mode 100644 subscriptions/templates/subscriptions/home.html create mode 100644 subscriptions/tests.py create mode 100644 subscriptions/urls.py create mode 100644 subscriptions/views.py diff --git a/SRCweb/settings.py b/SRCweb/settings.py index 97d7dbe..d08616b 100644 --- a/SRCweb/settings.py +++ b/SRCweb/settings.py @@ -37,6 +37,19 @@ # SECURITY WARNING: keep the API Key used in production secret! API_KEY = os.getenv("API_KEY") or "API_KEY" +# Polar subscriptions +POLAR_API_BASE_URL = os.getenv("POLAR_API_BASE_URL") or "https://api.polar.sh" +POLAR_ACCESS_TOKEN = os.getenv("POLAR_ACCESS_TOKEN") or "" +POLAR_WEBHOOK_SECRET = os.getenv("POLAR_WEBHOOK_SECRET") or "" +POLAR_CHECKOUT_SUCCESS_URL = os.getenv("POLAR_CHECKOUT_SUCCESS_URL") or "" +POLAR_CUSTOMER_PORTAL_URL = os.getenv("POLAR_CUSTOMER_PORTAL_URL") or "" + +# xRC server orchestrator +ORCHESTRATOR_API_BASE_URL = os.getenv("ORCHESTRATOR_API_BASE_URL") or "" +ORCHESTRATOR_API_TOKEN = os.getenv("ORCHESTRATOR_API_TOKEN") or "" +ORCHESTRATOR_REQUEST_TIMEOUT_SECONDS = float(os.getenv("ORCHESTRATOR_REQUEST_TIMEOUT_SECONDS") or 10) +SECONDWEBSITE_SERVICE_TOKEN = os.getenv("SECONDWEBSITE_SERVICE_TOKEN") or "" + # Sends an email to admins when debug = false and a 500 server error occurs ADMINS = [('Webmaster', 'webmaster@secondrobotics.org')] ADMIN_EMAILS = [email for name, email in ADMINS] @@ -73,7 +86,8 @@ # 'teamleague', # 'ladder', 'discordoauth2', - 'ranked' + 'ranked', + 'subscriptions', ] REST_FRAMEWORK = { diff --git a/SRCweb/urls.py b/SRCweb/urls.py index 31ab48c..bca3fb1 100644 --- a/SRCweb/urls.py +++ b/SRCweb/urls.py @@ -30,6 +30,8 @@ path('oauth2/', include('discordoauth2.urls')), path('ranked/', include('ranked.urls')), path('api/ranked/', include('ranked.api.urls', namespace="api-ranked")), + path('subscriptions/', include('subscriptions.urls')), + path('api/subscriptions/', include('subscriptions.api.urls', namespace="api-subscriptions")), ] if settings.DEBUG: diff --git a/home/templates/home/base.html b/home/templates/home/base.html index c19e808..970c734 100644 --- a/home/templates/home/base.html +++ b/home/templates/home/base.html @@ -243,6 +243,15 @@ + +