diff --git a/benchcoach/settings.py b/benchcoach/settings.py index d22ae61..c9a9752 100644 --- a/benchcoach/settings.py +++ b/benchcoach/settings.py @@ -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', diff --git a/benchcoach/urls.py b/benchcoach/urls.py index d7b5795..64d4e33 100644 --- a/benchcoach/urls.py +++ b/benchcoach/urls.py @@ -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) diff --git a/benchcoach/views.py b/benchcoach/views.py index 1a7c97f..9d919d4 100644 --- a/benchcoach/views.py +++ b/benchcoach/views.py @@ -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}) \ No newline at end of file diff --git a/lineups/migrations/0007_alter_positioning_position.py b/lineups/migrations/0007_alter_positioning_position.py new file mode 100644 index 0000000..08c686c --- /dev/null +++ b/lineups/migrations/0007_alter_positioning_position.py @@ -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), + ), + ] diff --git a/reload_teamsnap.sh b/reload_teamsnap.sh new file mode 100644 index 0000000..e5d85e2 --- /dev/null +++ b/reload_teamsnap.sh @@ -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" \ No newline at end of file diff --git a/teamsnap/__init__.py b/teamsnap/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/teamsnap/admin.py b/teamsnap/admin.py new file mode 100644 index 0000000..b5e2280 --- /dev/null +++ b/teamsnap/admin.py @@ -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) \ No newline at end of file diff --git a/teamsnap/apps.py b/teamsnap/apps.py new file mode 100644 index 0000000..ba18c28 --- /dev/null +++ b/teamsnap/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TeamsnapConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'teamsnap' diff --git a/teamsnap/forms.py b/teamsnap/forms.py new file mode 100644 index 0000000..f1fd691 --- /dev/null +++ b/teamsnap/forms.py @@ -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 +) diff --git a/teamsnap/migrations/0001_initial.py b/teamsnap/migrations/0001_initial.py new file mode 100644 index 0000000..d5ee178 --- /dev/null +++ b/teamsnap/migrations/0001_initial.py @@ -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, + }, + ), + ] diff --git a/teamsnap/migrations/0002_auto_20211121_0035.py b/teamsnap/migrations/0002_auto_20211121_0035.py new file mode 100644 index 0000000..7d9b4c8 --- /dev/null +++ b/teamsnap/migrations/0002_auto_20211121_0035.py @@ -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), + ), + ] diff --git a/teamsnap/migrations/0003_auto_20211121_1540.py b/teamsnap/migrations/0003_auto_20211121_1540.py new file mode 100644 index 0000000..2b78008 --- /dev/null +++ b/teamsnap/migrations/0003_auto_20211121_1540.py @@ -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'), + ), + ] diff --git a/teamsnap/migrations/0004_remove_member_name.py b/teamsnap/migrations/0004_remove_member_name.py new file mode 100644 index 0000000..19dff06 --- /dev/null +++ b/teamsnap/migrations/0004_remove_member_name.py @@ -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', + ), + ] diff --git a/teamsnap/migrations/0005_availability.py b/teamsnap/migrations/0005_availability.py new file mode 100644 index 0000000..02e08c5 --- /dev/null +++ b/teamsnap/migrations/0005_availability.py @@ -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, + }, + ), + ] diff --git a/teamsnap/migrations/0006_alter_availability_status_code.py b/teamsnap/migrations/0006_alter_availability_status_code.py new file mode 100644 index 0000000..24aeb64 --- /dev/null +++ b/teamsnap/migrations/0006_alter_availability_status_code.py @@ -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), + ), + ] diff --git a/teamsnap/migrations/0007_auto_20211121_1628.py b/teamsnap/migrations/0007_auto_20211121_1628.py new file mode 100644 index 0000000..638c677 --- /dev/null +++ b/teamsnap/migrations/0007_auto_20211121_1628.py @@ -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'), + ), + ] diff --git a/teamsnap/migrations/0008_alter_availability_options.py b/teamsnap/migrations/0008_alter_availability_options.py new file mode 100644 index 0000000..a9e2671 --- /dev/null +++ b/teamsnap/migrations/0008_alter_availability_options.py @@ -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'}, + ), + ] diff --git a/teamsnap/migrations/0009_auto_20211121_1757.py b/teamsnap/migrations/0009_auto_20211121_1757.py new file mode 100644 index 0000000..6b49c38 --- /dev/null +++ b/teamsnap/migrations/0009_auto_20211121_1757.py @@ -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), + ), + ] diff --git a/teamsnap/migrations/0010_event_is_game.py b/teamsnap/migrations/0010_event_is_game.py new file mode 100644 index 0000000..a7fde38 --- /dev/null +++ b/teamsnap/migrations/0010_event_is_game.py @@ -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, + ), + ] diff --git a/teamsnap/migrations/0011_lineupentry.py b/teamsnap/migrations/0011_lineupentry.py new file mode 100644 index 0000000..da9e16b --- /dev/null +++ b/teamsnap/migrations/0011_lineupentry.py @@ -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')}, + }, + ), + ] diff --git a/teamsnap/migrations/0012_auto_20211121_2010.py b/teamsnap/migrations/0012_auto_20211121_2010.py new file mode 100644 index 0000000..2294c80 --- /dev/null +++ b/teamsnap/migrations/0012_auto_20211121_2010.py @@ -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), + ), + ] diff --git a/teamsnap/migrations/0013_remove_lineupentry_name.py b/teamsnap/migrations/0013_remove_lineupentry_name.py new file mode 100644 index 0000000..763bba4 --- /dev/null +++ b/teamsnap/migrations/0013_remove_lineupentry_name.py @@ -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', + ), + ] diff --git a/teamsnap/migrations/0014_alter_lineupentry_teamsnap_id.py b/teamsnap/migrations/0014_alter_lineupentry_teamsnap_id.py new file mode 100644 index 0000000..214a198 --- /dev/null +++ b/teamsnap/migrations/0014_alter_lineupentry_teamsnap_id.py @@ -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), + ), + ] diff --git a/teamsnap/migrations/__init__.py b/teamsnap/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/teamsnap/models.py b/teamsnap/models.py new file mode 100644 index 0000000..f1aa5ea --- /dev/null +++ b/teamsnap/models.py @@ -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',) diff --git a/teamsnap/teamsnap/__init__.py b/teamsnap/teamsnap/__init__.py new file mode 100644 index 0000000..31d7a75 --- /dev/null +++ b/teamsnap/teamsnap/__init__.py @@ -0,0 +1,3 @@ +from .api import TeamSnap + +__all__ = ['TeamSnap'] \ No newline at end of file diff --git a/teamsnap/teamsnap/api.py b/teamsnap/teamsnap/api.py new file mode 100644 index 0000000..0d8d584 --- /dev/null +++ b/teamsnap/teamsnap/api.py @@ -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] + diff --git a/teamsnap/templates/teamsnap/event_list.html b/teamsnap/templates/teamsnap/event_list.html new file mode 100644 index 0000000..c973204 --- /dev/null +++ b/teamsnap/templates/teamsnap/event_list.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} +{% block title %} {{ title }}{% endblock %} + +{% block content %} + +

