Merge branch 'teamsnap_sync' into dev

This commit is contained in:
2021-12-10 11:21:34 -06:00
34 changed files with 1146 additions and 7 deletions

View File

@@ -36,6 +36,7 @@ INSTALLED_APPS = [
'venues.apps.VenuesConfig',
'players.apps.PlayersConfig',
'lineups.apps.LineupsConfig',
'teamsnap.apps.TeamsnapConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',

View File

@@ -27,5 +27,6 @@ urlpatterns = [
path('teams/', include('teams.urls')),
path('venues/', include('venues.urls')),
path('players/', include('players.urls')),
path('lineups/', include('lineups.urls'))
path('lineups/', include('lineups.urls')),
path('teamsnap/', include('teamsnap.urls'))
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@@ -2,5 +2,5 @@ from django.http import HttpResponse
from django.shortcuts import render
def welcome(request):
pages = ['events list', 'teams list', 'venues list', 'players list']
pages = ['events list', 'teams list', 'venues list', 'players list', 'teamsnap list events']
return render(request,'home.html',{'pages':pages})

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.6 on 2021-11-21 16:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('lineups', '0006_alter_positioning_order'),
]
operations = [
migrations.AlterField(
model_name='positioning',
name='position',
field=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),
),
]

8
reload_teamsnap.sh Normal file
View File

@@ -0,0 +1,8 @@
#!/bin/bash
PROJECT_PATH="/Users/asc/PycharmProjects_Local/benchcoach"
PYTHON_PATH="$PROJECT_PATH/venv/bin/python"
FIXTURES="2021cmba"
MANAGE_PY_PATH="$PROJECT_PATH/manage.py"
bash -cl "$PYTHON_PATH $MANAGE_PY_PATH migrate teamsnap zero"
bash -cl "$PYTHON_PATH $MANAGE_PY_PATH migrate teamsnap"
bash -cl "$PYTHON_PATH $PROJECT_PATH/teamsnap/scripts/import_teamsnap.py"

0
teamsnap/__init__.py Normal file
View File

10
teamsnap/admin.py Normal file
View File

@@ -0,0 +1,10 @@
from django.contrib import admin
from .models import User, Team, Location, Event, Member, Availability
# Register your models here.
admin.site.register(User)
admin.site.register(Team)
admin.site.register(Event)
admin.site.register(Location)
admin.site.register(Member)
admin.site.register(Availability)

6
teamsnap/apps.py Normal file
View File

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

21
teamsnap/forms.py Normal file
View File

@@ -0,0 +1,21 @@
from django import forms
from .models import LineupEntry
from events.models import Event
from players.models import Player
from django.forms import modelformset_factory, inlineformset_factory, BaseModelFormSet,formset_factory
from crispy_forms.helper import FormHelper, Layout
class LineupEntryForm(forms.ModelForm):
availability = None
class Meta:
model = LineupEntry
widgets = {
'label': forms.Select(attrs={'class': 'form-control form-control-sm'})
}
exclude = ()
LineupEntryFormSet = modelformset_factory(
model=LineupEntry,
form=LineupEntryForm,
extra=0
)

View File

@@ -0,0 +1,82 @@
# Generated by Django 3.2.6 on 2021-11-20 23:53
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('teams', '0001_initial'),
('players', '0003_player_team'),
('venues', '0001_initial'),
('events', '0004_delete_availability'),
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('teamsnap_id', models.CharField(max_length=10)),
('access_token', models.CharField(max_length=50)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Team',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('teamsnap_id', models.CharField(max_length=10)),
('name', models.CharField(max_length=50, null=True)),
('team', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='teams.team')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Member',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('teamsnap_id', models.CharField(max_length=10)),
('name', models.CharField(max_length=50, null=True)),
('player', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='players.player')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Location',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('teamsnap_id', models.CharField(max_length=10)),
('name', models.CharField(max_length=50, null=True)),
('venue', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='venues.venue')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Event',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('teamsnap_id', models.CharField(max_length=10)),
('name', models.CharField(max_length=50, null=True)),
('label', models.CharField(max_length=50, null=True)),
('start_date', models.DateTimeField(null=True)),
('event', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='events.event')),
('location', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='teamsnap.location')),
('opponent', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='teamsnap.team')),
],
options={
'abstract': False,
},
),
]

