From 0a27bbf9ce1f5b1a52bc62ff4d9e5c2f960b030c Mon Sep 17 00:00:00 2001 From: Michal Zimniewicz Date: Sun, 9 Oct 2016 11:27:06 +0200 Subject: Initial commit --- .gitignore | 5 ++ LICENSE | 21 ++++++++ README.md | 32 ++++++++++++ ql/__init__.py | 5 ++ ql/__main__.py | 16 ++++++ ql/completer.py | 29 ++++++++++ ql/console.py | 37 +++++++++++++ ql/lineup.py | 151 +++++++++++++++++++++++++++++++++++++++++++++++++++++ ql/orm/__init__.py | 0 ql/orm/models.py | 54 +++++++++++++++++++ ql/orm/utils.py | 7 +++ ql/settings.py | 16 ++++++ requirements.txt | 2 + 13 files changed, 375 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100755 ql/__init__.py create mode 100644 ql/__main__.py create mode 100644 ql/completer.py create mode 100755 ql/console.py create mode 100644 ql/lineup.py create mode 100644 ql/orm/__init__.py create mode 100644 ql/orm/models.py create mode 100644 ql/orm/utils.py create mode 100755 ql/settings.py create mode 100644 requirements.txt 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* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2ab5e17 --- /dev/null +++ b/LICENSE @@ -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 [] +``` + +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 '' 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 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 -- cgit v1.2.3