Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

15 add statistics #4

Merged
merged 7 commits into from
Sep 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,15 @@ Wrap all strings to be localized in `_l()` calls. For templates use: `{{ _() }}`

Initialize babel via `pybabel extract -F babel.cfg -k _l -o messages.pot .`. Generate the german language catalog via `pybabel init -i messages.pot -d app/translations -l de`. Compile the language files via `pybabel compile -d app/translations`.

To update the translation do `pybabel extract -F babel.cfg -k _l -o messages.pot .` to collect new translation, update the language files via `pybabel update -i messages.pot -d app/translations`. After that you need to recompile using `pybabel compile -d app/translations`.
To update the translation do `pybabel extract -F babel.cfg -k _l -o messages.pot .` to collect new translation, update the language files via `pybabel update -i messages.pot -d app/translations`. After that you need to recompile using `pybabel compile -d app/translations`.

## Changes

### 20190915 - denormalize Laps

In order to fetch racer and car based statistics the lap information must be denormalized:
```
flask shell
for r in Race.query.all():
r.denormalize_laps()
```
99 changes: 96 additions & 3 deletions app/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import datetime
import json
from sqlalchemy import inspect
from sqlalchemy import inspect, func
from app import app, db


Expand All @@ -13,7 +13,6 @@ class Race(db.Model):
created_at = db.Column(db.DateTime)
started_at = db.Column(db.DateTime)
finished_at = db.Column(db.DateTime)
demo_mode = db.Column(db.Boolean, default=True)

def parsed_grid(self):
if self.grid is None:
Expand Down Expand Up @@ -52,10 +51,32 @@ def controller_for_racer(self, racer):
def lap_count_by_racer(self, racer):
return self.lap_count_by_controller(racer)

def lap_count_by_racers(self):
lap_counts = {}
for grid_entry in self.parsed_grid():
lap_counts[grid_entry['Racer']] = self.lap_count_by_controller(grid_entry['Controller'])
return lap_counts

def lap_count_by_controller(self, controller):
if controller is None:
return 0
return Lap.query.filter_by(race_id=self.id, controller=controller).count()
return self.statistics().lap_count_by_controller(controller)

def laps_by_controller(self, controller):
if controller is None:
return 0
return Lap.query.filter_by(race_id=self.id, controller=controller)

def denormalize_laps(self):
for l in Lap.query.filter_by(race_id=self.id).all():
for grid_entry in self.parsed_grid():
if(int(grid_entry['controller']) == l.controller):
l.racer_id = grid_entry['racer'].id
l.car_id = grid_entry['car'].id
db.session.commit()

def statistics(self):
return Statistics(self)

@staticmethod
def current():
Expand All @@ -69,6 +90,9 @@ class Racer(db.Model):
def __repr__(self):
return '<Racer {}>'.format(self.name)

def fastest_laps(self):
return Lap.query.filter(Lap.time > 1000, Lap.racer_id == self.id).order_by(Lap.time).limit(5).all()


class Car(db.Model):
id = db.Column(db.Integer, primary_key=True)
Expand All @@ -80,10 +104,15 @@ class Car(db.Model):
def __repr__(self):
return '<Car {}>'.format(self.name)

def fastest_laps(self):
return Lap.query.filter(Lap.time > 1000, Lap.car_id == self.id).order_by(Lap.time).limit(5).all()


class Lap(db.Model):
id = db.Column(db.Integer, primary_key=True)
race_id = db.Column(db.Integer, db.ForeignKey('race.id'))
racer_id = db.Column(db.Integer, db.ForeignKey('racer.id'))
car_id = db.Column(db.Integer, db.ForeignKey('car.id'))
controller = db.Column(db.Integer)
time = db.Column(db.Integer, index=False, unique=False)

Expand All @@ -93,6 +122,15 @@ def __repr__(self):
def to_json(self):
return json.dumps({c.key: getattr(self, c.key) for c in inspect(self).mapper.column_attrs})

def racer(self):
return Racer.query.get(self.racer_id)

def car(self):
return Car.query.get(self.car_id)

def formatted_time(self):
return '{:05.3f} s'.format(self.time / 1000)


class Timing(object):
def __init__(self, num):
Expand All @@ -109,3 +147,58 @@ def newlap(self, timer):
self.best_time = self.lap_time
self.laps += 1
self.time = timer.timestamp


class Statistics:
def __init__(self, race):
self.race = race

