├── classic_tetris_project
├── __init__.py
├── web
│ ├── __init__.py
│ ├── assets
│ │ ├── admin.js
│ │ ├── stylesheets
│ │ │ ├── _profile.scss
│ │ │ ├── admin.scss
│ │ │ ├── _bootstrap.scss
│ │ │ ├── _qualify.scss
│ │ │ ├── app.scss
│ │ │ ├── _alerts.scss
│ │ │ ├── _status_tags.scss
│ │ │ ├── _markdown.scss
│ │ │ ├── _media.scss
│ │ │ ├── _toggle.scss
│ │ │ ├── _navbar.scss
│ │ │ ├── _forms.scss
│ │ │ ├── _colors.scss
│ │ │ └── _buttons.scss
│ │ ├── js
│ │ │ ├── stimulus
│ │ │ │ ├── react_controller.js
│ │ │ │ ├── site_theme_controller.js
│ │ │ │ └── date_picker_controller.js
│ │ │ └── react
│ │ │ │ └── bracket
│ │ │ │ └── tournament_bracket_controller.jsx
│ │ └── app.js
│ ├── .gitignore
│ ├── widgets
│ │ ├── __init__.py
│ │ └── date_time_picker.py
│ ├── templates
│ │ ├── event
│ │ │ ├── ineligible_reasons
│ │ │ │ ├── closed.html
│ │ │ │ ├── logged_out.html
│ │ │ │ ├── already_qualified.html
│ │ │ │ ├── link_twitch.html
│ │ │ │ └── link_discord.html
│ │ │ ├── all.html
│ │ │ ├── qual_rules.html
│ │ │ ├── qualify.html
│ │ │ ├── qualifier.haml
│ │ │ └── index.html
│ │ ├── templatetags
│ │ │ ├── markdown.html
│ │ │ ├── field_list_row.haml
│ │ │ ├── react_component.haml
│ │ │ ├── field_list_input_row.haml
│ │ │ └── module.html
│ │ ├── index.html
│ │ ├── pages
│ │ │ └── page.html
│ │ ├── admin
│ │ │ ├── base_site.html
│ │ │ └── event
│ │ │ │ └── duplicate.haml
│ │ ├── wiki
│ │ │ └── base.html
│ │ ├── tournament
│ │ │ └── bracket.haml
│ │ ├── icons
│ │ │ ├── twitch.html
│ │ │ ├── hamburger.html
│ │ │ ├── twitch_inverted.html
│ │ │ └── discord.html
│ │ ├── widgets
│ │ │ └── date_time_picker.html
│ │ ├── oauth
│ │ │ └── login.html
│ │ ├── live_notifications
│ │ │ └── submit.haml
│ │ ├── review_qualifiers
│ │ │ ├── index.html
│ │ │ └── review.haml
│ │ ├── user
│ │ │ └── show.haml
│ │ ├── tournament_match
│ │ │ ├── schedule.haml
│ │ │ └── report.haml
│ │ └── profile
│ │ │ └── edit.haml
│ ├── views
│ │ ├── index.py
│ │ ├── policy.py
│ │ ├── base.py
│ │ ├── autocomplete.py
│ │ ├── pages.py
│ │ ├── profile.py
│ │ ├── simulations.py
│ │ ├── user.py
│ │ ├── qualifiers.py
│ │ └── live_notifications.py
│ ├── setup.md
│ ├── tests
│ │ └── views
│ │ │ ├── index.py
│ │ │ ├── pages.py
│ │ │ ├── user.py
│ │ │ ├── oauth.py
│ │ │ └── profile.py
│ ├── forms
│ │ ├── profile.py
│ │ ├── live_notification.py
│ │ ├── review_qualifiers.py
│ │ ├── duplicate_event.py
│ │ └── tournament_match.py
│ ├── context_processors.py
│ └── oauth.py
├── migrations
│ ├── __init__.py
│ ├── 0013_delete_coin.py
│ ├── 0072_remove_tournament_bracket_color.py
│ ├── 0008_auto_20190825_0635.py
│ ├── 0011_user_same_piece_sets.py
│ ├── 0019_discorduser_username.py
│ ├── 0033_qualifier_review_data.py
│ ├── 0069_alter_qualifier_vod.py
│ ├── 0066_match_synced_at.py
│ ├── 0077_event_use_custom_font.py
│ ├── 0053_auto_20210607_0343.py
│ ├── 0065_twitchuser_display_name.py
│ ├── 0064_tournament_discord_emote_string.py
│ ├── 0035_qualifier_auth_word.py
│ ├── 0038_event_pre_qualifying_instructions.py
│ ├── 0070_tournament_bracket_color.py
│ ├── 0076_tournamentmatch_color.py
│ ├── 0017_user_pronouns.py
│ ├── 0020_auto_20200420_2028.py
│ ├── 0071_alter_tournament_bracket_color.py
│ ├── 0073_tournament_bracket_color.py
│ ├── 0052_auto_20210607_0311.py
│ ├── 0055_auto_20210802_1721.py
│ ├── 0029_auto_20201205_1921.py
│ ├── 0034_auto_20210125_0712.py
│ ├── 0014_auto_20200313_0819.py
│ ├── 0063_auto_20220131_0205.py
│ ├── 0006_auto_20190812_0423.py
│ ├── 0036_auto_20210201_0603.py
│ ├── 0026_auto_20200520_0553.py
│ ├── 0003_auto_20190628_1759.py
│ ├── 0010_auto_20191102_0433.py
│ ├── 0001_initial.py
│ ├── 0024_auto_20200520_0517.py
│ ├── 0057_auto_20220104_0251.py
│ ├── 0060_auto_20220123_0527.py
│ ├── 0041_auto_20210327_2206.py
│ ├── 0050_auto_20210515_0803.py
│ ├── 0009_twitchchannel.py
│ ├── 0051_auto_20210607_0114.py
│ ├── 0079_alter_user_pronouns.py
│ ├── 0046_auto_20210405_0650.py
│ ├── 0054_auto_20210802_1651.py
│ ├── 0059_auto_20220109_0846.py
│ ├── 0032_auto_20201226_2255.py
│ ├── 0061_auto_20220129_2001.py
│ ├── 0048_auto_20210417_2147.py
│ ├── 0016_auto_20200318_2227.py
│ ├── 0037_auto_20210201_0604.py
│ ├── 0062_auto_20220131_0118.py
│ ├── 0005_auto_20190810_0847.py
│ ├── 0045_auto_20210405_0043.py
│ ├── 0025_populate_discord_user_fields.py
│ ├── 0067_event_withdrawals_allowed.py
│ ├── 0074_auto_20221002_0630.py
│ ├── 0030_page.py
│ ├── 0047_auto_20210405_0653.py
│ ├── 0080_auto_20250714_0114.py
│ ├── 0018_websiteuser.py
│ ├── 0028_auto_20201130_0208.py
│ ├── 0068_auto_20220717_0652.py
│ ├── 0023_user_remove_pb_fields.py
│ ├── 0056_auto_20211231_0727.py
│ ├── 0004_twitchuser_username.py
│ ├── 0075_auto_20230429_1913.py
│ ├── 0012_coin_side.py
│ ├── 0078_auto_20240505_1551.py
│ ├── 0022_populate_scorepb.py
│ ├── 0015_auto_20200318_0315.py
│ ├── 0043_auto_20210327_2316.py
│ ├── 0042_auto_20210327_2218.py
│ ├── 0002_auto_20190628_1733.py
│ ├── 0039_auto_20210228_0824.py
│ ├── 0058_auto_20220104_1936.py
│ ├── 0044_auto_20210404_2242.py
│ ├── 0021_scorepb.py
│ ├── 0007_game_match.py
│ └── 0049_auto_20210417_2149.py
├── util
│ ├── fieldgen
│ │ ├── __init__.py
│ │ ├── img
│ │ │ ├── arrows.png
│ │ │ ├── numbers.png
│ │ │ ├── template.png
│ │ │ └── block_tiles.png
│ │ ├── assetpath.py
│ │ ├── gravity.py
│ │ ├── basecanvas.py
│ │ ├── activepiece.py
│ │ ├── garbage.py
│ │ └── ai.py
│ ├── __init__.py
│ ├── docs.py
│ ├── json_template.py
│ ├── memo.py
│ ├── constants.py
│ ├── cache.py
│ └── google_sheets.py
├── commands
│ ├── matches
│ │ └── __init__.py
│ ├── test.py
│ ├── __init__.py
│ ├── link.py
│ ├── testmsg.py
│ ├── pb_zip.py
│ ├── playstyle.py
│ ├── preferred_name.py
│ ├── country.py
│ └── countdown.py
├── admin.py
├── test_helper
│ ├── factories
│ │ ├── __init__.py
│ │ ├── pages.py
│ │ ├── scores.py
│ │ ├── users.py
│ │ └── events.py
│ └── discord.py
├── env.py
├── apps.py
├── models
│ ├── restreamers.py
│ ├── custom_redirect.py
│ ├── coin.py
│ ├── __init__.py
│ ├── pages.py
│ ├── twitch.py
│ ├── commands.py
│ └── scores.py
├── moderation
│ ├── all_caps.py
│ ├── rule.py
│ └── moderator.py
├── management
│ └── commands
│ │ └── report_matches.py
├── tests
│ └── commands
│ │ ├── test.py
│ │ └── country.py
├── templates
│ └── discord
│ │ ├── qualifier_submitted.txt
│ │ └── qualifier_reviewed.txt
├── words.py
├── discord_disco.py
├── countries.py
├── facades
│ └── user_permissions.py
└── logging.py
├── requirements_prod.txt
├── docs
└── img
│ ├── permissions.png
│ └── oauth_permissions.png
├── script
├── start_celery.sh
├── restore_dump.sh
└── patch_dump.sql
├── .gitmodules
├── pytest.ini
├── webpack.prod.js
├── classic_tetris_project_django
├── __init__.py
├── wsgi.py
├── celery.py
└── urls.py
├── .gitignore
├── webpack.dev.js
├── .editorconfig
├── conftest.py
├── manage.py
├── LICENSE
├── add_tournaments.py
├── package.json
├── webpack.common.js
└── export.py
/classic_tetris_project/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/requirements_prod.txt:
--------------------------------------------------------------------------------
1 | psycopg2==2.8.5
2 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/classic_tetris_project/util/fieldgen/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/classic_tetris_project/commands/matches/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/assets/admin.js:
--------------------------------------------------------------------------------
1 | import './stylesheets/admin.scss';
2 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/.gitignore:
--------------------------------------------------------------------------------
1 | *.swp
2 |
3 | __pycache__/
4 |
5 | assets/fonts/
--------------------------------------------------------------------------------
/classic_tetris_project/util/__init__.py:
--------------------------------------------------------------------------------
1 | from .constants import *
2 | from .memo import *
3 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/widgets/__init__.py:
--------------------------------------------------------------------------------
1 | from .date_time_picker import DateTimePicker
2 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/event/ineligible_reasons/closed.html:
--------------------------------------------------------------------------------
1 | Qualifying for this event is closed.
2 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/event/ineligible_reasons/logged_out.html:
--------------------------------------------------------------------------------
1 | You must be logged in to qualify.
2 |
--------------------------------------------------------------------------------
/docs/img/permissions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/professor-l/classic-tetris-project/HEAD/docs/img/permissions.png
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/templatetags/markdown.html:
--------------------------------------------------------------------------------
1 |
2 | {{ content }}
3 |
4 |
--------------------------------------------------------------------------------
/script/start_celery.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | CELERY_WORKER_RUNNING=1 celery -A classic_tetris_project_django worker -l INFO
4 |
--------------------------------------------------------------------------------
/docs/img/oauth_permissions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/professor-l/classic-tetris-project/HEAD/docs/img/oauth_permissions.png
--------------------------------------------------------------------------------
/classic_tetris_project/admin.py:
--------------------------------------------------------------------------------
1 | try:
2 | from .web.admin import *
3 | except ModuleNotFoundError:
4 | # private not loaded, ignore
5 | pass
6 |
--------------------------------------------------------------------------------
/classic_tetris_project/test_helper/factories/__init__.py:
--------------------------------------------------------------------------------
1 | from .events import *
2 | from .pages import *
3 | from .scores import *
4 | from .users import *
5 |
--------------------------------------------------------------------------------
/classic_tetris_project/env.py:
--------------------------------------------------------------------------------
1 | from environ import Env
2 | from django.conf import settings
3 |
4 | assert isinstance(settings.ENV, Env)
5 | env = settings.ENV
6 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/views/index.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render
2 |
3 |
4 | def index(request):
5 | return render(request, "index.html")
6 |
--------------------------------------------------------------------------------
/classic_tetris_project/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class ClassicTetrisProjectConfig(AppConfig):
5 | name = 'classic_tetris_project'
6 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/views/policy.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render
2 |
3 | def cookies(request):
4 | return render(request, "policy/cookies.html")
5 |
--------------------------------------------------------------------------------
/classic_tetris_project/util/fieldgen/img/arrows.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/professor-l/classic-tetris-project/HEAD/classic_tetris_project/util/fieldgen/img/arrows.png
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/templatetags/field_list_row.haml:
--------------------------------------------------------------------------------
1 | .field-list__row
2 | .field-list__label
3 | = label
4 | .field-list__value
5 | = value
6 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "classic_tetris_project/reportmatchmodule"]
2 | path = classic_tetris_project/reportmatchmodule
3 | url = https://github.com/alex-ong/CTMEditor.git
4 |
--------------------------------------------------------------------------------
/classic_tetris_project/util/fieldgen/img/numbers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/professor-l/classic-tetris-project/HEAD/classic_tetris_project/util/fieldgen/img/numbers.png
--------------------------------------------------------------------------------
/classic_tetris_project/util/fieldgen/img/template.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/professor-l/classic-tetris-project/HEAD/classic_tetris_project/util/fieldgen/img/template.png
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | DJANGO_SETTINGS_MODULE=classic_tetris_project_django.settings
3 | testpaths=
4 | classic_tetris_project/tests
5 | classic_tetris_project/web/tests
6 |
--------------------------------------------------------------------------------
/classic_tetris_project/util/fieldgen/img/block_tiles.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/professor-l/classic-tetris-project/HEAD/classic_tetris_project/util/fieldgen/img/block_tiles.png
--------------------------------------------------------------------------------
/webpack.prod.js:
--------------------------------------------------------------------------------
1 | const merge = require('webpack-merge');
2 | const common = require('./webpack.common.js');
3 |
4 | module.exports = merge(common, {
5 | mode: 'production',
6 | });
7 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/templatetags/react_component.haml:
--------------------------------------------------------------------------------
1 | %div{data-controller: "react", data-react-component-value: "{{ component }}", data-react-props-value: "{{ props }}"}
2 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/assets/stylesheets/_profile.scss:
--------------------------------------------------------------------------------
1 | @use "colors";
2 |
3 | .profile-header {
4 | &__title {
5 | margin: 0;
6 | }
7 |
8 | &__subtitle {
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/event/ineligible_reasons/already_qualified.html:
--------------------------------------------------------------------------------
1 | You have already qualified for this event. View your qualifier
2 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/event/ineligible_reasons/link_twitch.html:
--------------------------------------------------------------------------------
1 | You must have a Twitch account linked in order to qualify. Add one from your profile.
2 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/event/ineligible_reasons/link_discord.html:
--------------------------------------------------------------------------------
1 | You must have a Discord account linked in order to qualify. Add one from your profile.
2 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/index.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}Classic Tetris Monthly{% endblock %}
4 |
5 | {% block content %}
6 | {% page "index" %}
7 | {% endblock %}
8 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/pages/page.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}{{ page.title }}{% endblock %}j
4 |
5 | {% block content %}
6 | {% markdown page.content %}
7 | {% endblock %}
8 |
--------------------------------------------------------------------------------
/classic_tetris_project/util/docs.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | def parse_docstring(doc: str):
4 | doc = re.sub(r"\n *", r"\n", doc)
5 | doc = re.sub(r"([^\n])\n([^\n])", r"\1 \2", doc)
6 | doc = doc.strip()
7 | return doc
8 |
--------------------------------------------------------------------------------
/classic_tetris_project/test_helper/factories/pages.py:
--------------------------------------------------------------------------------
1 | import factory
2 |
3 | from classic_tetris_project.models import *
4 |
5 |
6 | class PageFactory(factory.django.DjangoModelFactory):
7 | class Meta:
8 | model = Page
9 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/admin/base_site.html:
--------------------------------------------------------------------------------
1 | {% extends "admin/base_site.html" %}
2 |
3 | {% load render_bundle from webpack_loader %}
4 |
5 | {% block extrahead %}
6 | {% render_bundle "admin" %}
7 | {% endblock %}
8 |
--------------------------------------------------------------------------------
/classic_tetris_project_django/__init__.py:
--------------------------------------------------------------------------------
1 | # This will make sure the app is always imported when
2 | # Django starts so that shared_task will use this app.
3 | from .celery import app as celery_app
4 |
5 | __all__ = ('celery_app',)
6 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/assets/stylesheets/admin.scss:
--------------------------------------------------------------------------------
1 | .markdownx {
2 | .markdownx-editor {
3 | font-family: monospace;
4 | }
5 | .markdownx-preview {
6 | background: #FFFFFF;
7 | color: #000000;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/assets/stylesheets/_bootstrap.scss:
--------------------------------------------------------------------------------
1 | // Collapse
2 | .collapse {
3 | &:not(.show) {
4 | display: none;
5 | height: 0;
6 | }
7 | }
8 |
9 | .collapsing {
10 | height: 0;
11 | overflow: hidden;
12 | }
13 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/setup.md:
--------------------------------------------------------------------------------
1 | - clone repo
2 | - setup virtualenv
3 | - install postgres 10 and `postgresql-server-dev-10`
4 | - `pip install -r requirements.txt`
5 | - `pip install -r requirements_prod.txt`
6 | - set up .env file
7 | - `npm install`
8 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/wiki/base.html:
--------------------------------------------------------------------------------
1 | {% extends "wiki/base_site.html" %}
2 |
3 | {% block wiki_site_title %} - Wiki{% endblock %}
4 |
5 | {% block wiki_header_branding %}
6 | CTM Wiki
7 | {% endblock %}
8 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/tests/views/index.py:
--------------------------------------------------------------------------------
1 | from classic_tetris_project.test_helper import *
2 |
3 | class Index(Spec):
4 | def test_renders(self):
5 | response = self.client.get("/")
6 |
7 | assert_that(response.status_code, equal_to(200))
8 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/event/all.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block content %}
4 | All tournaments
5 | {% for event in events %}
6 | {{ event.name }}
7 | {% endfor %}
8 | {% endblock %}
9 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/forms/profile.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from classic_tetris_project.models import User
3 |
4 | class ProfileForm(forms.ModelForm):
5 | class Meta:
6 | model = User
7 | fields = ["preferred_name", "pronouns", "country", "playstyle"]
8 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/assets/stylesheets/_qualify.scss:
--------------------------------------------------------------------------------
1 | @use 'colors';
2 |
3 | .auth-word {
4 | text-align: center;
5 | max-width: 600px;
6 | margin: auto;
7 | margin-bottom: 15px;
8 |
9 | &__word {
10 | font-size: 30px;
11 | font-weight: bold;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/classic_tetris_project/models/restreamers.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | from .users import User
4 |
5 |
6 | class Restreamer(models.Model):
7 | user = models.ForeignKey(User, on_delete=models.PROTECT, related_name="restreamer")
8 | active = models.BooleanField(default=True)
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode/
2 | *.swp
3 | *.sqlite3
4 | .ackrc
5 | .coverage
6 |
7 | __pycache__/
8 | static/
9 | logs/
10 | cache/
11 | data/
12 | node_modules/
13 | htmlcov/
14 | .env
15 | *.dump
16 |
17 | /export.py
18 | pbs.csv
19 | webpack-stats.json
20 |
21 | /.vs/
22 | /.vscode/
23 | /ctp/
24 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/tournament/bracket.haml:
--------------------------------------------------------------------------------
1 | - extends "base.html"
2 |
3 | - block title
4 | {{ tournament.name }} Bracket
5 |
6 | - block body
7 | %body{class: "{% if embed %}transparent{% endif %}", data-theme: "dark"}
8 | - react_component "TournamentBracketController" bracket_props
9 |
--------------------------------------------------------------------------------
/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const merge = require('webpack-merge');
2 | const common = require('./webpack.common.js');
3 |
4 | module.exports = merge(common, {
5 | mode: 'development',
6 | devtool: 'inline-source-map',
7 |
8 | watch: true,
9 | watchOptions: {
10 | ignored: /node_modules/
11 | },
12 | });
13 |
--------------------------------------------------------------------------------
/classic_tetris_project/util/json_template.py:
--------------------------------------------------------------------------------
1 | import json
2 | def match_template(template, **kwargs):
3 | str_message = template
4 | str_message = str_message.format(**kwargs)
5 | str_message = str_message.replace("{{","{")
6 | str_message = str_message.replace("}}","}")
7 | return json.loads(str_message)
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/templatetags/field_list_input_row.haml:
--------------------------------------------------------------------------------
1 | .field-list__row
2 | .field-list__label.field-list__label--input
3 | %label{for: "{{ field.id_for_label }}"}= label
4 | .field-list__value
5 | - if field
6 | = field
7 | = field.errors
8 | - else
9 | = content
10 |
--------------------------------------------------------------------------------
/classic_tetris_project/moderation/all_caps.py:
--------------------------------------------------------------------------------
1 | from .rule import DiscordRule
2 | import time
3 |
4 | class AllCapsRule(DiscordRule):
5 |
6 | def apply(self):
7 | if any(ch.islower() for ch in self.message.content):
8 | time.sleep(0.5) #wait half a second, due to clientside bug.
9 | self.delete_message()
10 |
11 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/templatetags/module.html:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 | {{ content }}
9 |
10 |
11 |
--------------------------------------------------------------------------------
/classic_tetris_project/test_helper/factories/scores.py:
--------------------------------------------------------------------------------
1 | import factory
2 |
3 | from classic_tetris_project.models import *
4 | from .users import *
5 |
6 |
7 | class ScorePBFactory(factory.django.DjangoModelFactory):
8 | class Meta:
9 | model = ScorePB
10 | current = True
11 | score = 100000
12 | user = factory.SubFactory(UserFactory)
13 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/icons/twitch.html:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/icons/hamburger.html:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/classic_tetris_project/models/custom_redirect.py:
--------------------------------------------------------------------------------
1 | from django.http import HttpResponseRedirect
2 | from django.contrib.redirects.middleware import RedirectFallbackMiddleware
3 |
4 | class HttpResponseTemporaryRedirect(HttpResponseRedirect):
5 | status_code = 307
6 |
7 | class CustomRedirect(RedirectFallbackMiddleware):
8 | response_redirect_class = HttpResponseTemporaryRedirect
9 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/icons/twitch_inverted.html:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/classic_tetris_project/util/fieldgen/assetpath.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 |
4 | class AssetPath(object):
5 | ASSET_FOLDER = "img"
6 |
7 | @staticmethod
8 | def get_file_root():
9 | return os.path.dirname(os.path.abspath(__file__))
10 |
11 | @staticmethod
12 | def get_asset_root():
13 | return os.path.join(AssetPath.get_file_root(), AssetPath.ASSET_FOLDER)
14 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/event/qual_rules.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}Qualifier rules for {{ event.name }}{% endblock %}
4 |
5 | {% block content %}
6 |
7 | {% if event.qualifying_instructions %}
8 | {% markdown event.qualifying_instructions %}
9 | {% else %}
10 | This event hasn't specified any qualifier rules.
11 | {% endif %}
12 |
13 | {% endblock %}
14 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/views/base.py:
--------------------------------------------------------------------------------
1 | from django.views import View
2 |
3 | from classic_tetris_project.util.memo import lazy
4 |
5 | class BaseView(View):
6 | @lazy
7 | def current_user(self):
8 | if self.request.user.is_authenticated and hasattr(self.request.user, "website_user"):
9 | return self.request.user.website_user.user
10 | else:
11 | return None
12 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0013_delete_coin.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.4 on 2020-02-19 03:20
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0012_coin_side'),
10 | ]
11 |
12 | operations = [
13 | migrations.DeleteModel(
14 | name='Coin',
15 | ),
16 | ]
17 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/views/autocomplete.py:
--------------------------------------------------------------------------------
1 | from dal import autocomplete
2 |
3 | from classic_tetris_project.models import TwitchChannel
4 |
5 |
6 | class TwitchChannelAutocomplete(autocomplete.Select2QuerySetView):
7 | def get_queryset(self):
8 | qs = TwitchChannel.objects.all()
9 | if self.q:
10 | qs = qs.filter(twitch_user__username__istartswith=self.q)
11 |
12 | return qs
13 |
--------------------------------------------------------------------------------
/classic_tetris_project/models/coin.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.utils import timezone
3 |
4 | from .users import User
5 |
6 |
7 | class Side(models.Model):
8 | user = models.ForeignKey(User, on_delete=models.PROTECT, related_name="+")
9 | timestamp = models.DateTimeField()
10 |
11 | @staticmethod
12 | def log(user):
13 | Side.objects.create(user=user, timestamp=timezone.now())
14 |
15 |
--------------------------------------------------------------------------------
/classic_tetris_project/moderation/rule.py:
--------------------------------------------------------------------------------
1 | from asgiref.sync import async_to_sync
2 |
3 | class DiscordRule:
4 | def __init__(self, moderator):
5 | self.moderator = moderator
6 | self.message = self.moderator.message
7 |
8 | def delete_message(self):
9 | async_to_sync(self.message.delete)()
10 |
11 | def notify_user(self, message):
12 | self.moderator.user.send_message(message)
13 |
--------------------------------------------------------------------------------
/classic_tetris_project/management/commands/report_matches.py:
--------------------------------------------------------------------------------
1 | from django.core.management.base import BaseCommand
2 |
3 | from classic_tetris_project.util.match_sheet_reporter import MatchSheetReporter
4 |
5 | class Command(BaseCommand):
6 | def handle(self, *args, **kwargs):
7 | reporter = MatchSheetReporter()
8 | match_count = reporter.sync_all()
9 | self.stdout.write(f"Reported {match_count} matches")
10 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/context_processors.py:
--------------------------------------------------------------------------------
1 | def session_processor(request):
2 | if request.path.startswith("/admin"):
3 | return {}
4 |
5 | if request.user.is_authenticated and hasattr(request.user, "website_user"):
6 | current_user = request.user.website_user.user
7 | else:
8 | current_user = None
9 |
10 | return {
11 | "auth_user": request.user,
12 | "current_user": current_user,
13 | }
14 |
--------------------------------------------------------------------------------
/classic_tetris_project/tests/commands/test.py:
--------------------------------------------------------------------------------
1 | from classic_tetris_project.test_helper import *
2 |
3 | class TestCommand_(CommandSpec):
4 | class discord:
5 | def test_sends_response(self):
6 | self.assert_discord("!test", [
7 | "Test!"
8 | ])
9 |
10 | class twitch:
11 | def test_sends_response(self):
12 | self.assert_twitch("!test", [
13 | "Test!"
14 | ])
15 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/views/pages.py:
--------------------------------------------------------------------------------
1 | from django.http import Http404
2 | from django.shortcuts import render
3 |
4 | from classic_tetris_project.models import Page
5 |
6 | def page(request, page_slug):
7 | try:
8 | page = Page.objects.filter(public=True).get(slug=page_slug)
9 | return render(request, "pages/page.html", {
10 | "page": page,
11 | })
12 | except Page.DoesNotExist:
13 | raise Http404()
14 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: https://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # Unix-style newlines with a newline ending every file
7 | [*]
8 | end_of_line = lf
9 | insert_final_newline = true
10 |
11 | # Matches multiple files with brace expansion notation
12 | # Set default charset
13 | [*.{js,py}]
14 | charset = utf-8
15 |
16 | # 4 space indentation
17 | [*.py]
18 | indent_style = space
19 | indent_size = 4
20 |
--------------------------------------------------------------------------------
/classic_tetris_project/commands/test.py:
--------------------------------------------------------------------------------
1 | from .command import Command
2 | from ..util import Platform, DocSection
3 |
4 | @Command.register()
5 | class TestCommand(Command):
6 | """
7 | Send a test message.
8 | """
9 | aliases = ("test", "devtest")
10 | supported_platforms = (Platform.DISCORD, Platform.TWITCH)
11 | usage = "test"
12 | section = DocSection.OTHER
13 |
14 | def execute(self):
15 | self.send_message("Test!")
16 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/assets/stylesheets/app.scss:
--------------------------------------------------------------------------------
1 | @use 'structure';
2 | @use 'alerts';
3 | @use 'bracket';
4 | @use 'buttons';
5 | @use 'bootstrap';
6 | @use 'forms';
7 | @use 'markdown';
8 | @use 'navbar';
9 | @use 'profile';
10 | @use 'qualify';
11 | @use 'status_tags';
12 | @use 'toggle';
13 |
14 | // External libraries
15 | @import '~simplepicker/dist/simplepicker.css';
16 | // Override datepicker text
17 | .simpilepicker-date-picker {
18 | color: #000000;
19 | }
20 |
--------------------------------------------------------------------------------
/classic_tetris_project/util/memo.py:
--------------------------------------------------------------------------------
1 | # Currently only memoizes nullary instance methods
2 | def memoize(f):
3 | var_name = f"_memo_{f.__name__}"
4 | def helper(self):
5 | if hasattr(self, var_name):
6 | return getattr(self, var_name)
7 | else:
8 | result = f(self)
9 | setattr(self, var_name, result)
10 | return result
11 | return helper
12 |
13 |
14 | def lazy(f):
15 | return property(memoize(f))
16 |
--------------------------------------------------------------------------------
/classic_tetris_project/models/__init__.py:
--------------------------------------------------------------------------------
1 | from .users import User, TwitchUser, DiscordUser, WebsiteUser
2 | from .scores import ScorePB
3 | from .matches import Match, Game
4 | from .twitch import TwitchChannel
5 | from .coin import Side
6 | from .commands import CustomCommand
7 | from .pages import Page
8 | from .events import Event
9 | from .qualifiers import Qualifier
10 | from .tournaments import Tournament, TournamentPlayer, TournamentMatch
11 | from .restreamers import Restreamer
12 |
--------------------------------------------------------------------------------
/classic_tetris_project/commands/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pkgutil
3 | from importlib import import_module
4 |
5 | def import_dir(dirname, package):
6 | for _, package_name, is_pkg in pkgutil.walk_packages([dirname]):
7 | if is_pkg:
8 | import_dir(os.path.join(dirname, package_name), f"{package}.{package_name}")
9 | else:
10 | import_module("." + package_name, package)
11 |
12 | import_dir(os.path.dirname(__file__), "classic_tetris_project.commands")
13 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0072_remove_tournament_bracket_color.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.11 on 2022-07-26 19:08
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0071_alter_tournament_bracket_color'),
10 | ]
11 |
12 | operations = [
13 | migrations.RemoveField(
14 | model_name='tournament',
15 | name='bracket_color',
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/assets/stylesheets/_alerts.scss:
--------------------------------------------------------------------------------
1 | @use "colors";
2 |
3 | .alert {
4 | font-size: 16px;
5 | padding: 13px 25px;
6 |
7 | &--info {
8 | background-color: colors.$alert-info-bg;
9 | color: colors.$alert-info-text;
10 | }
11 |
12 | &--light-info {
13 | border: 1px colors.$alert-light-info-border solid;
14 | background-color: colors.$alert-light-info-bg;
15 | color: colors.$alert-light-info-text;
16 | }
17 |
18 | a {
19 | color: inherit;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/widgets/date_time_picker.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from datetime import datetime, timedelta
3 |
4 | from classic_tetris_project.util import memoize
5 |
6 | class DateTimePicker(forms.widgets.Input):
7 | input_type = "hidden"
8 | template_name = "widgets/date_time_picker.html"
9 |
10 | def format_value(self, value):
11 | if isinstance(value, datetime):
12 | return value.strftime("%Y-%m-%d %H:%M:%S")
13 | else:
14 | return value
15 |
--------------------------------------------------------------------------------
/classic_tetris_project/util/constants.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 | class Platform(Enum):
4 | DISCORD = 0
5 | TWITCH = 1
6 | def display_name(self):
7 | if self == Platform.DISCORD:
8 | return "Discord"
9 | elif self == Platform.TWITCH:
10 | return "Twitch"
11 | else:
12 | raise ValueError("Unhandled platform", self)
13 |
14 | class DocSection(Enum):
15 | USER = 0
16 | ACCOUNT = 1
17 | QUEUE = 2
18 | UTIL = 3
19 | OTHER = 4
20 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0008_auto_20190825_0635.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.4 on 2019-08-25 06:35
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0007_game_match'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='game',
15 | name='losing_score',
16 | field=models.IntegerField(null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/classic_tetris_project/templates/discord/qualifier_submitted.txt:
--------------------------------------------------------------------------------
1 | {% load humanize %}\
2 | \
3 | {% autoescape off %}
4 | {{ qualifier.user.discord_user.user_tag|safe }} submitted a qualifier for [{{ event_name }}]({{ event_url }})!
5 |
6 | {% for label, value in qualifier.type.display_values %}\
7 | {{ label }}: {{ value|intcomma }}
8 | {% endfor %}\
9 | {% if qualifier.vod %}
10 | VOD: {{ qualifier.vod }}
11 | {% endif %}\
12 | {% if qualifier.details %}
13 | {{ qualifier.details }}
14 | {% endif %}
15 | {% endautoescape %}
16 |
--------------------------------------------------------------------------------
/classic_tetris_project/words.py:
--------------------------------------------------------------------------------
1 | import csv
2 | import random
3 | from pathlib import Path
4 |
5 | class Words:
6 | WORDS_CSV_PATH = Path(__file__).parent.resolve() / "data" / "words.csv"
7 | FULL_LIST = []
8 |
9 | @staticmethod
10 | def populate(path=WORDS_CSV_PATH):
11 | with open(path, "r") as f:
12 | Words.FULL_LIST = [row[0] for row in csv.reader(f)]
13 |
14 | @staticmethod
15 | def get_word():
16 | return random.choice(Words.FULL_LIST).upper()
17 |
18 | Words.populate()
19 |
--------------------------------------------------------------------------------
/classic_tetris_project_django/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for classic_tetris_project_django 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/2.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', 'classic_tetris_project_django.settings')
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/script/restore_dump.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | if [ -z "$1" ]; then
4 | echo "restore_dump.sh [path/to/file.dump]"
5 | exit 1;
6 | fi
7 |
8 | echo "===================="
9 | echo "Creating database"
10 | echo "===================="
11 | createdb tetris 2> /dev/null
12 |
13 | echo "===================="
14 | echo "Restoring dump"
15 | echo "===================="
16 | psql tetris -f $1
17 |
18 | echo "===================="
19 | echo "Applying patches"
20 | echo "===================="
21 | psql tetris -f script/patch_dump.sql
22 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0011_user_same_piece_sets.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.4 on 2020-02-04 20:57
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0010_auto_20191102_0433'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='user',
15 | name='same_piece_sets',
16 | field=models.BooleanField(default=False),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0019_discorduser_username.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.4 on 2020-04-02 07:27
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0018_websiteuser'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='discorduser',
15 | name='username',
16 | field=models.CharField(max_length=32, null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0033_qualifier_review_data.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2021-01-25 06:23
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0032_auto_20201226_2255'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='qualifier',
15 | name='review_data',
16 | field=models.JSONField(default=dict),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0069_alter_qualifier_vod.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.11 on 2022-07-17 08:31
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0068_auto_20220717_0652'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='qualifier',
15 | name='vod',
16 | field=models.URLField(blank=True, null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0066_match_synced_at.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.11 on 2022-04-10 17:01
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0065_twitchuser_display_name'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='match',
15 | name='synced_at',
16 | field=models.DateTimeField(blank=True, null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0077_event_use_custom_font.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.11 on 2023-11-17 02:05
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0076_tournamentmatch_color'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='event',
15 | name='use_custom_font',
16 | field=models.BooleanField(default=False),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0053_auto_20210607_0343.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2021-06-07 03:43
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0052_auto_20210607_0311'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterModelOptions(
14 | name='tournamentmatch',
15 | options={'permissions': [('restream', 'Can schedule and report restreamed matches')]},
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | @pytest.fixture(autouse=True)
4 | def enable_db_access_for_all_tests(db):
5 | pass
6 |
7 | # https://blog.jerrycodes.com/no-http-requests/
8 | @pytest.fixture(autouse=True)
9 | def no_http_requests(monkeypatch):
10 | def urlopen_mock(self, method, url, *args, **kwargs):
11 | raise RuntimeError(
12 | f"The test was about to {method} {self.scheme}://{self.host}{url}"
13 | )
14 |
15 | monkeypatch.setattr(
16 | "urllib3.connectionpool.HTTPConnectionPool.urlopen", urlopen_mock
17 | )
18 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0065_twitchuser_display_name.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.11 on 2022-02-05 09:18
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0064_tournament_discord_emote_string'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='twitchuser',
15 | name='display_name',
16 | field=models.CharField(max_length=25, null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/classic_tetris_project/templates/discord/qualifier_reviewed.txt:
--------------------------------------------------------------------------------
1 | {% autoescape off %}
2 | Your qualifier for [{{ event_name }}]({{ event_url }}) was **{% if qualifier.approved %}approved{% else %}rejected{% endif %}** by {{ qualifier.reviewed_by.display_name }}.\
3 | \
4 | {% if checks %}\
5 | {% for check, value in checks %}
6 |
7 | {% if value %}:white_check_mark:{% else %}:x:{% endif %} {{ check }}?\
8 | {% endfor %}\
9 | {% endif %}\
10 | \
11 | {% if qualifier.review_data.notes %}
12 |
13 | {{ qualifier.review_data.notes }}
14 | {% endif %}
15 | {% endautoescape %}
16 |
--------------------------------------------------------------------------------
/script/patch_dump.sql:
--------------------------------------------------------------------------------
1 | UPDATE django_site SET domain = 'dev.monthlytetris.info:8000', name = 'dev.monthlytetris.info:8000' WHERE id = 1;
2 | UPDATE classic_tetris_project_twitchchannel SET connected = false;
3 | UPDATE classic_tetris_project_page SET content = regexp_replace(content, 'http(s)?://ctm\.gg', 'http://dev.monthlytetris.info:8000', 'g');
4 | UPDATE classic_tetris_project_page SET content = regexp_replace(content, 'http(s)?://go\.ctm\.gg', 'http://dev.monthlytetris.info:8000', 'g');
5 | UPDATE classic_tetris_project_tournament SET google_sheets_id = NULL, google_sheets_range = NULL;
6 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0064_tournament_discord_emote_string.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.11 on 2022-02-02 05:28
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0063_auto_20220131_0205'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='tournament',
15 | name='discord_emote_string',
16 | field=models.CharField(blank=True, max_length=255, null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/widgets/date_time_picker.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0035_qualifier_auth_word.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2021-01-31 05:11
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0034_auto_20210125_0712'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='qualifier',
15 | name='auth_word',
16 | field=models.CharField(default='', max_length=6),
17 | preserve_default=False,
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0038_event_pre_qualifying_instructions.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2021-02-01 06:26
2 |
3 | from django.db import migrations
4 | import markdownx.models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('classic_tetris_project', '0037_auto_20210201_0604'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='event',
16 | name='pre_qualifying_instructions',
17 | field=markdownx.models.MarkdownxField(blank=True),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0070_tournament_bracket_color.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.11 on 2022-07-26 18:24
2 |
3 | import colorfield.fields
4 | from django.db import migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('classic_tetris_project', '0069_alter_qualifier_vod'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='tournament',
16 | name='bracket_color',
17 | field=colorfield.fields.ColorField(default='#b7b7b7', max_length=18),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/event/qualify.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}Qualify for {{ event.name }}{% endblock %}
4 |
5 | {% block content %}
6 | Qualify for {{ event.name }}
7 |
8 | {% if event.pre_qualifying_instructions %}
9 | {% markdown event.pre_qualifying_instructions %}
10 | {% endif %}
11 |
12 | Start Qualifier
13 | {% endblock %}
14 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0076_tournamentmatch_color.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.11 on 2023-04-29 22:07
2 |
3 | import colorfield.fields
4 | from django.db import migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('classic_tetris_project', '0075_auto_20230429_1913'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='tournamentmatch',
16 | name='color',
17 | field=colorfield.fields.ColorField(blank=True, default=None, max_length=18, null=True),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/admin/event/duplicate.haml:
--------------------------------------------------------------------------------
1 | - extends "admin/base_site.html"
2 |
3 | - block title
4 | Duplicate Event
5 |
6 | - block content
7 | %h1 Duplicate Event: {{ event.name }}
8 | #content-main
9 | %form{method: "post"}
10 | - csrf_token
11 | = form.non_field_errors
12 | %fieldset.module.aligned
13 | .form-row.field-name
14 | %div
15 | = form.name.errors
16 | %label.required{for: "{{ form.subject.id_for_label }}"}
17 | Name:
18 | = form.name
19 | %input{type: "submit", value: "Submit"}
20 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0017_user_pronouns.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.4 on 2020-04-01 02:11
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0016_auto_20200318_2227'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='user',
15 | name='pronouns',
16 | field=models.CharField(choices=[('he', 'He/him/his'), ('she', 'She/her/hers'), ('they', 'They/them/theirs')], default='they', max_length=16),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0020_auto_20200420_2028.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.4 on 2020-04-20 20:28
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0019_discorduser_username'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='user',
15 | name='pronouns',
16 | field=models.CharField(choices=[('he', 'He/him/his'), ('she', 'She/her/hers'), ('they', 'They/them/theirs')], max_length=16, null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0071_alter_tournament_bracket_color.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.11 on 2022-07-26 18:51
2 |
3 | import colorfield.fields
4 | from django.db import migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('classic_tetris_project', '0070_tournament_bracket_color'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='tournament',
16 | name='bracket_color',
17 | field=colorfield.fields.ColorField(blank=True, default=None, max_length=18, null=True),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0073_tournament_bracket_color.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.11 on 2022-07-26 19:08
2 |
3 | import colorfield.fields
4 | from django.db import migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('classic_tetris_project', '0072_remove_tournament_bracket_color'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='tournament',
16 | name='bracket_color',
17 | field=colorfield.fields.ColorField(blank=True, default=None, max_length=18, null=True),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/assets/stylesheets/_status_tags.scss:
--------------------------------------------------------------------------------
1 | @use 'colors';
2 |
3 | .status-tag {
4 | display: inline-block;
5 | padding: 0 0.5em;
6 | line-height: 1.5;
7 | color: colors.$status-tag-text;
8 |
9 | &--green {
10 | background-color: colors.$status-tag-green-bg;
11 | }
12 |
13 | &--yellow {
14 | background-color: colors.$status-tag-yellow-bg;
15 | }
16 |
17 | &--red {
18 | background-color: colors.$status-tag-red-bg;
19 | }
20 |
21 | &--gray {
22 | background-color: colors.$status-tag-gray-bg;
23 | }
24 |
25 |
26 | &--small {
27 | font-size: 12px;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/classic_tetris_project/models/pages.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.urls import reverse
3 | from markdownx.models import MarkdownxField
4 |
5 | class Page(models.Model):
6 | title = models.CharField(max_length=64)
7 | slug = models.SlugField(db_index=True)
8 | public = models.BooleanField(default=False)
9 | content = MarkdownxField()
10 |
11 | created_at = models.DateTimeField(auto_now_add=True)
12 | updated_at = models.DateTimeField(auto_now=True)
13 |
14 | def get_absolute_url(self):
15 | return reverse("page", args=[self.slug])
16 |
17 | def __str__(self):
18 | return self.title
19 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0052_auto_20210607_0311.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2021-06-07 03:11
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('classic_tetris_project', '0051_auto_20210607_0114'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='match',
16 | name='channel',
17 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='classic_tetris_project.twitchchannel'),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0055_auto_20210802_1721.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2021-08-02 17:21
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('classic_tetris_project', '0054_auto_20210802_1651'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='tournamentmatch',
16 | name='match',
17 | field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='classic_tetris_project.match'),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0029_auto_20201205_1921.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2020-12-05 19:21
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('classic_tetris_project', '0028_auto_20201130_0208'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='customcommand',
16 | name='alias_for',
17 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='classic_tetris_project.customcommand'),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/assets/stylesheets/_markdown.scss:
--------------------------------------------------------------------------------
1 | @use 'colors';
2 |
3 | .markdown {
4 | color: var(--text-primary);
5 |
6 | a {
7 | color: var(--link-primary);
8 | }
9 |
10 | table {
11 | border-collapse: collapse;
12 |
13 | th {
14 | border: 1px solid colors.$border-gray;
15 | background-color: colors.$bg-light-gray;
16 | padding: 8px;
17 |
18 | &:not(:first-child) {
19 | border-left: 0;
20 | }
21 | &:not(:last-child) {
22 | border-right: 0;
23 | }
24 | }
25 |
26 | td {
27 | border: 1px solid colors.$border-gray;
28 | padding: 8px;
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/forms/live_notification.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 |
3 | POSITIONS =[
4 | ('topleft', 'Top Left'),
5 | ('topright', 'Top Right'),
6 | ('bottomleft', 'Bottom Left'),
7 | ('bottomright', 'Bottom Right'),
8 | ('centerleft', 'Center Left'),
9 | ('centerright', 'Center Right'),
10 | ('center', 'Center'),
11 | ]
12 |
13 | class LiveNotificationForm(forms.Form):
14 | message = forms.CharField(widget=forms.Textarea, required=False)
15 | duration = forms.IntegerField(label="Duration", min_value=0, max_value=100, required=False)
16 | position = forms.ChoiceField(choices=POSITIONS, widget=forms.RadioSelect, initial="topleft")
17 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0034_auto_20210125_0712.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2021-01-25 07:12
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('classic_tetris_project', '0033_qualifier_review_data'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='qualifier',
16 | name='reviewed_by',
17 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='qualifiers_reviewed', to='classic_tetris_project.user'),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/classic_tetris_project/commands/link.py:
--------------------------------------------------------------------------------
1 | from .command import Command
2 | from ..util import Platform, DocSection
3 |
4 | @Command.register()
5 | class LinkCommand(Command):
6 | """
7 | Prints instructions for linking accounts from multiple platforms.
8 | """
9 | aliases = ("link", "linkaccount")
10 | supported_platforms = (Platform.DISCORD, Platform.TWITCH)
11 | usage = "link"
12 | section = DocSection.ACCOUNT
13 |
14 | def execute(self):
15 | self.send_message("To link your Twitch or Discord account, head to our website and click 'Login' at the top right: https://go.ctm.gg. Once there, you can view and edit your profile.")
16 |
17 |
18 | # TODO: unlink command lmao
19 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0014_auto_20200313_0819.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.4 on 2020-03-13 08:19
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0013_delete_coin'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='user',
15 | name='ntsc_pb_19',
16 | field=models.IntegerField(null=True),
17 | ),
18 | migrations.AddField(
19 | model_name='user',
20 | name='ntsc_pb_19_updated_at',
21 | field=models.DateTimeField(null=True),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0063_auto_20220131_0205.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.11 on 2022-01-31 02:05
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0062_auto_20220131_0118'),
10 | ]
11 |
12 | operations = [
13 | migrations.RemoveConstraint(
14 | model_name='tournamentplayer',
15 | name='unique_tournament_seed',
16 | ),
17 | migrations.AddIndex(
18 | model_name='tournamentplayer',
19 | index=models.Index(fields=['tournament', 'seed'], name='classic_tet_tournam_052ca2_idx'),
20 | ),
21 | ]
22 |
--------------------------------------------------------------------------------
/classic_tetris_project/commands/testmsg.py:
--------------------------------------------------------------------------------
1 | from .command import Command
2 | from ..util import Platform, DocSection
3 |
4 | # this is so abusable it should've been disabled years ago lmao
5 | # @Command.register()
6 | class TestMessageCommand(Command):
7 | """
8 | Send a test message to the specified user in a private channel.
9 | """
10 | aliases = ("testmsg",)
11 | supported_platforms = (Platform.DISCORD, Platform.TWITCH)
12 | usage = "testmsg "
13 | section = DocSection.OTHER
14 |
15 | def execute(self, *username):
16 | username = " ".join(username)
17 |
18 | platform_user = self.platform_user_from_username(username)
19 | platform_user.send_message("Test!")
20 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0006_auto_20190812_0423.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.2 on 2019-08-12 04:23
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0005_auto_20190810_0847'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='user',
15 | name='ntsc_pb_updated_at',
16 | field=models.DateTimeField(null=True),
17 | ),
18 | migrations.AddField(
19 | model_name='user',
20 | name='pal_pb_updated_at',
21 | field=models.DateTimeField(null=True),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0036_auto_20210201_0603.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2021-02-01 06:03
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0035_qualifier_auth_word'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='qualifier',
15 | name='submitted',
16 | field=models.BooleanField(default=False),
17 | ),
18 | migrations.AddField(
19 | model_name='qualifier',
20 | name='submitted_at',
21 | field=models.DateTimeField(null=True),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/assets/stylesheets/_media.scss:
--------------------------------------------------------------------------------
1 | $tablet-width: 768px;
2 | $desktop-width: 1024px;
3 |
4 | @mixin mobile-only {
5 | @media (max-width: #{$tablet-width - 1}) {
6 | @content;
7 | }
8 | }
9 |
10 | @mixin tablet-only {
11 | @media (min-width: #{$tablet-width}) and (max-width: #{$desktop-width - 1px}) {
12 | @content;
13 | }
14 | }
15 |
16 | @mixin desktop-only {
17 | @media (min-width: #{$desktop-width}) {
18 | @content;
19 | }
20 | }
21 |
22 | @mixin mobile-and-tablet {
23 | @media (max-width: #{$desktop-width - 1px}) {
24 | @content;
25 | }
26 | }
27 |
28 | @mixin tablet-and-desktop {
29 | @media (min-width: #{$tablet-width}) {
30 | @content;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0026_auto_20200520_0553.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.10 on 2020-05-20 05:53
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0025_populate_discord_user_fields'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='discorduser',
15 | name='discriminator',
16 | field=models.CharField(max_length=4),
17 | ),
18 | migrations.AlterField(
19 | model_name='discorduser',
20 | name='username',
21 | field=models.CharField(max_length=32),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0003_auto_20190628_1759.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.2 on 2019-06-28 17:59
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0002_auto_20190628_1733'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='discorduser',
15 | name='discord_id',
16 | field=models.CharField(max_length=64, unique=True),
17 | ),
18 | migrations.AlterField(
19 | model_name='twitchuser',
20 | name='twitch_id',
21 | field=models.CharField(max_length=64, unique=True),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0010_auto_20191102_0433.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.4 on 2019-11-02 04:33
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0009_twitchchannel'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterModelOptions(
14 | name='match',
15 | options={'verbose_name_plural': 'matches'},
16 | ),
17 | migrations.AddField(
18 | model_name='user',
19 | name='playstyle',
20 | field=models.CharField(choices=[('das', 'DAS'), ('hypertap', 'Hypertap'), ('hybrid', 'Hybrid')], max_length=16, null=True),
21 | ),
22 | ]
23 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/oauth/login.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block content %}
4 |
5 | By logging in, you agree to our cookie policy.
6 |
7 |
8 |
23 | {% endblock %}
24 |
--------------------------------------------------------------------------------
/classic_tetris_project/util/cache.py:
--------------------------------------------------------------------------------
1 | import os
2 | import os.path
3 | from django.conf import settings
4 |
5 |
6 | class FileCache:
7 | def __init__(self, name):
8 | self.name = name
9 | self.root = os.path.join(settings.BASE_DIR, "cache", name)
10 | os.makedirs(self.root, exist_ok=True)
11 |
12 | def full_path(self, filename):
13 | return os.path.join(self.root, filename)
14 |
15 | def cache_path(self, filename):
16 | return os.path.join("/cache", self.name, filename)
17 |
18 | def has(self, filename):
19 | return os.path.isfile(self.full_path(filename))
20 |
21 | def put(self, filename, content):
22 | with open(self.full_path(filename), "wb") as f:
23 | f.write(content)
24 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.2 on 2019-06-28 05:21
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | initial = True
9 |
10 | dependencies = [
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='User',
16 | fields=[
17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18 | ('preferred_name', models.CharField(max_length=64, null=True)),
19 | ('ntsc_pb', models.IntegerField(null=True)),
20 | ('pal_pb', models.IntegerField(null=True)),
21 | ],
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0024_auto_20200520_0517.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.10 on 2020-05-20 05:17
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0023_user_remove_pb_fields'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='discorduser',
15 | name='discriminator',
16 | field=models.CharField(max_length=4, null=True),
17 | ),
18 | migrations.AddConstraint(
19 | model_name='discorduser',
20 | constraint=models.UniqueConstraint(fields=('username', 'discriminator'), name='unique_username_discriminator'),
21 | ),
22 | ]
23 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0057_auto_20220104_0251.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2022-01-04 02:51
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0056_auto_20211231_0727'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterModelOptions(
14 | name='tournamentmatch',
15 | options={'permissions': [('restream', 'Can schedule and report restreamed matches')], 'verbose_name_plural': 'tournament matches'},
16 | ),
17 | migrations.AddField(
18 | model_name='match',
19 | name='vod',
20 | field=models.URLField(blank=True, null=True),
21 | ),
22 | ]
23 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0060_auto_20220123_0527.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2022-01-23 05:27
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0059_auto_20220109_0846'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='tournament',
15 | name='google_sheets_id',
16 | field=models.CharField(blank=True, max_length=255, null=True),
17 | ),
18 | migrations.AddField(
19 | model_name='tournament',
20 | name='google_sheets_range',
21 | field=models.CharField(blank=True, max_length=255, null=True),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/classic_tetris_project/util/fieldgen/gravity.py:
--------------------------------------------------------------------------------
1 | class GravityFrames(object):
2 | LEVELS = [
3 | 48,
4 | 43,
5 | 38,
6 | 33,
7 | 28,
8 | 23,
9 | 18,
10 | 13,
11 | 8,
12 | 6,
13 | 5,
14 | 5,
15 | 5,
16 | 4,
17 | 4,
18 | 4,
19 | 3,
20 | 3,
21 | 3,
22 | 2,
23 | 2,
24 | 2,
25 | 2,
26 | 2,
27 | 2,
28 | 2,
29 | 2,
30 | 2,
31 | 2,
32 | ]
33 |
34 | @staticmethod
35 | def get_gravityframes(level):
36 | result = 1
37 | level %= 256
38 | if level < 29:
39 | result = GravityFrames.LEVELS[level]
40 | return result
41 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0041_auto_20210327_2206.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2021-03-27 22:06
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0040_auto_20210327_1831'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='tournament',
15 | name='priority',
16 | field=models.IntegerField(default=1),
17 | preserve_default=False,
18 | ),
19 | migrations.AddField(
20 | model_name='tournament',
21 | name='seed_count',
22 | field=models.IntegerField(default=16),
23 | preserve_default=False,
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0050_auto_20210515_0803.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2021-05-15 08:03
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0049_auto_20210417_2149'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='tournament',
15 | name='public',
16 | field=models.BooleanField(default=False, help_text='Controls whether the tournament page is available to view'),
17 | ),
18 | migrations.AddField(
19 | model_name='tournamentmatch',
20 | name='restreamed',
21 | field=models.BooleanField(default=True),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0009_twitchchannel.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.4 on 2019-08-31 05:18
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('classic_tetris_project', '0008_auto_20190825_0635'),
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='TwitchChannel',
16 | fields=[
17 | ('twitch_user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='channel', serialize=False, to='classic_tetris_project.TwitchUser')),
18 | ('connected', models.BooleanField(db_index=True, default=False)),
19 | ],
20 | ),
21 | ]
22 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/assets/js/stimulus/react_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from '@hotwired/stimulus';
2 | import ReactDOM from 'react-dom/client';
3 | import React from 'react';
4 |
5 | export default class ReactController extends Controller {
6 | static values = {
7 | component: String,
8 | props: Object
9 | }
10 |
11 | connect() {
12 | const component = window.reactComponents[this.componentValue];
13 | if (component) {
14 | this.root = ReactDOM.createRoot(this.element);
15 | this.root.render(React.createElement(component, this.propsValue));
16 | } else {
17 | console.error(`No component named "${this.componentValue}"`);
18 | }
19 | }
20 |
21 | disconnect() {
22 | if (this.root) {
23 | this.root.unmount();
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0051_auto_20210607_0114.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2021-06-07 01:14
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0050_auto_20210515_0803'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='tournament',
15 | name='restreamed',
16 | field=models.BooleanField(default=False, help_text="Determines whether this tournament's matches can be restreamed by default"),
17 | ),
18 | migrations.AlterField(
19 | model_name='tournamentmatch',
20 | name='restreamed',
21 | field=models.BooleanField(default=False),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0079_alter_user_pronouns.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.11 on 2024-07-08 04:38
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0078_auto_20240505_1551'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='user',
15 | name='pronouns',
16 | field=models.CharField(blank=True, choices=[('he', 'He/him/his'), ('she', 'She/her/hers'), ('they', 'They/them/theirs'), ('he/they', 'He/they'), ('they/he', 'They/he'), ('she/they', 'She/they'), ('they/she', 'They/she'), ('it', 'It/its'), ('xe', 'Xe/xem/xir'), ('any', 'Any/all'), ('ask', 'Ask')], max_length=16, null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/classic_tetris_project/test_helper/factories/users.py:
--------------------------------------------------------------------------------
1 | import factory
2 |
3 | from classic_tetris_project.models import *
4 |
5 |
6 | class UserFactory(factory.django.DjangoModelFactory):
7 | class Meta:
8 | model = User
9 |
10 | class DiscordUserFactory(factory.django.DjangoModelFactory):
11 | class Meta:
12 | model = DiscordUser
13 | discord_id = factory.Sequence(lambda n: str(n))
14 | username = factory.Sequence(lambda n: f"User {n}")
15 | discriminator = factory.Sequence(lambda n: f"{n:04}")
16 |
17 | class TwitchUserFactory(factory.django.DjangoModelFactory):
18 | class Meta:
19 | model = TwitchUser
20 | twitch_id = factory.Sequence(lambda n: str(n))
21 | username = factory.Sequence(lambda n: f"user_{n}")
22 | display_name = factory.LazyAttribute(lambda o: o.username)
23 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/assets/js/stimulus/site_theme_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from '@hotwired/stimulus';
2 |
3 | export default class SiteThemeController extends Controller {
4 | static targets = ['checkbox'];
5 |
6 | connect() {
7 | const theme = localStorage.getItem('CTM_THEME') || 'light';
8 | if (theme === 'dark' && this.hasCheckboxTarget) {
9 | this.checkboxTarget.checked = true;
10 | }
11 | this.setTheme(theme);
12 | }
13 |
14 | setTheme(theme) {
15 | this.element.setAttribute('data-theme', theme);
16 | }
17 |
18 | updateTheme(e) {
19 | try {
20 | const theme = this.checkboxTarget.checked ? "dark" : "light";
21 | this.setTheme(theme);
22 | localStorage.setItem("CTM_THEME", theme);
23 | } catch (e) {
24 | console.error(e);
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0046_auto_20210405_0650.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2021-04-05 06:50
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0045_auto_20210405_0043'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='tournamentplayer',
15 | name='name_override',
16 | field=models.CharField(blank=True, max_length=64, null=True),
17 | ),
18 | migrations.AlterField(
19 | model_name='tournament',
20 | name='placeholders',
21 | field=models.JSONField(blank=True, default=dict, help_text="Reserves a seed that won't get automatically populated by a qualifier"),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0054_auto_20210802_1651.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2021-08-02 16:51
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('classic_tetris_project', '0053_auto_20210607_0343'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='match',
16 | name='start_date',
17 | field=models.DateTimeField(blank=True, null=True),
18 | ),
19 | migrations.AddField(
20 | model_name='tournamentmatch',
21 | name='match',
22 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='classic_tetris_project.match'),
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0059_auto_20220109_0846.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2022-01-09 08:46
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0058_auto_20220104_1936'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterModelOptions(
14 | name='tournamentmatch',
15 | options={'permissions': [('restream', 'Can report restreamable matches')], 'verbose_name_plural': 'tournament matches'},
16 | ),
17 | migrations.AddField(
18 | model_name='tournament',
19 | name='active',
20 | field=models.BooleanField(default=True, help_text="Controls whether this tournament's bracket should be updated on match completion"),
21 | ),
22 | ]
23 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0032_auto_20201226_2255.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2020-12-26 22:55
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0031_auto_20201220_0544'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='qualifier',
15 | name='qualifying_type',
16 | field=models.IntegerField(choices=[(1, 'Highest Score'), (2, 'Highest 3 Scores')], default=1),
17 | preserve_default=False,
18 | ),
19 | migrations.AlterField(
20 | model_name='event',
21 | name='qualifying_type',
22 | field=models.IntegerField(choices=[(1, 'Highest Score'), (2, 'Highest 3 Scores')]),
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0061_auto_20220129_2001.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2022-01-29 20:01
2 |
3 | from django.db import migrations
4 | import markdownx.models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('classic_tetris_project', '0060_auto_20220123_0527'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterModelOptions(
15 | name='tournamentmatch',
16 | options={'permissions': [('restream', 'Can schedule and report restreamed matches')], 'verbose_name_plural': 'tournament matches'},
17 | ),
18 | migrations.AddField(
19 | model_name='tournament',
20 | name='details',
21 | field=markdownx.models.MarkdownxField(blank=True, help_text='Details to show on the tournament page'),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/views/profile.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.mixins import LoginRequiredMixin
2 | from django.shortcuts import redirect
3 | from django.urls import reverse
4 | from django.views.generic.edit import UpdateView
5 |
6 | from classic_tetris_project.models import User
7 | from .base import BaseView
8 | from ..forms.profile import ProfileForm
9 |
10 | class ProfileView(LoginRequiredMixin, BaseView):
11 | def get(self, request):
12 | return redirect(self.current_user)
13 |
14 | class ProfileEditView(LoginRequiredMixin, UpdateView):
15 | form_class = ProfileForm
16 | template_name = "profile/edit.haml"
17 |
18 | def get_object(self):
19 | return self.request.user.website_user.user
20 |
21 | def get_context_data(self, **kwargs):
22 | context = super().get_context_data(**kwargs)
23 | return context
24 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0048_auto_20210417_2147.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2021-04-17 21:47
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('classic_tetris_project', '0047_auto_20210405_0653'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='tournamentmatch',
16 | name='round_number',
17 | field=models.IntegerField(blank=True, null=True),
18 | ),
19 | migrations.AlterField(
20 | model_name='tournamentmatch',
21 | name='tournament',
22 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='matches', to='classic_tetris_project.tournament'),
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0016_auto_20200318_2227.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.4 on 2020-03-18 22:27
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('classic_tetris_project', '0015_auto_20200318_0315'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='customcommand',
16 | name='alias_for',
17 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='classic_tetris_project.CustomCommand'),
18 | ),
19 | migrations.AlterField(
20 | model_name='customcommand',
21 | name='output',
22 | field=models.CharField(blank=True, max_length=400, null=True),
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0037_auto_20210201_0604.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2021-02-01 06:04
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0036_auto_20210201_0603'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='qualifier',
15 | name='qualifying_data',
16 | field=models.JSONField(null=True),
17 | ),
18 | migrations.AlterField(
19 | model_name='qualifier',
20 | name='qualifying_score',
21 | field=models.IntegerField(null=True),
22 | ),
23 | migrations.AlterField(
24 | model_name='qualifier',
25 | name='vod',
26 | field=models.URLField(null=True),
27 | ),
28 | ]
29 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/tests/views/pages.py:
--------------------------------------------------------------------------------
1 | from classic_tetris_project.test_helper import *
2 |
3 | class Pages(Spec):
4 | def test_invalid_page(self):
5 | response = self.client.get(f"/page/foo/")
6 |
7 | assert_that(response.status_code, equal_to(404))
8 |
9 | def test_private_page(self):
10 | PageFactory(slug="foo", public=False)
11 | response = self.client.get(f"/page/foo/")
12 |
13 | assert_that(response.status_code, equal_to(404))
14 |
15 | def test_public_page(self):
16 | PageFactory(slug="foo", public=True, content="This is the page body")
17 | response = self.client.get(f"/page/foo/")
18 |
19 | assert_that(response.status_code, equal_to(200))
20 | assert_that(response, uses_template("pages/page.html"))
21 | assert_that(str(response.content), contains_string("This is the page body"))
22 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Django's command-line utility for administrative tasks."""
3 | import os
4 | import sys
5 |
6 | # Provided commands: https://docs.djangoproject.com/en/3.2/ref/django-admin/
7 | # Custom commands can be found in classic_tetris_project/management/commands
8 | def main():
9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'classic_tetris_project_django.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 |
--------------------------------------------------------------------------------
/classic_tetris_project/util/fieldgen/basecanvas.py:
--------------------------------------------------------------------------------
1 | import io
2 |
3 |
4 | class BaseCanvas(object):
5 | def __init__(self, template_image):
6 | self.img = template_image.copy()
7 | self.frames = []
8 |
9 | def clone_baseimg(self):
10 | return self.img.copy()
11 |
12 | def add_frame(self, other_image):
13 | self.frames.append(other_image)
14 |
15 | def export_bytearray(self):
16 | byte_array = io.BytesIO()
17 | if len(self.frames) == 0:
18 | self.img.save(byte_array, format="png")
19 | else:
20 | self.img.save(
21 | byte_array,
22 | format="gif",
23 | save_all=True,
24 | append_images=self.frames,
25 | delay=0.060,
26 | loop=0,
27 | )
28 | byte_array.seek(0)
29 | return byte_array
30 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0062_auto_20220131_0118.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.11 on 2022-01-31 01:18
2 |
3 | from django.db import migrations
4 |
5 | def update_tournament_placeholders(apps, schema_editor):
6 | Tournament = apps.get_model("classic_tetris_project", "Tournament")
7 | for tournament in Tournament.objects.all():
8 | if tournament.placeholders:
9 | for key in tournament.placeholders.keys():
10 | if isinstance(tournament.placeholders[key], str):
11 | tournament.placeholders[key] = { "name": tournament.placeholders[key] }
12 | tournament.save()
13 |
14 | class Migration(migrations.Migration):
15 |
16 | dependencies = [
17 | ('classic_tetris_project', '0061_auto_20220129_2001'),
18 | ]
19 |
20 | operations = [
21 | migrations.RunPython(update_tournament_placeholders),
22 | ]
23 |
--------------------------------------------------------------------------------
/classic_tetris_project/util/fieldgen/activepiece.py:
--------------------------------------------------------------------------------
1 | from .tiles import TileMath
2 |
3 |
4 | class ActivePieceGenerator(object):
5 | TETROMINO_OFFSETS = [[0, 1], [0, 0], [0, -1], [0, -2]]
6 | # TETROMINO_OFFSETS = [[0,0],[-1,-1],[0,-1],[1,-1]]
7 | def __init__(self, tile_gen):
8 | self.tile_gen = tile_gen
9 |
10 | def draw_longbar(self, image, coords, level):
11 | tile = self.tile_gen.get_active_piece_tile(level)
12 | for offset in self.TETROMINO_OFFSETS:
13 | row = coords[1] + offset[1]
14 | if row >= 0:
15 | tile_pos = [coords[0] + offset[0], coords[1] + offset[1]]
16 | tile_pos = [
17 | tile_pos[0] + TileMath.FIELD_START[0],
18 | tile_pos[1] + TileMath.FIELD_START[1],
19 | ]
20 | image.paste(tile, TileMath.tile_indices_to_pixels(tile_pos))
21 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/assets/app.js:
--------------------------------------------------------------------------------
1 | import 'jquery';
2 | import 'jquery-ujs';
3 | import 'bootstrap';
4 | import { Application } from '@hotwired/stimulus';
5 | import { definitionsFromContext } from '@hotwired/stimulus-webpack-helpers';
6 |
7 | window.Stimulus = Application.start();
8 | const stimulusContext = require.context('./js/stimulus', true, /\.js$/);
9 | Stimulus.load(definitionsFromContext(stimulusContext));
10 |
11 | window.reactComponents = {};
12 | const reactContext = require.context('./js/react', true, /\.jsx?$/);
13 | window.reactContext = reactContext;
14 | reactContext.keys().forEach((key) => {
15 | const module = reactContext(key);
16 | if (module.COMPONENT_NAME) {
17 | // Names are minified
18 | // TODO figure about a better way of registering these
19 | window.reactComponents[module.COMPONENT_NAME] = module.default;
20 | }
21 | });
22 |
23 | import './stylesheets/app.scss';
24 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0005_auto_20190810_0847.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.2 on 2019-08-10 08:47
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('classic_tetris_project', '0004_twitchuser_username'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='discorduser',
16 | name='user',
17 | field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='discord_user', to='classic_tetris_project.User'),
18 | ),
19 | migrations.AlterField(
20 | model_name='twitchuser',
21 | name='user',
22 | field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='twitch_user', to='classic_tetris_project.User'),
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0045_auto_20210405_0043.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2021-04-05 00:43
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('classic_tetris_project', '0044_auto_20210404_2242'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='tournament',
16 | name='placeholders',
17 | field=models.JSONField(default=dict, help_text="Reserves a seed that won't get automatically populated by a qualifier"),
18 | ),
19 | migrations.AlterField(
20 | model_name='tournament',
21 | name='event',
22 | field=models.ForeignKey(db_index=False, on_delete=django.db.models.deletion.CASCADE, related_name='tournaments', to='classic_tetris_project.event'),
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0025_populate_discord_user_fields.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.10 on 2020-05-20 05:21
2 |
3 | from django.db import migrations
4 | import time
5 |
6 | from classic_tetris_project import discord
7 |
8 | def populate_discord_users(apps, schema_editor):
9 | DiscordUser = apps.get_model('classic_tetris_project', 'DiscordUser')
10 |
11 | for discord_user in DiscordUser.objects.all():
12 | user_obj = discord.API.user_from_id(discord_user.discord_id)
13 | discord_user.username = user_obj.name
14 | discord_user.discriminator = user_obj.discriminator
15 | discord_user.save()
16 | time.sleep(1) # Avoid running into Discord's rate limit
17 |
18 | class Migration(migrations.Migration):
19 |
20 | dependencies = [
21 | ('classic_tetris_project', '0024_auto_20200520_0517'),
22 | ]
23 |
24 | operations = [
25 | migrations.RunPython(populate_discord_users),
26 | ]
27 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0067_event_withdrawals_allowed.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.11 on 2022-05-01 07:20
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | def backfill_withdrawals_allowed(apps, schema_editor):
7 | Event = apps.get_model("classic_tetris_project", "Event")
8 | Event.objects.update(withdrawals_allowed=models.F("qualifying_open"))
9 |
10 |
11 | class Migration(migrations.Migration):
12 |
13 | dependencies = [
14 | ('classic_tetris_project', '0066_match_synced_at'),
15 | ]
16 |
17 | operations = [
18 | migrations.AddField(
19 | model_name='event',
20 | name='withdrawals_allowed',
21 | field=models.BooleanField(default=True, help_text='Controls whether users can withdraw their own qualifiers. Automatically disabled when tournaments are seeded.'),
22 | ),
23 | migrations.RunPython(backfill_withdrawals_allowed, migrations.RunPython.noop),
24 | ]
25 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0074_auto_20221002_0630.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.11 on 2022-10-02 06:30
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0073_tournament_bracket_color'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='event',
15 | name='qualifying_channel_id',
16 | field=models.CharField(blank=True, help_text='Discord channel id that qualifier announcements will be posted to. If blank, announcements will not be posted.', max_length=64),
17 | ),
18 | migrations.AddField(
19 | model_name='event',
20 | name='reporting_channel_id',
21 | field=models.CharField(blank=True, help_text='Discord channel id that match reports will be posted to. If blank, messages will not be posted.', max_length=64),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0030_page.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2020-12-06 08:40
2 |
3 | from django.db import migrations, models
4 | import markdownx.models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('classic_tetris_project', '0029_auto_20201205_1921'),
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='Page',
16 | fields=[
17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18 | ('title', models.CharField(max_length=64)),
19 | ('slug', models.SlugField()),
20 | ('public', models.BooleanField(default=False)),
21 | ('content', markdownx.models.MarkdownxField()),
22 | ('created_at', models.DateTimeField(auto_now_add=True)),
23 | ('updated_at', models.DateTimeField(auto_now=True)),
24 | ],
25 | ),
26 | ]
27 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0047_auto_20210405_0653.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2021-04-05 06:53
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('classic_tetris_project', '0046_auto_20210405_0650'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='tournamentplayer',
16 | name='qualifier',
17 | field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.RESTRICT, related_name='tournament_player', to='classic_tetris_project.qualifier'),
18 | ),
19 | migrations.AlterField(
20 | model_name='tournamentplayer',
21 | name='user',
22 | field=models.ForeignKey(blank=True, db_index=False, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='tournament_players', to='classic_tetris_project.user'),
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0080_auto_20250714_0114.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.11 on 2025-07-14 01:14
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0079_alter_user_pronouns'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='event',
15 | name='qualifying_type',
16 | field=models.IntegerField(choices=[(1, 'Highest Score'), (2, 'Highest 2 Scores'), (3, 'Highest 3 Scores'), (4, 'Most Maxouts'), (5, 'Lowest Time'), (6, 'Clipped Seven Game Average'), (7, 'CTW Format')]),
17 | ),
18 | migrations.AlterField(
19 | model_name='qualifier',
20 | name='qualifying_type',
21 | field=models.IntegerField(choices=[(1, 'Highest Score'), (2, 'Highest 2 Scores'), (3, 'Highest 3 Scores'), (4, 'Most Maxouts'), (5, 'Lowest Time'), (6, 'Clipped Seven Game Average'), (7, 'CTW Format')]),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0018_websiteuser.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.4 on 2020-04-02 04:19
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12 | ('classic_tetris_project', '0017_user_pronouns'),
13 | ]
14 |
15 | operations = [
16 | migrations.CreateModel(
17 | name='WebsiteUser',
18 | fields=[
19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
20 | ('auth_user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='website_user', to=settings.AUTH_USER_MODEL)),
21 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='website_user', to='classic_tetris_project.User')),
22 | ],
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/classic_tetris_project/moderation/moderator.py:
--------------------------------------------------------------------------------
1 | from ..env import env
2 | from ..models.users import DiscordUser
3 | from ..util import memoize
4 |
5 | # Explicitly importing all rules
6 | # We want to be very, very sure we WANT a rule before we add it
7 | from .all_caps import AllCapsRule
8 |
9 | class DiscordModerator:
10 | def __init__(self, message):
11 | self.message = message
12 |
13 | def dispatch(self):
14 | rule_class = DISCORD_MODERATION_MAP[str(self.message.channel.id)]
15 | rule = rule_class(self)
16 | rule.apply()
17 |
18 | @property
19 | @memoize
20 | def user(self):
21 | return DiscordUser.get_or_create_from_user_ob(self.message.author)
22 |
23 | @staticmethod
24 | def is_rule(message):
25 | return str(message.channel.id) in DISCORD_MODERATION_MAP.keys()
26 |
27 |
28 | DISCORD_MODERATION_MAP = {}
29 | discord_caps_channel = env("DISCORD_CAPS_CHANNEL", default=None)
30 | if discord_caps_channel:
31 | DISCORD_MODERATION_MAP[discord_caps_channel] = AllCapsRule
32 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0028_auto_20201130_0208.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2020-11-30 02:08
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('classic_tetris_project', '0027_auto_20200621_0117'),
11 | ]
12 |
13 | operations = [
14 | migrations.RemoveIndex(
15 | model_name='customcommand',
16 | name='classic_tet_twitch__a83c1c_idx',
17 | ),
18 | migrations.AlterField(
19 | model_name='customcommand',
20 | name='alias_for',
21 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='classic_tetris_project.customcommand'),
22 | ),
23 | migrations.AlterField(
24 | model_name='customcommand',
25 | name='output',
26 | field=models.CharField(blank=True, default='', max_length=400),
27 | preserve_default=False,
28 | ),
29 | ]
30 |
--------------------------------------------------------------------------------
/classic_tetris_project/test_helper/factories/events.py:
--------------------------------------------------------------------------------
1 | import factory
2 | from django.utils.text import slugify
3 |
4 | from classic_tetris_project.models import *
5 | from classic_tetris_project.test_helper.factories.users import *
6 |
7 |
8 | class EventFactory(factory.django.DjangoModelFactory):
9 | class Meta:
10 | model = Event
11 | name = factory.Sequence(lambda n: f"Event {n}")
12 | slug = factory.LazyAttribute(lambda o: slugify(o.name))
13 | qualifying_type = 1
14 |
15 | class QualifierFactory(factory.django.DjangoModelFactory):
16 | class Meta:
17 | model = Qualifier
18 | user = factory.SubFactory(UserFactory)
19 | event = factory.SubFactory(EventFactory)
20 |
21 | class Params:
22 | submitted_ = factory.Trait(
23 | qualifying_score=500000,
24 | qualifying_data=[500000],
25 | vod="https://twitch.tv/asdf",
26 | submitted=True,
27 | )
28 |
29 | approved_ = factory.Trait(
30 | submitted_=True,
31 | approved=True
32 | )
33 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/live_notifications/submit.haml:
--------------------------------------------------------------------------------
1 | - extends "base.html"
2 |
3 | - block title
4 | Submit Live Notification
5 |
6 | - block content
7 | %form.form{method: "post"}
8 | - csrf_token
9 |
10 | .section
11 | .section__header
12 | %h2.section__title
13 | Live Notifiation Submission
14 | .form.form--narrow
15 | .form__field
16 | %label{for: "{{ live_notification_form.message.id_for_label }}"}
17 | Message contents
18 | = live_notification_form.message
19 | = live_notification_form.message.errors
20 |
21 | .form__field
22 | %label{for: "{{ live_notification_form.duration.id_for_label }}"}
23 | Duration (seconds)
24 | = live_notification_form.duration
25 | = live_notification_form.duration.errors
26 |
27 | .form__field
28 | - for radio in live_notification_form.position
29 | %div= radio
30 |
31 | .form__actions
32 | %input.btn.btn--primary{type: "submit", value: "Submit"}
33 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0068_auto_20220717_0652.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.11 on 2022-07-17 06:52
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0067_event_withdrawals_allowed'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='event',
15 | name='vod_required',
16 | field=models.BooleanField(default=True),
17 | ),
18 | migrations.AlterField(
19 | model_name='event',
20 | name='qualifying_type',
21 | field=models.IntegerField(choices=[(1, 'Highest Score'), (2, 'Highest 2 Scores'), (3, 'Highest 3 Scores'), (4, 'Most Maxouts')]),
22 | ),
23 | migrations.AlterField(
24 | model_name='qualifier',
25 | name='qualifying_type',
26 | field=models.IntegerField(choices=[(1, 'Highest Score'), (2, 'Highest 2 Scores'), (3, 'Highest 3 Scores'), (4, 'Most Maxouts')]),
27 | ),
28 | ]
29 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/review_qualifiers/index.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}Review Qualifiers{% endblock %}
4 |
5 | {% block content %}
6 | Review Qualifiers
7 | {% if qualifiers %}
8 |
26 | {% else %}
27 | No qualifiers to review!
28 | {% endif %}
29 | {% endblock %}
30 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0023_user_remove_pb_fields.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.4 on 2020-04-03 04:59
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0022_populate_scorepb'),
10 | ]
11 |
12 | operations = [
13 | migrations.RemoveField(
14 | model_name='user',
15 | name='ntsc_pb',
16 | ),
17 | migrations.RemoveField(
18 | model_name='user',
19 | name='ntsc_pb_19',
20 | ),
21 | migrations.RemoveField(
22 | model_name='user',
23 | name='ntsc_pb_19_updated_at',
24 | ),
25 | migrations.RemoveField(
26 | model_name='user',
27 | name='ntsc_pb_updated_at',
28 | ),
29 | migrations.RemoveField(
30 | model_name='user',
31 | name='pal_pb',
32 | ),
33 | migrations.RemoveField(
34 | model_name='user',
35 | name='pal_pb_updated_at',
36 | ),
37 | ]
38 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/event/qualifier.haml:
--------------------------------------------------------------------------------
1 | - extends "base.html"
2 |
3 | - block title
4 | Qualify for {{ event.name }}
5 |
6 | - block content
7 | %h1
8 | Qualify for {{ event.name }}
9 |
10 | - if event.qualify_instructions
11 | - markdown event.qualifying_instructions
12 |
13 | .auth-word.alert.alert--light-info
14 | Your auth word is:
15 | .auth-word__word
16 | = qualifier.auth_word|upper
17 | When you complete your first game with a score of over 10000, enter this word on the leaderboard. This helps us verify that you're recording your qualifier in real time.
18 |
19 | %form.form{method: "post"}
20 | - csrf_token
21 | = form.non_field_errors
22 |
23 | .field-list
24 | - for field in form.submit_fields
25 | - field_list_input_row field.label_tag
26 | = field
27 | = field.errors
28 | - if field.help_text
29 | .field-list__hint
30 | = field.help_text|safe
31 |
32 | - field_list_row ""
33 | %input.btn.btn--primary{type: "submit", value: "Submit"}
34 |
--------------------------------------------------------------------------------
/classic_tetris_project/discord_disco.py:
--------------------------------------------------------------------------------
1 | # Uses disco instead of discordpy. This file is temporary; we'll probably end up switching
2 | # everything over to disco eventually.
3 |
4 | from disco.api.client import APIClient
5 |
6 | from .env import env
7 |
8 |
9 | DISCORD_USER_ID_WHITELIST = env("DISCORD_USER_ID_WHITELIST")
10 |
11 | API = APIClient(env("DISCORD_TOKEN", default=""))
12 |
13 |
14 | def send_direct_message(discord_user_id, *args, **kwargs):
15 | if env("DEBUG") and discord_user_id not in DISCORD_USER_ID_WHITELIST:
16 | print(f"Tried to send message to Discord user {discord_user_id}:")
17 | print(f"{args}, {kwargs}")
18 | return
19 | channel = API.users_me_dms_create(discord_user_id)
20 | API.channels_messages_create(channel, *args, **kwargs)
21 |
22 | def send_channel_message(channel_id, *args, **kwargs):
23 | if env("DEBUG") and not env("DISCORD_CHANNEL_MESSAGES"):
24 | print(f"Tried to send message to Discord channel {channel_id}:")
25 | print(f"{args}, {kwargs}")
26 | return
27 | API.channels_messages_create(channel_id, *args, **kwargs)
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019-2020 Elle Nolan
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 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0056_auto_20211231_0727.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2021-12-31 07:27
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('classic_tetris_project', '0055_auto_20210802_1721'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='user',
16 | name='playstyle',
17 | field=models.CharField(blank=True, choices=[('das', 'DAS'), ('hypertap', 'Hypertap'), ('hybrid', 'Hybrid'), ('roll', 'Roll')], max_length=16, null=True),
18 | ),
19 | migrations.CreateModel(
20 | name='Restreamer',
21 | fields=[
22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
23 | ('active', models.BooleanField(default=True)),
24 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='restreamer', to='classic_tetris_project.user')),
25 | ],
26 | ),
27 | ]
28 |
--------------------------------------------------------------------------------
/classic_tetris_project/countries.py:
--------------------------------------------------------------------------------
1 | import csv
2 | from pathlib import Path
3 |
4 | COUNTRIES_CSV_PATH = Path(__file__).parent.resolve() / "data" / "countries.csv"
5 |
6 | class Country:
7 | ACCEPTED_MAPPINGS = {}
8 | ALL = []
9 |
10 | def __init__(self, two_letter, full_name):
11 | self.abbreviation = two_letter
12 | self.full_name = full_name
13 |
14 | def get_flag(self):
15 | return f":flag_{self.abbreviation}:"
16 |
17 | @staticmethod
18 | def populate_mappings(path=COUNTRIES_CSV_PATH):
19 | with open(path, "r") as f:
20 | rows = csv.reader(f)
21 | for row in rows:
22 | country = Country(row[0], row[1])
23 | Country.ALL.append(country)
24 | for column in row:
25 | Country.ACCEPTED_MAPPINGS[column.lower()] = country
26 | Country.ALL.sort(key=lambda country: country.full_name)
27 |
28 |
29 | @staticmethod
30 | def get_country(input_string):
31 | try:
32 | return Country.ACCEPTED_MAPPINGS[input_string.lower()]
33 | except KeyError:
34 | return None
35 |
36 | Country.populate_mappings()
37 |
--------------------------------------------------------------------------------
/classic_tetris_project/tests/commands/country.py:
--------------------------------------------------------------------------------
1 | from classic_tetris_project.test_helper import *
2 |
3 | class GetCountryCommand_(CommandSpec):
4 | class discord:
5 | def test_with_own_user_and_no_country(self):
6 | self.assert_discord("!country", [
7 | "User has not set a country."
8 | ])
9 |
10 | def test_with_own_user_and_country(self):
11 | self.discord_user.user.set_country("us")
12 | self.assert_discord("!country", [
13 | f"{self.discord_api_user.name} is from United States!"
14 | ])
15 |
16 | def test_with_other_user_and_no_country(self):
17 | discord_user = DiscordUserFactory(username="Other User")
18 | self.assert_discord("!country Other User", [
19 | "User has not set a country."
20 | ])
21 |
22 | def test_with_other_user_and_country(self):
23 | discord_user = DiscordUserFactory(username="Other User")
24 | discord_user.user.set_country("us")
25 | self.assert_discord("!country Other User", [
26 | "Other User is from United States!"
27 | ])
28 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0004_twitchuser_username.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.2 on 2019-08-07 00:36
2 |
3 | from django.db import migrations, models
4 |
5 | from .. import twitch
6 |
7 | def gen_usernames(apps, schema_editor):
8 | TwitchUser = apps.get_model('classic_tetris_project', 'TwitchUser')
9 | for twitch_user in TwitchUser.objects.all():
10 | twitch_user.username = twitch.client.get_user(twitch_user.twitch_id).username
11 | twitch_user.save()
12 |
13 | class Migration(migrations.Migration):
14 |
15 | dependencies = [
16 | ('classic_tetris_project', '0003_auto_20190628_1759'),
17 | ]
18 |
19 | operations = [
20 | migrations.AddField(
21 | model_name='twitchuser',
22 | name='username',
23 | field=models.CharField(default='', max_length=25),
24 | preserve_default=False,
25 | ),
26 | migrations.RunPython(gen_usernames, reverse_code=migrations.RunPython.noop),
27 | migrations.AlterField(
28 | model_name='twitchuser',
29 | name='username',
30 | field=models.CharField(max_length=25, unique=True),
31 | ),
32 | ]
33 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0075_auto_20230429_1913.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.11 on 2023-04-29 19:13
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0074_auto_20221002_0630'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='tournament',
15 | name='bracket_type',
16 | field=models.CharField(choices=[('SINGLE', 'Single Elimination'), ('DOUBLE', 'Double Elimination')], default='SINGLE', max_length=64),
17 | ),
18 | migrations.AlterField(
19 | model_name='event',
20 | name='qualifying_type',
21 | field=models.IntegerField(choices=[(1, 'Highest Score'), (2, 'Highest 2 Scores'), (3, 'Highest 3 Scores'), (4, 'Most Maxouts'), (5, 'Lowest Time')]),
22 | ),
23 | migrations.AlterField(
24 | model_name='qualifier',
25 | name='qualifying_type',
26 | field=models.IntegerField(choices=[(1, 'Highest Score'), (2, 'Highest 2 Scores'), (3, 'Highest 3 Scores'), (4, 'Most Maxouts'), (5, 'Lowest Time')]),
27 | ),
28 | ]
29 |
--------------------------------------------------------------------------------
/classic_tetris_project/util/fieldgen/garbage.py:
--------------------------------------------------------------------------------
1 | from .tiles import TileMath
2 | from .ai import Aesthetics
3 |
4 |
5 | class GarbageGenerator(object):
6 | TETRIS_HEIGHT = 4
7 |
8 | def __init__(self, tile_gen):
9 | self.tile_gen = tile_gen
10 |
11 | def draw_garbage(self, image, garbage_height, level, target_column):
12 | # draws garbage tiles on field. Leaves 4 rows in target_column at the top
13 | block_choices = self.tile_gen.get_block_tiles(level)
14 |
15 | # tile coordinate starts.
16 | x_start = TileMath.FIELD_START[0]
17 | y_start = TileMath.FIELD_START[1] + (TileMath.FIELD_HEIGHT - garbage_height)
18 |
19 | for y in range(garbage_height):
20 | if y < self.TETRIS_HEIGHT:
21 | tc = target_column
22 | else:
23 | tc = Aesthetics.which_garbage_hole(target_column, y)
24 | for x in range(TileMath.FIELD_WIDTH):
25 | if x == tc:
26 | continue
27 | block = Aesthetics.which_garbage_tile(x, y, block_choices)
28 | coord = [x_start + x, y_start + y]
29 | image.paste(block, TileMath.tile_indices_to_pixels(coord))
30 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/assets/stylesheets/_toggle.scss:
--------------------------------------------------------------------------------
1 | @use "colors";
2 |
3 | $toggle-width: 75px;
4 | $toggle-height: 30px;
5 | $toggle-offset: 6px;
6 | $toggle-inner: calc($toggle-height - $toggle-offset);
7 |
8 | .toggle {
9 | position: relative;
10 | display: inline-block;
11 | width: $toggle-width;
12 | height: $toggle-height;
13 |
14 | input {
15 | opacity: 0;
16 | height: 0;
17 | width: 0;
18 |
19 | &:checked {
20 | + .slide {
21 | background-color: gray;
22 |
23 | &:before {
24 | transform: translateX(calc($toggle-width - $toggle-offset * 2 - $toggle-inner));
25 | }
26 | }
27 | }
28 | }
29 |
30 | .slide {
31 | position: absolute;
32 | top: 0;
33 | left: 0;
34 | right: 0;
35 | bottom: 0;
36 | cursor: pointer;
37 | background-color: colors.$bg-light-gray;
38 | transition: 0.3s;
39 | border-radius: $toggle-height;
40 |
41 | &:before {
42 | position: absolute;
43 | content: "";
44 | height: $toggle-inner;
45 | width: $toggle-inner;
46 | left: $toggle-offset;
47 | bottom: calc($toggle-offset / 2);
48 | background-color: white;
49 | border-radius: 100%;
50 | }
51 | }
52 | }
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0012_coin_side.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.4 on 2020-02-11 06:27
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('classic_tetris_project', '0011_user_same_piece_sets'),
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='Coin',
16 | fields=[
17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18 | ('heads', models.IntegerField(default=0)),
19 | ('tails', models.IntegerField(default=0)),
20 | ('sides', models.IntegerField(default=0)),
21 | ],
22 | ),
23 | migrations.CreateModel(
24 | name='Side',
25 | fields=[
26 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
27 | ('timestamp', models.DateTimeField()),
28 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='classic_tetris_project.User')),
29 | ],
30 | ),
31 | ]
32 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/event/index.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% load humanize %}
4 |
5 | {% block title %}{{ event.name }}{% endblock %}
6 |
7 | {% block content %}
8 | {{ event.name }}
9 |
10 | {% if event.event_info %}
11 | {% markdown event.event_info %}
12 | {% endif %}
13 |
14 | {% if user_ineligible_reason %}
15 |
16 | {% include "event/ineligible_reasons/"|add:user_ineligible_reason|add:".html" %}
17 |
18 | {% else %}
19 | Qualify
20 | {% endif %}
21 |
22 |
23 |
24 | {% if qualifier_groups %}
25 |
26 |
37 |
38 | {% include "event/qualifier_table.html" with qualifier_groups=qualifier_groups %}
39 |
40 |
41 | {% endif %}
42 |
43 | {% endblock %}
44 |
--------------------------------------------------------------------------------
/classic_tetris_project_django/celery.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from celery import Celery
4 | from celery.signals import task_failure
5 |
6 | # set the default Django settings module for the 'celery' program.
7 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'classic_tetris_project_django.settings')
8 |
9 | app = Celery('classic_tetris_project_django')
10 |
11 | # Using a string here means the worker doesn't have to serialize
12 | # the configuration object to child processes.
13 | # - namespace='CELERY' means all celery-related configuration keys
14 | # should have a `CELERY_` prefix.
15 | app.config_from_object('django.conf:settings', namespace='CELERY')
16 |
17 | # Load task modules from all registered Django app configs.
18 | app.autodiscover_tasks()
19 |
20 |
21 | # https://www.mattlayman.com/blog/2017/django-celery-rollbar/
22 | if bool(os.environ.get('CELERY_WORKER_RUNNING', False)):
23 | from django.conf import settings
24 | import rollbar
25 | rollbar.init(**settings.ROLLBAR)
26 |
27 | def celery_base_data_hook(request, data):
28 | data['framework'] = 'celery'
29 |
30 | rollbar.BASE_DATA_HOOK = celery_base_data_hook
31 |
32 | @task_failure.connect
33 | def handle_task_failure(**kw):
34 | rollbar.report_exc_info(extra_data=kw)
35 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/user/show.haml:
--------------------------------------------------------------------------------
1 | - extends "base.html"
2 |
3 | - block title
4 | = user.preferred_display_name
5 |
6 | - block content
7 | .profile
8 | .profile-header
9 | %h1.profile-header__title
10 | = user.preferred_display_name
11 | - if user.pronouns
12 | .profile-header__subtitle
13 | = user.get_pronouns_display
14 |
15 | .section
16 | .section__header
17 | %h2.section__title
18 | Profile
19 | - if user == current_user
20 | %a{href: '{% url "profile:edit" %}'}
21 | Edit
22 | .section__body
23 | .field-list
24 | - if user.country
25 | - field_list_row "Country:"
26 | = user.get_country_display
27 |
28 | - if discord_user
29 | - field_list_row "Discord:"
30 | = discord_user.username_with_discriminator
31 |
32 | - if twitch_user
33 | - field_list_row "Twitch:"
34 | %a{href: "{{ twitch_user.twitch_url }}"}= twitch_user.display_name
35 |
36 | - if user.playstyle
37 | - field_list_row "Playstyle:"
38 | = user.get_playstyle_display
39 |
40 | - if pb
41 | - field_list_row "Overall PB:"
42 | = pb
43 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/tests/views/user.py:
--------------------------------------------------------------------------------
1 | from classic_tetris_project.test_helper import *
2 |
3 | class UserView_(Spec):
4 | def test_with_own_profile(self):
5 | self.sign_in()
6 | response = self.client.get(f"/user/{self.current_user.id}/")
7 |
8 | assert_that(response.status_code, equal_to(200))
9 | assert_that(response, uses_template("user/show.haml"))
10 | assert_that(response, has_html("a[href='/profile/edit/']", "Edit"))
11 |
12 | def test_with_other_profile_id(self):
13 | user = UserFactory()
14 | response = self.client.get(f"/user/{user.id}/")
15 |
16 | assert_that(response.status_code, equal_to(200))
17 |
18 | def test_with_other_profile_username(self):
19 | user = UserFactory()
20 | twitch_user = TwitchUserFactory(user=user, username="twitch_username")
21 | response = self.client.get(f"/user/{user.id}/")
22 |
23 | assert_that(response, redirects_to("/user/twitch_username/"))
24 |
25 | response = self.client.get(f"/user/twitch_username/")
26 |
27 | assert_that(response.status_code, equal_to(200))
28 |
29 | def test_with_nonexistent_profile(self):
30 | response = self.client.get(f"/user/nonexistent_user/")
31 | assert_that(response.status_code, equal_to(404))
32 |
--------------------------------------------------------------------------------
/classic_tetris_project_django/urls.py:
--------------------------------------------------------------------------------
1 | """classic_tetris_project_django URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/2.2/topics/http/urls/
5 | Examples:
6 | Function views
7 | 1. Add an import: from my_app import views
8 | 2. Add a URL to urlpatterns: path('', views.home, name='home')
9 | Class-based views
10 | 1. Add an import: from other_app.views import Home
11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
12 | Including another URLconf
13 | 1. Import the include() function: from django.urls import include, path
14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
15 | """
16 | from django.contrib import admin
17 | from django.urls import include, path
18 | from django.conf import settings
19 | from django.conf.urls.static import static
20 |
21 | try:
22 | urlpatterns = [
23 | path("admin/", admin.site.urls),
24 | path("", include("classic_tetris_project.web.urls")),
25 | ]
26 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
27 | except ModuleNotFoundError as e:
28 | print("Private urls could not be loaded")
29 | print(e)
30 | # web not loaded, ignore all urls
31 | urlpatterns = []
32 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0078_auto_20240505_1551.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.11 on 2024-05-05 15:51
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0077_event_use_custom_font'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='event',
15 | name='qualifying_type',
16 | field=models.IntegerField(choices=[(1, 'Highest Score'), (2, 'Highest 2 Scores'), (3, 'Highest 3 Scores'), (4, 'Most Maxouts'), (5, 'Lowest Time'), (6, 'Clipped Seven Game Average')]),
17 | ),
18 | migrations.AlterField(
19 | model_name='qualifier',
20 | name='qualifying_type',
21 | field=models.IntegerField(choices=[(1, 'Highest Score'), (2, 'Highest 2 Scores'), (3, 'Highest 3 Scores'), (4, 'Most Maxouts'), (5, 'Lowest Time'), (6, 'Clipped Seven Game Average')]),
22 | ),
23 | migrations.AlterField(
24 | model_name='user',
25 | name='pronouns',
26 | field=models.CharField(blank=True, choices=[('he', 'He/him/his'), ('she', 'She/her/hers'), ('they', 'They/them/theirs'), ('it', 'It/its'), ('xe', 'Xe/xem/xir')], max_length=16, null=True),
27 | ),
28 | ]
29 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/tests/views/oauth.py:
--------------------------------------------------------------------------------
1 | from classic_tetris_project.test_helper import *
2 |
3 |
4 | class Login(Spec):
5 | url = "/oauth/login/"
6 |
7 | def test_renders(self):
8 | response = self.get()
9 |
10 | assert_that(response.status_code, equal_to(200))
11 | assert_that(response, uses_template("oauth/login.html"))
12 | assert_that(response, has_html("a[href='/oauth/login/discord/']"))
13 | assert_that(response, has_html("a[href='/oauth/login/twitch/']"))
14 |
15 | def test_renders_with_next_path(self):
16 | response = self.get({ "next": "/profile/" })
17 |
18 | assert_that(response.status_code, equal_to(200))
19 | assert_that(response, uses_template("oauth/login.html"))
20 | assert_that(response, has_html("a[href='/oauth/login/discord/?next=%2Fprofile%2F']"))
21 | assert_that(response, has_html("a[href='/oauth/login/twitch/?next=%2Fprofile%2F']"))
22 |
23 |
24 | class Logout(Spec):
25 | url = "/oauth/logout/"
26 |
27 | def test_redirects_when_logged_in(self):
28 | self.sign_in()
29 | response = self.get()
30 |
31 | assert_that(response, redirects_to("/"))
32 |
33 | def test_redirects_when_logged_out(self):
34 | response = self.get()
35 |
36 | assert_that(response, redirects_to("/"))
37 |
--------------------------------------------------------------------------------
/classic_tetris_project/models/twitch.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | from .users import TwitchUser
4 | from ..util import memoize
5 | from .. import twitch
6 |
7 |
8 | class TwitchChannel(models.Model):
9 | twitch_user = models.OneToOneField(TwitchUser, on_delete=models.CASCADE, related_name="channel",
10 | primary_key=True)
11 | connected = models.BooleanField(default=False, db_index=True)
12 |
13 | @property
14 | def name(self):
15 | return self.twitch_user.display_name
16 |
17 | def summon_bot(self):
18 | self.connected = True
19 | self.save()
20 | # TODO add ability to join/leave channels from a different process
21 | if twitch.client.connection.connected:
22 | twitch.client.join_channel(self.name)
23 |
24 | def eject_bot(self):
25 | self.connected = False
26 | self.save()
27 | twitch.client.leave_channel(self.name)
28 |
29 | @property
30 | @memoize
31 | def client_channel(self):
32 | return twitch.client.get_channel(self.name)
33 |
34 | def send_message(self, message):
35 | self.client_channel.send_message(message)
36 |
37 | def twitch_url(self):
38 | return self.twitch_user.twitch_url
39 |
40 | def __str__(self):
41 | return str(self.twitch_user)
42 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/views/simulations.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.http import HttpResponse, HttpResponseBadRequest
3 | from django.views.static import serve
4 |
5 | from classic_tetris_project.util.fieldgen.hz_simulation import HzSimulation
6 | from classic_tetris_project.util.memo import lazy
7 | from .base import BaseView
8 |
9 |
10 | class HzView(BaseView):
11 | def get(self, request):
12 | try:
13 | self.simulation.cache_image()
14 | except (KeyError, ValueError):
15 | return HttpResponseBadRequest("Invalid params")
16 |
17 | if settings.DEBUG:
18 | return serve(request, self.simulation.filename, HzSimulation.IMAGE_CACHE.root)
19 | else:
20 | # Serve up the cached file much more efficiently using nginx:
21 | # https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
22 | response = HttpResponse(content_type="image/gif")
23 | response["X-Accel-Redirect"] = HzSimulation.IMAGE_CACHE.cache_path(self.simulation.filename)
24 | return response
25 |
26 | @lazy
27 | def simulation(self):
28 | level = int(self.request.GET["level"])
29 | height = int(self.request.GET["height"])
30 | taps = int(self.request.GET["taps"])
31 | return HzSimulation(level, height, taps)
32 |
--------------------------------------------------------------------------------
/classic_tetris_project/models/commands.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.db.utils import OperationalError
3 |
4 | from .twitch import TwitchChannel
5 | from .users import TwitchUser
6 |
7 |
8 | class CustomCommand(models.Model):
9 |
10 | twitch_channel = models.ForeignKey(TwitchChannel, on_delete=models.CASCADE)
11 |
12 | name = models.CharField(max_length=20)
13 | output = models.CharField(max_length=400, blank=True)
14 | alias_for = models.ForeignKey("self", null=True, blank=True, on_delete=models.CASCADE)
15 |
16 | class Meta:
17 | constraints = [
18 | models.UniqueConstraint(
19 | # implies index on (twitch_chanel, name)
20 | fields=["twitch_channel", "name"],
21 | name="unique channel plus command"
22 | )
23 | ]
24 |
25 | @staticmethod
26 | def get_command(channel, command_name):
27 | try:
28 | command = CustomCommand.objects.filter(twitch_channel=channel).get(name=command_name)
29 | except (CustomCommand.DoesNotExist, OperationalError):
30 | return False
31 |
32 |
33 | return command
34 |
35 | def wrap(self, context):
36 | from ..commands.command import CustomTwitchCommand
37 | return CustomTwitchCommand(context, self)
38 |
39 | def __str__(self):
40 | return self.name
41 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/views/user.py:
--------------------------------------------------------------------------------
1 | from django.http import Http404
2 | from django.shortcuts import render, redirect
3 | from django.urls import reverse
4 |
5 | from classic_tetris_project.models import User, TwitchUser
6 | from classic_tetris_project.util.memo import lazy
7 | from .base import BaseView
8 |
9 |
10 | class UserView(BaseView):
11 | def get(self, request, id):
12 | user = self.get_user(id)
13 | if hasattr(user, "twitch_user") and id != user.twitch_user.username:
14 | return redirect(reverse("user", args=[user.twitch_user.username]))
15 |
16 | pb = user.get_pb()
17 | if pb is None:
18 | pb = "Not set"
19 | else:
20 | pb = f"{pb:,}"
21 |
22 | return render(request, "user/show.haml", {
23 | "user": user,
24 | "discord_user": (user.discord_user if hasattr(user, "discord_user") else None),
25 | "twitch_user": (user.twitch_user if hasattr(user, "twitch_user") else None),
26 | "pb": pb,
27 | })
28 |
29 | def get_user(self, id):
30 | twitch_user = TwitchUser.from_username(id)
31 | if twitch_user is not None:
32 | return twitch_user.user
33 |
34 | try:
35 | return User.objects.get(id=int(id))
36 | except (ValueError, User.DoesNotExist):
37 | raise Http404("User does not exist")
38 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0022_populate_scorepb.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.4 on 2020-04-03 04:05
2 |
3 | from django.db import migrations
4 | from django.utils import timezone
5 |
6 | def populate_score_pb(apps, schema_editor):
7 | User = apps.get_model("classic_tetris_project", "User")
8 | ScorePB = apps.get_model("classic_tetris_project", "ScorePB")
9 | now = timezone.now()
10 | for user in User.objects.all():
11 | if user.ntsc_pb:
12 | ScorePB.objects.create(user=user, score=user.ntsc_pb, console_type="ntsc",
13 | current=True, created_at=user.ntsc_pb_updated_at or now)
14 | if user.pal_pb:
15 | ScorePB.objects.create(user=user, score=user.pal_pb, console_type="pal",
16 | current=True, created_at=user.pal_pb_updated_at or now)
17 | if user.ntsc_pb_19:
18 | ScorePB.objects.create(user=user, score=user.ntsc_pb_19, console_type="ntsc",
19 | starting_level=19,
20 | current=True, created_at=user.ntsc_pb_19_updated_at or now)
21 |
22 |
23 | class Migration(migrations.Migration):
24 |
25 | dependencies = [
26 | ('classic_tetris_project', '0021_scorepb'),
27 | ]
28 |
29 | operations = [
30 | migrations.RunPython(populate_score_pb, migrations.RunPython.noop),
31 | ]
32 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/forms/review_qualifiers.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.utils.safestring import mark_safe
3 |
4 | from classic_tetris_project.models import Qualifier
5 |
6 |
7 | class ReviewQualifierFormMeta(forms.forms.DeclarativeFieldsMetaclass):
8 | def __new__(mcs, name, bases, attrs):
9 | for check, label in Qualifier.REVIEWER_CHECKS:
10 | field = forms.BooleanField(label=label, label_suffix="?", initial=True, required=False)
11 | attrs[check] = field
12 | return super().__new__(mcs, name, bases, attrs)
13 |
14 |
15 | class ReviewQualifierForm(forms.Form, metaclass=ReviewQualifierFormMeta):
16 | notes = forms.CharField(widget=forms.Textarea, required=False)
17 | approved = forms.TypedChoiceField(coerce=lambda x: x == "True",
18 | choices=((True, "Approve"), (False, "Reject")),
19 | widget=forms.RadioSelect)
20 |
21 | def checks(self):
22 | for check, _ in Qualifier.REVIEWER_CHECKS:
23 | yield self[check]
24 |
25 | def save(self, qualifier, reviewed_by):
26 | qualifier.review(
27 | self.cleaned_data["approved"],
28 | reviewed_by,
29 | checks={ key: self.cleaned_data[key] for key, _ in Qualifier.REVIEWER_CHECKS },
30 | notes=self.cleaned_data["notes"]
31 | )
32 |
--------------------------------------------------------------------------------
/classic_tetris_project/facades/user_permissions.py:
--------------------------------------------------------------------------------
1 | from ..util import lazy
2 |
3 | class UserPermissions:
4 | REVIEW_QUALIFIERS = "classic_tetris_project.change_qualifier"
5 | RESTREAM = "classic_tetris_project.restream"
6 | REPORT_ALL = "classic_tetris_project.change_tournamentmatch"
7 | CHANGE_TOURNAMENT = "classic_tetris_project.change_tournament"
8 | SEND_LIVE_NOTIFICATIONS = "classic_tetris_project.send_live_notifications"
9 |
10 | def __init__(self, user):
11 | self.user = user
12 |
13 | @lazy
14 | def auth_user(self):
15 | if (self.user and hasattr(self.user, "website_user")
16 | and hasattr(self.user.website_user, "auth_user")):
17 | return self.user.website_user.auth_user
18 |
19 | def review_qualifiers(self):
20 | return self.auth_user is not None and self.auth_user.has_perm(self.REVIEW_QUALIFIERS)
21 |
22 | def restream(self):
23 | return self.auth_user is not None and self.auth_user.has_perm(self.RESTREAM)
24 |
25 | def report_all(self):
26 | return self.auth_user is not None and self.auth_user.has_perm(self.REPORT_ALL)
27 |
28 | def change_tournament(self):
29 | return self.auth_user is not None and self.auth_user.has_perm(self.CHANGE_TOURNAMENT)
30 |
31 | def send_live_notifications(self):
32 | return self.auth_user is not None and self.auth_user.has_perm(self.SEND_LIVE_NOTIFICATIONS)
33 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/tournament_match/schedule.haml:
--------------------------------------------------------------------------------
1 | - extends "base.html"
2 |
3 | - block title
4 | Schedule Match
5 |
6 | - block content
7 | %a{href: "{{ match.get_absolute_url }}"} Back to Match
8 |
9 | .section
10 | .section__header
11 | %h2.section__title
12 | Schedule Match
13 |
14 | .section__body
15 | %form.form{method: "post"}
16 | - csrf_token
17 | = form.non_field_errors
18 |
19 | .field-list
20 | - field_list_row "Tournament:"
21 | %a{href: "{{ match.tournament.get_absolute_url }}"}= match.tournament.name
22 |
23 | - field_list_row "Player 1:"
24 | - if match.player1.user
25 | %a{href: "{{ match.player1.user.get_absolute_url }}"}= match.player1.display_name
26 | - else
27 | = match_display.player1_display_name
28 |
29 | - field_list_row "Player 2:"
30 | - if match.player2.user
31 | %a{href: "{{ match.player2.user.get_absolute_url }}"}= match.player2.display_name
32 | - else
33 | = match_display.player2_display_name
34 |
35 | - field_list_input_row "Twitch channel:" form.channel
36 | - field_list_input_row "Start time:" form.start_date
37 |
38 | - field_list_row ""
39 | %input.btn.btn--primary{type: "submit", value: "Schedule"}
40 |
41 | - block footer
42 | = form.media
43 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/forms/duplicate_event.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.db import transaction
3 |
4 | from classic_tetris_project.models import Event
5 |
6 |
7 | class DuplicateEventForm(forms.ModelForm):
8 | class Meta:
9 | model = Event
10 | fields = ["name"]
11 |
12 | @transaction.atomic
13 | def save(self, base_event):
14 | event = Event.objects.create(
15 | name=self.cleaned_data["name"],
16 | qualifying_type=base_event.qualifying_type,
17 | vod_required=base_event.vod_required,
18 | pre_qualifying_instructions=base_event.pre_qualifying_instructions,
19 | qualifying_instructions=base_event.qualifying_instructions,
20 | event_info=base_event.event_info,
21 | qualifying_channel_id=base_event.qualifying_channel_id,
22 | reporting_channel_id=base_event.reporting_channel_id,
23 | )
24 | for tournament in base_event.tournaments.all():
25 | event.tournaments.create(
26 | short_name=tournament.short_name,
27 | order=tournament.order,
28 | seed_count=tournament.seed_count,
29 | color=tournament.color,
30 | restreamed=tournament.restreamed,
31 | details=tournament.details,
32 | discord_emote_string=tournament.discord_emote_string
33 | )
34 | return event
35 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0015_auto_20200318_0315.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.4 on 2020-03-18 03:15
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('classic_tetris_project', '0014_auto_20200313_0819'),
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='CustomCommand',
16 | fields=[
17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18 | ('name', models.CharField(max_length=20)),
19 | ('output', models.CharField(max_length=400, null=True)),
20 | ('alias_for', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='classic_tetris_project.CustomCommand')),
21 | ('twitch_channel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='classic_tetris_project.TwitchChannel')),
22 | ],
23 | ),
24 | migrations.AddIndex(
25 | model_name='customcommand',
26 | index=models.Index(fields=['twitch_channel'], name='classic_tet_twitch__a83c1c_idx'),
27 | ),
28 | migrations.AddConstraint(
29 | model_name='customcommand',
30 | constraint=models.UniqueConstraint(fields=('twitch_channel', 'name'), name='unique channel plus command'),
31 | ),
32 | ]
33 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/assets/stylesheets/_navbar.scss:
--------------------------------------------------------------------------------
1 | @use "colors";
2 | @use "media";
3 |
4 | .navbar {
5 | display: flex;
6 | flex-flow: row nowrap;
7 | width: 100%;
8 | min-height: 60px;
9 | background-color: var(--bg-nav);
10 | @include media.mobile-only {
11 | flex-flow: row wrap;
12 | }
13 |
14 | &__collapse {
15 | display: flex;
16 | transition: height 0.5s;
17 |
18 | @include media.tablet-and-desktop {
19 | display: flex !important;
20 | }
21 |
22 | @include media.mobile-only {
23 | flex: 0 0 100%;
24 | flex-flow: column;
25 | background-color: var(--bg-nav-drawer);
26 | }
27 | }
28 |
29 | &__item {
30 | flex: 0 0 auto;
31 | padding: 15px 20px;
32 | box-sizing: border-box;
33 | height: 60px;
34 | font-size: 18px;
35 | line-height: 30px;
36 | text-align: right;
37 | color: colors.$navbar-text;
38 | text-decoration: none;
39 |
40 | a {
41 | color: colors.$navbar-text;
42 | }
43 |
44 | &:hover {
45 | color: colors.$navbar-text;
46 | text-decoration: none;
47 | }
48 |
49 | &--title {
50 | font-size: 24px;
51 | }
52 | }
53 |
54 | &__spacer {
55 | flex: 1 0 0px;
56 | }
57 |
58 | &__toggler {
59 | @include media.tablet-and-desktop {
60 | display: none;
61 | }
62 |
63 | .icon {
64 | stroke: colors.$icon-white;
65 | }
66 | }
67 |
68 | .theme-toggle {
69 | position: relative;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/classic_tetris_project/logging.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | import logging
3 | import logging.handlers
4 | import os
5 | import sys
6 |
7 | from . import discord, twitch
8 |
9 | internal_logger = logging.getLogger("internal")
10 |
11 | class LoggingManager:
12 | def __init__(self):
13 | self.formatter = logging.Formatter("%(asctime)s [%(name)s] %(levelname)s: %(message)s")
14 |
15 | self.console_handler = logging.StreamHandler()
16 | self.console_handler.setLevel(logging.INFO)
17 | self.console_handler.setFormatter(self.formatter)
18 |
19 | self.file_handler = logging.handlers.TimedRotatingFileHandler(
20 | filename="logs/bot.log",
21 | when="midnight",
22 | interval=1
23 | )
24 | self.file_handler.setLevel(logging.INFO)
25 | self.file_handler.setFormatter(self.formatter)
26 | self.file_handler.namer = lambda name: datetime.now().strftime("logs/bot-%Y-%m-%d.log")
27 |
28 | def bind(self, logger):
29 | logger.propagate = False
30 | logger.addHandler(self.console_handler)
31 | logger.addHandler(self.file_handler)
32 | logger.setLevel(logging.INFO)
33 |
34 | @staticmethod
35 | def setup():
36 | try:
37 | os.mkdir("logs")
38 | except FileExistsError:
39 | pass
40 |
41 | manager = LoggingManager()
42 | manager.bind(discord.logger)
43 | manager.bind(twitch.logger)
44 | manager.bind(internal_logger)
45 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/assets/js/stimulus/date_picker_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from '@hotwired/stimulus';
2 | import { DateTime } from 'luxon';
3 | import SimplePicker from 'simplepicker';
4 |
5 | export default class DatePickerController extends Controller {
6 | static targets = ['input', 'displayLocal', 'displayUTC'];
7 |
8 | connect() {
9 | this.datetime = DateTime.fromFormat(this.inputTarget.value, 'yyyy-MM-dd HH:mm:ss', { zone: 'UTC' });
10 | const pickerOptions = this.datetime.invalid ? {} : { selectedDate: this.datetime.toJSDate() };
11 | this.picker = new SimplePicker(pickerOptions);
12 | this.picker.on('submit', this.onChange.bind(this));
13 | this.displayTimes();
14 | }
15 |
16 | open(e) {
17 | e.preventDefault();
18 | this.picker.open();
19 | }
20 |
21 | onChange(date, readableDate) {
22 | this.datetime = DateTime.fromJSDate(date);
23 | this.inputTarget.value = this.datetime.toUTC().toISO();
24 | this.displayTimes();
25 | }
26 |
27 | displayTimes() {
28 | console.log(this.datetime);
29 | if (this.datetime.invalid) {
30 | this.displayLocalTarget.innerText = '';
31 | this.displayUTCTarget.innerText = '';
32 | } else {
33 | const local = this.datetime.toLocal();
34 | const utc = this.datetime.toUTC();
35 | this.displayLocalTarget.innerText = `${local.toLocaleString(DateTime.DATETIME_MED)} (${local.zoneName})`;
36 | this.displayUTCTarget.innerText = `${utc.toLocaleString(DateTime.DATETIME_MED)} (UTC)`;
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0043_auto_20210327_2316.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2021-03-27 23:16
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0042_auto_20210327_2218'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterModelOptions(
14 | name='tournament',
15 | options={'ordering': ['order']},
16 | ),
17 | migrations.RemoveField(
18 | model_name='tournament',
19 | name='priority',
20 | ),
21 | migrations.AddField(
22 | model_name='tournament',
23 | name='order',
24 | field=models.PositiveIntegerField(default=0, help_text='Used to order tournaments for seeding'),
25 | ),
26 | migrations.AlterField(
27 | model_name='tournament',
28 | name='name',
29 | field=models.CharField(blank=True, help_text='Full name of the tournament. Leave blank to automatically set to event name + short name.', max_length=64),
30 | ),
31 | migrations.AlterField(
32 | model_name='tournament',
33 | name='seed_count',
34 | field=models.IntegerField(help_text='Number of players to seed into this tournament'),
35 | ),
36 | migrations.AlterField(
37 | model_name='tournament',
38 | name='slug',
39 | field=models.SlugField(blank=True, db_index=False),
40 | ),
41 | ]
42 |
--------------------------------------------------------------------------------
/classic_tetris_project/commands/pb_zip.py:
--------------------------------------------------------------------------------
1 | from io import BytesIO
2 | import zipfile
3 | from .command import Command
4 | from ..models.users import TwitchUser
5 | from ..util import Platform, DocSection
6 |
7 | @Command.register()
8 | class ZipFileCommand(Command):
9 | """
10 | Sends a zipped list of pbs for each Twitch user.
11 | """
12 | aliases = ("obsfiles",)
13 | supported_platforms = (Platform.DISCORD,)
14 | usage = "obsfiles [type=ntsc]"
15 | notes = ("Moderator-only",)
16 | section = DocSection.OTHER
17 |
18 | def execute(self, pb_type="ntsc"):
19 |
20 | self.check_moderator()
21 |
22 | buffer = BytesIO()
23 | with zipfile.ZipFile(buffer, "a", zipfile.ZIP_DEFLATED, False) as z:
24 | for twitch_user in TwitchUser.objects.all():
25 | pb = twitch_user.user.get_pb(pb_type)
26 |
27 | if pb is not None:
28 | name = f"{twitch_user.username}_{pb_type}_pb.txt"
29 | contents = "{n:,}".format(n=pb)
30 | z.writestr(name, contents)
31 |
32 | name = f"{twitch_user.username}_playstyle.txt"
33 | contents = ""
34 | if twitch_user.user.playstyle is not None:
35 | contents = twitch_user.user.playstyle[:3].upper()
36 | contents = contents if contents != "HYP" else "TAP"
37 | z.writestr(name, contents)
38 |
39 | buffer.seek(0)
40 | self.context.send_file(buffer, "pbs.zip")
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/forms/tournament_match.py:
--------------------------------------------------------------------------------
1 | from dal import autocomplete
2 | from datetime import datetime
3 | from django import forms
4 | from django.core.exceptions import ValidationError
5 | from django.urls import reverse
6 | from django_select2 import forms as s2forms
7 |
8 | from classic_tetris_project.models import TwitchChannel
9 | from classic_tetris_project.models import Match
10 | from .. import widgets
11 |
12 |
13 | class ScheduleForm(forms.ModelForm):
14 | class Meta:
15 | model = Match
16 | fields = ["channel", "start_date"]
17 |
18 | channel = forms.ModelChoiceField(
19 | queryset=TwitchChannel.objects.all(),
20 | widget=autocomplete.ModelSelect2(url=reverse("autocomplete:twitch_channel")),
21 | )
22 | start_date = forms.DateTimeField(widget=widgets.DateTimePicker)
23 |
24 |
25 | class ReportForm(forms.ModelForm):
26 | class Meta:
27 | model = Match
28 | fields = ["wins1", "wins2", "channel", "vod", "ended_at"]
29 |
30 | channel = forms.ModelChoiceField(
31 | queryset=TwitchChannel.objects.all(),
32 | widget=autocomplete.ModelSelect2(url=reverse("autocomplete:twitch_channel")),
33 | )
34 | ended_at = forms.DateTimeField(widget=widgets.DateTimePicker, required=False)
35 |
36 | def clean(self):
37 | if self.cleaned_data["wins1"] == self.cleaned_data["wins2"]:
38 | raise ValidationError("One player must have won more games than the other")
39 |
40 | def save(self, reported_by):
41 | super().save()
42 | self.instance.end(reported_by)
43 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0042_auto_20210327_2218.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2021-03-27 22:18
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('classic_tetris_project', '0041_auto_20210327_2206'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='tournament',
16 | name='short_name',
17 | field=models.CharField(default='', help_text='Used in the context of an event, e.g. "Challenger\'s Circuit"', max_length=64),
18 | preserve_default=False,
19 | ),
20 | migrations.AddField(
21 | model_name='tournament',
22 | name='slug',
23 | field=models.SlugField(db_index=False, default=''),
24 | preserve_default=False,
25 | ),
26 | migrations.AlterField(
27 | model_name='tournament',
28 | name='event',
29 | field=models.ForeignKey(db_index=False, on_delete=django.db.models.deletion.CASCADE, to='classic_tetris_project.event'),
30 | ),
31 | migrations.AlterField(
32 | model_name='tournament',
33 | name='name',
34 | field=models.CharField(help_text='Full name of the tournament', max_length=64),
35 | ),
36 | migrations.AddConstraint(
37 | model_name='tournament',
38 | constraint=models.UniqueConstraint(fields=('event_id', 'slug'), name='unique_event_slug'),
39 | ),
40 | ]
41 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0002_auto_20190628_1733.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.2 on 2019-06-28 17:33
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('classic_tetris_project', '0001_initial'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='user',
16 | name='country',
17 | field=models.CharField(max_length=3, null=True),
18 | ),
19 | migrations.CreateModel(
20 | name='TwitchUser',
21 | fields=[
22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
23 | ('twitch_id', models.CharField(max_length=64)),
24 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='classic_tetris_project.User')),
25 | ],
26 | options={
27 | 'abstract': False,
28 | },
29 | ),
30 | migrations.CreateModel(
31 | name='DiscordUser',
32 | fields=[
33 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
34 | ('discord_id', models.CharField(max_length=64)),
35 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='classic_tetris_project.User')),
36 | ],
37 | options={
38 | 'abstract': False,
39 | },
40 | ),
41 | ]
42 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/profile/edit.haml:
--------------------------------------------------------------------------------
1 | - extends "base.html"
2 |
3 | - block title
4 | Edit Profile
5 |
6 | - block content
7 | .profile
8 | .section
9 | .section__header
10 | %h2.section__title
11 | Edit Profile
12 | %form.form{method: "post"}
13 | - csrf_token
14 |
15 | .section__body
16 | = form.non_field_errors
17 | .field-list
18 | - field_list_input_row "Preferred name:" form.preferred_name
19 | - field_list_input_row "Pronouns:" form.pronouns
20 | - field_list_input_row "Country:" form.country
21 | - field_list_row "Discord:"
22 | - if user.discord_user
23 | = user.discord_user.username_with_discriminator
24 | - else
25 | %a{href: '{% url "oauth:login" provider="discord" %}?merge_accounts=true&next={% url "profile:edit" %}'}
26 | Link your Discord account
27 |
28 | - field_list_row "Twitch:"
29 | - if user.twitch_user
30 | %a{href: "{{ user.twitch_user.twitch_url }}"}= user.twitch_user.display_name
31 | - else
32 | %a{href: '{% url "oauth:login" provider="twitch"%}?merge_accounts=true&next={% url "profile:edit" %}'}
33 | Link your Twitch account
34 |
35 | - field_list_input_row "Playstyle:" form.playstyle
36 |
37 | .field-list__row
38 | .field-list__label
39 | .field-list__value
40 | %input.btn.btn--primary{type: "submit", value: "Update"}
41 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0039_auto_20210228_0824.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2021-02-28 08:24
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('classic_tetris_project', '0038_event_pre_qualifying_instructions'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='qualifier',
15 | name='withdrawn',
16 | field=models.BooleanField(default=False),
17 | ),
18 | migrations.AlterField(
19 | model_name='event',
20 | name='qualifying_type',
21 | field=models.IntegerField(choices=[(1, 'Highest Score'), (2, 'Highest 2 Scores'), (3, 'Highest 3 Scores')]),
22 | ),
23 | migrations.AlterField(
24 | model_name='qualifier',
25 | name='qualifying_data',
26 | field=models.JSONField(blank=True, null=True),
27 | ),
28 | migrations.AlterField(
29 | model_name='qualifier',
30 | name='qualifying_score',
31 | field=models.IntegerField(blank=True, null=True),
32 | ),
33 | migrations.AlterField(
34 | model_name='qualifier',
35 | name='qualifying_type',
36 | field=models.IntegerField(choices=[(1, 'Highest Score'), (2, 'Highest 2 Scores'), (3, 'Highest 3 Scores')]),
37 | ),
38 | migrations.AlterField(
39 | model_name='qualifier',
40 | name='submitted_at',
41 | field=models.DateTimeField(blank=True, null=True),
42 | ),
43 | ]
44 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/assets/stylesheets/_forms.scss:
--------------------------------------------------------------------------------
1 | @use 'colors';
2 |
3 | .field-list {
4 | &__row {
5 | display: table-row;
6 | }
7 |
8 | &__label {
9 | display: table-cell;
10 | padding: 5px;
11 | width: 120px;
12 | text-align: right;
13 | vertical-align: top;
14 |
15 | &--input {
16 | padding: 11px 5px;
17 | }
18 | }
19 |
20 | &__value {
21 | display: table-cell;
22 | padding: 5px;
23 | vertical-align: top;
24 | }
25 |
26 | &__hint {
27 | margin-top: 5px;
28 | font-size: 12px;
29 | }
30 | }
31 |
32 | .errorlist {
33 | list-style: none;
34 | margin: 0;
35 | padding: 0;
36 | color: colors.$error-text;
37 | font-size: 14px;
38 | }
39 |
40 | .form {
41 | &--narrow {
42 | width: 400px;
43 | max-width: 100%;
44 | margin: auto;
45 |
46 | input[type="text"], input[type="number"], input[type="url"], select, textarea {
47 | width: 100%;
48 | }
49 | }
50 |
51 | &__field + &__field {
52 | margin-top: 5px;
53 | }
54 |
55 | &__actions {
56 | margin-top: 15px;
57 | }
58 |
59 | // To be refactored once form rendering is streamlined
60 | input[type="text"], input[type="number"], input[type="url"], select, textarea {
61 | padding: 5px;
62 | font-size: 16px;
63 | line-height: 20px;
64 | border: 1px solid colors.$border-gray;
65 |
66 | &:focus {
67 | outline: 2px solid colors.$border-blue;
68 | }
69 | }
70 |
71 | select:after {
72 | color: colors.$border-gray;
73 | }
74 |
75 | textarea {
76 | display: block;
77 | resize: none;
78 | box-sizing: border-box;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0058_auto_20220104_1936.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2022-01-04 19:36
2 |
3 | import django.core.validators
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('classic_tetris_project', '0057_auto_20220104_0251'),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name='match',
17 | name='reported_by',
18 | field=models.ForeignKey(blank=True, db_index=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='classic_tetris_project.user'),
19 | ),
20 | migrations.AlterField(
21 | model_name='match',
22 | name='ended_at',
23 | field=models.DateTimeField(blank=True, null=True),
24 | ),
25 | migrations.AlterField(
26 | model_name='match',
27 | name='wins1',
28 | field=models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0)]),
29 | ),
30 | migrations.AlterField(
31 | model_name='match',
32 | name='wins2',
33 | field=models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0)]),
34 | ),
35 | migrations.AlterField(
36 | model_name='tournamentmatch',
37 | name='match',
38 | field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='tournament_match', to='classic_tetris_project.match'),
39 | ),
40 | ]
41 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0044_auto_20210404_2242.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2021-04-04 22:42
2 |
3 | import colorfield.fields
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('classic_tetris_project', '0043_auto_20210327_2316'),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name='tournament',
17 | name='color',
18 | field=colorfield.fields.ColorField(default='#000000', max_length=18),
19 | ),
20 | migrations.AlterField(
21 | model_name='tournament',
22 | name='short_name',
23 | field=models.CharField(help_text='Used in the context of an event, e.g. "Masters Event"', max_length=64),
24 | ),
25 | migrations.AlterField(
26 | model_name='tournamentplayer',
27 | name='tournament',
28 | field=models.ForeignKey(db_index=False, on_delete=django.db.models.deletion.CASCADE, related_name='tournament_players', to='classic_tetris_project.tournament'),
29 | ),
30 | migrations.AlterField(
31 | model_name='tournamentplayer',
32 | name='user',
33 | field=models.ForeignKey(db_index=False, on_delete=django.db.models.deletion.PROTECT, related_name='tournament_players', to='classic_tetris_project.user'),
34 | ),
35 | migrations.AddConstraint(
36 | model_name='tournamentplayer',
37 | constraint=models.UniqueConstraint(fields=('tournament', 'seed'), name='unique_tournament_seed'),
38 | ),
39 | ]
40 |
--------------------------------------------------------------------------------
/classic_tetris_project/util/fieldgen/ai.py:
--------------------------------------------------------------------------------
1 | import random
2 | from .tiles import TileMath
3 |
4 |
5 | class Aesthetics(object):
6 | # Artificially intelligent aesthetics class
7 | @staticmethod
8 | def is_odd(number):
9 | # artificial intelligence for determining pseudo-random results
10 | return number % 2 == 1
11 |
12 | @staticmethod
13 | def get_target_column(sequence):
14 | # returns an artificially intelligently derived column to target,
15 | # based on a given sequence
16 | CENTER_COLUMN = TileMath.FIELD_WIDTH // 2
17 | left_right = Aesthetics.get_piece_shift_direction(sequence)
18 | return CENTER_COLUMN + len(sequence) * left_right
19 |
20 | @staticmethod
21 | def get_piece_shift_direction(sequence):
22 | # returns -1 or 1 depending on offset from centre column
23 | left_right = -1 if Aesthetics.is_odd(len(sequence)) else 1
24 | return left_right
25 |
26 | @staticmethod
27 | def which_garbage_hole(excluded_column, row):
28 | # given a target column, decides a column for garbage based on
29 | # artificial intelligence and digital aesthetics
30 | # to do: extend using neural networks.
31 | cells = [i for i in range(TileMath.FIELD_WIDTH)]
32 | cells.remove(excluded_column)
33 | return random.choice(cells)
34 |
35 | @staticmethod
36 | def which_garbage_tile(target_column, target_row, choices):
37 | # given a target board position, decides which tile to use,
38 | # using artificial intelligence and digital aesthetics
39 | # TODO: extend using machine learning.
40 | return random.choice(choices)
41 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/assets/js/react/bracket/tournament_bracket_controller.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import useBracketState from './use_bracket_state';
5 | import TournamentBracket from './tournament_bracket';
6 | import BracketControls from './bracket_controls';
7 |
8 |
9 | export const COMPONENT_NAME = 'TournamentBracketController';
10 |
11 | const TournamentBracketController = (props) => {
12 | const { bracketUrl, refreshUrl } = props;
13 | const { state, dispatch } = useBracketState(props);
14 |
15 | const [matches, setMatches] = useState(props.matches);
16 | const [timestamp, setTimestamp] = useState(props.ts);
17 |
18 | useEffect(() => {
19 | let id = null;
20 | if (state.autoRefresh) {
21 | id = window.setInterval(() => {
22 | const url = new URL(refreshUrl);
23 | url.searchParams.set('ts', timestamp);
24 | fetch(url.href)
25 | .then(response => response.json())
26 | .then(data => {
27 | if (data['matches']) {
28 | setMatches(data.matches);
29 | setTimestamp(data.ts);
30 | }
31 | });
32 | }, 60000);
33 | }
34 |
35 | return () => {
36 | if (id !== null) {
37 | window.clearInterval(id);
38 | }
39 | };
40 | }, [state.autoRefresh, timestamp]);
41 |
42 | return (
43 |
44 |
45 | {!state.embed &&
46 |
47 | }
48 |
49 | );
50 | };
51 |
52 |
53 | export default TournamentBracketController;
54 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0021_scorepb.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.4 on 2020-04-07 04:04
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('classic_tetris_project', '0020_auto_20200420_2028'),
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='ScorePB',
16 | fields=[
17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18 | ('score', models.IntegerField(blank=True, null=True)),
19 | ('lines', models.IntegerField(blank=True, null=True)),
20 | ('starting_level', models.IntegerField(blank=True, null=True)),
21 | ('console_type', models.CharField(choices=[('ntsc', 'NTSC'), ('pal', 'PAL')], default='ntsc', max_length=5)),
22 | ('current', models.IntegerField()),
23 | ('created_at', models.DateTimeField(blank=True)),
24 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='score_pbs', related_query_name='score_pb', to='classic_tetris_project.User')),
25 | ],
26 | ),
27 | migrations.AddIndex(
28 | model_name='scorepb',
29 | index=models.Index(condition=models.Q(current=True), fields=['user', 'current'], name='user_current'),
30 | ),
31 | migrations.AddConstraint(
32 | model_name='scorepb',
33 | constraint=models.UniqueConstraint(condition=models.Q(current=True), fields=('user', 'starting_level', 'console_type'), name='unique_when_current'),
34 | ),
35 | ]
36 |
--------------------------------------------------------------------------------
/classic_tetris_project/util/google_sheets.py:
--------------------------------------------------------------------------------
1 | from google.oauth2 import service_account
2 | from googleapiclient import errors
3 | from googleapiclient.discovery import build
4 |
5 | from django.conf import settings
6 |
7 |
8 | class GoogleSheetsError(Exception):
9 | pass
10 |
11 | class GoogleSheetsService:
12 | SCOPES = ["https://www.googleapis.com/auth/spreadsheets"]
13 | SERVICE_ACCOUNT_FILE = settings.ENV("GOOGLE_SERVICE_ACCOUNT_FILE")
14 |
15 | def __init__(self):
16 | if not self.__class__.SERVICE_ACCOUNT_FILE:
17 | raise GoogleSheetsError("Service account not configured")
18 | credentials = service_account.Credentials.from_service_account_file(
19 | self.__class__.SERVICE_ACCOUNT_FILE,
20 | scopes=self.__class__.SCOPES)
21 | self.service = build("sheets", "v4", credentials=credentials)
22 |
23 | def update(self, spreadsheet_id, sheet_range, data):
24 | request = self.service.spreadsheets().values().update(
25 | spreadsheetId=spreadsheet_id,
26 | range=sheet_range,
27 | valueInputOption="RAW",
28 | body={ "values": data },
29 | )
30 | try:
31 | return request.execute()
32 | except errors.HttpError as e:
33 | raise GoogleSheetsError(e.error_details)
34 |
35 | def append(self, spreadsheet_id, sheet_range, data):
36 | request = self.service.spreadsheets().values().append(
37 | spreadsheetId=spreadsheet_id,
38 | range=sheet_range,
39 | valueInputOption="RAW",
40 | body={ "values": data },
41 | )
42 | try:
43 | return request.execute()
44 | except errors.HttpError as e:
45 | raise GoogleSheetsError(e.error_details)
46 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/icons/discord.html:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/classic_tetris_project/commands/playstyle.py:
--------------------------------------------------------------------------------
1 | from .command import Command, CommandException
2 | from ..util import Platform, DocSection
3 |
4 | @Command.register()
5 | class GetPlaystyleCommand(Command):
6 | """
7 | Prints the user's playstyle, or yours if no argument is provided.
8 | """
9 | aliases = ("playstyle", "getplaystyle")
10 | supported_platforms = (Platform.DISCORD, Platform.TWITCH)
11 | usage = "playstyle [username] (default username you)"
12 | section = DocSection.USER
13 |
14 | def execute(self, *username):
15 | username = username[0] if len(username) == 1 else self.context.args_string
16 | platform_user = (self.platform_user_from_username(username) if username
17 | else self.context.platform_user)
18 | user = platform_user.user if platform_user else None
19 |
20 | if user and user.playstyle:
21 | self.send_message("{name}'s playstyle is {style}!".format(
22 | name=self.context.display_name(platform_user),
23 | style=user.get_playstyle_display()
24 | ))
25 | else:
26 | self.send_message("User has not set a playstyle.")
27 |
28 | @Command.register()
29 | class SetPlaystyleCommand(Command):
30 | """
31 | Sets your playstyle to any of the valid options.
32 | """
33 | aliases = ("setplaystyle", "setstyle")
34 | supported_platforms = (Platform.DISCORD, Platform.TWITCH)
35 | usage = "setplaystyle "
36 | section = DocSection.USER
37 |
38 | def execute(self, style):
39 | if self.context.user.set_playstyle(style):
40 | self.send_message(f"Your playstyle is now {self.context.user.get_playstyle_display()}!")
41 | else:
42 | raise CommandException("Invalid playstyle. Valid playstyles: DAS, Hypertap, Hybrid, Roll.")
43 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/tournament_match/report.haml:
--------------------------------------------------------------------------------
1 | - extends "base.html"
2 |
3 | - block title
4 | Report Match
5 |
6 | - block content
7 | %a{href: "{{ match.get_absolute_url }}"} Back to Match
8 |
9 | .section
10 | .section__header
11 | %h2.section__title
12 | Report Match Results
13 |
14 | .section__body
15 | %form.form{method: "post"}
16 | - csrf_token
17 | = form.non_field_errors
18 |
19 | .field-list
20 | - field_list_row "Tournament:"
21 | %a{href: "{{ match.tournament.get_absolute_url }}"}= match.tournament.name
22 |
23 | - field_list_row "Player 1:"
24 | - if match.player1.user
25 | %a{href: "{{ match.player1.user.get_absolute_url }}"}= match.player1.display_name
26 | - else
27 | = match_display.player1_display_name
28 | - field_list_input_row "Games won:" form.wins1
29 |
30 | - field_list_row "Player 2:"
31 | - if match.player2.user
32 | %a{href: "{{ match.player2.user.get_absolute_url }}"}= match.player2.display_name
33 | - else
34 | = match_display.player2_display_name
35 | - field_list_input_row "Games won:" form.wins2
36 |
37 | - field_list_input_row "Twitch channel:" form.channel
38 | - field_list_input_row "VOD:" form.vod
39 | - field_list_input_row "End time:"
40 | = form.ended_at
41 | = form.ended_at.errors
42 | .field-list__hint
43 | Leave blank to set to now
44 |
45 | - field_list_row ""
46 | %input.btn.btn--primary{type: "submit", value: "Submit", data-confirm: "Are you sure this is correct? Once this match is reported, only a moderator will be able to change the results."}
47 |
48 | - block footer
49 | = form.media
50 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0007_game_match.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.2 on 2019-08-12 16:10
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('classic_tetris_project', '0006_auto_20190812_0423'),
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='Match',
16 | fields=[
17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18 | ('wins1', models.IntegerField(default=0)),
19 | ('wins2', models.IntegerField(default=0)),
20 | ('ended_at', models.DateTimeField(null=True)),
21 | ('channel', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='classic_tetris_project.TwitchUser')),
22 | ('player1', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='classic_tetris_project.User')),
23 | ('player2', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='classic_tetris_project.User')),
24 | ],
25 | ),
26 | migrations.CreateModel(
27 | name='Game',
28 | fields=[
29 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
30 | ('losing_score', models.IntegerField()),
31 | ('ended_at', models.DateTimeField(auto_now_add=True)),
32 | ('match', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='classic_tetris_project.Match')),
33 | ('winner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='classic_tetris_project.User')),
34 | ],
35 | ),
36 | ]
37 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/assets/stylesheets/_colors.scss:
--------------------------------------------------------------------------------
1 | $text-black: #000000;
2 | $text-white: #FFFFFF;
3 |
4 | $navbar-text: $text-white;
5 |
6 | $alert-info-bg: #2D9CDB;
7 | $alert-info-text: $text-white;
8 | $alert-light-info-border: #2D9CD8;
9 | $alert-light-info-bg: #BDEFFF;
10 | $alert-light-info-text: #2D9CD8;
11 |
12 | $bg-light-gray: #DDDDDD;
13 |
14 | $border-gray: #BDBDBD;
15 | $border-blue: #2D9CDB;
16 |
17 | $icon-white: #FFFFFF;
18 |
19 | $error-text: #B40000;
20 |
21 | $button-primary-bg: #27AE60;
22 | $button-primary-text: $text-white;
23 | $button-secondary-bg: #2D9CDB;
24 | $button-secondary-text: $text-white;
25 | $button-discord-bg: #7289DA;
26 | $button-discord-text: $text-white;
27 | $button-twitch-bg: #9147FF;
28 | $button-twitch-text: $text-white;
29 | $button-red-bg: #EB5757;
30 | $button-red-text: $text-white;
31 |
32 | $module-bg: #FFFFFF;
33 | $module-header-text: $text-white;
34 |
35 | $qualifier-table-text: $text-white;
36 |
37 | $status-tag-text: $text-white;
38 | $status-tag-green-bg: #00C314;
39 | $status-tag-yellow-bg: #F2C94C;
40 | $status-tag-red-bg: #C30000;
41 | $status-tag-gray-bg: #828282;
42 |
43 | $bracket-default: #b7b7b7;
44 |
45 |
46 | [data-theme='light'], :root {
47 | --bg-primary: #FFFFFF;
48 | --bg-nav: #424242;
49 | --bg-nav-drawer: #333333;
50 | --bg-table-data-even: #F2F2F2;
51 | --bg-table-data-odd: #FFFFFF;
52 | --bg-module-header: #9B51E0;
53 |
54 | --drop-shadow: 0, 0, 0;
55 |
56 | --text-primary: #{$text-black};
57 | --link-primary: #2e8500;
58 | }
59 |
60 | [data-theme='dark'] {
61 | --bg-primary: #0d1117;
62 | --bg-nav: #0e0e0e;
63 | --bg-nav-drawer: #161616;
64 | --bg-table-data-even: #363636;
65 | --bg-table-data-odd: #444444;
66 | --bg-module-header: #3f007a;
67 |
68 | --drop-shadow: 255, 255, 255;
69 |
70 | --text-primary: #{$alert-info-text};
71 | --link-primary: #4bce06;
72 | }
73 |
--------------------------------------------------------------------------------
/classic_tetris_project/commands/preferred_name.py:
--------------------------------------------------------------------------------
1 | from .command import Command, CommandException
2 | from ..util import Platform, DocSection
3 |
4 | @Command.register()
5 | class GetPreferredNameCommand(Command):
6 | """
7 | Prints the specified user's preferred name, or yours if no argument is
8 | provided.
9 | """
10 | aliases = ("name", "getname")
11 | supported_platforms = (Platform.DISCORD, Platform.TWITCH)
12 | usage = "name [username] (default username you)"
13 | section = DocSection.USER
14 |
15 | def execute(self, *username):
16 | username = username[0] if len(username) == 1 else self.context.args_string
17 | platform_user = (self.platform_user_from_username(username) if username
18 | else self.context.platform_user)
19 |
20 | if platform_user and platform_user.user.preferred_name:
21 | self.send_message("{user_tag} goes by {preferred_name}".format(
22 | user_tag=platform_user.user_tag,
23 | preferred_name=platform_user.user.preferred_name
24 | ))
25 | else:
26 | self.send_message("User has not set a preferred name.")
27 |
28 |
29 | @Command.register()
30 | class SetPreferredNameCommand(Command):
31 | """
32 | Sets your preferred name. Can contain letters, numbers, spaces, hyphens,
33 | underscores, and periods.
34 | """
35 | aliases = ("setname",)
36 | supported_platforms = (Platform.DISCORD, Platform.TWITCH)
37 | usage = "setname "
38 | section = DocSection.USER
39 |
40 | def execute(self, *name):
41 | name = " ".join(name)
42 | if self.context.user.set_preferred_name(name):
43 | self.send_message(f"Your preferred name is set to \"{name}\".")
44 |
45 | else:
46 | raise CommandException("Invalid name. Valid characters are "
47 | "letters, numbers, spaces, dashes, underscores, and periods.")
48 |
--------------------------------------------------------------------------------
/classic_tetris_project/migrations/0049_auto_20210417_2149.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2021-04-17 21:49
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('classic_tetris_project', '0048_auto_20210417_2147'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='tournamentmatch',
16 | name='loser',
17 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.RESTRICT, related_name='+', to='classic_tetris_project.tournamentplayer'),
18 | ),
19 | migrations.AlterField(
20 | model_name='tournamentmatch',
21 | name='player1',
22 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.RESTRICT, related_name='+', to='classic_tetris_project.tournamentplayer'),
23 | ),
24 | migrations.AlterField(
25 | model_name='tournamentmatch',
26 | name='player2',
27 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.RESTRICT, related_name='+', to='classic_tetris_project.tournamentplayer'),
28 | ),
29 | migrations.AlterField(
30 | model_name='tournamentmatch',
31 | name='source1_data',
32 | field=models.IntegerField(blank=True, null=True),
33 | ),
34 | migrations.AlterField(
35 | model_name='tournamentmatch',
36 | name='source2_data',
37 | field=models.IntegerField(blank=True, null=True),
38 | ),
39 | migrations.AlterField(
40 | model_name='tournamentmatch',
41 | name='winner',
42 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.RESTRICT, related_name='+', to='classic_tetris_project.tournamentplayer'),
43 | ),
44 | ]
45 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/views/qualifiers.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.contrib import messages
3 | from django.core.exceptions import PermissionDenied
4 | from django.http import Http404
5 | from django.shortcuts import redirect, render, reverse
6 | from furl import furl
7 |
8 | from classic_tetris_project.models import Qualifier
9 | from classic_tetris_project.util import lazy
10 | from .base import BaseView
11 |
12 |
13 | class WithdrawForm(forms.ModelForm):
14 | class Meta:
15 | model = Qualifier
16 | fields = ["withdrawn"]
17 | widgets = {
18 | "withdrawn": forms.HiddenInput(),
19 | }
20 |
21 |
22 | class QualifierView(BaseView):
23 | def get(self, request, id):
24 | return render(request, "qualifiers/show.html", {
25 | "qualifier": self.qualifier,
26 | "can_withdraw": self.can_withdraw(),
27 | "withdraw_form": WithdrawForm(initial={"withdrawn": True}),
28 | })
29 |
30 | # Withdraw qualifier
31 | def post(self, request, id):
32 | if not self.can_withdraw():
33 | raise PermissionDenied
34 | withdraw_form = WithdrawForm(request.POST, instance=self.qualifier)
35 | if withdraw_form.is_valid():
36 | withdraw_form.save()
37 | if withdraw_form.cleaned_data["withdrawn"]:
38 | messages.info(self.request, "Your qualifier has been withdrawn.")
39 | return redirect(reverse("qualifier", args=[self.qualifier.id]))
40 |
41 |
42 | @lazy
43 | def qualifier(self):
44 | try:
45 | return Qualifier.objects.get(id=self.kwargs["id"])
46 | except Qualifier.DoesNotExist:
47 | raise Http404()
48 |
49 | def can_withdraw(self):
50 | return (self.qualifier.user == self.current_user and
51 | self.qualifier.event.withdrawals_allowed and
52 | self.qualifier.submitted and
53 | (not self.qualifier.withdrawn))
54 |
--------------------------------------------------------------------------------
/classic_tetris_project/commands/country.py:
--------------------------------------------------------------------------------
1 | from .command import Command, CommandException
2 | from ..countries import Country
3 | from ..util import Platform, DocSection
4 |
5 | @Command.register()
6 | class GetCountryCommand(Command):
7 | """
8 | Outputs the country of the specified user, or yourself if no argument is
9 | provided.
10 | """
11 | aliases = ("country", "getcountry")
12 | supported_platforms = (Platform.DISCORD, Platform.TWITCH)
13 | usage = "country [username] (default username you)"
14 | section = DocSection.USER
15 |
16 | def execute(self, *username):
17 | username = username[0] if len(username) == 1 else self.context.args_string
18 | platform_user = (self.platform_user_from_username(username) if username
19 | else self.context.platform_user)
20 | user = platform_user.user if platform_user else None
21 |
22 | if user and user.country:
23 | self.send_message("{name} is from {country}!".format(
24 | name=self.context.display_name(platform_user),
25 | country=Country.get_country(user.country).full_name
26 | ))
27 | else:
28 | self.send_message("User has not set a country.")
29 |
30 | @Command.register()
31 | class SetCountryCommand(Command):
32 | """
33 | Sets your country in the database. You can find a list of the three-letter
34 | codes [here](https://www.iban.com/country-codes) under the "Alpha-3 codes"
35 | column.
36 | """
37 | aliases = ("setcountry",)
38 | supported_platforms = (Platform.DISCORD, Platform.TWITCH)
39 | usage = "setcountry "
40 | section = DocSection.USER
41 |
42 | def execute(self, country):
43 | if not self.context.user.set_country(country):
44 | raise CommandException("Invalid country.")
45 | else:
46 | country = Country.get_country(self.context.user.country)
47 | self.send_message(f"Your country is now {country.full_name}!")
48 |
--------------------------------------------------------------------------------
/classic_tetris_project/test_helper/discord.py:
--------------------------------------------------------------------------------
1 | import factory
2 |
3 | from classic_tetris_project.commands.command_context import DiscordCommandContext
4 | from .factories import DiscordUserFactory
5 |
6 |
7 | class MockDiscordGuild:
8 | def __init__(self, members):
9 | self.members = members
10 |
11 |
12 | class MockDiscordChannel:
13 | def __init__(self, name="mock"):
14 | self.sent_messages = []
15 | self.name = name
16 |
17 | async def send(self, message):
18 | self.sent_messages.append(message)
19 |
20 | def poll(self):
21 | # Convenience method for taking the latest messages sent since the last poll
22 | messages = self.sent_messages
23 | self.sent_messages = []
24 | return messages
25 |
26 |
27 | class MockDiscordAPIUser:
28 | def __init__(self, id, name, discriminator, display_name=None):
29 | self.id = id
30 | self.name = name
31 | self.discriminator = discriminator
32 | self.display_name = display_name or name
33 |
34 | def send(self, channel, content):
35 | message = MockDiscordMessage(self, content, channel)
36 | context = DiscordCommandContext(message)
37 | context.dispatch()
38 |
39 | def create_discord_user(self):
40 | return DiscordUserFactory(discord_id=self.id,
41 | username=self.name)
42 |
43 | @staticmethod
44 | def create(*args, **kwargs):
45 | return MockDiscordAPIUserFactory(*args, **kwargs)
46 |
47 | class MockDiscordAPIUserFactory(factory.Factory):
48 | class Meta:
49 | model = MockDiscordAPIUser
50 | id = factory.Sequence(lambda n: n)
51 | name = factory.Sequence(lambda n: f"Mock Discord User {n}")
52 | discriminator = factory.Sequence(lambda n: f"{n:04}")
53 |
54 |
55 | class MockDiscordMessage:
56 | def __init__(self, author, content, channel, guild=None):
57 | self.author = author
58 | self.content = content
59 | self.channel = channel
60 | self.guild = guild
61 |
--------------------------------------------------------------------------------
/add_tournaments.py:
--------------------------------------------------------------------------------
1 | from classic_tetris_project.models import *
2 |
3 | def add_tournaments(event):
4 | for i, tournament_data in enumerate([
5 | {
6 | "name": "Masters Event",
7 | "color": "#37761D",
8 | "size": 16,
9 | "placeholders": {"16":"CC Winner"},
10 | },
11 | {
12 | "name": "Challengers Circuit",
13 | "color": "#E69138",
14 | "size": 16,
15 | "placeholders": {},
16 | },
17 | {
18 | "name": "Futures Circuit",
19 | "color": "#741C47",
20 | "size": 32,
21 | "placeholders": {},
22 | },
23 | {
24 | "name": "Community T1",
25 | "color": "#6FA8DC",
26 | "size": 32,
27 | "placeholders": {},
28 | },
29 | {
30 | "name": "Community T2",
31 | "color": "#3E85C6",
32 | "size": 32,
33 | "placeholders": {},
34 | },
35 | {
36 | "name": "Community T3",
37 | "color": "#0C5394",
38 | "size": 32,
39 | "placeholders": {},
40 | },
41 | {
42 | "name": "Community T4",
43 | "color": "#F0C233",
44 | "size": 32,
45 | "placeholders": {},
46 | },
47 | {
48 | "name": "Community T5",
49 | "color": "#FFE599",
50 | "size": 32,
51 | "placeholders": {},
52 | },
53 | ]):
54 | Tournament.objects.create(
55 | event=event,
56 | short_name=tournament_data["name"],
57 | order=i,
58 | seed_count=tournament_data["size"],
59 | placeholders=tournament_data["placeholders"],
60 | color=tournament_data["color"],
61 | )
62 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/views/live_notifications.py:
--------------------------------------------------------------------------------
1 | from django.contrib import messages
2 | from django.contrib.auth.mixins import PermissionRequiredMixin
3 | from django.shortcuts import render
4 | from datetime import datetime
5 |
6 | from classic_tetris_project.facades.user_permissions import UserPermissions
7 | from classic_tetris_project.util import lazy
8 | from ..forms.live_notification import LiveNotificationForm
9 | from .base import BaseView
10 |
11 | import firebase_admin
12 | from firebase_admin import credentials
13 | from firebase_admin import firestore
14 |
15 | cred = credentials.Certificate('data/firebase_credentials.json')
16 | app = firebase_admin.initialize_app(cred)
17 | db = firestore.client()
18 |
19 | class LiveNotificationsView(PermissionRequiredMixin, BaseView):
20 | permission_required = UserPermissions.SEND_LIVE_NOTIFICATIONS
21 |
22 | class LiveView(LiveNotificationsView):
23 | def get(self, request):
24 | return render(request, "live_notifications/live.haml")
25 |
26 |
27 | class SubmitView(LiveNotificationsView):
28 | def get(self, request):
29 | return render(request, "live_notifications/submit.haml", {
30 | "live_notification_form": LiveNotificationForm(),
31 | })
32 |
33 | def post(self, request):
34 | live_notification_form = LiveNotificationForm(request.POST)
35 | if live_notification_form.is_valid():
36 | doc_ref = db.collection(u'notifications').document(u'current')
37 | doc_ref.set({
38 | u'message': live_notification_form.cleaned_data.get("message"),
39 | u'duration': live_notification_form.cleaned_data.get("duration"),
40 | u'position': live_notification_form.cleaned_data.get("position"),
41 | u'created_at': datetime.now()
42 | })
43 | messages.info(self.request, "Message Submitted")
44 |
45 | return render(request, "live_notifications/submit.haml", {
46 | "live_notification_form": LiveNotificationForm(),
47 | })
48 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/tests/views/profile.py:
--------------------------------------------------------------------------------
1 | from classic_tetris_project.test_helper import *
2 |
3 |
4 | class ProfileView_(Spec):
5 | url = "/profile/"
6 | def test_redirects_when_logged_in(self):
7 | self.sign_in()
8 | response = self.get()
9 |
10 | assert_that(response, redirects_to(f"/user/{self.current_user.id}/"))
11 |
12 | def test_redirects_when_logged_out(self):
13 | response = self.get()
14 |
15 | assert_that(response, redirects_to(f"/oauth/login/?next=/profile/"))
16 |
17 |
18 | class ProfileEditView_(Spec):
19 | url = "/profile/edit/"
20 | class GET:
21 | def test_redirects_when_logged_out(self):
22 | response = self.get()
23 |
24 | assert_that(response, redirects_to(f"/oauth/login/?next=/profile/edit/"))
25 |
26 | def test_renders(self):
27 | self.sign_in()
28 | response = self.get()
29 |
30 | assert_that(response.status_code, equal_to(200))
31 | assert_that(response, uses_template("profile/edit.haml"))
32 |
33 | class POST:
34 | def test_redirects_when_logged_out(self):
35 | response = self.post()
36 |
37 | assert_that(response, redirects_to(f"/oauth/login/?next=/profile/edit/"))
38 |
39 | def test_updates_and_redirects(self):
40 | self.sign_in()
41 | response = self.post({
42 | "preferred_name": "Preferred Name",
43 | "pronouns": "he",
44 | "country": "us",
45 | "playstyle": "das",
46 | "id": -1,
47 | })
48 |
49 | assert_that(response, redirects_to(f"/user/{self.current_user.id}/"))
50 | self.current_user.refresh_from_db()
51 | assert_that(self.current_user, has_properties(
52 | preferred_name=equal_to("Preferred Name"),
53 | pronouns=equal_to("he"),
54 | country=equal_to("us"),
55 | playstyle=equal_to("das"),
56 | id=not_(equal_to(-1))
57 | ))
58 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "classic_tetris_project_django",
3 | "version": "1.0.0",
4 | "description": "This repository is a new and improved bot for Classic Tetris Monthly that is designed to be accessible to the whole community, both in and out of CTM contexts. It is currently being run on a cloud computing instance rather than a home PC, and as such its uptime is drastically improved from the strikingly mediocre [former bot](https://github.com/professor-l/lsq-bot).",
5 | "main": "index.js",
6 | "directories": {
7 | "doc": "docs",
8 | "test": "tests"
9 | },
10 | "scripts": {
11 | "test": "echo \"Error: no test specified\" && exit 1",
12 | "start": "webpack --config webpack.dev.js",
13 | "build": "webpack --config webpack.prod.js"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/professor-l/classic-tetris-project.git"
18 | },
19 | "keywords": [],
20 | "author": "",
21 | "license": "ISC",
22 | "bugs": {
23 | "url": "https://github.com/professor-l/classic-tetris-project/issues"
24 | },
25 | "homepage": "https://github.com/professor-l/classic-tetris-project#readme",
26 | "dependencies": {
27 | "@babel/core": "^7.16.5",
28 | "@babel/preset-env": "^7.16.5",
29 | "@babel/preset-react": "^7.16.7",
30 | "@hotwired/stimulus": "^3.0.1",
31 | "@hotwired/stimulus-webpack-helpers": "^1.0.1",
32 | "babel-loader": "^8.2.3",
33 | "bootstrap": "^4.6.0",
34 | "css-loader": "^3.6.0",
35 | "expose-loader": "^1.0.3",
36 | "jquery": "^3.5.1",
37 | "jquery-ujs": "^1.2.2",
38 | "luxon": "^2.2.0",
39 | "mini-css-extract-plugin": "^0.9.0",
40 | "popper.js": "^1.16.1",
41 | "prop-types": "^15.8.1",
42 | "react": "^18.0.0",
43 | "react-dom": "^18.0.0",
44 | "sass": "^1.49.9",
45 | "sass-loader": "^8.0.2",
46 | "simplepicker": "^2.0.4",
47 | "webpack": "^4.43.0",
48 | "webpack-bundle-tracker": "^0.4.3",
49 | "webpack-cli": "^3.3.11",
50 | "webpack-merge": "^4.2.2"
51 | },
52 | "devDependencies": {
53 | "file-loader": "^6.2.0"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/webpack.common.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const webpack = require('webpack')
3 | const BundleTracker = require('webpack-bundle-tracker')
4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
5 |
6 | const devMode = process.env.NODE_ENV !== 'production';
7 |
8 | module.exports = {
9 | context: __dirname,
10 |
11 | entry: {
12 | main: './classic_tetris_project/web/assets/app',
13 | admin: './classic_tetris_project/web/assets/admin',
14 | },
15 |
16 | output: {
17 | path: path.resolve('./static/bundles/'),
18 | filename: '[name]-[hash].js',
19 | },
20 |
21 | plugins: [
22 | new BundleTracker({
23 | path: __dirname,
24 | filename: './webpack-stats.json',
25 | }),
26 | new MiniCssExtractPlugin({
27 | filename: '[name]-[hash].css',
28 | }),
29 | new webpack.HotModuleReplacementPlugin(),
30 |
31 | new webpack.ProvidePlugin({
32 | $: "jquery",
33 | jQuery: "jquery"
34 | }),
35 | ],
36 |
37 | module: {
38 | rules: [
39 | { test: /\.scss/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader' ], },
40 | {
41 | test: require.resolve('jquery'),
42 | loader: 'expose-loader',
43 | options: {
44 | exposes: ['$', 'jQuery'],
45 | },
46 | },
47 | {
48 | test: /\.m?jsx?$/,
49 | exclude: /node_modules/,
50 | use: {
51 | loader: 'babel-loader',
52 | options: {
53 | presets: [
54 | ['@babel/preset-env', { targets: "defaults" }],
55 | '@babel/preset-react'
56 | ]
57 | }
58 | }
59 | },
60 | {
61 | test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
62 | use: [
63 | {
64 | loader: 'file-loader',
65 | options: {
66 | name: '[name].[ext]',
67 | outputPath: 'fonts/'
68 | }
69 | }
70 | ]
71 | }
72 | ],
73 | },
74 |
75 | resolve: {
76 | modules: ['node_modules'],
77 | extensions: ['.js', '.jsx']
78 | },
79 | }
80 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/assets/stylesheets/_buttons.scss:
--------------------------------------------------------------------------------
1 | @use 'colors';
2 |
3 | .btn {
4 | display: inline-flex;
5 | box-sizing: border-box;
6 | height: 40px;
7 | padding: 10px 20px;
8 | justify-content: center;
9 | font-size: 16px;
10 | line-height: 20px;
11 | border: none;
12 | outline: none;
13 | text-decoration: none;
14 | cursor: pointer;
15 |
16 | // a {
17 | // color: var(--link-primary);
18 | // }
19 |
20 | &:hover {
21 | border: none;
22 | box-shadow: 0px 3px 10px rgba(0, 0, 0, 0.4);
23 | }
24 |
25 | &:active {
26 | border: none;
27 | box-shadow: none;
28 | }
29 |
30 | &--primary {
31 | background-color: colors.$button-primary-bg;
32 | color: colors.$button-primary-text;
33 | &:hover {
34 | background-color: lighten(colors.$button-primary-bg, 5%);
35 | }
36 | }
37 |
38 | &--secondary {
39 | background-color: colors.$button-secondary-bg;
40 | color: colors.$button-secondary-text;
41 | &:hover {
42 | background-color: lighten(colors.$button-secondary-bg, 5%);
43 | }
44 | }
45 |
46 | &--red {
47 | background-color: colors.$button-red-bg;
48 | color: colors.$button-red-text;
49 | &:hover {
50 | background-color: lighten(colors.$button-red-bg, 5%);
51 | }
52 | }
53 |
54 | &--link {
55 | &:hover {
56 | box-shadow: none;
57 | }
58 | }
59 |
60 | &--full {
61 | display: flex;
62 | width: 100%;
63 | }
64 |
65 | &--discord {
66 | background-color: colors.$button-discord-bg;
67 | color: colors.$button-discord-text;
68 | &:hover {
69 | background-color: lighten(colors.$button-discord-bg, 5%);
70 | }
71 | }
72 |
73 | &--twitch {
74 | background-color: colors.$button-twitch-bg;
75 | color: colors.$button-twitch-text;
76 | &:hover {
77 | background-color: lighten(colors.$button-twitch-bg, 5%);
78 | }
79 | }
80 |
81 | &__icon {
82 | width: 30px;
83 | height: 30px;
84 | margin-top: -5px;
85 | margin-right: 10px;
86 | }
87 | }
88 |
89 |
90 | .login-buttons {
91 | max-width: 400px;
92 | margin: auto;
93 | }
94 |
--------------------------------------------------------------------------------
/classic_tetris_project/models/scores.py:
--------------------------------------------------------------------------------
1 | from django.db import models, transaction
2 | from django.utils import timezone
3 |
4 | from .users import User
5 |
6 | class ScorePB(models.Model):
7 | class Meta:
8 | indexes = [
9 | models.Index(fields=["user", "current"], condition=models.Q(current=True),
10 | name="user_current"),
11 | ]
12 | constraints = [
13 | models.UniqueConstraint(fields=["user", "starting_level", "console_type"],
14 | condition=models.Q(current=True),
15 | name="unique_when_current"),
16 | ]
17 |
18 | TYPE_CHOICES = [
19 | ("ntsc", "NTSC"),
20 | ("pal", "PAL"),
21 | ]
22 |
23 | user = models.ForeignKey(User, on_delete=models.CASCADE,
24 | related_name="score_pbs", related_query_name="score_pb")
25 |
26 | score = models.IntegerField(null=True, blank=True)
27 | lines = models.IntegerField(null=True, blank=True)
28 | starting_level = models.IntegerField(null=True, blank=True)
29 | console_type = models.CharField(max_length=5, choices=TYPE_CHOICES, default="ntsc", null=False)
30 | current = models.IntegerField(null=False)
31 | created_at = models.DateTimeField(null=False, blank=True)
32 |
33 |
34 | @staticmethod
35 | @transaction.atomic
36 | def log(user, score, starting_level, console_type="ntsc", **params):
37 | ScorePB.objects.filter(user=user, starting_level=starting_level, console_type=console_type,
38 | current=True).update(current=False)
39 | return ScorePB.objects.create(user=user, score=score, starting_level=starting_level,
40 | console_type=console_type, current=True,
41 | **params)
42 |
43 | @staticmethod
44 | def before_save(sender, instance, **kwargs):
45 | if instance.id is None:
46 | instance.created_at = instance.created_at or timezone.now()
47 |
48 | models.signals.pre_save.connect(ScorePB.before_save, sender=ScorePB)
49 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/oauth.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | from authlib.integrations.django_client import OAuth
3 | from django.core.cache import cache
4 |
5 | from ..env import env
6 |
7 | oauth = OAuth()
8 | oauth.register(
9 | name="discord",
10 | client_id=env("DISCORD_CLIENT_ID"),
11 | client_secret=env("DISCORD_CLIENT_SECRET"),
12 | access_token_url="https://discordapp.com/api/oauth2/token",
13 | authorize_url="https://discordapp.com/api/oauth2/authorize",
14 | api_base_url="https://discordapp.com/api/v6/",
15 | client_kwargs={"scope": "identify"}
16 | )
17 | oauth.register(
18 | name="twitch",
19 | client_id=env("TWITCH_CLIENT_ID"),
20 | client_secret=env("TWITCH_CLIENT_SECRET"),
21 | access_token_url="https://id.twitch.tv/oauth2/token",
22 | authorize_params={"force_verify": "true"},
23 | authorize_url="https://id.twitch.tv/oauth2/authorize",
24 | access_token_params={"client_id": env("TWITCH_CLIENT_ID"),
25 | "client_secret": env("TWITCH_CLIENT_SECRET")},
26 | client_kwargs={"scope": ""}
27 | )
28 |
29 | discord = oauth.discord
30 | twitch = oauth.twitch
31 |
32 | class State:
33 | CACHE_TIMEOUT = 12 * 60 * 60 # 12 hours
34 | ALLOWED_KEYS = ["next", "merge_accounts"]
35 |
36 | def __init__(self, params={}, state_id=None):
37 | self.state_id = state_id or uuid.uuid4().hex
38 | self.params = { k: v for k, v in params.items() if k in State.ALLOWED_KEYS }
39 |
40 | def __getitem__(self, key):
41 | return self.params.get(key)
42 |
43 | def store(self):
44 | cache.set(f"oauth_states.{self.state_id}", self.params, timeout=State.CACHE_TIMEOUT)
45 | return self.state_id
46 |
47 | def expire(self):
48 | cache.delete(f"oauth_state.{self.state_id}")
49 |
50 | @staticmethod
51 | def from_request(request):
52 | return State(request.GET)
53 |
54 | @staticmethod
55 | def retrieve(state_id):
56 | params = cache.get(f"oauth_states.{state_id}")
57 | if params is None:
58 | return None
59 | else:
60 | return State(params, state_id)
61 |
--------------------------------------------------------------------------------
/classic_tetris_project/commands/countdown.py:
--------------------------------------------------------------------------------
1 | import time
2 | from django.core.cache import cache
3 | from random import randint
4 |
5 | from .command import Command, CommandException
6 | from ..util import Platform, DocSection
7 |
8 | MIN_COUNTDOWN = 3
9 | MAX_COUNTDOWN = 10
10 |
11 | @Command.register()
12 | class Countdown(Command):
13 | """
14 | Counts down from 3 before saying "Tetris!" in the chat. Works for any
15 | number from 3-10.
16 |
17 | **Note:** If the bot is not a moderator in your Twitch channel, it will not
18 | be able to say more than one message per second, and this restriction
19 | (which is built into Twitch) will interfere with countdowns. You can make
20 | the bot a moderator by typing `/mod @ClassicTetrisBot` after you have
21 | `!summon`ed it.
22 | """
23 | aliases = tuple(str(i) for i in range(MIN_COUNTDOWN, MAX_COUNTDOWN+1))
24 | supported_platforms = (Platform.TWITCH,)
25 | usage = "n (3 <= n <= 10)"
26 | notes = ("Moderator-only",)
27 | section = DocSection.OTHER
28 |
29 | def __init__(self, context):
30 | super().__init__(context)
31 | self.usage = self.context.command_name
32 |
33 | def execute(self):
34 | self.check_public()
35 | self.check_moderator()
36 |
37 | n = int(self.context.command_name)
38 | self.check_validity(n)
39 |
40 | for i in range(n, 0, -1):
41 | self.send_message(str(i))
42 | time.sleep(1)
43 |
44 | s = "Tetris!"
45 | i = randint(1, 1000)
46 | if i == 42:
47 | s = "Tortoise!"
48 | elif i == 43:
49 | s = "Texas!"
50 | elif i == 44:
51 | s = "Tetris™!"
52 | elif i == 45:
53 | s = "I'm contacting you regarding your car's extended warranty."
54 | self.send_message(s)
55 |
56 | def check_validity(self, n):
57 | channel = self.context.channel.name
58 | if cache.get(f"countdown_in_progress.{channel}") is not None:
59 | raise CommandException()
60 | else:
61 | cache.set(f"countdown_in_progress.{channel}", True, timeout=(n+1))
62 |
63 |
--------------------------------------------------------------------------------
/export.py:
--------------------------------------------------------------------------------
1 | from classic_tetris_project import discord
2 | from classic_tetris_project.env import env
3 |
4 | @discord.client.event
5 | async def on_ready():
6 | import csv
7 | from tqdm import tqdm
8 |
9 | from classic_tetris_project import discord
10 | from classic_tetris_project.countries import countries
11 | from classic_tetris_project.models import User
12 |
13 | with open('pbs.csv', 'w') as csvfile:
14 | writer = csv.DictWriter(csvfile, fieldnames=['twitch_id', 'twitch_username',
15 | 'discord_id', 'discord_username',
16 | 'ntsc_pb', 'ntsc_pb_updated_at',
17 | 'pal_pb', 'pal_pb_updated_at',
18 | 'country_code', 'country'])
19 | writer.writeheader()
20 | for user in tqdm(User.objects.all()):
21 | if user.ntsc_pb or user.pal_pb:
22 | d = {
23 | 'ntsc_pb': user.ntsc_pb,
24 | 'ntsc_pb_updated_at': (user.ntsc_pb_updated_at.isoformat()
25 | if user.ntsc_pb_updated_at else None),
26 | 'pal_pb': user.pal_pb,
27 | 'pal_pb_updated_at': (user.pal_pb_updated_at.isoformat()
28 | if user.pal_pb_updated_at else None),
29 | 'country_code': user.country,
30 | 'country': (countries[user.country] if user.country else None),
31 | }
32 | if hasattr(user, 'twitch_user'):
33 | d['twitch_id'] = user.twitch_user.twitch_id
34 | d['twitch_username'] = user.twitch_user.username
35 | if hasattr(user, 'discord_user'):
36 | d['discord_id'] = user.discord_user.discord_id
37 | if user.discord_user.user_obj:
38 | d['discord_username'] = user.discord_user.username
39 | writer.writerow(d)
40 | await discord.client.logout()
41 |
42 | discord.client.run(env("DISCORD_TOKEN"))
43 |
--------------------------------------------------------------------------------
/classic_tetris_project/web/templates/review_qualifiers/review.haml:
--------------------------------------------------------------------------------
1 | - extends "base.html"
2 |
3 | - block title
4 | Review Qualifier
5 |
6 | - block content
7 | %form.form{method: "post"}
8 | - csrf_token
9 |
10 | .section
11 | .section__header
12 | %h2.section__title
13 | Review Qualifier
14 |
15 | .section__body
16 | .field-list
17 | - field_list_row "Event:"
18 | %a{href: "{{ qualifier.event.get_absolute_url }}"}= qualifier.event.name
19 | - field_list_row "User:"
20 | %a{href: "{{ qualifier.user.get_absolute_url }}"}= qualifier.user.display_name
21 |
22 | - for field in edit_form.edit_fields
23 | - field_list_input_row field.label_tag
24 | = field
25 | = field.errors
26 |
27 | - field_list_row "Total score:"
28 | = qualifier.type.format_score
29 | - field_list_row "VOD:"
30 | - if qualifier.vod
31 | %a{href: "{{ qualifier.vod }}"}= qualifier.vod
32 | - field_list_row "Auth word:"
33 | = qualifier.auth_word
34 | - field_list_row "Details:"
35 | = qualifier.details
36 | - field_list_row "Started at:"
37 | = qualifier.created_at
38 | - field_list_row "Submitted at:"
39 | = qualifier.submitted_at
40 | %a{href: "{% url 'admin:classic_tetris_project_qualifier_change' qualifier.id %}"}
41 | View/edit in admin
42 |
43 | - module "Reviewer Feedback"
44 | .form.form--narrow
45 | - for check in review_form.checks
46 | .form__field
47 | = check
48 | = check.label_tag
49 | = check.errors
50 |
51 | .form__field
52 | %label{for: "{{ review_form.notes.id_for_label }}"}
53 | Notes for the player:
54 | = review_form.notes
55 | = review_form.notes.errors
56 |
57 | .form__field
58 | - for radio in review_form.approved
59 | %div= radio
60 | = review_form.approved.errors
61 |
62 | .form__actions
63 | %input.btn.btn--primary{type: "submit", value: "Submit"}
64 |
--------------------------------------------------------------------------------