Merge branch 'dev-gamechanger' into dev

# Conflicts:
#	gamecard/templates/gamecard/gamecard.html
This commit is contained in:
2022-06-10 17:17:31 -05:00
33 changed files with 833 additions and 66 deletions

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

View File

@@ -32,3 +32,9 @@
vertical-align: middle;
white-space: nowrap;
}
.btn-gamechanger {
color: #fff;
border-color: #1b73bc;
background-color: #1b73bc;
}

View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 741.9 69.6" style="enable-background:new 0 0 741.9 69.6;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
.st1{filter:url(#Adobe_OpacityMaskFilter);}
.st2{mask:url(#mask-2_00000067194953457310049560000009970419005216943005_);fill-rule:evenodd;clip-rule:evenodd;fill:#00F092;}
</style>
<g id="Logo-Presentation">
<g id="Slide-1-Copy-23" transform="translate(-194.000000, -546.000000)">
<g id="Group-10" transform="translate(194.000000, 546.500000)">
<path id="Fill-1" class="st0" d="M497.6,38.9h-9.4l7.9-15l8,15H497.6z M492.2,15.6l-19.8,37.5h8.3l3.6-6.7h23.8l3.6,6.7h8.3
l-19.9-37.5H492.2L492.2,15.6z"/>
<polyline id="Fill-2" class="st0" points="557.8,42.2 557.7,42.1 533.8,15.6 525.3,15.6 525.3,53.1 532.7,53.1 532.7,25.1
532.8,25.2 557.9,53.1 565.2,53.1 565.2,15.6 557.8,15.6 557.8,42.2 "/>
<path id="Fill-3" class="st0" d="M702.5,33.6h-18.7V23h18.7c1.4,0,2.7,0.6,3.7,1.6l0,0c1,1,1.5,2.3,1.5,3.7s-0.5,2.7-1.5,3.7
C705.2,33.1,703.9,33.6,702.5,33.6z M706.9,40.3l0.5-0.2c2.3-1,4.2-2.6,5.5-4.6c1.4-2.1,2.2-4.6,2.2-7.1c0-3.4-1.3-6.6-3.7-9
c-2.4-2.5-5.5-3.8-8.9-3.8h-26v37.5h7.3V41h13.9l0,0l10.5,12.1h9.8L706.9,40.3L706.9,40.3z"/>
<polyline id="Fill-4" class="st0" points="459.8,30.6 434.9,30.6 434.9,15.6 427.5,15.6 427.5,53.1 434.9,53.1 434.9,38.1
459.8,38.1 459.8,53.1 467.1,53.1 467.1,15.6 459.8,15.6 459.8,30.6 "/>
<polyline id="Fill-5" class="st0" points="318.6,15.6 315.1,15.6 303.2,42.8 291.2,15.6 287.7,15.6 287.7,15.6 280.7,15.6
280.7,53.1 288.2,53.1 288.2,25.7 300.2,53.1 306.1,53.1 318.2,25.6 318.2,53.1 325.6,53.1 325.6,15.6 318.6,15.6 318.6,15.6
"/>
<g id="Group-9" transform="translate(0.000000, 0.008595)">
<g id="Clip-7">
</g>
<path id="Fill-6" class="st0" d="M252.6,38.9L252.6,38.9h-9.4l7.9-15l8,15H252.6z M247.2,15.6l-19.8,37.5h8.3l3.6-6.7h23.8
l3.6,6.7h8.3l-19.9-37.5H247.2L247.2,15.6z"/>
<path id="Fill-8" class="st0" d="M199.2,36.9v0.4H218v0.1c-1.4,4.8-5.8,8.2-10.8,8.2h-10c-6.1,0-11.1-5-11.2-11.1
c-0.1-3,1.1-5.9,3.2-8c2.2-2.2,5.2-3.5,8.2-3.5h9.1c4,0,7.3-3,7.8-7v-0.4h-17.2c-5,0-9.7,2-13.3,5.6c-3.5,3.6-5.4,8.4-5.3,13.4
c0.2,10.2,8.6,18.5,18.9,18.5h9.5h0.1c10.1-0.1,18.5-8.4,18.7-18.5c0-0.1,0-0.2,0-0.2c0-0.1,0-0.1,0-0.2v-0.1l0,0v-4.2H207
C203,29.9,199.6,32.9,199.2,36.9"/>
</g>
<path id="Fill-10" class="st0" d="M594.4,36.9v0.4h18.8v0.1c-1.4,4.8-5.8,8.2-10.8,8.2h-10c-6.1,0-11.1-5-11.2-11.1
c-0.1-3,1.1-5.9,3.2-8c2.2-2.2,5.2-3.5,8.2-3.5h9.1c4,0,7.3-3,7.8-7v-0.4h-17.1c-5,0-9.7,2-13.3,5.6c-3.5,3.6-5.4,8.4-5.4,13.4
c0.2,10.2,8.6,18.5,18.9,18.5h9.5h0.1c10.2-0.1,18.6-8.4,18.7-18.4c0-0.1,0-0.2,0-0.2c0-0.1,0-0.1,0-0.2v-0.1l0,0V30h-18.7
C598.1,30,594.8,33,594.4,36.9"/>
<path id="Fill-11" class="st0" d="M398,23h13.7c4,0,7.3-3,7.8-7v-0.4H398c-5,0-9.7,2-13.3,5.6c-3.5,3.6-5.4,8.4-5.4,13.4
c0.2,10.2,8.6,18.5,18.9,18.5h13.4c4,0,7.3-3,7.8-7v-0.4h-21.3c-3.1,0-6.1-1.3-8.2-3.5s-3.3-5-3.2-8C386.9,28,391.9,23,398,23"/>
<path id="Fill-12" class="st0" d="M367.1,23c4,0,7.3-3,7.7-7v-0.4h-38.6v37.5h30.9c4,0,7.3-3,7.7-7v-0.4h-31.3v-7.6h14
c3.8,0,7-3.1,7-7v-0.5h-20.9V23H367.1"/>
<path id="Fill-13" class="st0" d="M660.4,23c4,0,7.3-3,7.7-7v-0.4h-38.6v37.5h30.9c4,0,7.3-3,7.7-7v-0.4h-31.3v-7.6h13.9
c3.8,0,7-3.1,7-7v-0.5h-20.9V23H660.4"/>
<polyline id="Fill-14" class="st0" points="728.9,53.1 727.5,53.1 727.5,45.6 724.9,45.6 724.9,44.3 731.4,44.3 731.4,45.6
728.9,45.6 728.9,53.1 "/>
<path id="Fill-15" class="st0" d="M736.8,53.1l-2.5-7.2l0,0c0,1.1,0.1,2.1,0.1,3.1v4.2h-1.2v-8.7h1.9l2.4,6.9l0,0l2.5-6.9h1.9
v8.7h-1.4v-4.3c0-0.4,0-1,0-1.7s0-1.1,0-1.2l0,0l-2.6,7.2H736.8"/>
</g>
</g>
</g>
<g id="Logo-Presentation_00000113341718855537428600000001450668188223146168_">
<g id="Slide-1-Copy-23_00000093891632931889047450000011681990669367062677_" transform="translate(-551.000000, -849.000000)">
<g id="DSG_GameChanger_Logomark-Copy-7" transform="translate(551.000000, 849.000000)">
<g id="Clip-2">
</g>
<defs>
<filter id="Adobe_OpacityMaskFilter" filterUnits="userSpaceOnUse" x="0" y="0" width="142.9" height="69.6">
<feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
</filter>
</defs>
<mask maskUnits="userSpaceOnUse" x="0" y="0" width="142.9" height="69.6" id="mask-2_00000067194953457310049560000009970419005216943005_">
<g class="st1">
<polygon id="path-1_00000010310054758688279420000011862642130963442847_" class="st0" points="0,0 142.9,0 142.9,69.6 0,69.6
"/>
</g>
</mask>
<path id="Fill-1_00000052789438335131278620000014152493238395228080_" class="st2" d="M104.9,55.5c-2.9,0-5.8-0.7-8.4-1.9
c-2.5-1.2-4.8-3-6.7-5.2c-1.8-2.2-3.2-4.8-4-7.5c-0.8-2.9-1-6-0.6-9c1.2-8.3,7.4-15.2,15.5-17.2c0.5-0.1,3.2-0.6,6.7-0.6l22.4,0
c0.1,0,3.1,0,5.8-1.3c4.4-2.5,7.2-7.3,7.2-12.4V0h-38C89,0,75.3,10.9,71.5,26.5c0,0.1,0,0.1,0,0.2l0,0.1H50.7
c-7.5,0-13.7,6-14,13.6l0,0.5h33l0,0c0,0.2-0.1,0.5-0.2,0.8c-2.3,6.8-8.1,12.1-15.1,13.9c0,0-2.9,0.7-6.8,0.6H37
c-3.6,0-6.2-0.5-6.7-0.6c-9.1-2.3-15.6-10.4-15.7-19.8c0-1.1,0-2.4,0.2-3.7c1.2-8.3,7.4-15.3,15.5-17.3c0.5-0.1,3.2-0.6,6.7-0.6
l11.4,0c0.2,0,4,0,7-1.9c3.9-2.7,6.2-7.1,6.2-11.8V0H34.4C15.7,0,0.3,15.3,0,34.2c-0.2,9.2,3.3,18,9.9,24.7
c6.6,6.8,15.8,10.7,25.1,10.7h15.7c10.6,0,20.7-5.1,27.2-13.6c6.7,8.5,16.9,13.6,27.6,13.6h23.4c7.5,0,13.7-6,14-13.6l0-0.5
H104.9"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -36,7 +36,7 @@
<body class="bg-light">
<div class="mb-1">
<div class="mb-1 bg-light">
<nav class="navbar navbar-expand-md navbar-dark bg-navbar">
<div class="container-fluid">
<button class="navbar-toggler navbar-toggler-right" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
@@ -57,28 +57,45 @@
<a class="nav-link" href="{% url 'about' %}">About</a>
</li>
{% if request.user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="{% url 'teamsnap_dashboard' %}">{% translate "Dashboard" %}</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'teamsnap_schedule' %}">{% translate "Schedule" %}</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'teamsnap_choose_multiple_lineups' team_id=request.user.preferences.managed_team_id%}?num=3">{% translate "Multi-Lineup" %}</a>
</li>
{% if request.user.teamsnap_preferences %}
<li class="nav-item">
<a class="nav-link" href="{% url 'teamsnap_dashboard' %}">{% translate "Dashboard" %}</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'teamsnap_schedule' %}">{% translate "Schedule" %}</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'teamsnap_choose_multiple_lineups' team_id=request.user.teamsnap_preferences.managed_team_id%}?num=3">{% translate "Multi-Lineup" %}</a>
</li>
{% endif %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Profile
</a>
<ul class="dropdown-menu dropdown-menu-dark" aria-labelledby="navbarDropdown">
<li class="dropdown-item">
<a class="nav-link" href="{% url 'teamsnap_preferences' %}">{% translate "Preferences" %}</a>
</li>
<li class="dropdown-item">
<a class="nav-link" href="{% url 'users:detail' request.user.username %}">{% translate "My Profile" %}</a>
</li>
<li><hr class="dropdown-divider"></li>
{% if request.user.teamsnap_preferences %}
<li class="dropdown-item">
<a class="nav-link" href="{% url 'teamsnap_preferences' %}">{% translate "TeamSnap Preferences" %}</a>
</li>
{% endif %}
<li><hr class="dropdown-divider"></li>
{% if request.user.gamechanger_account %}
<li class="dropdown-item">
<a class="nav-link" href="{% url 'gamechanger_account' %}">{% translate "GameChanger Account" %}</a>
</li>
<li class="dropdown-item">
<a class="nav-link" href="{% url 'gamechanger_preferences' %}">{% translate "GameChanger Preferences" %}</a>
</li>
<li class="dropdown-item">
<a class="nav-link" href="{% url 'gamechanger_import_roster' %}">{% translate "GameChanger Import Roster" %}</a>
</li>
<li><hr class="dropdown-divider"></li>
{% endif %}
<li class="dropdown-item">
<a class="nav-link" href="{% url 'account_logout' %}">{% translate "Sign Out" %}</a>
</li>

View File

@@ -17,6 +17,7 @@ urlpatterns = [
path("accounts/", include("allauth.urls")),
path("", include("teamsnap.urls")),
path("", include("instagen.urls")),
path("gc/", include("gamechanger.urls")),
path("", include("gamecard.urls")),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@@ -126,7 +126,9 @@
<td class="cell-square"></td>
<td class="cell-square cell-smalltext cell-mono available-status-code-{{ member.availability.status_code }} {% if member.in_starting_lineup %}in-starting-lineup{% endif %}">{{ member.member.jersey_number }}</td>
<td class="cell-info-lastname available-status-code-{{ member.availability.status_code }} {% if member.in_starting_lineup %}in-starting-lineup{% endif %}">{{ member.member.last_name }}</td>
<td class="statscell"></td>
<td class="statscell">
{{ member.stats.offensive.AVG }}/{{ member.stats.offensive.OBP }}/{{ member.stats.offensive.SLG }}:{{ member.stats.offensive.PA }}
</td>
<td class="cell-square cell-checkbox narrow no-inside-border">
{% if "P" in member.member.position %}

View File

@@ -2,6 +2,8 @@
import vcr
from django.shortcuts import render
import gamechanger.models as GcModels
import gamechanger.utils.gamechanger
from teamsnap.models import Opponent, Team
from teamsnap.utils import get_teamsnap_client
@@ -62,6 +64,12 @@ def gamecard(request, team_id, event_id):
ts_availability_lookup = {m.data["id"]: m for m in ts_availabilities}
ts_lineup_entries_lookup = {m.data["member_id"]: m for m in ts_lineup_entries}
stats = gamechanger.utils.gamechanger.stats(request)
stats_lookup = {
GcModels.Player.objects.filter(id=k).first().teamsnap_member_id: stat_row
for k, stat_row in stats.items()
}
members = []
for member in ts_members:
@@ -91,6 +99,7 @@ def gamecard(request, team_id, event_id):
ts_availabilities_past,
)
)[:4],
"stats": stats_lookup.get(member.data["id"]),
}
)

0
gamechanger/__init__.py Normal file
View File

8
gamechanger/admin.py Normal file
View File

@@ -0,0 +1,8 @@
from django.contrib import admin
from .models import Account, Player, Preferences
# Register your models here.
admin.site.register(Account)
admin.site.register(Preferences)
admin.site.register(Player)

6
gamechanger/apps.py Normal file
View File

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

45
gamechanger/forms.py Normal file
View File

@@ -0,0 +1,45 @@
from django import forms
from django.forms import ModelForm, formset_factory
from .models import Account, Player, Preferences
class PreferencesForm(ModelForm):
class Meta:
model = Preferences
fields = ["user", "season_id", "team_id"]
widgets = {
"user": forms.HiddenInput(),
"managed_team_id": forms.TextInput(),
}
labels = {"managed_team_id": "Selected Team"}
class AccountForm(ModelForm):
class Meta:
model = Account
fields = ["user", "email", "password"]
widgets = {
"user": forms.HiddenInput(),
"email": forms.EmailInput(),
"password": forms.PasswordInput(),
}
class PlayerForm(ModelForm):
gamechanger_name = forms.Field()
teamsnap_name = forms.Field()
fname = forms.Field()
lname = forms.Field()
class Meta:
model = Player
fields = ["id", "teamsnap_member_id"]
widgets = {
"teamsnap_member_id": forms.Select(
choices=(), attrs={"class": "form-control"}
),
}
PlayerFormSet = formset_factory(PlayerForm, can_delete=True, extra=0)

View File

@@ -0,0 +1,34 @@
# Generated by Django 3.2.13 on 2022-06-07 16:50
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Preferences',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('managed_team_id', models.IntegerField()),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='gamechanger_preferences', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Account',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.EmailField(max_length=254)),
('password', models.CharField(max_length=255)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='gamechanger_account', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 3.2.13 on 2022-06-07 17:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('gamechanger', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='preferences',
name='managed_team_id',
),
migrations.AddField(
model_name='preferences',
name='season_id',
field=models.CharField(default=1, max_length=255),
preserve_default=False,
),
migrations.AddField(
model_name='preferences',
name='team_id',
field=models.CharField(default=1, max_length=255),
preserve_default=False,
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 3.2.13 on 2022-06-08 12:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('gamechanger', '0002_auto_20220607_1259'),
]
operations = [
migrations.CreateModel(
name='Player',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('teamsnap_member_id', models.IntegerField()),
('fname', models.CharField(max_length=30)),
('lname', models.CharField(max_length=30)),
],
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.13 on 2022-06-08 12:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('gamechanger', '0003_player'),
]
operations = [
migrations.AlterField(
model_name='player',
name='id',
field=models.CharField(max_length=30, primary_key=True, serialize=False),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.2.13 on 2022-06-10 21:58
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('gamechanger', '0004_alter_player_id'),
]
operations = [
migrations.AlterModelOptions(
name='preferences',
options={'verbose_name_plural': 'preferences'},
),
]

View File

36
gamechanger/models.py Normal file
View File

@@ -0,0 +1,36 @@
from django import forms
from django.db import models
from django.db.models import CharField, EmailField
from benchcoach.users.models import User
# Create your models here.
class Account(models.Model):
user = models.OneToOneField(
User, on_delete=models.CASCADE, related_name="gamechanger_account"
)
email = EmailField()
password = CharField(max_length=255)
class Preferences(models.Model):
user = models.OneToOneField(
User, on_delete=models.CASCADE, related_name="gamechanger_preferences"
)
season_id = CharField(max_length=255)
team_id = CharField(max_length=255)
class Meta:
verbose_name_plural = "preferences"
class Player(models.Model):
id = models.CharField(primary_key=True, max_length=30)
teamsnap_member_id = models.IntegerField()
fname = CharField(max_length=30)
lname = CharField(max_length=30)
widgets = {
"teamsnap_member_id": forms.Select(choices=(), attrs={"class": "form-control"}),
}

View File

@@ -0,0 +1,7 @@
{% extends "base.html" %}
{% block content %}
<form method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Save">
</form>
{% endblock content %}

View File

@@ -0,0 +1,48 @@
{% extends "base.html" %}
{% block content %}
{# {% for player in roster %}#}
{# <li class="list-group-item">#}
{# {{ player.fname }} {{ player.lname }}#}
{# </li>#}
{# {% endfor %}#}
<form method="post" action='{% url 'gamechanger_import_roster' %}' >
{{ formset.management_form }}
{% csrf_token %}
<table>
<tr>
<td>
{{ formset.0.gamechanger_name.label }}
</td>
<td>
{{ formset.0.teamsnap_name.label }}
</td>
<td>
{{ formset.0.DELETE.label }}
</td>
</tr>
{% for form in formset %}
<tr>
<td>
{{ form.gamechanger_name.value }}
</td>
<td>
{{ form.teamsnap_member_id }}
</td>
<td>
{{ form.DELETE }}
</td>
{{ form.gamechanger_name.as_hidden }}
{{ form.teamsnap_name.as_hidden }}
{{ form.id.as_hidden }}
{{ form.teamsnap_member_id.as_hidden }}
{{ form.fname.as_hidden }}
{{ form.lname.as_hidden }}
</tr>
{% endfor %}
</table>
<button class="btn btn-success" type="submit">
{# <i class="bi bi-arrow-right"></i>#}
Import
</button>
</form>
{% endblock %}

View File

@@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block content %}
{% for id, stat_row in stats.items %}
<p>
{{ id }}: {{ stat_row }}
</p>
{% endfor %}
{% endblock %}

3
gamechanger/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

17
gamechanger/urls.py Normal file
View File

@@ -0,0 +1,17 @@
from django.urls import path
from .views import (
AccountFormView,
PreferencesFormView,
lineup_submit,
roster_import,
stats,
)
urlpatterns = [
path("account/", AccountFormView.as_view(), name="gamechanger_account"),
path("preferences/", PreferencesFormView.as_view(), name="gamechanger_preferences"),
path("roster/import", roster_import, name="gamechanger_import_roster"),
path("lineup/submit", lineup_submit, name="gamechanger_lineup_submit"),
path("stats", stats, name="gamechanger_stats"),
]

View File

View File

@@ -0,0 +1,139 @@
import csv
import json
import re
import requests
import vcr
url = "https://gc.com/t/{season_id}/{team_id}/{page}"
# TODO Remove VCR
@vcr.use_cassette(
"gamechanger/fixtures/authenticated_session.yaml", record_mode="new_episodes"
)
def get_authenticated_session(request):
gc_username = request.user.gamechanger_account.user
gc_password = request.user.gamechanger_account.password
s = requests.Session()
s.headers.update({"referer": "https://gc.com/do-login"})
s.get("https://gc.com/login")
r2 = s.post(
"https://gc.com/do-login",
cookies=s.cookies,
data={
"csrfmiddlewaretoken": s.cookies.get("csrftoken"),
"email": gc_username,
"password": gc_password,
},
)
if r2.status_code == 200:
return s
else:
raise requests.exceptions.RequestException(
f"Returned {r2.status_code} for {r2.reason}"
)
def submit_lineup(request, lineup):
authenticated_session = get_authenticated_session(request)
season_id = request.user.gamechanger_preferences.season_id
team_id = request.user.gamechanger_preferences.team_id
authenticated_session.headers.update(
{
"referer": url.format(
season_id=season_id, team_id=team_id, page="lineup_edit"
),
"x-csrftoken": authenticated_session.cookies.get("csrftoken"),
"Content-Type": "application/x-www-form-urlencoded;",
}
)
r = authenticated_session.post(
cookies=authenticated_session.cookies,
url="https://gc.com/do-save-lineup/{team_id}".format(
team_id=team_id.split("-").pop()
),
json={"lineup": lineup},
)
if r.status_code == 200:
return r
else:
raise requests.exceptions.RequestException(
f"Returned {r.status_code} for {r.reason}"
)
def scrape_page(season_id, team_id, page):
r = requests.get(url.format(season_id=season_id, team_id=team_id, page=page))
initialize_page_json = re.search(
r'page.initialize\(\$.parseJSON\("(.*?)"\)', r.content.decode("unicode_escape")
)
m = initialize_page_json.group(1)
return json.loads(m)
# TODO Remove VCR
@vcr.use_cassette("gamechanger/fixtures/stats.yaml", record_mode="new_episodes")
def stats(request):
authenticated_session = get_authenticated_session(request)
season_id = request.user.gamechanger_preferences.season_id
team_id = request.user.gamechanger_preferences.team_id
page = "stats/batting/Qualified/standard/csv"
r = authenticated_session.get(
url.format(season_id=season_id, team_id=team_id, page=page)
)
with vcr.use_cassette(
"gamechanger/fixtures/roster.yaml", record_mode="new_episodes"
):
roster = scrape_page(season_id, team_id, "roster")
id_lookup = {
(p.get("fname"), p.get("lname")): p.get("player_id")
for p in roster["roster"]
}
decoded_content = r.content.decode("utf-8")
cr = csv.reader(decoded_content.splitlines(), delimiter=",")
my_list = list(cr)
player_keys = [
(i, key)
for i, key in enumerate(my_list[1][: my_list[0].index("Offensive Stats")])
]
offensive_keys = [
(i, key)
for i, key in enumerate(
my_list[1][
my_list[0]
.index("Offensive Stats") : my_list[0]
.index("Defensive Stats")
- 1
],
start=my_list[0].index("Offensive Stats"),
)
]
defensive_keys = [
(i, key)
for i, key in enumerate(
my_list[1][my_list[0].index("Defensive Stats") :],
start=my_list[0].index("Defensive Stats"),
)
]
stats = {}
for row in my_list[2:]:
player_keys
number, lname, fname = row[:3]
if number == "Team":
break
gamechanger_id = id_lookup[(fname, lname)]
stats[gamechanger_id] = {
"offensive": {k: row[i] for i, k in offensive_keys},
"defensive": {k: row[i] for i, k in defensive_keys},
}
return stats
# d = scrape_page(season_id, team_id, page)
pass

180
gamechanger/views.py Normal file
View File

@@ -0,0 +1,180 @@
from django.http import HttpResponse, HttpResponseNotAllowed, HttpResponseServerError
from django.shortcuts import render
from django.views.generic.edit import FormView
from teamsnap.views import get_teamsnap_client
from .forms import AccountForm, PlayerFormSet, PreferencesForm
from .models import Player
from .utils import gamechanger
class PreferencesFormView(FormView):
template_name = "gamechanger/form.html"
form_class = PreferencesForm
success_url = "/"
def form_valid(self, form):
# This method is called when valid form data has been POSTed.
# It should return an HttpResponse.
if form.data["user"] == str(self.request.user.id):
form.save()
return super().form_valid(form)
def get_initial(self):
"""
Returns the initial data to use for forms on this view.
"""
initial = super().get_initial()
initial["user"] = self.request.user
initial["email"] = self.request.user.username
# initial['managed_team_id']
return initial
class AccountFormView(FormView):
template_name = "gamechanger/form.html"
form_class = AccountForm
success_url = "/"
def form_valid(self, form):
# This method is called when valid form data has been POSTed.
# It should return an HttpResponse.
if form.data["user"] == str(self.request.user.id):
form.save()
return super().form_valid(form)
def get_initial(self):
"""
Returns the initial data to use for forms on this view.
"""
initial = super().get_initial()
initial["user"] = self.request.user
initial["email"] = self.request.user.username
# initial['managed_team_id']
return initial
def roster_import(request):
if request.method == "GET":
from pyteamsnap.api import Member
client = get_teamsnap_client(request)
season_id = request.user.gamechanger_preferences.season_id
team_id = request.user.gamechanger_preferences.team_id
teamsnap_team_id = request.user.teamsnap_preferences.managed_team_id
teamsnap_members = {
f"{member.data['first_name']} {member.data['last_name']}": member
for member in Member.search(client, team_id=teamsnap_team_id)
}
page = "roster"
d = gamechanger.scrape_page(season_id, team_id, page)
roster = d["roster"]
initial = [
{
"gamechanger_name": f"{player['fname']} {player['lname']}",
"fname": player["fname"],
"lname": player["lname"],
"teamsnap_name": "{first_name} {last_name}".format(
**teamsnap_members[f"{player['fname']} {player['lname']}"].data
),
"id": player.get("player_id"),
"teamsnap_member_id": teamsnap_members[
f"{player['fname']} {player['lname']}"
].data["id"],
}
for player in roster
]
formset = PlayerFormSet(initial=initial)
choices = [
(
teamsnap_member.data["id"],
f"{teamsnap_member.data['first_name']} {teamsnap_member.data['last_name']}",
)
for teamsnap_member in teamsnap_members.values()
]
for form in formset:
form.fields["teamsnap_member_id"].widget.choices = choices
pass
return render(
request,
"gamechanger/roster.html",
context={"roster": roster, "formset": formset},
)
elif request.POST:
formset = PlayerFormSet(request.POST)
if formset.is_valid():
r = []
for form in formset:
data = form.cleaned_data
data.pop("DELETE")
data.pop("gamechanger_name")
data.pop("teamsnap_name")
obj, did_create = Player.objects.update_or_create(**data)
obj.save()
r.append(obj)
return HttpResponse(status=200)
else:
return HttpResponseServerError()
return HttpResponseServerError()
else:
return HttpResponseServerError()
def lineup_submit(request):
from teamsnap.forms import LineupEntryFormset
if request.GET:
return HttpResponseNotAllowed()
if request.POST:
formset = LineupEntryFormset(request.POST)
if formset.is_valid():
lineup_data = [
form.cleaned_data
for form in formset
if form.cleaned_data.get("label")
and form.cleaned_data.get("sequence") is not None
]
lineup_data.sort(key=lambda x: x.get("sequence"))
lineup = []
for lineup_entry in lineup_data:
d = {
"player_id": lineup_entry["gamechanger_player_id"],
"position": lineup_entry["label"],
}
if lineup_entry["label"] == "DH":
for_whom = [
e
for e in lineup_data
if e["position_only"] and e["label"] != "DR"
][0]
d["forwhom"] = for_whom["gamechanger_player_id"]
lineup.append(d)
if for_whom in lineup:
lineup_data.remove(for_whom)
if for_whom in lineup_data:
lineup_data.remove(for_whom)
lineup.append(
{
"player_id": for_whom["gamechanger_player_id"],
"position": for_whom["label"],
}
)
elif lineup_entry["label"] != "DR":
lineup.append(d)
gamechanger.submit_lineup(request, lineup)
return HttpResponse(status=200)
return HttpResponseServerError()
def stats(request):
s = gamechanger.stats(request)
return render(request, "gamechanger/stats.html", context={"stats": s})
pass

View File

@@ -21,6 +21,7 @@ class LineupEntryForm(forms.Form):
member = None
availability = None
lineup_entry = None
gamechanger_player_id = forms.Field(required=False)
event_lineup_entry_id = forms.Field(required=False)
event_lineup_id = forms.Field(required=False)

View File

@@ -25,7 +25,6 @@
<tbody>
<tr class="align-top mx-1">
{% for event_data in contexts %}
<td class="px-1">
{% include "lineup/widgets/lineup.html" with event=event_data.event event_id=event_data.event.data.id formset=event_data.formset formset_startinglineup=event_data.formset_startinglineup formset_bench=event_data.formset_bench formset_out=event_data.formset_out formset_startingpositionalonly=event_data.formset_startingpositionalonly %}
</td>
@@ -45,20 +44,20 @@
window.addEventListener('DOMContentLoaded', () => {
/* Run whatever you want */
const postForms = document.querySelectorAll("[id^=form-lineup]");
for (postForm of postForms) {
function handleSubmit(postForm) {
postForm.addEventListener("submit", e => {
const postSubmits = document.querySelectorAll("[id^=submit-lineup]");
for (postSubmit of postSubmits) {
function handleSubmit(postSubmit) {
postSubmit.addEventListener("click", e => {
e.preventDefault();
formData = new FormData(postForm);
fetch(postForm.action, {
formData = new FormData(postSubmit.form);
fetch(postSubmit.formAction, {
method: 'POST',
body: formData,
})
.then(response => response.json())
.then(response => response)
.then(data => {
{#postForm.reset();#}
document.querySelector("#popup-messages-content").innerHTML = `<div class="alert alert-dismissible alert-success" role="alert">
<strong>Success!</strong> ${data.formatted_title} <strong>saved</strong>.
<strong>Success!</strong>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div> `
})
@@ -68,7 +67,7 @@
})
}
handleSubmit(postForm)
handleSubmit(postSubmit)
}
});
</script>

View File

@@ -1,6 +1,6 @@
{% load static %}
<div class="card mx-auto benchcoach-lineup" style="max-width: 455px" id="benchcoach-lineup-{{ event_id }}">
<form method="post" action='{% url 'teamsnap_submit_lineup' team_id=event.data.team_id event_id=event.data.id %}' id="form-lineup-{{ event.data.id }}">
<form method="post" id="form-lineup-{{ event.data.id }}">
{{ formset.management_form }}
{% csrf_token %}
<div class="border-bottom p-2">
@@ -12,33 +12,45 @@
<button class="btn btn-primary btn-sm py-0 m-1" onclick="importFromClipboard(this)" type="button"><i class="bi bi-arrow-90deg-down"></i></i><i class="bi bi-file-spreadsheet"></i> </button>
</div>
<div class="col text-end d-inline">
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle btn-sm py-0 m-1" type="button" id="dropdownMenuButton1" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-share"></i> Export
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
<li>
<a class="dropdown-item" href="javascript:;" onclick="copyEmailTable(this, '{{ event.data.start_date|date:"D, F j, Y g:i A" }}, {{ event.data.location_name }}, ({% if event.data.game_type == 'Away' %}@{% endif %}{{ event.data.opponent_name }})', '{% for form in formset %}{{ form.member.data.email_addresses.0 }},{% endfor %}')">
<i class="bi bi-envelope"></i> Generate Lineup Email
</a>
</li>
<li>
<a class="dropdown-item" onclick="sendToClipboard(this)">
<i class="bi bi-file-spreadsheet"></i> Sheet format to Clipboard
</a>
</li>
</ul>
</div>
<div>
<button class="btn btn-teamsnap btn-sm py-0 m-1" type="submit">
<i class="bi bi-arrow-right"></i>
TeamSnap
</button>
<div class="btn-group">
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle btn-sm py-0 m-1" type="button" id="dropdownMenuButton1"
data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-share"></i> Export
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
<li>
<a class="dropdown-item" href="javascript:;"
onclick="copyEmailTable(this, '{{ event.data.start_date|date:"D, F j, Y g:i A" }}, {{ event.data.location_name }}, ({% if event.data.game_type == 'Away' %}@{% endif %}{{ event.data.opponent_name }})', '
{% for form in formset %}{{ form.member.data.email_addresses.0 }},{% endfor %}')">
<i class="bi bi-envelope"></i> Generate Lineup Email
</a>
</li>
<li>
<a class="dropdown-item" onclick="sendToClipboard(this)">
<i class="bi bi-file-spreadsheet"></i> Sheet format to Clipboard
</a>
</li>
</ul>
</div>
<div>
<button type="Submit" class="btn btn-teamsnap btn-sm py-0 m-1" formaction="{% url 'teamsnap_submit_lineup' team_id=event.data.team_id event_id=event.data.id %}" form="form-lineup-{{ event.data.id }}" id="submit-lineup-teamsnap-{{ event.data.id }}">
<i class="bi bi-arrow-right"></i>
TeamSnap
</button>
</div>
<div>
{% if request.user.gamechanger_account %}
<button type="Submit" class="btn btn-gamechanger btn-sm py-0 m-1 text-nowrap" formaction="{% url 'gamechanger_lineup_submit' %}" form="form-lineup-{{ event.data.id }}" id="submit-lineup-gamechanger-{{ event.data.id }}">
<i class="bi bi-arrow-right"></i>
GameChanger
</button>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<div class="card-body p-0 m-0">
<div>
<div class="row m-0">

View File

@@ -8,6 +8,7 @@
data-player-name="{{ form.member.data.last_name }}, {{ form.member.data.first_name }}"
data-availability-statuscode="{{ form.availability.data.status_code }}"
>
{{ form.gamechanger_player_id.as_hidden }}
{{ form.event_lineup_entry_id.as_hidden }}
{{ form.event_lineup_id.as_hidden }}
{{ form.event_id.as_hidden }}

View File

@@ -234,7 +234,12 @@ def submit_lineup(request, team_id, event_id):
return HttpResponseServerError
def multi_lineup_choose(request, team_id):
def multi_lineup_choose(request, team_id=None):
if not team_id:
return redirect(
"teamsnap_choose_multiple_lineups",
team_id=request.user.teamsnap_preferences.managed_team_id,
)
from django.forms import formset_factory
from pyteamsnap.api import Event

View File

@@ -5,9 +5,14 @@ from benchcoach.users.models import User
class Preferences(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
user = models.OneToOneField(
User, on_delete=models.CASCADE, related_name="teamsnap_preferences"
)
managed_team_id = models.IntegerField()
class Meta:
verbose_name_plural = "preferences"
class Team(models.Model):
id = models.IntegerField(primary_key=True)

View File

@@ -6,15 +6,12 @@ from allauth.socialaccount.providers.oauth2.views import (
OAuth2CallbackView,
OAuth2LoginView,
)
from django.http import (
HttpResponse,
HttpResponseNotAllowed,
HttpResponseServerError,
JsonResponse,
)
from django.http import HttpResponse, HttpResponseNotAllowed, HttpResponseServerError
from django.shortcuts import redirect, render
from django.views.generic.edit import FormView
from gamechanger.models import Player as GamechangerPlayer
from .forms import PreferencesForm
from .models import Preferences
from .provider import TeamsnapProvider
@@ -112,7 +109,8 @@ class PreferencesFormView(FormView):
def schedule_view(request, team_id=None):
if not team_id:
return redirect(
"teamsnap_schedule", team_id=request.user.preferences.managed_team_id
"teamsnap_schedule",
team_id=request.user.teamsnap_preferences.managed_team_id,
)
client = get_teamsnap_client(request)
no_past = bool(request.GET.get("no_past", 0))
@@ -141,7 +139,7 @@ def schedule_view(request, team_id=None):
def view_event(request, event_id, team_id=None):
if not team_id:
return redirect(
"teamsnap_event", team_id=request.user.preferences.managed_team_id
"teamsnap_event", team_id=request.user.teamsnap_preferences.managed_team_id
)
from pyteamsnap.api import (
@@ -217,6 +215,16 @@ def edit_lineup(request, event_ids, team_id):
ts_members = [i for i in ts_bulkload if isinstance(i, Member)]
ts_member_lookup = {m.data["id"]: m for m in ts_members}
gc_player_lookup = {
m.data["id"]: getattr(
GamechangerPlayer.objects.filter(
teamsnap_member_id=m.data["id"]
).first(),
"id",
None,
)
for m in ts_members
}
ts_availability_lookup = {m.data["member_id"]: m for m in ts_availabilities}
ts_lineup_entries_lookup = {m.data["member_id"]: m for m in ts_lineup_entries}
@@ -292,6 +300,9 @@ def edit_lineup(request, event_ids, team_id):
"member_id": member["member"]["id"],
"sequence": member["lineup_entry"].get("sequence"),
"label": position,
"gamechanger_player_id": gc_player_lookup.get(
member["member"]["id"]
),
}
)
@@ -350,12 +361,11 @@ def edit_lineup(request, event_ids, team_id):
def submit_lineup(request, team_id, event_id):
from pyteamsnap.api import Event, EventLineup, EventLineupEntry
from pyteamsnap.api import EventLineup, EventLineupEntry
from teamsnap.forms import LineupEntryFormset
client = get_teamsnap_client(request)
ts_event = Event.get(client, event_id)
ts_lineup = EventLineup.search(client, event_id=event_id)
event_lineup_id = ts_lineup[0].data["id"]
if request.GET:
@@ -397,13 +407,9 @@ def submit_lineup(request, team_id, event_id):
else:
pass
else:
# breakpoint()
pass
# breakpoint()
pass
return JsonResponse(ts_event.data)
pass
return HttpResponseServerError
return HttpResponse(status=200)
return HttpResponseServerError()
def multi_lineup_choose(request, team_id):