def race_time_by_controller(self, controller):
time = self.function_on_laps_by_controller(controller, func.sum(Lap.time).label('race_time'))
return self.format_duration(time)

def lap_count_by_controller(self, controller):
if controller is None:
return 0
return Lap.query.filter_by(race_id=self.race.id, controller=controller).count()

def fastest_lap_by_controller(self, controller):
time = self.function_on_laps_by_controller(controller, func.min(Lap.time).label('minimum_lap_time'))
return self.format_duration(time)

def slowest_lap_by_controller(self, controller):
time = self.function_on_laps_by_controller(controller, func.max(Lap.time).label('maximum_lap_time'))
return self.format_duration(time)

def average_lap_by_controller(self, controller):
time = self.function_on_laps_by_controller(controller, func.avg(Lap.time).label('average_lap_time'))
return self.format_duration(time)

def function_on_laps_by_controller(self, controller, function):
if controller is None:
return None
calculated = Lap.query.with_entities(function).filter(
Lap.time > 1000,
Lap.controller == controller,
Lap.race_id == self.race.id
).one()[0]
if calculated is None:
return None
try:
return datetime.timedelta(milliseconds=calculated)
except TypeError:
return None

def format_duration(self, duration):
if duration is None:
return None
total_seconds = duration.total_seconds()
hours, remainder = divmod(total_seconds, 60 * 60)
minutes, seconds = divmod(remainder, 60)

if hours > 0:
return '{:02d}:{:02d}:{:05.3f} hours'.format(int(hours), int(minutes), seconds)
if minutes > 0:
return '{:02d}:{:05.3f} min'.format(int(minutes), seconds)

return '{:05.3f} s'.format(seconds)
22 changes: 22 additions & 0 deletions app/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ def cars():
return render_template('cars.html', title='Fuhrpark', cars=cars)


@app.route('/cars/<int:car_id>')
def car(car_id):
return render_template('car.html', title='Auto', car=Car.query.get(car_id))


@app.route('/racers')
def racers():
racers = Racer.query.all()
Expand Down Expand Up @@ -48,6 +53,18 @@ def race_stop(race_id):
return render_template('races.html', title='Erstellte Rennen', races=Race.query.all())


@app.route('/races/<int:race_id>/delete')
def race_delete(race_id):
race = Race.query.get(race_id)
if race is not None:
race.stop()
db.session.delete(race)
db.session.commit()
app.logger.info('deleting race:' + repr(race))
flash(_l('Race deleted'))
return render_template('races.html', title='Erstellte Rennen', races=Race.query.all())


@app.route('/demo')
def demo():
services.mock_control_unit_connection()
Expand All @@ -60,6 +77,11 @@ def current_race():
return render_template('current_race.html', title='Aktuelles Rennen', current_race=Race.current())


@app.route('/races/<int:race_id>')
def race(race_id):
return render_template('race.html', title='Rennen vom ', race=Race.query.get(race_id))


@app.route('/racer_registration', methods=['GET', 'POST'])
def racer_registration():
form = RacerRegistrationForm()
Expand Down
12 changes: 9 additions & 3 deletions app/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,14 @@ def handle_control_unit_events(serial_port):
elif isinstance(status_or_timer, ControlUnit.Timer):
timing = timings[status_or_timer.address]
timing.newlap(status_or_timer)
lap = Lap(race_id=current_race.id, controller=status_or_timer.address, time=timing.lap_time)
controller = int(status_or_timer.address)
lap = Lap(
race_id=current_race.id,
controller=controller,
time=timing.lap_time,
racer_id=current_race.racer(controller).id,
car_id=current_race.car(controller).id
)
db.session.add(lap)
db.session.commit()
socketio.emit(
Expand All @@ -82,7 +89,6 @@ def handle_control_unit_events(serial_port):
)
app.logger.info("new lap " + repr(lap))
last_status_or_timer = status_or_timer
timeouts = 0
eventlet.sleep(0.3)
except serial.serialutil.SerialException:
app.logger.info("control unit disconnected, exiting loop")
Expand All @@ -91,7 +97,7 @@ def handle_control_unit_events(serial_port):
except connection.TimeoutError:
app.logger.info("Timeout while retrieving status from control unit({})", repr(timeouts))
socketio.emit('status', 'Timeout', namespace='/control_unit_events')
timeouts += 1
cu = connect_control_unit(serial_port)