{{ title }}

+{#
    #} +{% for item in object_list %} + +{% endfor %} +
+{% endblock %} \ No newline at end of file diff --git a/teamsnap/templates/teamsnap/lineup.html b/teamsnap/templates/teamsnap/lineup.html new file mode 100644 index 0000000..14d0dd1 --- /dev/null +++ b/teamsnap/templates/teamsnap/lineup.html @@ -0,0 +1,157 @@ +{% extends 'base.html' %}{% block title %} {{ title }} {% endblock %}{% load crispy_forms_tags %}{% load static %} + +{% block content %} +
+

<{{ event.formatted_title }}>

+
+

{{ event.start_date|date:"l, F j, Y g:i A" }}
{{ event.location.name }}

+
+
+
+
+ {% csrf_token %} + {{ formset.management_form }} +
+{# #} +
+
+
Lineup
+
+ {% include 'teamsnap/player-table.html' with table_id="lineup" formset=formset_lineup available_class="d-none"%} +
+
+
+
+
DH'd
+
+ {% include 'teamsnap/player-table.html' with table_id="dhd" formset=formset_dhd available_class="d-none" sequence_class="d-none"%} +
+
+
+
+ +
+
+
Bench
+
+ {% include 'teamsnap/player-table.html' with table_id="bench" formset=formset_bench sequence_class="d-none"%} +
+
+
+ +
+
+
+ + + +{% endblock %} \ No newline at end of file diff --git a/teamsnap/templates/teamsnap/player-table.html b/teamsnap/templates/teamsnap/player-table.html new file mode 100644 index 0000000..e56044b --- /dev/null +++ b/teamsnap/templates/teamsnap/player-table.html @@ -0,0 +1,86 @@ + + + + {# #} + {# #} + {# #} + {# #} + + + + {% for form in formset %} + + {{ form.id.as_hidden }} + {{ form.event.as_hidden }} + {{ form.sequence.as_hidden }} + {{ form.member.as_hidden }} + {{ form.teamsnap_id.as_hidden }} + + + + + {# #} + + {% endfor %} + +
NamePos
+ {% if form.availability.status_code == 2 %} + + {% elif form.availability.status_code == 1%} + + Maybe + {% elif form.availability.status_code == 0%} + + No + {% else %} + + Unknown + {% endif %} + + {% if form.sequence.value %} + + {% elif form.sequence.value == 0 %} + + {% endif %} + + {{ form.instance.member.first_name }} {{ form.instance.member.last_name }}  + #{{ form.instance.member.jersey_number }} + {#
{{ form.statline }}#} +
+ {{ form.label }} + {{ form.instance.position }}
\ No newline at end of file diff --git a/teamsnap/tests.py b/teamsnap/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/teamsnap/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/teamsnap/urls.py b/teamsnap/urls.py new file mode 100644 index 0000000..fbc48f4 --- /dev/null +++ b/teamsnap/urls.py @@ -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/', views.edit_event, name='teamsnap edit event'), + path('edit/lineup/', views.edit_lineup, name='teamsnap edit lineup') +] \ No newline at end of file diff --git a/teamsnap/views.py b/teamsnap/views.py new file mode 100644 index 0000000..c083154 --- /dev/null +++ b/teamsnap/views.py @@ -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 + }) \ No newline at end of file diff --git a/templates/success.html b/templates/success.html index e83e9e8..ef2da08 100644 --- a/templates/success.html +++ b/templates/success.html @@ -5,13 +5,20 @@ - + - Success! Redirecting... - {% for error in errors %} - error - {% endfor %} + {% if errors %} + Errors... + {% for error in errors %} + error + {% endfor %} + + {% else %} + Success! + {% endif %} + Redirecting... + {% endblock %} \ No newline at end of file