View File

@@ -0,0 +1,43 @@
# Generated by Django 3.2.6 on 2021-11-21 00:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('teamsnap', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='event',
name='formatted_title',
field=models.CharField(max_length=50, null=True),
),
migrations.AlterField(
model_name='event',
name='teamsnap_id',
field=models.CharField(max_length=10, unique=True),
),
migrations.AlterField(
model_name='location',
name='teamsnap_id',
field=models.CharField(max_length=10, unique=True),
),
migrations.AlterField(
model_name='member',
name='teamsnap_id',
field=models.CharField(max_length=10, unique=True),
),
migrations.AlterField(
model_name='team',
name='teamsnap_id',
field=models.CharField(max_length=10, unique=True),
),
migrations.AlterField(
model_name='user',
name='teamsnap_id',
field=models.CharField(max_length=10, unique=True),
),
]

View File

@@ -0,0 +1,60 @@
# Generated by Django 3.2.6 on 2021-11-21 15:40
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('teamsnap', '0002_auto_20211121_0035'),
]
operations = [
migrations.RenameField(
model_name='event',
old_name='event',
new_name='bencoach_event',
),
migrations.RenameField(
model_name='location',
old_name='venue',
new_name='bencoach_venue',
),
migrations.RenameField(
model_name='member',
old_name='player',
new_name='bencoach_player',
),
migrations.RenameField(
model_name='team',
old_name='team',
new_name='bencoach_team',
),
migrations.AddField(
model_name='member',
name='first_name',
field=models.CharField(max_length=50, null=True),
),
migrations.AddField(
model_name='member',
name='is_non_player',
field=models.BooleanField(default=False),
preserve_default=False,
),
migrations.AddField(
model_name='member',
name='jersey_number',
field=models.IntegerField(null=True),
),
migrations.AddField(
model_name='member',
name='last_name',
field=models.CharField(max_length=50, null=True),
),
migrations.AddField(
model_name='member',
name='team',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='teamsnap.team'),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.2.6 on 2021-11-21 15:44
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('teamsnap', '0003_auto_20211121_1540'),
]
operations = [
migrations.RemoveField(
model_name='member',
name='name',
),
]

View File

