consolidated objects into one benchcoach app

created benchcoachproject app to contain all the settings and stuff
some other changes that got grouped with this commit is the adding the ApiObject to the TeamSnap object model
This commit is contained in:
2021-12-21 17:14:52 -06:00
parent e9f91126e5
commit 95697ef4fe
174 changed files with 5351 additions and 16551 deletions

View File

@@ -1,6 +1,11 @@
from django.contrib import admin
from .models import Profile, TeamsnapSettings
from .models import Event, Availability, Positioning, Team, Venue, Player, StatLine
# Register your models here.
admin.site.register(Profile)
admin.site.register(TeamsnapSettings)
admin.site.register(Event)
admin.site.register(Availability)
admin.site.register(Positioning)
admin.site.register(Team)
admin.site.register(Venue)
admin.site.register(Player)
admin.site.register(StatLine)

6
benchcoach/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class BenchcoachConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'benchcoach'

View File

@@ -1,16 +0,0 @@
"""
ASGI config for events project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'events.settings')
application = get_asgi_application()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

60
benchcoach/forms.py Normal file
View File

@@ -0,0 +1,60 @@
from django import forms
from .models import Event, Positioning, Team, Venue, Player
from teamsnap.models import Event as TeamsnapEvent
from django.forms import modelformset_factory
class EventForm(forms.ModelForm):
class Meta:
model = Event
fields = ['start', 'home_team', 'away_team', 'venue']
class PositioningForm(forms.ModelForm):
availability = None
class Meta:
model = Positioning
widgets = {
'position': forms.Select(attrs={'class': 'form-control form-control-sm'})
}
exclude = ()
PositioningFormSet = modelformset_factory(
model=Positioning,
form=PositioningForm,
# fields=['order', 'position','player'],
# min_num=9,
extra=0
)
class TeamsnapEventForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(TeamsnapEventForm, self).__init__(*args, **kwargs)
if self.instance and self.instance.teamsnap_event.first():
initial = (self.instance.teamsnap_event.first().id, self.instance.teamsnap_event.first())
else:
initial = None
self.fields = {}
choices = [("","-----")]
choices += [(choice.id, choice) for choice in TeamsnapEvent.objects.all()]
self.fields['teamsnap event'] = forms.MultipleChoiceField(
widget=forms.Select(attrs={'class': 'form-control'}),
choices=choices,
initial=initial
)
class Meta:
model = Event
fields = ['start', 'home_team', 'away_team', 'venue']
class PlayerForm(forms.ModelForm):
class Meta:
model = Player
fields = ['first_name', 'last_name', 'jersey_number', 'team']
class TeamForm(forms.ModelForm):
class Meta:
model = Team
fields = ['name']
class VenueForm(forms.ModelForm):
class Meta:
model = Venue
fields = ['name']

View File

@@ -1,6 +1,8 @@
# Generated by Django 3.2.6 on 2021-12-12 23:36
# Generated by Django 3.2.6 on 2021-12-17 21:33
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
@@ -12,13 +14,89 @@ class Migration(migrations.Migration):
operations = [
migrations.CreateModel(
name='User',
name='Player',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_name', models.CharField(max_length=50, null=True)),
('last_name', models.CharField(max_length=50, null=True)),
('email', models.EmailField(max_length=254)),
('teamsnap_access_token', models.CharField(max_length=50, null=True)),
('first_name', models.CharField(max_length=200)),
('last_name', models.CharField(max_length=200)),
('jersey_number', models.IntegerField()),
],
),
migrations.CreateModel(
name='Season',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
],
),
migrations.CreateModel(
name='Team',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
('image', models.FileField(null=True, upload_to='images/', validators=[django.core.validators.FileExtensionValidator(['jpg', 'png', 'svg'])])),
],
),
migrations.CreateModel(
name='Venue',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
],
),
migrations.CreateModel(
name='StatLine',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('batting_avg', models.DecimalField(decimal_places=3, default=0, max_digits=4)),
('onbase_pct', models.DecimalField(decimal_places=3, default=0, max_digits=4)),
('slugging_pct', models.DecimalField(decimal_places=3, default=0, max_digits=4)),
('player', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='benchcoach.player')),
],
),
migrations.AddField(
model_name='player',
name='team',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='benchcoach.team'),
),
migrations.CreateModel(
name='Event',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('start', models.DateTimeField(null=True)),
('away_team', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='away_team', to='benchcoach.team')),
('home_team', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='home_team', to='benchcoach.team')),
('venue', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='benchcoach.venue')),
],
),
migrations.CreateModel(
name='Positioning',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('position', models.CharField(blank=True, choices=[('EH', 'EH'), ('P', 'P'), ('C', 'C'), ('1B', '1B'), ('2B', '2B'), ('3B', '3B'), ('SS', 'SS'), ('LF', 'LF'), ('CF', 'CF'), ('RF', 'RF'), ('DH', 'DH')], default=None, max_length=2, null=True)),
('order', models.PositiveSmallIntegerField(blank=True, default=None, null=True)),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='benchcoach.event')),
('player', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='benchcoach.player')),
],
options={
'unique_together': {('player', 'event')},
},
),
migrations.AlterUniqueTogether(
name='player',
unique_together={('first_name', 'last_name')},
),
migrations.CreateModel(
name='Availability',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('available', models.IntegerField(choices=[(2, 'Yes'), (0, 'No'), (1, 'Maybe'), (-1, 'Unknown')], default=-1)),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='benchcoach.event')),
('player', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='benchcoach.player')),
],
options={
'verbose_name_plural': 'availabilities',
'unique_together': {('event', 'player')},
},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.6 on 2021-12-19 01:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('benchcoach', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='player',
name='jersey_number',
field=models.IntegerField(null=True),
),
]

View File

@@ -1,20 +0,0 @@
# Generated by Django 3.2.6 on 2021-12-12 23:37
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('teamsnap', '0016_auto_20211212_2240'),
('benchcoach', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='user',
name='teamsnap_user',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='teamsnap.user'),
),
]

View File

@@ -1,29 +0,0 @@
# Generated by Django 3.2.6 on 2021-12-13 00:58
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('teamsnap', '0018_user_managed_teams'),
('benchcoach', '0002_user_teamsnap_user'),
]
operations = [
migrations.CreateModel(
name='Profile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('teamsnap_access_token', models.CharField(max_length=50, null=True)),
('teamsnap_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='teamsnap.user')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.DeleteModel(
name='User',
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 3.2.6 on 2021-12-13 01:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('benchcoach', '0003_auto_20211213_0058'),
]
operations = [
migrations.AddField(
model_name='profile',
name='avatar',
field=models.ImageField(blank=True, null=True, upload_to='benchcoach/static/user_files/'),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 3.2.6 on 2021-12-13 01:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('benchcoach', '0004_profile_avatar'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='avatar',
field=models.ImageField(blank=True, null=True, upload_to='avatars'),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 3.2.6 on 2021-12-13 01:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('benchcoach', '0005_alter_profile_avatar'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='avatar',
field=models.ImageField(blank=True, null=True, upload_to='avatar'),
),
]

View File

@@ -1,23 +0,0 @@
# Generated by Django 3.2.6 on 2021-12-16 18:53
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('teamsnap', '0019_auto_20211216_1851'),
('benchcoach', '0006_alter_profile_avatar'),
]
operations = [
migrations.CreateModel(
name='TeamsnapSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('managed_team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='teamsnap.team')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='benchcoach.profile')),
],
),
]

View File

@@ -1,20 +0,0 @@
# Generated by Django 3.2.6 on 2021-12-16 18:56
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('teamsnap', '0019_auto_20211216_1851'),
('benchcoach', '0007_teamsnapsettings'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='teamsnap_user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='teamsnap.user'),
),
]

View File

@@ -1,18 +1,128 @@
from django.db import models
from teamsnap.models import User as TeamsnapUser, Team as TeamsnapTeam
from django.contrib.auth.models import User
from django.core.validators import FileExtensionValidator
def user_directory_path(instance, filename):
# file will be uploaded to MEDIA_ROOT / user_<id>/<filename>
return 'benchcoach/static/user_files/user_{0}/{1}'.format(instance.user.id, filename)
class Team(models.Model):
name = models.CharField(max_length=50)
image = models.FileField(
upload_to="images/",
validators=[FileExtensionValidator(["jpg", "png", "svg"])],
null=True,
)
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
teamsnap_access_token = models.CharField(null=True, max_length=50)
teamsnap_user = models.ForeignKey(TeamsnapUser, on_delete=models.CASCADE, null=True, blank=True)
avatar = models.ImageField(upload_to="avatar", null=True, blank=True)
def __str__(self):
return f"{self.name}"
class TeamsnapSettings(models.Model):
user = models.OneToOneField(Profile, on_delete=models.CASCADE)
managed_team = models.ForeignKey(TeamsnapTeam, on_delete=models.CASCADE)
class Venue(models.Model):
name = models.CharField(max_length=50)
def __str__(self):
return f"{self.name}"
class Event(models.Model):
start = models.DateTimeField(null=True)
venue = models.ForeignKey(Venue, null=True, on_delete=models.CASCADE)
home_team = models.ForeignKey(
Team, null=True, on_delete=models.CASCADE, related_name="home_team"
)
away_team = models.ForeignKey(
Team, null=True, on_delete=models.CASCADE, related_name="away_team"
)
def __str__(self):
return f"{self.start:%Y-%m-%d %H:%M}"
class Season(models.Model):
name = models.CharField(max_length=50)
class Player(models.Model):
first_name = models.CharField(max_length=200)
last_name = models.CharField(max_length=200)
jersey_number = models.IntegerField(null=True)
team = models.ForeignKey(Team, null=True, on_delete=models.CASCADE)
def __str__(self):
return f"{self.last_name}, {self.first_name}"
class Meta:
unique_together = (
"first_name",
"last_name",
)
class StatLine(models.Model):
player = models.ForeignKey(Player, on_delete=models.CASCADE)
batting_avg = models.DecimalField(max_digits=4, decimal_places=3, default=0)
onbase_pct = models.DecimalField(max_digits=4, decimal_places=3, default=0)
slugging_pct = models.DecimalField(max_digits=4, decimal_places=3, default=0)
def __str__(self):
return f"{self.slash_line}"
@property
def slash_line(self):
return "/".join(
[
f"{self.batting_avg:.3f}".lstrip("0"),
f"{self.onbase_pct:.3f}".lstrip("0"),
f"{self.slugging_pct:.3f}".replace("0.", "."),
]
)
class Positioning(models.Model):
player = models.ForeignKey(Player, on_delete=models.CASCADE)
event = models.ForeignKey(Event, on_delete=models.CASCADE)
positions = [
("EH", "EH"),
("P", "P"),
("C", "C"),
("1B", "1B"),
("2B", "2B"),
("3B", "3B"),
("SS", "SS"),
("LF", "LF"),
("CF", "CF"),
("RF", "RF"),
("DH", "DH"),
]
position = models.CharField(
choices=positions, default=None, max_length=2, null=True, blank=True
)
order = models.PositiveSmallIntegerField(default=None, null=True, blank=True)
class Meta:
unique_together = (
"player",
"event",
)
def __str__(self):
return f"{self.player}; {self.event};"
class Availability(models.Model):
YES = 2
MAYBE = 1
NO = 0
UNKNOWN = -1
event = models.ForeignKey(Event, on_delete=models.CASCADE)
player = models.ForeignKey(Player, on_delete=models.CASCADE)
choices = [(YES, "Yes"), (NO, "No"), (MAYBE, "Maybe"), (UNKNOWN, "Unknown")]
available = models.IntegerField(choices=choices, default=UNKNOWN)
def __str__(self):
return f"{self.event}; {self.player}; {self.available}"
class Meta:
unique_together = (
"event",
"player",
)
verbose_name_plural = "availabilities"

View File

@@ -1,140 +0,0 @@
"""
Django settings for events project.
Generated by 'django-admin startproject' using Django 3.2.6.
For more information on this file, see
https://docs.djangoproject.com/en/3.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/
"""
from pathlib import Path
import os
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-qib_j&47o$5l3*gi7y#8#3pjh_88sfdqn@dmp&gx+2)&1nzmor'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ["smithers-ii.local", "127.0.0.1", "10.0.1.4", "benchcoach.ascorrea.com"]
# Application definition
INSTALLED_APPS = [
'events.apps.EventsConfig',
'teams.apps.TeamsConfig',
'venues.apps.VenuesConfig',
'players.apps.PlayersConfig',
'lineups.apps.LineupsConfig',
'teamsnap.apps.TeamsnapConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'crispy_forms',
'benchcoach'
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'benchcoach.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates']
,
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'benchcoach.wsgi.application'
# Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = False
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/
STATIC_URL = '/static/'
# Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
CRISPY_TEMPLATE_PACK = 'bootstrap4'

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1 +0,0 @@
@import url("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css");

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,485 +0,0 @@
/*!
* Bootstrap Reboot v5.1.3 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
:root {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg-rgb: 255, 255, 255;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-bg: #fff;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
background-color: currentColor;
border: 0;
opacity: 0.25;
}
hr:not([size]) {
height: 1px;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-bs-original-title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-left: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.2em;
background-color: #fcf8e3;
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: #0d6efd;
text-decoration: underline;
}
a:hover {
color: #0a58ca;
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
direction: ltr /* rtl:ignore */;
unicode-bidi: bidi-override;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: #d63384;
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.2rem 0.4rem;
font-size: 0.875em;
color: #fff;
background-color: #212529;
border-radius: 0.2rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
font-weight: 700;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: #6c757d;
text-align: left;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]::-webkit-calendar-picker-indicator {
display: none;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: left;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: left;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
outline-offset: -2px;
-webkit-appearance: textfield;
}
/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
}
::file-selector-button {
font: inherit;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,482 +0,0 @@
/*!
* Bootstrap Reboot v5.1.3 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
:root {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg-rgb: 255, 255, 255;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-bg: #fff;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
background-color: currentColor;
border: 0;
opacity: 0.25;
}
hr:not([size]) {
height: 1px;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-bs-original-title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-right: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-right: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.2em;
background-color: #fcf8e3;
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: #0d6efd;
text-decoration: underline;
}
a:hover {
color: #0a58ca;
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
direction: ltr ;
unicode-bidi: bidi-override;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: #d63384;
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.2rem 0.4rem;
font-size: 0.875em;
color: #fff;
background-color: #212529;
border-radius: 0.2rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
font-weight: 700;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: #6c757d;
text-align: right;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]::-webkit-calendar-picker-indicator {
display: none;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: right;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: right;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
outline-offset: -2px;
-webkit-appearance: textfield;
}
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
}
::file-selector-button {
font: inherit;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

281
benchcoach/tests.py Normal file
View File

@@ -0,0 +1,281 @@
from django.test import TestCase
from .models import Event, Player, Team, Venue, Positioning
from .forms import PositioningFormSet
from datetime import datetime
from django.urls import reverse
FIXTURES = ["blaseball"]
class TestEventModel(TestCase):
fixtures = FIXTURES
def test_query_event(self):
"""
Return the desired event
"""
event = Event.objects.get(id=1)
self.assertEqual("Chicago Firefighters", event.away_team.name)
self.assertEqual("Dallas Steaks", event.home_team.name)
self.assertEqual("George Fourman Stadium", event.venue.name)
self.assertEqual(
datetime(year=2020, month=8, day=24, hour=16, minute=0, second=1),
event.start,
)
class TestEventViews(TestCase):
fixtures = FIXTURES
def test_event_list(self):
response = self.client.get(reverse("schedule"))
self.assertEqual(200, response.status_code)
def test_event_edit(self):
response = self.client.get(reverse("edit event", args=[2]))
self.assertEqual(200, response.status_code)
# create new event
new_event_data = {
"home_team": 23,
"away_team": 24,
"start": datetime(year=2021, month=1, day=1, hour=9, minute=0, second=0),
"venue": 19,
}
response = self.client.post(
reverse("edit event", args=[0]), data=new_event_data
)
self.assertEqual(201, response.status_code)
new_event = Event.objects.get(id=response.context["id"])
self.assertEqual(new_event_data["home_team"], new_event.home_team.id)
self.assertEqual(new_event_data["away_team"], new_event.away_team_id)
self.assertEqual(new_event_data["start"], new_event.start)
# modify event
modified_event_data = {
"home_team": 23,
"away_team": 24,
"start": datetime(year=2021, month=1, day=1, hour=9, minute=0, second=0),
"venue": 19,
}
response = self.client.post(
reverse("edit event", args=[1]), data=modified_event_data
)
self.assertEqual(200, response.status_code)
self.assertEqual(1, response.context["id"])
modified_event = Event.objects.get(id=response.context["id"])
self.assertEqual(modified_event_data["home_team"], modified_event.home_team.id)
self.assertEqual(modified_event_data["away_team"], modified_event.away_team.id)
self.assertEqual(modified_event_data["start"], modified_event.start)
self.assertEqual(modified_event_data["venue"], modified_event.venue.id)
class TestVenueViews(TestCase):
fixtures = ["blaseball"]
def test_positioning_list(self):
response = self.client.get(reverse("edit lineup", args=[1]))
self.assertEqual(response.status_code, 200)
def test_positioning_formset(self):
event = 1
sample_data = [
# first player positioning
(1, Player.objects.get(id=1).id, "P"),
(2, Player.objects.get(id=2).id, "C"),
(3, Player.objects.get(id=3).id, "1B"),
]
data = {}
for i, (order, player, position) in enumerate(sample_data):
data[f"form-{i}-order"] = order
data[f"form-{i}-player"] = player
data[f"form-{i}-position"] = position
management = {
"form-INITIAL_FORMS": "0",
"form-TOTAL_FORMS": len(sample_data),
"form-MAX_NUM_FORMS": "",
}
formset = PositioningFormSet({**management, **data})
self.assertTrue(formset.is_valid())
for form in formset:
self.assertTrue(form.is_valid())
response = self.client.post(
reverse("edit lineup", args=[event]), {**management, **data}
)
self.assertEqual(response.status_code, 200)
for d in sample_data:
with self.subTest(d):
p = Positioning.objects.get(player_id=d[1], event_id=event)
self.assertEqual(d[0], p.order)
self.assertEqual(d[2], p.position)
pass
class TestPlayerModel(TestCase):
fixtures = FIXTURES
def test_query_player(self):
"""
Return the desired player
"""
player = Player.objects.get(id=1)
self.assertEqual(player.first_name, "Edric")
self.assertEqual(player.last_name, "Tosser")
self.assertEqual(player.jersey_number, 1)
self.assertEqual(player.team.name, "Chicago Firefighters")
class TestPlayerViews(TestCase):
fixtures = FIXTURES
def test_player_list(self):
response = self.client.get(reverse("players list"))
self.assertEqual(response.status_code, 200)
def test_player_edit(self):
response = self.client.get(reverse("edit player", args=[1]))
self.assertEqual(response.status_code, 200)
# create new player
new_player_data = {
"first_name": "A new player first name",
"last_name": "A new player last name",
"jersey_number": 99,
"team": 1,
}
response = self.client.post(
reverse("edit player", args=[0]), data=new_player_data
)
self.assertEqual(201, response.status_code)
new_player = Player.objects.get(id=response.context["id"])
self.assertEqual(new_player_data["first_name"], new_player.first_name)
self.assertEqual(new_player_data["last_name"], new_player.last_name)
# modify player
modified_player_data = {
"first_name": "A changed player first name",
"last_name": "A changed player last name",
"jersey_number": 99,
"team": 1,
}
response = self.client.post(
reverse("edit player", args=[1]), data=modified_player_data
)
self.assertEqual(200, response.status_code)
self.assertEqual(1, response.context["id"])
modified_player = Player.objects.get(id=response.context["id"])
self.assertEqual(modified_player_data["first_name"], modified_player.first_name)
self.assertEqual(modified_player_data["last_name"], modified_player.last_name)
class TestTeamModel(TestCase):
fixtures = ["blaseball"]
def test_query_team(self):
"""
Return the desired team
"""
team = Team.objects.get(id=1)
self.assertEqual(team.name, "Chicago Firefighters")
class TestTeamViews(TestCase):
fixtures = ["blaseball"]
def test_team_list(self):
response = self.client.get(reverse("teams list"))
self.assertEqual(response.status_code, 200)
self.assertIn(
{"id": 1, "title": "Chicago Firefighters"}, response.context["items"]
)
self.assertIn({"id": 2, "title": "Boston Flowers"}, response.context["items"])
self.assertIn({"id": 24, "title": "Baltimore Crabs"}, response.context["items"])
def test_team_edit(self):
response = self.client.get(reverse("edit team", args=[1]))
self.assertEqual(response.status_code, 200)
# create new team
response = self.client.post(
reverse("edit team", args=[0]), data={"name": "A new team"}
)
self.assertEqual(201, response.status_code)
new_team = Team.objects.get(id=response.context["id"])
self.assertEqual(
"A new team",
new_team.name,
)
# modify team
response = self.client.post(
reverse("edit team", args=[1]), data={"name": "A different team name"}
)
self.assertEqual(200, response.status_code)
self.assertEqual(1, response.context["id"])
modified_team = Team.objects.get(id=response.context["id"])
self.assertEqual("A different team name", modified_team.name)
class TestVenueModel(TestCase):
fixtures = ["blaseball"]
def test_query_venue(self):
"""
Return the desired venue
"""
venue = Venue.objects.get(id=1)
self.assertEqual(venue.name, "Chesapeake Racetrack and Ballpark")
class TestVenueViews(TestCase):
fixtures = ["blaseball"]
def test_venue_list(self):
response = self.client.get(reverse("venues list"))
self.assertEqual(response.status_code, 200)
self.assertIn(
{"id": 1, "title": "Chesapeake Racetrack and Ballpark"},
response.context["items"],
)
self.assertIn(
{"id": 2, "title": "Tokyo Fitness Center"}, response.context["items"]
)
self.assertIn(
{"id": 25, "title": "ILB Historical Preservation Site"},
response.context["items"],
)
def test_venue_edit(self):
response = self.client.get(reverse("edit venue", args=[1]))
self.assertEqual(response.status_code, 200)
# create new venue
response = self.client.post(
reverse("edit venue", args=[0]), data={"name": "A new venue"}
)
self.assertEqual(201, response.status_code)
new_venue = Venue.objects.get(id=response.context["id"])
self.assertEqual(
"A new venue",
new_venue.name,
)
# modify venue
response = self.client.post(
reverse("edit venue", args=[1]), data={"name": "A different venue name"}
)
self.assertEqual(200, response.status_code)
self.assertEqual(1, response.context["id"])
modified_venue = Venue.objects.get(id=response.context["id"])
self.assertEqual("A different venue name", modified_venue.name)

View File

@@ -1,33 +1,12 @@
"""events URL Configuration
from django.urls import path
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.conf.urls.static import static
from django.conf import settings
from .views import welcome, user_login
from . import views
urlpatterns = [
path('', welcome, name="home"),
path('admin/', admin.site.urls),
path('events/', include('events.urls')),
path('teams/', include('teams.urls')),
path('venues/', include('venues.urls')),
path('players/', include('players.urls')),
path('lineups/', include('lineups.urls')),
path('teamsnap/', include('teamsnap.urls')),
path('login', user_login, name="login")
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
path('lineup/edit/<int:event_id>', views.lineup_edit, name="edit lineup"),
path('events/list', views.EventListView.as_view(), name="event list"),
path('events/<int:pk>/detail', views.EventDetailView.as_view(), name="event detail"),
path('players/list', views.PlayerListView.as_view(), name="player list"),
path('teams/list', views.TeamListView.as_view(), name="team list"),
path('venues/list', views.VenueListView.as_view(), name="venue list")
]

