summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore5
-rw-r--r--LICENSE21
-rw-r--r--README.md32
-rwxr-xr-xql/__init__.py5
-rw-r--r--ql/__main__.py16
-rw-r--r--ql/completer.py29
-rwxr-xr-xql/console.py37
-rw-r--r--ql/lineup.py151
-rw-r--r--ql/orm/__init__.py0
-rw-r--r--ql/orm/models.py54
-rw-r--r--ql/orm/utils.py7
-rwxr-xr-xql/settings.py16
-rw-r--r--requirements.txt2
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*
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 <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