def try_control_unit_connection():
Expand Down
10 changes: 8 additions & 2 deletions app/static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ td {
}

tr {
border-top-width: 2px;
border-top-width: 1px;
border-top-color: black;
border-top-style: solid;
}

.trackday {
Expand Down Expand Up @@ -47,7 +48,12 @@ tr {
font-size: 2rem;
}

.lap-time small {
.lap-statistics {
font-family: 'Faster One', cursive;
font-size: 2rem;
}

.lap-time .lap_statistics small {
font-size: 50%;
}

Expand Down
30 changes: 30 additions & 0 deletions app/templates/car.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{% extends "base.html" %}
{% block app_content %}

<h1>{{ car.name }}</h1>
<p>
{{ car.description }}
</p>
<div class = "d-flex flex-column bd-highlight mb-3">
<div class = "d-flex flex-row mb-2">
<div class = "p-2 car-image">
<img class = "car-image" src = "{{ car.image_link }}" />
</div>
<div class = "p-2 car-data">
<h3>{{ _('Fastest Laps') }}</h3>
<table width = "80%">
<tr>
<th>{{ _('Time') }}</th>
<th>{{ _('Racer') }}</th>
</tr>
{% for lap in car.fastest_laps() %}
<tr>
<td>{{ lap.formatted_time() }}</td>
<td>{{ lap.racer().name }}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
</div>
{% endblock %}
5 changes: 2 additions & 3 deletions app/templates/cars.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,18 @@
<h1>{{ _('Car park') }}</h1>
<div class = "d-flex flex-column bd-highlight mb-3">
{% for car in cars %}
<div class = "p-2 bd-highlight car">
<div class = "d-flex flex-row mb-2">
<div class = "p-2 car-image">
<img class = "car-image" src = "{{ car.image_link }}" />
</div>
<div class = "p-2 car-data">
<ul>
<li>{{ car.name }}</li>
<li><a href="{{ url_for('car', car_id= car.id) }}">{{ car.name }}</a></li>
<li>{{ car.order_number }}</li>
<li>{{ car.description }}</li>
</ul>
</div>
</div>
</div>
{% endfor %}
</div>

Expand Down
52 changes: 52 additions & 0 deletions app/templates/race.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{% extends "base.html" %}
{% block app_content %}

<h1>{{ _('Previous race of') }} {{ race.created_at }}</h1>

<a href="{{ url_for('race_delete', race_id=race.id) }}">{{ _('Delete this race') }}</a>

<div class="d-flex flex-row justify-content-center mb-3 bd-highlight ">
{% for grid_entry in race.parsed_grid()%}
<div class= "p-2 flex-fill grid-entry">
<div class="horizontal-line">
</div>
<div class= "racer-and-car">
<p class= "racer-name">{{ grid_entry['racer'].name }}</p>
<p class= "car-name">({{ grid_entry['car'].name }})</p>
</div>
<div class = "d-flex flex-row lap-data">
<div class="p-2 flex-fill">
<p>{{ _('Laps') }}:</p>
<p class="lap-statistics">{{ race.statistics().lap_count_by_controller(grid_entry['controller'])}}</p>
</div>
</div>
<div class = "d-flex flex-row lap-data">
<div class="p-2 flex-fill">
<p>{{ _('Race duration') }}:</p>
<p class="lap-statistics">{{ race.statistics().race_time_by_controller(grid_entry['controller'])}}</p>
</div>
</div>
<div class = "d-flex flex-row lap-data">
<div class="p-2 flex-fill">
<p>{{ _('Fastest Lap') }}:</p>
<p class="lap-statistics">{{race.statistics().fastest_lap_by_controller(grid_entry['controller']) }}</p>
</div>
</div>
<div class = "d-flex flex-row lap-data">
<div class="p-2 flex-fill">
<p>{{ _('Average Lap') }}:</p>
<p class="lap-statistics">{{race.statistics().average_lap_by_controller(grid_entry['controller']) }}</p>
</div>
</div>
<div class = "d-flex flex-row lap-data">
<div class="p-2 flex-fill">
<p>{{ _('Slowest Lap') }}:</p>
<p class="lap-statistics">{{race.statistics().slowest_lap_by_controller(grid_entry['controller']) }}</p>
</div>
</div>
<div class="horizontal-line" id="grid-entry-{{grid_entry['controller']}}">
</div>
</div>
{% endfor %}
</div>
{% endblock %}
Loading