@@ -0,0 +1,30 @@
# Generated by Django 3.2.6 on 2021-11-21 16:11
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('lineups', '0007_alter_positioning_position'),
('teamsnap', '0004_remove_member_name'),
]
operations = [
migrations.CreateModel(
name='Availability',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('teamsnap_id', models.CharField(max_length=10, unique=True)),
('status_code', models.SmallIntegerField(null=True)),
('benchcoach_availability', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='lineups.availability')),
('event', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='teamsnap.event')),
('member', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='teamsnap.member')),
('team', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='teamsnap.team')),
],
options={
'abstract': False,
},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.6 on 2021-11-21 16:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('teamsnap', '0005_availability'),
]
operations = [
migrations.AlterField(
model_name='availability',
name='status_code',
field=models.SmallIntegerField(choices=[(1, 'Yes'), (0, 'No'), (2, 'Maybe'), (None, 'Unknown')], default=None, null=True),
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 3.2.6 on 2021-11-21 16:28
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('teamsnap', '0006_alter_availability_status_code'),
]
operations = [
migrations.AddField(
model_name='event',
name='team',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='teamsnap.team'),
),
migrations.AlterField(
model_name='event',
name='opponent',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='opponent', to='teamsnap.team'),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.2.6 on 2021-11-21 16:36
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('teamsnap', '0007_auto_20211121_1628'),
]
operations = [
migrations.AlterModelOptions(
name='availability',
options={'verbose_name_plural': 'availabilities'},
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.2.6 on 2021-11-21 17:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('teamsnap', '0008_alter_availability_options'),
]
operations = [
migrations.AddField(
model_name='event',
name='points_for_opponent',
field=models.PositiveSmallIntegerField(null=True),
),
migrations.AddField(
model_name='event',
name='points_for_team',
field=models.PositiveSmallIntegerField(null=True),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.2.6 on 2021-11-21 18:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('teamsnap', '0009_auto_20211121_1757'),
]
operations = [
migrations.AddField(
model_name='event',
name='is_game',
field=models.BooleanField(default=False),
preserve_default=False,
),
]

View File

@@ -0,0 +1,27 @@
# Generated by Django 3.2.6 on 2021-11-21 18:47
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('teamsnap', '0010_event_is_game'),
]
operations = [
migrations.CreateModel(
name='LineupEntry',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('label', models.PositiveSmallIntegerField(blank=True, choices=[(11, 'EH'), (1, 'P'), (2, 'C'), (3, '1B'), (4, '2B'), (5, '3B'), (6, 'SS'), (7, 'LF'), (8, 'CF'), (9, 'RF'), (10, 'DH')], default=None, null=True)),
('sequence', models.PositiveSmallIntegerField(default=0, null=True)),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='teamsnap.event')),
('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='teamsnap.member')),
],
options={
'unique_together': {('member', 'event')},
},
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.2.6 on 2021-11-21 20:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('teamsnap', '0011_lineupentry'),
]
operations = [
migrations.AddField(
model_name='lineupentry',
name='name',
field=models.CharField(max_length=50, null=True),
),
migrations.AddField(
model_name='lineupentry',
name='teamsnap_id',
field=models.CharField(max_length=10, null=True, unique=True),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.2.6 on 2021-11-21 20:10
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('teamsnap', '0012_auto_20211121_2010'),
]
operations = [
migrations.RemoveField(
model_name='lineupentry',
name='name',
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.6 on 2021-11-21 20:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('teamsnap', '0013_remove_lineupentry_name'),
]
operations = [
migrations.AlterField(
model_name='lineupentry',
name='teamsnap_id',
field=models.CharField(blank=True, max_length=10, null=True, unique=True),
),
]

View File

133
teamsnap/models.py Normal file
View File

@@ -0,0 +1,133 @@
from django.db import models
import lineups.models
import teams.models
import venues.models
import players.models
import events.models
class TeamsnapBaseModel(models.Model):
teamsnap_id = models.CharField(max_length=10, unique=True)
name = models.CharField(max_length=50, null=True)
class Meta:
abstract = True
def __str__(self):
return f"{self.name} ({self.teamsnap_id})"
class User(TeamsnapBaseModel):
access_token = models.CharField(max_length = 50)
name = None
def __str__(self):
return f"{self.teamsnap_id}"
class Team(TeamsnapBaseModel):
bencoach_team = models.ForeignKey(teams.models.Team, null=True, on_delete=models.CASCADE)
@property
def view_url(self):
return f"https://go.teamsnap.com/{self.team.teamsnap_id}/team/view/{self.teamsnap_id}"
@property
def edit_url(self):
return f"https://go.teamsnap.com/{self.team.teamsnap_id}/team/edit/{self.teamsnap_id}"
class Location(TeamsnapBaseModel):
bencoach_venue = models.ForeignKey(venues.models.Venue, null=True, on_delete=models.CASCADE)
@property
def view_url(self):
return f"https://go.teamsnap.com/{self.team.teamsnap_id}/location/view/{self.teamsnap_id}"
@property
def edit_url(self):
return f"https://go.teamsnap.com/{self.team.teamsnap_id}/location/edit/{self.teamsnap_id}"
class Member(TeamsnapBaseModel):
name = None
bencoach_player = models.ForeignKey(players.models.Player, null=True, on_delete=models.CASCADE)
team = models.ForeignKey(Team, null=True, on_delete=models.CASCADE)
first_name = models.CharField(max_length = 50, null=True)
last_name = models.CharField(max_length = 50, null=True)
jersey_number = models.IntegerField(null=True)
is_non_player = models.BooleanField()
def __str__(self):
return f"{self.last_name}, {self.first_name} ({self.teamsnap_id})"
@property
def view_url(self):
return f"https://go.teamsnap.com/{self.team.teamsnap_id}/roster/player/{self.teamsnap_id}"
@property
def edit_url(self):
return f"https://go.teamsnap.com/{self.team.teamsnap_id}/roster/edit/{self.teamsnap_id}"
class Event(TeamsnapBaseModel):
bencoach_event = models.ForeignKey(events.models.Event, null=True, on_delete=models.CASCADE)
label = models.CharField(max_length = 50, null=True)
start_date = models.DateTimeField(null=True)
opponent = models.ForeignKey(Team, null=True, on_delete=models.CASCADE, related_name="opponent")
team = models.ForeignKey(Team, null=True, on_delete=models.CASCADE)
location = models.ForeignKey(Location, null=True, on_delete=models.CASCADE)
formatted_title = models.CharField(max_length = 50, null=True)
points_for_opponent = models.PositiveSmallIntegerField(null=True)
points_for_team = models.PositiveSmallIntegerField(null=True)
is_game = models.BooleanField()
@property
def view_url(self):
return f"https://go.teamsnap.com/{self.team.teamsnap_id}/schedule/view_game/{self.teamsnap_id}"
@property
def edit_url(self):
return f"https://go.teamsnap.com/{self.team.teamsnap_id}/schedule/edit_game/{self.teamsnap_id}"
def __str__(self):
return f"{self.formatted_title} ({self.teamsnap_id})"
class Availability(TeamsnapBaseModel):
status_codes = [
(1, 'Yes'),
(0, 'No'),
(2, 'Maybe'),
(None, 'Unknown')
]
name = None
team = models.ForeignKey(Team, null=True, on_delete=models.CASCADE)
event = models.ForeignKey(Event, null=True, on_delete=models.CASCADE)
member = models.ForeignKey(Member, null=True, on_delete=models.CASCADE)
benchcoach_availability = models.ForeignKey(lineups.models.Availability, null=True, on_delete=models.CASCADE)
status_code = models.SmallIntegerField(null=True, choices=status_codes, default=None)
def __str__(self):
return f"{self.member} - {self.event} ({self.teamsnap_id})"
class Meta:
verbose_name_plural = "availabilities"
class LineupEntry(TeamsnapBaseModel):
name = None
teamsnap_id = models.CharField(max_length=10, unique=True, null=True, blank=True)
member = models.ForeignKey(Member, on_delete=models.CASCADE)
event = models.ForeignKey(Event, on_delete=models.CASCADE)
positions = [
(11, 'EH'),
(1, 'P'),
(2, 'C'),
(3, '1B'),
(4, '2B'),
(5, '3B'),
(6, 'SS'),
(7, 'LF'),
(8, 'CF'),
(9, 'RF'),
(10,'DH')
]
label = models.PositiveSmallIntegerField(choices=positions, default=None, null=True, blank=True)
sequence = models.PositiveSmallIntegerField(default=0, null=True, blank=True)
class Meta:
unique_together = ('member', 'event',)

View File

@@ -0,0 +1,3 @@
from .api import TeamSnap
__all__ = ['TeamSnap']

118
teamsnap/teamsnap/api.py Normal file
View File

@@ -0,0 +1,118 @@
__all__ = ['TeamSnap', 'Team', 'Event', 'Availability', 'Member', 'Location', 'Me']
from apiclient import APIClient, HeaderAuthentication, JsonResponseHandler
class ApiObject():
rel = None
def __init__(self, client, rel=rel, data={}):
self.client = client
self.data = data
self.rel = rel
@classmethod
def search(cls, client, **kwargs):
results = client.query(cls.rel, "search", **kwargs)
return [cls(client,rel=cls.rel, data=r) for r in results]
@classmethod
def get(cls, client, id):
r = client.get(f"{client.link(cls.rel)}/{id}")
return cls(client, cls.rel, client.parse_response(r)[0])
class Me (ApiObject):
rel = "me"
def __init__(self, client):
super().__init__(client=client, rel=self.rel, data=client.get(client.link(self.rel)))
class Event (ApiObject):
rel = "events"
class Team (ApiObject):
rel = "teams"
pass
class Availability (ApiObject):
rel = "availabilities"
pass
class Member (ApiObject):
rel = "members"
class Location (ApiObject):
rel = "locations"
class Opponent (ApiObject):
rel = "opponents"
class EventLineupEntry (ApiObject):
rel = "event_lineup_entries"
class Statistics (ApiObject):
rel = "statistics"
class MemberStatistics (ApiObject):
rel = "member_statistics"
class TeamSnap(APIClient):
base_url = 'https://api.teamsnap.com/v3'
def __init__(self, token, *args, **kwargs):
super().__init__(*args,
authentication_method=HeaderAuthentication(token=token),
response_handler=JsonResponseHandler,
**kwargs)
self._root_collection = self.get(self.base_url)['collection']
self._links = self._by_rel(self.base_url, 'links')
self._queries = self._by_rel(self.base_url, 'queries')
self._commands = self._by_rel(self.base_url, 'commands')
pass
def link(self, link_name):
d = {l['rel']:l['href'] for l in self._root_collection["links"]}
return d.get(link_name)
def _by_rel (self, url, k):
try:
{l['rel']: l for l in self._root_collection[k]}
except Exception as e:
return {}
self.get(url)['collection'][k]
return {l['rel']:l for l in self.get(url)['collection'][k]}
def query (self, rel, query, **kwargs):
queries = self._by_rel(self._get_href(rel), 'queries')
response = self.get(self._get_href(query, queries), params=kwargs)
return self.parse_response(response)
def command (self, rel, command, **kwargs):
commands = self._by_rel(self._get_href(rel), 'commands')
response = self.get(self._get_href(command, commands), params=kwargs)
return self.parse_response(response)
def _get_href (self, rel: str, links:dict = None, url = base_url) -> str:
"""returns a hyperlink from a the links dictionary. Each item in the links dictionary is a
dictionary with a rel and href key"""
if links is None: links = self._by_rel(url, 'links')
link = links[rel]['href']
return link
def get_item (self, rel, id):
r = self.get(f"{self.link(rel)}/{id}")
return self.parse_response(r)[0]
@classmethod
def parse_response(self, response):
result = []
items = [item['data'] for item in response['collection'].get('items',[])]
for item in response['collection'].get('items',[]):
details = {}
for detail in item['data']:
# TODO type casting and validation based on item['type']
details[detail['name']] = detail['value']
result.append(details)
return result
# return [{detail['name']: detail['value'] for detail in item} for item in items]

View File

@@ -0,0 +1,23 @@
{% extends "base.html" %}
{% block title %} {{ title }}{% endblock %}
{% block content %}
<h1>{{ title }}</h1>
{#<ol class="list-group">#}
{% for item in object_list %}
<ul class="card">
<span class="card-title">{{ item.formatted_title }}</span>
<span class="fs-6">{{ item.subtitle }}</span>
{# {% if item.body %}#}
{# <br><span class="fs-6">{{ item.body }}</span>#}
<br>{{ item.start_date|date:"D, M j, g:i A" }},<br>{{item.location.name}}
{# {% endif %}#}
<br>
{% for button in item.buttons %}
<a class="btn btn-primary btn-sm" href="{{ button.href }}" role="button">{{ button.label }}</a>
{% endfor %}
</ul>
{% endfor %}
</ol>
{% endblock %}

View File

@@ -0,0 +1,157 @@
{% extends 'base.html' %}{% block title %} {{ title }} {% endblock %}{% load crispy_forms_tags %}{% load static %}
{% block content %}
<div class="w-100 text-center mx-auto text-center">
<h1 class="display-5 fw-bold"><a href="#" class="btn btn-primary btn-secondary p-1 mx-3"><</a>{{ event.formatted_title }}<a href="#" class="btn btn-primary btn-secondary p-1 mx-3">></a></h1>
<div class="lead">
<p class="">{{ event.start_date|date:"l, F j, Y g:i A" }}<br>{{ event.location.name }}</p>
</div>
</div>
<div class="container">
<form action="{% url 'teamsnap edit lineup' event_id=event.id%}" method="post">
{% csrf_token %}
{{ formset.management_form }}
<div class="row">
{# <input type="submit" value="Submit" class="btn btn-sm btn-success mx-3 my-0 my-0">#}
<div class="col-md-6">
<div class="card my-1">
<div class="card-header"><h5>Lineup</h5></div>
<div class="card-body p-0">
{% include 'teamsnap/player-table.html' with table_id="lineup" formset=formset_lineup available_class="d-none"%}
<div class="justify-content-md-end d-md-flex m-2"><input type="submit" value="Submit" class="btn btn-primary"></div>
</div>
</div>
<div class="card my-1">
<div class="card-header"><h5>DH'd</h5></div>
<div class="card-body p-0">
{% include 'teamsnap/player-table.html' with table_id="dhd" formset=formset_dhd available_class="d-none" sequence_class="d-none"%}
<div class="justify-content-md-end d-md-flex m-2"><input type="submit" value="Submit" class="btn btn-primary"></div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card my-1">
<div class="card-header"><h5>Bench</h5></div>
<div class="card-body p-0">
{% include 'teamsnap/player-table.html' with table_id="bench" formset=formset_bench sequence_class="d-none"%}
</div>
</div>
</div>
</div>
</form>
</div>
<script src="{% static 'js/Sortable.js' %}"></script>
<script id="sortable">
function refresh_lineup_sequence (){
var member_rows = document.getElementById('lineup').querySelectorAll('tr')
var has_dh = false
for (let i = 0; i < member_rows.length; i++) {
if (member_rows[i].dataset.sequence == 0) {
has_dh = true
continue
}
if (has_dh) {
member_rows[i].dataset.sequence = i
}
else {
member_rows[i].dataset.sequence = i+1
}
var member_sequence = member_rows[i].querySelector('[id^="member-sequence-button"]')
var form_element_sequence = member_rows[i].querySelector('[id$="sequence"]')
member_sequence.innerText = parseInt(member_rows[i].dataset.sequence)
form_element_sequence.value = parseInt(member_rows[i].dataset.sequence)
}
}
var lineup = new Sortable.create(
document.getElementById('dhd'), {
animation: 150,
ghostClass: "ghost",
{#handle: ".bars-move",#}
group: {
put: true,
pull: true
}
})
var lineup = new Sortable.create(
document.getElementById('lineup'), {
animation: 150,
ghostClass:"ghost",
{#handle: ".bars-move",#}
group:{
put:true,
pull:true
},
onAdd: function (/**Event*/evt) {
// Add to Lineup
var itemEl = evt.item; // dragged HTMLElement
var member_id = itemEl.dataset.memberId
console.log(itemEl)
var form_element_sequence =itemEl.querySelector('[id$="sequence"]')
var member_sequence = itemEl.querySelector('[id^="member-sequence"]')
var member_available =itemEl.querySelector('[id^="member-available"]')
var member_sequence_button =itemEl.querySelector('[id^="member-sequence-button"]')
console.log(member_sequence.parentElement.dataset)
toggle_in_lineup(member_sequence_button)
member_sequence.parentElement.dataset.sequence = evt.newIndex
refresh_lineup_sequence()
{#member_available.parentElement.style.display="none"#}
member_available.parentElement.classList.add('d-none')
{#member_sequence.style.display="table-cell"#}
member_sequence.classList.remove('d-none')
},
onUpdate: function (/**Event*/evt) {
console.log('update to lineup')
var itemEl = evt.item; // dragged HTMLElement
refresh_lineup_sequence()
},
});
var bench = new Sortable.create(
document.getElementById('bench'), {
animation: 150,
ghostClass:"ghost",
sort: false,
{#handle: ".bars-move",#}
group:{
put:true,
pull:true
},
onAdd: function (/**Event*/evt) {
console.log('added to bench')
var itemEl = evt.item; // dragged HTMLElement
console.log(itemEl)
var form_element_sequence =itemEl.querySelector('[id$="sequence"]')
var member_sequence = itemEl.querySelector('[id^="member-sequence"]')
var member_available =itemEl.querySelector('[id^="member-available"]')
{#member_available.parentElement.style.display="table-cell"#}
member_available.parentElement.classList.remove('d-none')
form_element_sequence.value = 0
member_sequence.innerHTML = 1
{#member_sequence.style.display="none"#}
member_sequence.classList.add('d-none')
var member_id = itemEl.dataset.memberId
refresh_lineup_sequence()
}
});
function toggle_in_lineup(sequence_button){
var member_row = sequence_button.parentNode.parentNode
if (member_row.dataset.sequence == 0) {
sequence_button.innerText = "1"
sequence_button.classList.add("btn-light")
sequence_button.classList.remove("btn-dark")
member_row.dataset.sequence = 1
}
else {
sequence_button.innerText = "D"
sequence_button.classList.remove("btn-light")
sequence_button.classList.add("btn-dark")
member_row.dataset.sequence = 0
}
refresh_lineup_sequence()
}
</script>
{% endblock %}

View File

@@ -0,0 +1,86 @@
<table class="table">
<thead>
<tr class="border border-light">
{# <th scope="col" style="display: none"></th>#}
{# <th scope="col" class="border border-light"></th>#}
{# <th scope="col">Name</th>#}
{# <th scope="col">Pos</th>#}
</tr>
</thead>
<tbody id={{ table_id }}>
{% for form in formset %}
<tr data-member-id="{{ form.instance.member.id }}"
data-position="{{ form.instance.position }}"
data-sequence="{{ form.instance.sequence }}">
{{ form.id.as_hidden }}
{{ form.event.as_hidden }}
{{ form.sequence.as_hidden }}
{{ form.member.as_hidden }}
{{ form.teamsnap_id.as_hidden }}
<td id="member-availability-{{ form.instance.member.id }}"
class="{{ available_class }}"
>
{% if form.availability.status_code == 2 %}
<button class="btn btn-light bg-info p-1"
id="member-available-{{ form.instance.member.id }}"
>
<span style="visibility: hidden">2</span>
<span class="visually-hidden">Maybe</span>
</button>
{% elif form.availability.status_code == 1%}
<button class="btn btn-light bg-success p-1"
id="member-available-{{ form.instance.member.id }}"
>
<span style="visibility: hidden">1</span>
</button>
<span class="visually-hidden">Maybe</span>
{% elif form.availability.status_code == 0%}
<button class="btn btn-light bg-danger p-1"
id="member-available-{{ form.instance.member.id }}"
>
<span style="visibility: hidden">0</span>
</button>
<span class="visually-hidden">No</span>
{% else %}
<button class="btn btn-light bg-secondary p-1"
id="member-available-{{ form.instance.member.id }}"
>
<span style="visibility: hidden">X</span>
</button>
<span class="visually-hidden">Unknown</span>
{% endif %}
</td>
<th scope="row"
id="member-sequence-{{ form.instance.member.id }}"
class="{{ sequence_class }}">
{% if form.sequence.value %}
<button type="button"
class="btn btn-light p-1"
id="member-sequence-button-{{ form.instance.member.id }}"
onclick="toggle_in_lineup(this)"
>
{{ form.sequence.value }}
</button>
{% elif form.sequence.value == 0 %}
<button type="button"
class="btn btn-dark p-1"s
id="member-sequence-button-{{ form.instance.member.id }}"
onclick="toggle_in_lineup(this)"
>
D
</button>
{% endif %}
</th>
<th>
{{ form.instance.member.first_name }}&nbsp;{{ form.instance.member.last_name }}&nbsp;
<small class="text-muted fw-light">#{{ form.instance.member.jersey_number }}</small>
{# <br><code><small>{{ form.statline }}</small></code>#}
</th>
<td>
{{ form.label }}
</td>
{# <td>{{ form.instance.position }}</td>#}
</tr>
{% endfor %}
</tbody>
</table>

3
teamsnap/tests.py Normal file
View File

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

12
teamsnap/urls.py Normal file
View File

@@ -0,0 +1,12 @@
from django.contrib import admin
from django.urls import path, include
from functools import partial
from . import views
urlpatterns = [
path('events', views.EventsListView.as_view(), name="teamsnap list events"),
path('edit/event/<int:id>', views.edit_event, name='teamsnap edit event'),
path('edit/lineup/<int:event_id>', views.edit_lineup, name='teamsnap edit lineup')
]

114
teamsnap/views.py Normal file
View File

@@ -0,0 +1,114 @@
from django.shortcuts import render, redirect
# from .teamsnap.api import TeamSnap, Team, Event, Availability
from .models import User, Member, Team, Event, Location, LineupEntry
from django.views.generic.list import ListView
from lib.views import BenchcoachListView
from .forms import LineupEntryForm, LineupEntryFormSet
from django.forms.models import model_to_dict
from django.urls import reverse
from django.db.models import Case, When
def queryset_from_ids(Model, id_list):
#https://stackoverflow.com/questions/4916851/django-get-a-queryset-from-array-of-ids-in-specific-order
preserved = Case(*[When(pk=pk, then=pos) for pos, pk in enumerate(id_list)])
queryset = Model.objects.filter(pk__in=id_list).order_by(preserved)
return queryset
def edit_event(request, id):
event = Event.objects.get(id = id)
return redirect(event.edit_url)
class EventsListView(BenchcoachListView):
Model = Event
edit_url = 'teamsnap edit event'
list_url = 'teamsnap list events'
page_title = "TeamSnap Events"
title_strf = '{item.formatted_title}'
body_strf = "{item.start_date:%a, %b %-d, %-I:%M %p},\n{item.location.name}"
def get_context_data(self):
context = super().get_context_data()
for item in context['items']:
item['buttons'].append(
{
'label': 'Edit Lineup',
'href': reverse('teamsnap edit lineup', args=[item['id']])
}
)
return context
class TeamListView(BenchcoachListView):
Model = Team
edit_url = 'teamsnap edit team'
list_url = 'teamsnap list teams'
page_title = "TeamSnap Teams"
class LocationListView(BenchcoachListView):
Model = Location
edit_url = 'teamsnap edit location'
list_url = 'teamsnap list locations'
page_title = "TeamSnap Locations"
def edit_lineup(request, event_id):
if request.method == 'POST':
# create a form instance and populate it with data from the request:
formset = LineupEntryFormSet(request.POST)
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'], LineupEntry):
positioning_id = form.cleaned_data.pop('id').id #FIXME this is a workaround, not sure why it is necessary
positioning = LineupEntry.objects.filter(id=positioning_id)
positioning.update(**form.cleaned_data)
did_create = False
else:
positioning = LineupEntry.objects.create(**form.cleaned_data, event_id=event_id)
did_create = True
else:
pass
return render(request, 'success.html', {'call_back':'teamsnap edit lineup','id':event_id, 'errors':[error for error in formset.errors if error]}, status=200)
# return render(request, 'success.html', {'call_back':'schedule'})
event = Event.objects.get(id=event_id)
members = Member.objects.filter(is_non_player=False).prefetch_related('availability_set', 'lineupentry_set')
# players_d.sort(key=lambda d: (-d['availability'].available, d['last_name']))
for member in members:
LineupEntry.objects.get_or_create(member_id=member.id, event_id=event_id)
qs_starting_lineup = LineupEntry.objects.filter(event_id=event_id, sequence__isnull=False, sequence__gt=0).order_by('sequence')
qs_bench = LineupEntry.objects.filter(event_id=event_id, sequence=0).prefetch_related('member__availability_set').order_by('member__last_name')
# This is all a compromise to get the sorting just the way I wanted. THERE'S GOT TO BE A BETTER WAY
ids_starting_lineup = [item.id for item in qs_starting_lineup]
ids_bench_available = [item.id for item in qs_bench
if item.member.availability_set.get(event_id=event_id).status_code == 1]
ids_bench_maybe = [item.id for item in qs_bench
if item.member.availability_set.get(event_id=event_id).status_code == 2]
ids_bench_no = [item.id for item in qs_bench
if item.member.availability_set.get(event_id=event_id).status_code == 0]
ids_bench_unknown = [item.id for item in qs_bench
if item.member.availability_set.get(event_id=event_id).status_code is None]
qset = queryset_from_ids(LineupEntry, ids_starting_lineup + ids_bench_available + ids_bench_maybe + ids_bench_no + ids_bench_unknown)
formset = LineupEntryFormSet(queryset=qset)
for f in formset:
if f.instance.member_id:
f.availability = f.instance.member.availability_set.get(event_id=event_id)
# f.statline = f.instance.member.statline_set.get()
formset_lineup = [f for f in formset if f.instance.sequence]
formset_bench = [f for f in formset if f not in formset_lineup]
formset_dhd = [f for f in formset if not f.instance.sequence and f.instance.label]
return render(request, 'teamsnap/lineup.html', {'title': 'Lineup',
'event': event,
'formset_lineup': formset_lineup,
'formset_bench':formset_bench,
'formset_dhd':formset_dhd
})

View File

@@ -5,13 +5,20 @@
<head>
<meta http-equiv="refresh" content="2;url={{ call_back_url }}" />
<meta http-equiv="refresh" content="2;url={{ call_back_url }}" />
</head>
<span class="text-success">Success!</span> <span class="text-muted">Redirecting...</span>
{% for error in errors %}
error
{% endfor %}
{% if errors %}
<span class="text-danger">Errors...</span>
{% for error in errors %}
error
{% endfor %}
{% else %}
<span class="text-success">Success!</span>
{% endif %}
<span class="text-muted">Redirecting...</span>
{% endblock %}