View File

@@ -1,31 +1,142 @@
from django.http import HttpResponse
from django.shortcuts import render,redirect, reverse, HttpResponseRedirect
from django.contrib.auth import login,authenticate
from django.shortcuts import render
from .models import Event, Team, Player, Positioning, Venue
from .forms import PositioningFormSet, TeamsnapEventForm
from django.contrib import messages
from django.db.models import F
from django.views.generic import ListView, DetailView
def welcome(request):
pages = ['events list', 'teams list', 'venues list', 'players list', 'teamsnap list events', 'teamsnap home', 'login']
return render(request,'home.html',{'pages':pages})
class BenchCoachListView(ListView):
title = None
class EventDetailView(DetailView):
model = Event
context_object_name = "event"
template_name = 'benchcoach/event-detail.html'
def user_login(request):
if request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')
class EventListView(ListView):
model = Event
context_object_name = "events"
template_name = 'benchcoach/event-list.html'
try:
user = authenticate(request, username=username, password=password)
if user is not None:
print('Login')
login(request,user)
return redirect(reverse('home'))
else:
print("Someone tried to login and failed.")
print("They used username: {} and password: {}".format(username, password))
class PlayerListView(ListView):
model = Player
template_name = 'benchcoach/list.html'
return redirect('/')
except Exception as identifier:
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = "Players"
context['members_tab_active'] ='active'
return context
return redirect('/')
class TeamListView(ListView):
model = Team
template_name = 'benchcoach/list.html'
else:
return render(request, 'login.html')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = "Teams"
context['opponents_tab_active'] ='active'
return context
class VenueListView(ListView):
model = Venue
template_name = 'benchcoach/list.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = "Venues"
context['venues_tab_active'] ='active'
return context
def lineup_edit(request, event_id):
if request.method == "POST":
# create a form instance and populate it with data from the request:
formset = PositioningFormSet(request.POST)
is_valid = [f.is_valid() for f in formset]
for form in formset:
if form.is_valid():
# process the data in form.cleaned_data as required
# ...
# redirect to a new URL:
if isinstance(form.cleaned_data["id"], Positioning):
positioning_id = form.cleaned_data.pop(
"id"
).id # FIXME this is a workaround, not sure why it is necessary
positioning = Positioning.objects.filter(id=positioning_id)
positioning.update(**form.cleaned_data)
did_create = False
else:
positioning = Positioning.objects.create(
**form.cleaned_data, event_id=event_id
)
did_create = True
else:
messages.error(
request, f"Error submitting lineup. {form.instance} {form.errors}"
)
pass
if (True in is_valid) and (False in is_valid):
messages.warning(request, "Lineup partially submitted.")
elif True in is_valid:
messages.success(request, "Lineup submitted successfully.")
elif True not in is_valid:
messages.error(request, f"Error submitting lineup.")
else:
messages.error(request, f"Error submitting lineup.")
# return HttpResponse(status=204)
# return render(request, 'success.html', {'call_back':'edit lineup','id':event_id, 'errors':[error for error in formset.errors if error]}, status=200)
previous_event = Event.objects.filter(id=event_id - 1).first()
event = Event.objects.get(id=event_id)
next_event = Event.objects.get(id=event_id + 1)
players = Player.objects.prefetch_related("availability_set", "positioning_set")
for player in players:
Positioning.objects.get_or_create(player_id=player.id, event_id=event_id)
qs = (
event.positioning_set.all()
.filter(player__availability__event=event_id)
.order_by("order", "-player__availability__available", "player__last_name")
.annotate(event_availability=F("player__availability__available"))
)
formset = PositioningFormSet(queryset=qs)
formset_lineup = [f for f in formset if f.instance.order]
formset_dhd = [f for f in formset if not f.instance.order and f.instance.position]
formset_bench = [
f for f in formset if f not in formset_lineup and f not in formset_dhd
]
teamsnap_form = TeamsnapEventForm(instance=event)
details = {
"Away Team": event.away_team,
"Home Team": event.home_team,
"Date": event.start.date(),
"Time": event.start.time(),
"Venue": event.venue,
"TeamSnap": teamsnap_form
# "TeamSnap Link": event.event_set.first()
# "TeamSnap Link": f'<a href="{reverse("teamsnap edit event", kwargs={"id": event.event_set.first().id})}"> {event.event_set.first()} </a>' if event.event_set.first() else None
}
return render(
request,
"benchcoach/lineup.html",
{
"title": "Lineup",
"event": event,
"details": details,
"previous_event": previous_event,
"teamsnap_form": teamsnap_form,
"next_event": next_event,
"formset": formset,
"formset_lineup": formset_lineup,
"formset_bench": formset_bench,
"formset_dhd": formset_dhd,
},
)

View File

@@ -1,16 +0,0 @@
"""
WSGI config for events project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'events.settings')
application = get_wsgi_application()