Files
benchcoach-django/teamsnap/utils/teamsnap_sync_engine.py
2022-01-03 12:12:37 -06:00

524 lines
26 KiB
Python

import django.db.models
from typing import List, Tuple
import benchcoach.models
from benchcoach.models import BenchcoachModel, Availability, Player, Team, Positioning, Event, Venue
from teamsnap.teamsnap.api import TeamSnap
import teamsnap.models
from django.db.models import QuerySet
from benchcoach.utils.sync_engine import AbstractSyncEngine
class TeamsnapSyncEngine(AbstractSyncEngine):
models = [
Availability,
Player,
Team,
# Positioning, # Not Implemented
Event,
Venue
]
def __init__(self, managed_team_teamsnap_id, teamsnap_token):
self.managed_teamsnap_team_id = managed_team_teamsnap_id
self.client = TeamSnap(token=teamsnap_token)
def _bulk_sync_from_teamsnap(self, qs:QuerySet)-> List[BenchcoachModel]:
'''
Syncs BenchCoach instances (in the form of a QuerySet) from TeamSnap.
This function fetches the actual information from teamsnap, then hands it off to the self._update* functions.
This funciton cuts down on the number of API calls and should be speedier then doing them one by one.
Upating of models from the data is still done one by one.
:param benchcoach_instance: instance to be synced
:return: List of BenchCoach objects that have been processed (but not necessarily changed) during sync.
'''
# I hate having this translation. What I really want is just a property for "teamsnap_object"
# which would simplify all this, but I couldn't figure out how to implement in the
# teamsnap model foreign key and "related_name" that didn't cause conflicts. I don't
# think I need to be too much smarter to figure this out, but alas I am not smart enough.
if qs.model not in self.models:
raise TypeError(f"Sync engine does not sync {qs.model} models")
benchcoachmodel_to_teamsnapfield = {
Availability:'teamsnap_availability',
Player:'teamsnap_member',
Team:'teamsnap_opponent',
# Positioning:'teamsnap_lineupentry', # Not Implemented Yet, but will be 'teamsnap_lineupentry'
Event:'teamsnap_event',
Venue:'teamsnap_location'
}
teamsnapmodel_to_apiobject = {
teamsnap.models.Availability: teamsnap.teamsnap.api.Availability,
teamsnap.models.Event: teamsnap.teamsnap.api.Event,
# teamsnap.models.LineupEntry:teamsnap.teamsnap.api.LineupEntry, # Not implemented Yet
teamsnap.models.Location: teamsnap.teamsnap.api.Location,
teamsnap.models.Member: teamsnap.teamsnap.api.Member,
teamsnap.models.Opponent: teamsnap.teamsnap.api.Opponent,
teamsnap.models.Team: teamsnap.teamsnap.api.Team,
# teamsnap.models.User:teamsnap.teamsnap.api.User # Not implemented yet
}
apiobject_to_teamsnapmodel = {v:k for k,v in teamsnapmodel_to_apiobject.items()}
if isinstance(qs.first(), benchcoach.models.Team):
# This situation requires special attention because opponents and teams share a table in BenchCoach
if getattr(qs.first(), 'teamsnap_team', None):
teamsnap_attribute_name = 'teamsnap_team'
elif getattr(qs.first(), 'teamsnap_opponent', None):
teamsnap_attribute_name = 'teamsnap_opponent'
else:
teamsnap_attribute_name = benchcoachmodel_to_teamsnapfield.get(type(qs.first()))
ids = [getattr(i, teamsnap_attribute_name).id for i in qs]
ApiObject = teamsnapmodel_to_apiobject.get(type(getattr(qs.first(), teamsnap_attribute_name)))
api_responses = ApiObject.search(client=self.client, id=",".join(ids))
r = []
for api_response in api_responses:
teamsnap_instance = apiobject_to_teamsnapmodel.get(type(api_response)).objects.get(id=api_response.data['id'])
response = self._update_from_teamsnapdata(teamsnap_instance, api_response)
response = self._update_teamsnapdb_to_benchcoachdb(teamsnap_instance, teamsnap_instance.benchcoach_object)
r.append(response)
return r
def _sync_from_teamsnap(self, benchcoach_instance:BenchcoachModel)->BenchcoachModel:
'''
Syncs BenchCoach instance from TeamSnap. This function fetches the actual information from teamsnap, then
hands it off to the self._update* functions.
:param benchcoach_instance: instance to be synced
:return: BenchCoach object that has been processed (but not necessarily changed) during sync.
'''
# I hate having this translation. What I really want is just a property for "teamsnap_object"
# which would simplify all this, but I couldn't figure out how to implement in the
# teamsnap model foreign key and "related_name" that didn't cause conflicts. I don't
# think I need to be too much smarter to figure this out, but alas I am not smart enough.
benchcoachmodel_to_teamsnapfield = {
Availability:'teamsnap_availability',
Player:'teamsnap_member',
Team:'teamsnap_opponent',
# Positioning:'teamsnap_lineupentry', # Not Implemented Yet, but will be 'teamsnap_lineupentry'
Event:'teamsnap_event',
Venue:'teamsnap_location'
}
teamsnapmodel_to_apiobject = {
teamsnap.models.Availability: teamsnap.teamsnap.api.Availability,
teamsnap.models.Event: teamsnap.teamsnap.api.Event,
# teamsnap.models.LineupEntry:teamsnap.teamsnap.api.LineupEntry, # Not implemented Yet
teamsnap.models.Location: teamsnap.teamsnap.api.Location,
teamsnap.models.Member: teamsnap.teamsnap.api.Member,
teamsnap.models.Opponent: teamsnap.teamsnap.api.Opponent,
teamsnap.models.Team: teamsnap.teamsnap.api.Team,
# teamsnap.models.User:teamsnap.teamsnap.api.User # Not implemented yet
}
if isinstance(benchcoach_instance, benchcoach.models.Team):
# This situation requires special attention because opponents and teams share a table in BenchCoach
teamsnap_instance = getattr(benchcoach_instance, 'teamsnap_team', None)
if not teamsnap_instance: teamsnap_instance = getattr(benchcoach_instance, 'teamsnap_opponent')
else:
teamsnap_instance = getattr(benchcoach_instance, benchcoachmodel_to_teamsnapfield.get(type(benchcoach_instance)))
ApiObject = teamsnapmodel_to_apiobject.get(type(teamsnap_instance))
api_response = ApiObject.get(self.client, teamsnap_instance.id)
r = self._update_from_teamsnapdata(teamsnap_instance, api_response)
r = self._update_teamsnapdb_to_benchcoachdb(teamsnap_instance, benchcoach_instance)
return r
def _update_from_teamsnapdata(self, teamsnap_instance:teamsnap.models.TeamsnapBaseModel, teamsnap_data: teamsnap.teamsnap.api.ApiObject) -> teamsnap.models.TeamsnapBaseModel:
''''''
if isinstance(teamsnap_data, teamsnap.teamsnap.api.ApiObject):
teamsnap_data = teamsnap_data.data
else:
raise TypeError
if not teamsnap_data['type'] == teamsnap_instance.type:
raise Exception()
data_type = teamsnap_data['type']
fields = ['id', 'created_at', 'updated_at']
related_objects = {}
if data_type in ['opponent', 'team', 'location']:
fields += ['name']
elif data_type == 'event':
fields += [
'label',
'start_date',
'formatted_title',
'points_for_opponent',
'points_for_team',
'is_game',
'game_type'
]
if teamsnap_data.get('location_id'):
related_objects['location'] = teamsnap.models.Location.objects.get(id=teamsnap_data['location_id'])
if teamsnap_data.get('opponent_id'):
related_objects['opponent'] = teamsnap.models.Opponent.objects.get(id=teamsnap_data['opponent_id'])
pass
elif data_type == 'member':
fields += [
'first_name',
'last_name',
'jersey_number',
'is_non_player'
]
elif data_type == 'availability':
fields += ['status_code']
related_objects['member'] = teamsnap.models.Member.objects.get(id=teamsnap_data['member_id'])
related_objects['event'] = teamsnap.models.Event.objects.get(id=teamsnap_data['event_id'])
else:
raise ValueError
if teamsnap_data.get('team_id'):
related_objects['team'] = teamsnap.models.Team.objects.filter(id=teamsnap_data['team_id']).first()
for field in fields:
value = teamsnap_data.get(field)
# if value is None:
# continue
# else:
setattr(teamsnap_instance,field,value)
for related_object_name, related_object in related_objects.items():
setattr(teamsnap_instance, related_object_name, related_object)
teamsnap_instance.save()
return teamsnap_instance
def _update_teamsnapdb_to_benchcoachdb(self, teamsnap_instance, benchcoach_instance) -> List[Tuple[django.db.models.Model, bool]]:
if isinstance(teamsnap_instance, teamsnap.models.Event):
benchcoach_model = Event
d = {
'start': teamsnap_instance.start_date,
}
if teamsnap_instance.team:
if teamsnap_instance.team.benchcoach_object:
if teamsnap_instance.game_type == "Home":
d['home_team'] = teamsnap_instance.team.benchcoach_object
elif teamsnap_instance.game_type == "Away":
d['away_team'] = teamsnap_instance.team.benchcoach_object
elif not teamsnap_instance.team.benchcoach_object:
raise Team.DoesNotExist
if teamsnap_instance.opponent:
if teamsnap_instance.opponent.benchcoach_object:
if teamsnap_instance.game_type == 'Home':
d['away_team'] = teamsnap_instance.opponent.benchcoach_object
elif teamsnap_instance.game_type == 'Away':
d['home_team'] = teamsnap_instance.opponent.benchcoach_object
elif not teamsnap_instance.opponent.benchcoach_object:
raise Team.DoesNotExist
pass
if teamsnap_instance.location:
if teamsnap_instance.location.benchcoach_object:
if teamsnap_instance.location:
d['venue'] = teamsnap_instance.location.benchcoach_object
elif not teamsnap_instance.location.benchcoach_object:
raise Venue.DoesNotExist
elif isinstance(teamsnap_instance, teamsnap.models.Opponent):
benchcoach_model = Team
d = {
'name': teamsnap_instance.name,
}
elif isinstance(teamsnap_instance, teamsnap.models.Team):
benchcoach_model = Team
d = {
'name': teamsnap_instance.name,
}
elif isinstance(teamsnap_instance, teamsnap.models.Location):
benchcoach_model = Venue
d = {
'name': teamsnap_instance.name,
}
elif isinstance(teamsnap_instance, teamsnap.models.Member):
benchcoach_model = Player
d = {
'first_name': teamsnap_instance.first_name,
'last_name': teamsnap_instance.last_name,
'jersey_number': teamsnap_instance.jersey_number,
}
elif isinstance(teamsnap_instance, teamsnap.models.Availability):
benchcoach_model = Availability
translation = {
teamsnap_instance.YES: Availability.YES,
teamsnap_instance.NO: Availability.NO,
teamsnap_instance.MAYBE: Availability.MAYBE
}
d = {
'available': translation.get(teamsnap_instance.status_code, Availability.UNKNOWN),
'player': teamsnap_instance.member.benchcoach_object,
'event': teamsnap_instance.event.benchcoach_object
}
r = []
if teamsnap_instance.member.benchcoach_object:
d['player'] = teamsnap_instance.member.benchcoach_object
elif not teamsnap_instance.member.benchcoach_object:
raise Player.DoesNotExist
if teamsnap_instance.event.benchcoach_object:
d['event'] = teamsnap_instance.event.benchcoach_object
elif not teamsnap_instance.event.benchcoach_object:
raise Event.DoesNotExist
else:
raise ValueError
for field, value in d.items():
setattr(benchcoach_instance, field, value)
benchcoach_instance.save()
teamsnap_instance.benchcoach_object = benchcoach_instance
teamsnap_instance.save()
return benchcoach_instance
def _find_counterpart(self, instance):
'''
find the counterpart BenchCoach object from the TeamSnap object.
NOT CURRENTLY USED.
:param instance:
:return:
'''
instance_type = type(instance)
counterpart_instance = None
if instance_type == Availability:
counterpart_instance = instance.teamsnap_availability
elif instance_type == Player:
counterpart_instance = instance.teamsnap_member
elif instance_type == Event:
counterpart_instance = instance.teamsnap_event
elif instance_type == Venue:
counterpart_instance = instance.teamsnap_location
elif instance_type == Team:
if hasattr(instance, 'teamsnap_opponent'):
counterpart_instance = instance.teamsnap_opponent
elif hasattr(instance, 'teamsnap_team'):
counterpart_instance = instance.teamsnap_team
else:
raise ValueError("instance doesn't seem to be an teamsnap opponent or a teamsnap team")
elif instance_type == Positioning:
counterpart_instance = instance.teamsnap_lineupentry
else:
raise Exception()
return counterpart_instance
def _sync_qs (self, qs, direction):
if direction == 'download':
if qs.model not in self.models:
raise TypeError(f"Sync engine does not sync {qs.model} models")
r=[]
r = self._bulk_sync_from_teamsnap(qs)
elif direction == 'upload':
raise NotImplementedError('Uploading not supported by this sync engine yet.')
else:
raise TypeError(f"Direction {direction} not supported. 'upload' or 'download' must be specified")
return r
def _sync_instance(self, instance, direction, data=None):
r=[]
if direction == 'download':
r.append(self._sync_from_teamsnap(instance))
elif direction == 'upload':
raise NotImplementedError('Uploading not supported by this sync engine yet.')
else:
raise TypeError(f"Direction {direction} not supported. 'upload' or 'download' must be specified")
return r
def sync(self, qs: django.db.models.QuerySet = None, instance: benchcoach.models.BenchcoachModel = None,
direction='download') -> List[Tuple[django.db.models.Model, bool]]:
if not isinstance(qs, QuerySet) and not isinstance(instance, benchcoach.models.BenchcoachModel):
raise TypeError(f"sync requires either a QuerySet or model instance to be provided")
if qs and instance:
raise TypeError(f"sync requires either a QuerySet or model instance to be provided, but not both")
elif qs:
r = self._sync_qs(qs, direction)
elif instance:
r = self._sync_instance(instance, direction)
return r
def import_items(self):
'''
Implementation of import items from the abstract base class AbstractSyncEngine.
Imports objects from TeamSnap into BenchCoach, creating BenchCoach objects when necessary.
It runs through all supported TeamSnap Objects every execution.
NOTE: The number of availability objects causes this function to choke, so consider not updating
those on import.
:return:
'''
['team', 'opponent', 'location', 'member', 'event', 'availability']
# the common kwargs for searching the API. Most objects just need the client and currently managed team id.
kwargs = {'client':self.client,'team_id': self.managed_teamsnap_team_id}
# r is the result dictionary, the key is the name of the benchcoach object, and the value is the list of BenchCoach objects that
# have been iterated (but not necessarily changed) during this import.
r = {}
# Walking through each TeamSnap object. There is a fair amount of repetition that could use clean-up.
# ---team---
r['team'] = []
# Search API for objects belonging to currently managed team, and iterate
for teamsnap_data in teamsnap.teamsnap.api.Team.search(client=self.client, id=self.managed_teamsnap_team_id):
# check if TeamSnap ID already exists in the Teamsnap DB.
if teamsnap.models.Team.objects.filter(id=teamsnap_data.data['id']):
teamsnap_instance = teamsnap.models.Team.objects.filter(id=teamsnap_data.data['id']).first()
# If it does, retrieve the BenchCoach instance attached.
# It is enforced (by this import function) that every teamsnap instance has a related BenchCoach instance attached.
# No other function can create TeamSnap instances (or create related BenchCoach instances from the TeamSnap service)
benchcoach_instance = teamsnap_instance.benchcoach_object
else:
# Otherwise create TeamSnap instance
teamsnap_instance = teamsnap.models.Team()
# and create related BenchCoach instance
benchcoach_instance = benchcoach.models.Team()
# and attach it to the BenchCoach instance
teamsnap_instance.benchcoach_object=benchcoach_instance
benchcoach_instance.save()
# Now, update the data from the API to the retrieved/created instances
response = self._update_from_teamsnapdata(teamsnap_instance, teamsnap_data)
teamsnap_instance.save()
response = self._update_teamsnapdb_to_benchcoachdb(teamsnap_instance, benchcoach_instance)
r['team'].append(response)
# ---opponent---
# See first object for additional comments on the steps followed.
# Opponents from teamsnap go to the BenchCoach "Team" database.
# Dependent on Team. These objects need to be available to attach as related objects or the functions
# self._update_from teamsnapdata and self.update_teamsnapdb_to_benchcoachdb may fail.
for teamsnap_data in teamsnap.teamsnap.api.Opponent.search(**kwargs):
if teamsnap.models.Opponent.objects.filter(id=teamsnap_data.data['id']):
teamsnap_instance = teamsnap.models.Opponent.objects.filter(id=teamsnap_data.data['id']).first()
benchcoach_instance = teamsnap_instance.benchcoach_object
else:
teamsnap_instance = teamsnap.models.Opponent()
benchcoach_instance = benchcoach.models.Team()
teamsnap_instance.benchcoach_object = benchcoach_instance
benchcoach_instance.save()
response = self._update_from_teamsnapdata(teamsnap_instance, teamsnap_data)
response = self._update_teamsnapdb_to_benchcoachdb(teamsnap_instance, benchcoach_instance)
r['team'].append(response)
# ---location---
# See first object for additional comments on the steps followed.
# Dependent on Team. These objects need to be available to attach as related objects or the functions
# self._update_from teamsnapdata and self.update_teamsnapdb_to_benchcoachdb may fail.
r['location'] = []
for teamsnap_data in teamsnap.teamsnap.api.Location.search(**kwargs):
if teamsnap.models.Location.objects.filter(id=teamsnap_data.data['id']):
teamsnap_instance = teamsnap.models.Location.objects.filter(id=teamsnap_data.data['id']).first()
benchcoach_instance = teamsnap_instance.benchcoach_object
else:
teamsnap_instance = teamsnap.models.Location()
benchcoach_instance = benchcoach.models.Venue()
teamsnap_instance.benchcoach_object = benchcoach_instance
benchcoach_instance.save()
response = self._update_from_teamsnapdata(teamsnap_instance, teamsnap_data)
response = self._update_teamsnapdb_to_benchcoachdb(teamsnap_instance, benchcoach_instance)
r['location'].append(response)
# ---member---
# See first object for additional comments on the steps followed.
# Dependent on Team. These objects need to be available to attach as related objects or the functions
# self._update_from teamsnapdata and self.update_teamsnapdb_to_benchcoachdb may fail.
r['member'] = []
# Search API for members to import. Note: Non players are not included in sync.
for teamsnap_data in teamsnap.teamsnap.api.Member.search(**kwargs,
is_non_player = False
):
if teamsnap_data.data['is_non_player'] == True:
continue
if teamsnap.models.Member.objects.filter(id=teamsnap_data.data['id']):
teamsnap_instance = teamsnap.models.Member.objects.filter(id=teamsnap_data.data['id']).first()
benchcoach_instance = teamsnap_instance.benchcoach_object
else:
teamsnap_instance = teamsnap.models.Member()
benchcoach_instance = benchcoach.models.Player()
teamsnap_instance.benchcoach_object = benchcoach_instance
benchcoach_instance.save()
response = self._update_from_teamsnapdata(teamsnap_instance, teamsnap_data)
response = self._update_teamsnapdb_to_benchcoachdb(teamsnap_instance, benchcoach_instance)
r['member'].append(response)
# ---event---
# See first object for additional comments on the steps followed.
# Dependent on Team, Opponent, Location. These objects need to be available to attach as related objects or the functions
# self._update_from teamsnapdata and self.update_teamsnapdb_to_benchcoachdb may fail.
r['event'] = []
for teamsnap_data in teamsnap.teamsnap.api.Event.search(**kwargs):
if teamsnap.models.Event.objects.filter(id=teamsnap_data.data['id']):
teamsnap_instance = teamsnap.models.Event.objects.filter(id=teamsnap_data.data['id']).first()
benchcoach_instance = teamsnap_instance.benchcoach_object
else:
teamsnap_instance = teamsnap.models.Event()
benchcoach_instance = benchcoach.models.Event()
teamsnap_instance.benchcoach_object = benchcoach_instance
benchcoach_instance.save()
response = self._update_from_teamsnapdata(teamsnap_instance, teamsnap_data)
response = self._update_teamsnapdb_to_benchcoachdb(teamsnap_instance, benchcoach_instance)
r['event'].append(response)
# ---availability---
# See first object for additional comments on the steps followed.
# Availability was a bit tricky to implement, because there are "not null" contstraints for the Availability object in Bench Coach
# Ideally, there probably should be more "not null" constraints on more of the BenchCoach models, so a generalized function should
# look more like this than the ones above.
# Dependent on Team, Member, Event. These objects need to be available to attach as related objects or the functions
# self._update_from teamsnapdata and self.update_teamsnapdb_to_benchcoachdb may fail.
#TODO this import is wonky and causes errors on servers. maybe skip this import, or do it in chunks?
r['availability'] = []
# Search API for members to import. Note: Non players are not included in sync.
player_ids = [member.id for member in teamsnap.models.Member.objects.filter(is_non_player=False)]
for teamsnap_data in teamsnap.teamsnap.api.Availability.search(**kwargs,
member_id=",".join(player_ids)
):
if teamsnap.models.Availability.objects.filter(id=teamsnap_data.data['id']):
teamsnap_instance = teamsnap.models.Availability.objects.filter(id=teamsnap_data.data['id']).first()
benchcoach_instance = teamsnap_instance.benchcoach_object
else:
teamsnap_instance = teamsnap.models.Availability()
event_instance = benchcoach.models.Event.objects.get(teamsnap_event__id=teamsnap_data.data['event_id'])
player_instance = benchcoach.models.Player.objects.get(teamsnap_member__id=teamsnap_data.data['member_id'])
benchcoach_instance = benchcoach.models.Availability(event=event_instance, player=player_instance)
benchcoach_instance.save()
teamsnap_instance.benchcoach_object_id = benchcoach_instance.id
response = self._update_from_teamsnapdata(teamsnap_instance, teamsnap_data)
response = self._update_teamsnapdb_to_benchcoachdb(teamsnap_instance, benchcoach_instance)
r['availability'].append(response)
return r