diff options
author | Michal Zimniewicz <michzimny@gmail.com> | 2016-10-09 11:27:06 +0200 |
---|---|---|
committer | Michal Zimniewicz <michzimny@gmail.com> | 2016-10-09 11:27:06 +0200 |
commit | 0a27bbf9ce1f5b1a52bc62ff4d9e5c2f960b030c (patch) | |
tree | df4a562b9e2452bc288264156c1e47bf6fcedeb3 |
Initial commit
-rw-r--r-- | .gitignore | 5 | ||||
-rw-r--r-- | LICENSE | 21 | ||||
-rw-r--r-- | README.md | 32 | ||||
-rwxr-xr-x | ql/__init__.py | 5 | ||||
-rw-r--r-- | ql/__main__.py | 16 | ||||
-rw-r--r-- | ql/completer.py | 29 | ||||
-rwxr-xr-x | ql/console.py | 37 | ||||
-rw-r--r-- | ql/lineup.py | 151 | ||||
-rw-r--r-- | ql/orm/__init__.py | 0 | ||||
-rw-r--r-- | ql/orm/models.py | 54 | ||||
-rw-r--r-- | ql/orm/utils.py | 7 | ||||
-rwxr-xr-x | ql/settings.py | 16 | ||||
-rw-r--r-- | requirements.txt | 2 |
13 files changed, 375 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b20b868 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.* +!.gitignore +*.pyc +__pycache__ +/venv* @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 MichaĆ Zimniewicz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7113301 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# teamy-quick-lineup +Command-line interface for line-up management in JFR Teamy. + +# Installation + +Prerequisites: + +* Python 3 +* pip +* the prerequisites of mysqlclient-python - https://github.com/PyMySQL/mysqlclient-python + +``` +pip install -r requirements.txt +``` + +# Configuration + +Set MySQL settings in ql/settings.py. + +# Usage + +``` +python -m ql <round> <segment> [<start from table>] +``` + +For instance, to process round 3, segment 2, starting from table 1 run: + +``` +python -m ql 3 2 1 +``` + +The script will iterate pair by pair in each match. It presents the currently assigned players and let you confirm them - pressing ENTER without any input - or change - providing player names (press TAB to autocomplete). diff --git a/ql/__init__.py b/ql/__init__.py new file mode 100755 index 0000000..de05759 --- /dev/null +++ b/ql/__init__.py @@ -0,0 +1,5 @@ +# bootstrap Django for ORM
+import os
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ql.settings")
+from django.core.wsgi import get_wsgi_application
+get_wsgi_application()
diff --git a/ql/__main__.py b/ql/__main__.py new file mode 100644 index 0000000..e79056d --- /dev/null +++ b/ql/__main__.py @@ -0,0 +1,16 @@ +import sys
+from .console import Console
+
+
+if len(sys.argv) < 3 or len(sys.argv) > 4:
+ print('Give correct parameters: round, segment and (optionally) table')
+ sys.exit(1)
+
+round = int(sys.argv[1])
+segment = int(sys.argv[2])
+if len(sys.argv) == 4:
+ table = int(sys.argv[3])
+else:
+ table = None
+
+Console(round, segment, table).run()
diff --git a/ql/completer.py b/ql/completer.py new file mode 100644 index 0000000..a9b7d8d --- /dev/null +++ b/ql/completer.py @@ -0,0 +1,29 @@ +import readline
+
+
+class Completer(object):
+
+ @classmethod
+ def install_new_completer(cls, options):
+ completer = cls(options)
+ readline.set_completer(completer.complete)
+ readline.set_completer_delims('') # allow options with whitespace
+ readline.parse_and_bind('tab: complete')
+
+ def __init__(self, options):
+ self.options = options
+
+ def complete(self, text, state):
+ text = text.lower()
+ if state == 0: # on first trigger, build possible matches
+ if text: # cache matches (entries that start with entered text)
+ self.matches = [s for s in self.options
+ if s and s.lower().startswith(text)]
+ else: # no text entered, all matches possible
+ self.matches = self.options[:]
+
+ # return match indexed by state
+ try:
+ return self.matches[state]
+ except IndexError:
+ return None
diff --git a/ql/console.py b/ql/console.py new file mode 100755 index 0000000..0c741ca --- /dev/null +++ b/ql/console.py @@ -0,0 +1,37 @@ +from .orm.utils import get_num_of_tables
+from .lineup import Lineup
+from .completer import Completer
+
+
+class Console(object):
+
+ def __init__(self, round, segment, table=None):
+ self.round = round
+ self.segment = segment
+ self.start_from_table = table if table is not None else 1
+
+ @property
+ def tables(self):
+ return [ i for i in range(self.start_from_table, get_num_of_tables() + 1) ]
+
+ def run(self):
+ for table in self.tables:
+ self.process_table(table)
+
+ def process_table(self, table):
+ lineup = self.get_lineup(table)
+ print(lineup.info)
+ print()
+ for team in lineup.teams:
+ Completer.install_new_completer(team.player_names)
+ for pair in team.pairs:
+ while True:
+ print(pair.info)
+ value = input("Player: ")
+ if not value:
+ print()
+ break
+ pair.set_player(value)
+
+ def get_lineup(self, table):
+ return Lineup(self.round, self.segment, table)
diff --git a/ql/lineup.py b/ql/lineup.py new file mode 100644 index 0000000..b0d5847 --- /dev/null +++ b/ql/lineup.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*-
+from django.utils.functional import cached_property
+from .orm.models import Segment, Team, Player
+
+
+class TeamInSegment(object):
+
+ OPEN_NS = 'open NS'
+ OPEN_EW = 'open EW'
+ CLOSED_NS = 'closed NS'
+ CLOSED_EW = 'closed EW'
+
+ def __init__(self, team, segment):
+ self.team = team
+ self.segment = segment
+
+ def get_paired_players_fields(self):
+ raise NotImplementedError()
+
+ @property
+ def name(self):
+ return self.team.name
+
+ @property
+ def players(self):
+ return self.team.players
+
+ @property
+ def pairs(self):
+ for players_fields_entity in self.get_paired_players_fields():
+ yield Pair(self, players_fields_entity['fields'], players_fields_entity['label'])
+
+ @property
+ def player_names(self):
+ return [ '%s %s' % (p.last_name, p.first_name) for p in self.team.players.order_by('last_name', 'first_name').all() ]
+
+
+class HomeTeamInSegment(TeamInSegment):
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ def get_paired_players_fields(self):
+ return [
+ {
+ 'fields': ['openN', 'openS'],
+ 'label': self.OPEN_NS,
+ },
+ {
+ 'fields': [ 'closeE', 'closeW' ],
+ 'label': self.CLOSED_EW,
+ },
+ ]
+
+
+class AwayTeamInSegment(TeamInSegment):
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ def get_paired_players_fields(self):
+ return [
+ {
+ 'fields': ['openE', 'openW' ],
+ 'label': self.OPEN_EW,
+ },
+ {
+ 'fields': [ 'closeN', 'closeS' ],
+ 'label': self.CLOSED_NS,
+ },
+ ]
+
+
+class Pair(object):
+
+ def __init__(self, team, players_fields, label):
+ assert len(players_fields) == 2
+ self.team = team
+ self.players_fields = players_fields
+ self.label = label
+ self.last_changed_player_num = None
+
+ @property
+ def players(self):
+ return [ self._load_player(field) for field in self.players_fields ]
+
+ def _load_player(self, player_field):
+ try:
+ return getattr(self.team.segment, player_field)
+ except Player.DoesNotExist:
+ return None
+
+ @property
+ def info(self):
+ return 'Team: %s - %s - %s' % (
+ self.team.name,
+ self.label,
+ [ p.info if p is not None else '<blank>' for p in self.players ]
+ )
+
+ def set_player(self, name):
+ try:
+ last_name, first_name = name.split(' ')
+ player = self.team.players.get(first_name=first_name, last_name=last_name)
+ except (ValueError, Player.DoesNotExist):
+ player = None
+
+ if not player:
+ print('Unknown player: %s' % name)
+ else:
+ player_to_be_changed_num = self._deduce_player_to_be_changed()
+ print('changing %s to %s ' % (self.players[player_to_be_changed_num], player))
+
+ field_name = self.players_fields[player_to_be_changed_num]
+ self.team.segment.update(**{field_name: player})
+
+ self.last_changed_player_num = player_to_be_changed_num
+
+ def _deduce_player_to_be_changed(self):
+ if self.players[0] is None:
+ return 0
+ if self.players[1] is None:
+ return 1
+ if self.last_changed_player_num is None:
+ return 0 # cannot make reasonable decision
+ else:
+ return 1 - self.last_changed_player_num # return the other player num
+
+
+class Lineup(object):
+
+ def __init__(self, round, segment, table):
+ self.round = round
+ self.segment = segment
+ self.table = table
+
+ @property
+ def info(self):
+ return 'Round %s, Segment %s, Table %s: %s vs %s' % \
+ (self.round, self.segment, self.table, self.segment_obj.home_team.name, self.segment_obj.away_team.name)
+
+ @cached_property
+ def segment_obj(self):
+ return Segment.objects.get(round=self.round, segment=self.segment, table=self.table)
+
+ @property
+ def teams(self):
+ return [
+ HomeTeamInSegment(self.segment_obj.home_team, self.segment_obj),
+ AwayTeamInSegment(self.segment_obj.away_team, self.segment_obj),
+ ]
diff --git a/ql/orm/__init__.py b/ql/orm/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/ql/orm/__init__.py diff --git a/ql/orm/models.py b/ql/orm/models.py new file mode 100644 index 0000000..be31137 --- /dev/null +++ b/ql/orm/models.py @@ -0,0 +1,54 @@ +from django.db import models
+
+
+class Team(models.Model):
+
+ class Meta:
+ db_table = 'teams'
+
+ name = models.CharField(db_column='fullname', max_length=50)
+
+
+class Player(models.Model):
+
+ class Meta:
+ db_table = 'players'
+
+ first_name = models.CharField(db_column='gname', max_length=30)
+ last_name = models.CharField(db_column='sname', max_length=30)
+ team = models.ForeignKey(Team, on_delete=models.PROTECT, related_name='players', db_column='team')
+
+ @property
+ def info(self):
+ return '%s %s' % (self.first_name, self.last_name)
+
+ def __str__(self):
+ return '%s %s (%s)' % (self.first_name, self.last_name, self.team.name)
+
+
+class Segment(models.Model):
+
+ ''' This class has no single primary key, so not all standard ORM API will work. '''
+
+ class Meta:
+ db_table = 'segments'
+
+ round = models.IntegerField(db_column='rnd', primary_key=True)
+ segment = models.IntegerField(db_column='segment', primary_key=True)
+ table = models.IntegerField(db_column='tabl', primary_key=True)
+ home_team = models.ForeignKey(Team, on_delete=models.PROTECT, related_name='+', db_column='homet')
+ away_team = models.ForeignKey(Team, on_delete=models.PROTECT, related_name='+', db_column='visit')
+ openN = models.ForeignKey(Player, on_delete=models.PROTECT, related_name='+', db_column='openN')
+ openS = models.ForeignKey(Player, on_delete=models.PROTECT, related_name='+', db_column='openS')
+ openE = models.ForeignKey(Player, on_delete=models.PROTECT, related_name='+', db_column='openE')
+ openW = models.ForeignKey(Player, on_delete=models.PROTECT, related_name='+', db_column='openW')
+ closeN = models.ForeignKey(Player, on_delete=models.PROTECT, related_name='+', db_column='closeN')
+ closeS = models.ForeignKey(Player, on_delete=models.PROTECT, related_name='+', db_column='closeS')
+ closeE = models.ForeignKey(Player, on_delete=models.PROTECT, related_name='+', db_column='closeE')
+ closeW = models.ForeignKey(Player, on_delete=models.PROTECT, related_name='+', db_column='closeW')
+
+ def update(self, **kwargs):
+ affected = Segment.objects.filter(round=self.round, segment=self.segment, table=self.table).update(**kwargs)
+ assert affected == 1
+ for field, value in kwargs.items():
+ setattr(self, field, value)
diff --git a/ql/orm/utils.py b/ql/orm/utils.py new file mode 100644 index 0000000..e5d96e2 --- /dev/null +++ b/ql/orm/utils.py @@ -0,0 +1,7 @@ +from .models import Team
+
+
+def get_num_of_tables():
+ num_of_teams = Team.objects.count()
+ assert num_of_teams % 2 == 0
+ return int(num_of_teams / 2)
diff --git a/ql/settings.py b/ql/settings.py new file mode 100755 index 0000000..a975f14 --- /dev/null +++ b/ql/settings.py @@ -0,0 +1,16 @@ +DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.mysql',
+ 'HOST': '127.0.0.1',
+ 'PORT': '3306',
+ 'USER': 'your-username',
+ 'PASSWORD': 'your-password',
+ 'NAME': 'your-database-name',
+ }
+}
+
+INSTALLED_APPS = (
+ 'ql.orm',
+)
+
+SECRET_KEY = 'f5a73e42-a600-4925-a860-b40b72acf497'
diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b2ef732 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +django==1.10
+mysqlclient==1.3.9
|