diff --git a/aacstats.py b/aacstats.py index 09d5a25..2d3a7a6 100644 --- a/aacstats.py +++ b/aacstats.py @@ -1,17 +1,154 @@ from flask import Flask, abort, request, render_template, url_for +import datetime as dt import math import MySQLdb import MySQLdb.cursors -import datetime as dt +import re app = Flask(__name__) PAGE_SIZE=20 -# TODO: Search +MIN_MONTHS_FOR_LISTINGS=3 + + +def read_db(start=0, limit=PAGE_SIZE, listing=None, event=None, person=None, licence=None, search=dict(), year=None): + db = MySQLdb.connect(user='aac', passwd='saOAcCWHg4LaoSSA', db='AAC', + use_unicode=True, charset="utf8", cursorclass=MySQLdb.cursors.DictCursor) + c = db.cursor() + + count = 0 + + select = '*' + where = 'WHERE club LIKE "AAC"' + group = '' + order = 'date DESC, event, position' + close = '' + if event: + if isinstance(event, str): + event = db.escape_string(event).decode() + where += ' AND event LIKE "{}"'.format(event.lower()) + if person: + if isinstance(person, str): + person = db.escape_string(person).decode() + where += ' AND CONCAT_WS(" ", name, surname) LIKE "{}"'.format(person.lower()) + if licence: + if isinstance(licence, str): + licence = db.escape_string(licence).decode() + where += ' AND licence LIKE "{}"'.format(licence.lower()) + if year: + if not isinstance(year, (int, float)): + year = int(db.escape_string(year).decode()) + firstdate = dt.datetime.min + lastdate = dt.datetime.max + firstdate = firstdate.replace(year=int(year)) + lastdate = lastdate.replace(year=int(year)) + where += ' AND date > "{}" AND date < "{}"'.format(firstdate, lastdate) + + for column in search.keys(): + if isinstance(column, str): + column = db.escape_string(column).decode() + if isinstance(search[column], str): + query = db.escape_string(search[column]).decode() + if not query: + next + if column in ( 'licence', 'event', ): + where += ' AND {} LIKE "%{}%"'.format(column, query.lower()) + elif column in ( 'name', ): + where += ' AND ( CONCAT_WS(" ", name, surname) LIKE "%{}%" )'.format(query.lower()) + elif column in ( 'position', ): + where += ' AND {} LIKE "{}"'.format(column, query) + elif column == 'after': + try: + after = dt.datetime.strptime(query, '%Y-%m-%d') + date = dt.datetime.min + date = date.replace(year=after.year, month=after.month, day=after.day) + where += ' AND date > "{}"'.format(date) + except: + pass + elif column == 'before': + try: + before = dt.datetime.strptime(query, '%Y-%m-%d') + date = dt.datetime.max + date = date.replace(year=before.year, month=before.month, day=before.day) + where += ' AND date < "{}"'.format(date) + except: + pass + else: + pass + + if listing: + if listing == 'races': + select = 'event, date' + group = 'GROUP BY event' + elif listing == 'runners': + select = 'CONCAT_WS(" ", name, surname) person, FORMAT(SUM(distance),0) total' + where += ' AND CONCAT_WS(" ", name, surname) NOT LIKE "%NO RETURN%"' + where += ' AND CONCAT_WS(" ", name, surname) NOT LIKE "%BLANK CARD%"' + group = 'GROUP BY CONCAT_WS(" ", name, surname)' + order = 'SUM(distance) DESC, surname' + elif listing == 'rankings': + select = 'CONCAT_WS(" ", name, surname) person, SUM(position) positions, COUNT(event) races, SUM(position)/COUNT(event) podiums, FORMAT(SUM(position)/COUNT(event),1) score' + group = 'GROUP BY CONCAT_WS(" ", name, surname)' + order = 'podiums, races DESC' + elif listing == 'licence': + select = 'licence, date, CONCAT_WS(" ", name, surname) person' + group = 'GROUP BY licence' + order = 'surname, date DESC' + + sql = 'SELECT {} FROM `results` {} {} ORDER BY {} LIMIT {},{} {};'.format(select, where, group, order, start, limit, close) + app.logger.debug(sql) + c.execute(sql) + queryresults = c.fetchall() + + select = 'COUNT(*)' + if listing: + if listing == 'races': + select = 'COUNT(*) FROM ( SELECT COUNT(event)' + close = ') races' + elif listing == 'runners': + select = 'COUNT(*) FROM ( SELECT COUNT(name)' + group = 'GROUP BY CONCAT_WS(" ", name, surname)' + close = ') runners' + elif listing == 'rankings': + select = 'COUNT(*) FROM ( SELECT COUNT(name)' + group = 'GROUP BY CONCAT_WS(" ", name, surname)' + close = ') rankings' + elif listing == 'licence': + pass + + sql = 'SELECT {} FROM `results` {} {} {};'.format(select, where, group, close) + #app.logger.debug(sql) + c.execute(sql) + countresult = c.fetchone() + for x in countresult.keys(): + count = countresult[x] + #app.logger.debug(count) + + return { 'count': int(count), 'rows': queryresults } def now(): - # TODO: If it's January and no races have been run this year, return last year return dt.datetime.now() + +@app.template_filter('pace') +def pace(time): + return (dt.datetime(1,1,1) + time).strftime('%M:%S') + +@app.template_filter('year') +def year(time): + return time.strftime('%Y') + +@app.template_filter('cleandate') +def clean_date(time): + if time.month == 1 and time.day == 1: + return time.strftime('%Y') + return time.strftime('%Y-%m-%d') + +@app.template_filter('ordinal') +def ordinal(n): + if not isinstance(n, int) or n < 1: + return + return "%d%s" % (n,"tsnrhtdd"[(math.floor(n/10)%10!=1)*(n%10<4)*n%10::4]) + @app.route('/') @app.route('/list') @app.route('/list/') # title = { races, rankings, runners, licence } @@ -23,6 +160,18 @@ def list(title=None, year=None): title = 'runners' if title not in ( 'races', 'rankings', 'runners', 'licence', ): abort(404) + + ''' In early January, we'll be left with blank pages in listings, since there won't + be any results yet, so hack it to return last year's info ''' + date = dt.datetime.now() + if date.year == year and date.month <= MIN_MONTHS_FOR_LISTINGS: + results = read_db(year=date.year) + app.logger.debug(results['count']) + if results['count'] < 1: + ''' get an approximate time about two months ago (at the end of the previous year) ''' + lastyear = date - dt.timedelta(days=(MIN_MONTHS_FOR_LISTINGS * 31)) + year = lastyear.year + start = int(request.args.get('start', '0')) limit = int(request.args.get('limit', PAGE_SIZE)) results = read_db(start, limit, listing=title, year=year) @@ -70,99 +219,14 @@ def licence(year=now().year, title=None): results=results, start=start, limit=limit, request=request, now=now(), PAGE_SIZE=PAGE_SIZE) - -def read_db(start=0, limit=PAGE_SIZE, listing=None, event=None, person=None, licence=None, year=None): - db = MySQLdb.connect(user = 'aac', passwd = 'saOAcCWHg4LaoSSA', db = 'AAC', cursorclass = MySQLdb.cursors.DictCursor) - c = db.cursor() - - count = 0 - - select = '*' - where = 'WHERE club LIKE "AAC"' - group = '' - order = 'date DESC, event, position' - close = '' - if event: - where += ' AND event LIKE "{}"'.format(event) - if person: - where += ' AND CONCAT_WS(" ", name, surname) LIKE "{}"'.format(person) - if licence: - where += ' AND licence LIKE "{}"'.format(licence) - if year: - firstdate = dt.datetime.min - lastdate = dt.datetime.max - firstdate = firstdate.replace(year=int(year)) - lastdate = lastdate.replace(year=int(year)) - where += ' AND date > "{}" AND date < "{}"'.format(firstdate, lastdate) - - if listing: - if listing == 'races': - select = 'event, date' - group = 'GROUP BY event' - elif listing == 'runners': - select = 'CONCAT_WS(" ", name, surname) person, FORMAT(SUM(distance),0) total' - where += ' AND CONCAT_WS(" ", name, surname) NOT LIKE "%NO RETURN%"' - where += ' AND CONCAT_WS(" ", name, surname) NOT LIKE "%BLANK CARD%"' - group = 'GROUP BY CONCAT_WS(" ", name, surname)' - order = 'SUM(distance) DESC, surname' - elif listing == 'rankings': - select = 'CONCAT_WS(" ", name, surname) person, SUM(position) positions, COUNT(event) races, SUM(position)/COUNT(event) podiums, FORMAT(SUM(position)/COUNT(event),1) score' - group = 'GROUP BY CONCAT_WS(" ", name, surname)' - order = 'podiums, races DESC' - elif listing == 'licence': - select = 'licence, date, CONCAT_WS(" ", name, surname) person' - group = 'GROUP BY licence' - order = 'surname, date DESC' - - sql = 'SELECT {} FROM `results` {} {} ORDER BY {} LIMIT {},{} {};'.format(select, where, group, order, start, limit, close) - #app.logger.debug(sql) - c.execute(sql) - queryresults = c.fetchall() - - select = 'COUNT(*)' - if listing: - if listing == 'races': - select = 'COUNT(*) FROM ( SELECT COUNT(event)' - close = ') races' - elif listing == 'runners': - select = 'COUNT(*) FROM ( SELECT COUNT(name)' - group = 'GROUP BY CONCAT_WS(" ", name, surname)' - close = ') runners' - elif listing == 'rankings': - select = 'COUNT(*) FROM ( SELECT COUNT(name)' - group = 'GROUP BY CONCAT_WS(" ", name, surname)' - close = ') rankings' - elif listing == 'licence': - pass - - sql = 'SELECT {} FROM `results` {} {} {};'.format(select, where, group, close) - #app.logger.debug(sql) - c.execute(sql) - countresult = c.fetchone() - for x in countresult.keys(): - count = countresult[x] - app.logger.debug(count) - - return { 'count': int(count), 'rows': queryresults } - - -@app.template_filter('pace') -def pace(time): - return (dt.datetime(1,1,1) + time).strftime('%M:%S') - -@app.template_filter('year') -def year(time): - return time.strftime('%Y') - -@app.template_filter('cleandate') -def clean_date(time): - if time.month == 1 and time.day == 1: - return time.strftime('%Y') - return time.strftime('%Y-%m-%d') - -@app.template_filter('ordinal') -def ordinal(n): - return "%d%s" % (n,"tsnrhtdd"[(math.floor(n/10)%10!=1)*(n%10<4)*n%10::4]) +@app.route('/search') +def search(): + start = int(request.args.get('start', '0')) + limit = int(request.args.get('limit', PAGE_SIZE)) + results = read_db(start, limit, search=request.args) + return render_template('search.html', ltype='index', year=None, + results=results, start=start, limit=limit, + request=request, now=now(), PAGE_SIZE=PAGE_SIZE) if __name__ == '__main__': diff --git a/load_spreadsheet.py b/load_spreadsheet.py index d724296..b2cf825 100755 --- a/load_spreadsheet.py +++ b/load_spreadsheet.py @@ -154,7 +154,8 @@ def load_into_db(rows): if rows is None or len(rows) < 1: log.warning("No data found in spreadsheet") else: - db = MySQLdb.connect(user='aac', passwd='saOAcCWHg4LaoSSA', db='AAC', use_unicode=True, charset="utf8") + db = MySQLdb.connect(user='aac', passwd='saOAcCWHg4LaoSSA', db='AAC', + use_unicode=True, charset="utf8") c = db.cursor() ''' Check for duplicate values by DATE and POSITION and RACE and EVENT ''' diff --git a/static/style.css b/static/style.css index d06f674..73999ee 100644 --- a/static/style.css +++ b/static/style.css @@ -1,6 +1,5 @@ body { - margin: 0 auto 15px; - padding: 0 8px; + margin: 0 auto; max-width: 800px; font-family: 'Roboto Condensed', sans-serif; font-size: 11pt; @@ -11,6 +10,9 @@ a { h1 { text-align: center; } +article { + padding: 0 8px; +} table { border-collapse: collapse; width: 100%; @@ -38,6 +40,7 @@ nav span { flex: 1 1 auto; } nav span { + white-space: nowrap; } nav span a { display: block; @@ -68,6 +71,33 @@ nav.tabs span:last-child { } nav.tabs span a { padding: 15px 10px; - font-size: 11pt; + font-size: 10pt; font-weight: bold; } +div.search { + margin: 0 10px 15px; + display: flex; + flex-wrap: wrap; + align-items: stretch; +} +div.search > div { + flex: 1 1 300px; + padding: 5px; + display: flex; + align-items: center; +} +div.search label { + flex: 1; + text-align: right; + padding-right: 10px; +} +div.search input { + flex: 2; + font-family: 'Roboto Condensed', sans-serif; /* Override firefox's occassional monospace weirdness */ + padding-top: 5px; /* Override Firefox weirdness */ + padding-bottom: 5px; /* Override Firefox weirdness */ +} +div.search input#searchsubmit { + flex: none; + margin-left: auto; +} diff --git a/templates/index.html b/templates/index.html index c11cdee..be85535 100644 --- a/templates/index.html +++ b/templates/index.html @@ -42,9 +42,9 @@ {%- endif -%} <td>{{ row.date | cleandate }}</td> <td> - {%- if row.sex and row.sexposition and row.sexposition <= 100 %}{{ row.sexposition | ordinal }} {{ row.sex.lower() }}{% endif -%} - {%- if row.sexposition and row.catposition %}/{% endif -%} - {%- if row.catposition and row.catposition <= 100 %}{{ row.catposition | ordinal }} in category{% endif -%} + {%- if row.sex and row.sexposition and row.sexposition | int <= 100 %}{{ row.sexposition | ordinal }} {{ row.sex.lower() }}{% endif -%} + {%- if row.sexposition and row.sexposition | int <= 100 and row.catposition and row.catposition | int <= 100 %} and {% endif -%} + {%- if row.catposition and row.catposition | int <= 100 %}{{ row.catposition | ordinal }} in category{% endif -%} </td> </tr> {%- endfor -%} diff --git a/templates/search.html b/templates/search.html new file mode 100644 index 0000000..c761246 --- /dev/null +++ b/templates/search.html @@ -0,0 +1,80 @@ +{% set ns = namespace() -%} + +{% include 'head.html' with context %} +<article> +<h1>AAC Statistics {% if title %}: {{ title }}{% endif %}{% if year %} {{ year }}{% endif %}</h1> +<form class="search"> + <div class="search"> + <div> + <label for="searchname">Name</label> + <input type="text" id="searchname" name="name" value="{{ request.args.get('name', '') }}" /> + </div> + <div> + <label for="searchlicence">Licence</label> + <input type="text" id="searchlicence" name="licence" value="{{ request.args.get('licence', '') }}" /> + </div> + <div> + <label for="searchafter">Older than</label> + <input type="date" id="searchafter" name="after" max="" value="{{ request.args.get('after', '') }}" /> + </div> + <div> + <label for="searchbefore">Newer than</label> + <input type="date" id="searchbefore" name="before" min="" value="{{ request.args.get('before', '') }}" /> + </div> + <div> + <label for="searchevent">Race</label> + <input type="text" id="searchevent" name="event" value="{{ request.args.get('event', '') }}" /> + </div> + <!--div> + <label for="searchposition">Position</label> + <input type="text" id="searchposition" name="position" value="{{ request.args.get('position', '') }}" /> + </div--> + <div><input type="submit" id="searchsubmit" name="submit" /></div> + </div> +</form> +{% if results -%} + {%- set ns.total = 0 -%} + {%- if 'count' in results -%} + {%- set ns.total = results['count'] -%} + {%- endif -%} +<table> + <thead> + <tr> + <th>Position</th> + <th>Name</th> + <th>Licence</th> + <th>Time</th> + <th>Average Pace</th> + <th>Event</th> + <th>Date</th> + <th>Notes</th> + </tr> + </thead> + <tbody> + {%- for row in results['rows'] -%} + {%- set person='{} {}'.format(row.name, row.surname) -%} + {%- if distance %}{# set total_km += row.distance #}{% endif -%} + <tr> + <td>{{ row.position }}</td> + <td><a href="{{ url_for('person', title=person, start=None) }}">{{ person }}</a></td> + <td><a href="{{ url_for('licence', title=row.licence, year=row.date|year, start=None, limit=ns.limit) }}">{{ row.licence }}</a></td> + <td>{{ row.time }}</td> + <td class="nowrap">{% if row.distance is number %}{{ (row.time / row.distance) | pace }} min/KM{% endif %}</td> + <td><a href="{{ url_for('race', title=row.event, year=row.date|year, start=None, limit=ns.limit) }}">{{ row.event }} ({{ row.distance }} KM)</a></td> + <td>{{ row.date | cleandate }}</td> + <td> + {%- if row.sex and row.sexposition and row.sexposition | int <= 100 %}{{ row.sexposition | ordinal }} {{ row.sex.lower() }}{% endif -%} + {%- if row.sexposition and row.sexposition | int <= 100 and row.catposition and row.catposition | int <= 100 %} and {% endif -%} + {%- if row.catposition and row.catposition | int <= 100 %}{{ row.catposition | ordinal }} in category{% endif -%} + </td> + </tr> + {%- endfor -%} + </tbody> +</table> +{%- endif %} +</article> +<footer> +{% include 'prevnext.html' with context %} +</footer> +</body> +</html> diff --git a/templates/tabs.html b/templates/tabs.html index ba34592..0abbc70 100644 --- a/templates/tabs.html +++ b/templates/tabs.html @@ -2,6 +2,7 @@ <span><a href="{{ url_for('list', title='runners', year=now|year, start=None, limit=ns.limit) }}">Runners</a></span> <span><a href="{{ url_for('list', title='rankings', year=now|year, start=None, limit=ns.limit) }}">Rankings</a></span> <span><a href="{{ url_for('list', title='races', year=now|year, start=None, limit=ns.limit) }}">Races</a></span> - <span><a href="{{ url_for('list', title='licences', year=now|year, start=None, limit=ns.limit) }}">Licences</a></span> + <span><a href="{{ url_for('list', title='licence', year=now|year, start=None, limit=ns.limit) }}">Licences</a></span> <span><a href="{{ url_for('index', title=None, year=None, start=None, limit=ns.limit) }}">All Results</a></span> + <span><a href="{{ url_for('search', title=None, year=None, start=None, limit=ns.limit) }}">Search</a></span> </nav>