Private
Public Access
1
0

Merge branch 'release/v18.5.16'

This commit is contained in:
Sander Roosendaal
2022-07-04 07:41:11 +02:00
43 changed files with 3811 additions and 1078 deletions

0
boatmovers/__init__.py Normal file
View File

36
boatmovers/admin.py Normal file
View File

@@ -0,0 +1,36 @@
from django.contrib import admin
from.models import Athlete, Crew, Race, Result
# Register your models here.
class AthleteInline(admin.StackedInline):
model = Athlete
class AthleteAdmin(admin.ModelAdmin):
list_display = ('first_name', 'last_name', 'birth_year')
class CrewInline(admin.StackedInline):
model = Athlete
class CrewAdmin(admin.ModelAdmin):
list_display = ('name',)
class RaceInline(admin.StackedInline):
model = Race
class RaceAdmin(admin.ModelAdmin):
list_display = ('name',)
class ResultInline(admin.StackedInline):
model = Result
class ResultAdmin(admin.ModelAdmin):
list_display = ('race','crew','order')
admin.site.register(Athlete, AthleteAdmin)
admin.site.register(Crew, CrewAdmin)
admin.site.register(Race, RaceAdmin)
admin.site.register(Result, ResultAdmin)

6
boatmovers/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class BoatmoversConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'boatmovers'

5
boatmovers/forms.py Normal file
View File

@@ -0,0 +1,5 @@
from django import forms
class CsvForm(forms.Form):
file = forms.FileField(label='CSV File')
# comment = forms.CharField(required=False)

View File

@@ -0,0 +1,58 @@
# Generated by Django 3.2.12 on 2022-06-22 17:41
import boatmovers.models
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Athlete',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_name', models.CharField(max_length=200)),
('last_name', models.CharField(max_length=200)),
('club', models.CharField(max_length=200)),
('trueskill_mu', models.FloatField(default=25.0)),
('trueskill_sigma', models.FloatField(default=8.333)),
('birth_year', models.IntegerField(default=1972)),
],
),
migrations.CreateModel(
name='Crew',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('athletes', models.ManyToManyField(to='boatmovers.Athlete')),
],
),
migrations.CreateModel(
name='Race',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField(default=boatmovers.models.current_day)),
('crew_size', models.IntegerField(default=1)),
],
),
migrations.CreateModel(
name='Result',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('order', models.PositiveIntegerField()),
('crew', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='boatmovers.crew')),
('race', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='boatmovers.race')),
],
),
migrations.AddField(
model_name='race',
name='resultlist',
field=models.ManyToManyField(through='boatmovers.Result', to='boatmovers.Crew'),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.2.12 on 2022-06-22 17:49
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('boatmovers', '0001_initial'),
]
operations = [
migrations.AlterUniqueTogether(
name='athlete',
unique_together={('first_name', 'last_name', 'birth_year')},
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 3.2.12 on 2022-06-22 17:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('boatmovers', '0002_alter_athlete_unique_together'),
]
operations = [
migrations.AddField(
model_name='race',
name='name',
field=models.CharField(default='Race1', max_length=200),
preserve_default=False,
),
migrations.AddField(
model_name='race',
name='resulturl',
field=models.URLField(null=True),
),
migrations.AlterUniqueTogether(
name='race',
unique_together={('date', 'name')},
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 3.2.12 on 2022-06-22 18:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('boatmovers', '0003_auto_20220622_1753'),
]
operations = [
migrations.AddField(
model_name='race',
name='verified',
field=models.BooleanField(default=False),
),
migrations.AlterUniqueTogether(
name='result',
unique_together={('crew', 'race', 'order')},
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.2.12 on 2022-06-22 18:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('boatmovers', '0004_auto_20220622_1835'),
]
operations = [
migrations.AddField(
model_name='athlete',
name='gender',
field=models.CharField(choices=[('m', 'M'), ('f', 'F')], default='m', max_length=200),
preserve_default=False,
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.2.12 on 2022-06-24 08:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('boatmovers', '0005_athlete_gender'),
]
operations = [
migrations.AddField(
model_name='athlete',
name='trueskill_exposed',
field=models.FloatField(default=0),
),
migrations.AlterField(
model_name='athlete',
name='trueskill_sigma',
field=models.FloatField(default=8.333333333333334),
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 3.2.12 on 2022-06-24 08:20
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('boatmovers', '0006_auto_20220624_0811'),
]
operations = [
migrations.AlterField(
model_name='result',
name='crew',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='results', to='boatmovers.crew'),
),
migrations.AlterField(
model_name='result',
name='race',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='results', to='boatmovers.race'),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.2.12 on 2022-06-24 11:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('boatmovers', '0007_auto_20220624_0820'),
]
operations = [
migrations.AddField(
model_name='race',
name='processed',
field=models.BooleanField(default=True),
),
migrations.AlterField(
model_name='race',
name='crew_size',
field=models.IntegerField(default=1, verbose_name='Nr of rowers per crew (1, 2, 4, 8)'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2022-06-24 12:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('boatmovers', '0008_auto_20220624_1135'),
]
operations = [
migrations.AlterField(
model_name='race',
name='crew_size',
field=models.IntegerField(choices=[(1, 1), (2, 2), (4, 4), (8, 8)], default=1, verbose_name='Nr of rowers per crew (1, 2, 4, 8)'),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.2.12 on 2022-06-24 12:50
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('boatmovers', '0009_alter_race_crew_size'),
]
operations = [
migrations.RemoveField(
model_name='race',
name='resultlist',
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2022-06-24 12:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('boatmovers', '0010_remove_race_resultlist'),
]
operations = [
migrations.AlterField(
model_name='race',
name='processed',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,33 @@
# Generated by Django 3.2.12 on 2022-06-25 13:28
import boatmovers.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('boatmovers', '0011_alter_race_processed'),
]
operations = [
migrations.AlterField(
model_name='crew',
name='athletes',
field=models.ManyToManyField(related_name='crews', to='boatmovers.Athlete'),
),
migrations.AlterField(
model_name='race',
name='date',
field=models.DateField(default=boatmovers.models.current_day, verbose_name='Race Date'),
),
migrations.AlterField(
model_name='race',
name='resulturl',
field=models.URLField(null=True, verbose_name='URL Link to results'),
),
migrations.AlterUniqueTogether(
name='result',
unique_together={('crew', 'order')},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2022-06-25 13:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('boatmovers', '0012_auto_20220625_1328'),
]
operations = [
migrations.AlterField(
model_name='crew',
name='athletes',
field=models.ManyToManyField(related_name='athlete_crews', to='boatmovers.Athlete'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2022-06-29 15:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('boatmovers', '0013_alter_crew_athletes'),
]
operations = [
migrations.AddField(
model_name='athlete',
name='dummy',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.2.12 on 2022-06-29 17:44
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('boatmovers', '0014_athlete_dummy'),
]
operations = [
migrations.AlterUniqueTogether(
name='athlete',
unique_together={('first_name', 'last_name', 'birth_year', 'gender')},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2022-06-29 18:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('boatmovers', '0015_alter_athlete_unique_together'),
]
operations = [
migrations.AddField(
model_name='race',
name='gender',
field=models.CharField(choices=[('m', 'M'), ('f', 'F')], default='m', max_length=200),
),
]

View File

256
boatmovers/models.py Normal file
View File

@@ -0,0 +1,256 @@
from django.db import models
from django import forms
from django.core.exceptions import ValidationError
import collections
import datetime
from django.utils import timezone
import trueskill
from rowers.utils import dologging
def current_day(ttz=None):
if ttz is None:
return (datetime.datetime.now(tz=timezone.utc)).date()
return datetime.datetime.utcnow().astimezone(pytz.timezone(ttz)).date()
# Create your models here.
class Athlete(models.Model):
first_name = models.CharField(max_length=200)
last_name = models.CharField(max_length=200)
club = models.CharField(max_length=200)
trueskill_mu = models.FloatField(default=25.)
trueskill_sigma = models.FloatField(default=25./3.)
trueskill_exposed = models.FloatField(default=0)
birth_year = models.IntegerField(default=1972)
gender = models.CharField(max_length=200, choices=(('m','M'),('f','F')))
dummy = models.BooleanField(default=False)
class Meta:
unique_together = ('first_name','last_name','birth_year','gender')
def __str__(self):
return u'{f} {l}'.format(f = self.first_name, l=self.last_name)
def save(self, *args, **kwargs):
name = '{f} {l}'.format(f = self.first_name, l = self.last_name)
athletes = Athlete.objects.filter(gender=self.gender)
if self.pk is not None:
athletes = athletes.exclude(pk=self.pk)
for a in athletes:
aname = '{f} {l}'.format(f = a.first_name, l = a.last_name)
if name == aname:
raise ValidationError("Duplicate:{id}".format(id=a.id))
rating = trueskill.Rating(self.trueskill_mu, self.trueskill_sigma)
self.trueskill_exposed = trueskill.expose(rating)
super(Athlete, self).save(*args, **kwargs)
def get_absolute_url(self):
return "/boatmovers/athlete/%i/" % self.id
class athleteForm(forms.ModelForm):
class Meta:
model = Athlete
fields = ['first_name','last_name','club','birth_year']
class Crew(models.Model):
athletes = models.ManyToManyField(Athlete, related_name='athlete_crews')
name = models.CharField(max_length=200)
def __str__(self):
return u'{n}'.format(n=self.name)
def save(self, *args, **kwargs):
super(Crew, self).save(*args, **kwargs)
def size(self):
return self.athletes.all().count()
class crewForm(forms.ModelForm):
class Meta:
model = Crew
fields = ['name', 'athletes']
class Race(models.Model):
name = models.CharField(max_length=200)
resulturl = models.URLField(null=True, verbose_name='URL Link to results')
date = models.DateField(default=current_day, verbose_name='Race Date')
#resultlist = models.ManyToManyField(Result,through='Result')
crew_size = models.IntegerField(default=1,verbose_name='Nr of rowers per crew (1, 2, 4, 8)',
choices=((1,1),(2,2),(4,4),(8,8)))
verified = models.BooleanField(default=False)
processed = models.BooleanField(default=False)
gender = models.CharField(max_length=200,choices=(('m','M'),('f','F')),default='m')
class Meta:
unique_together = ('date','name')
def __str__(self):
return self.name
def save(self, *args, **kwargs):
results = self.results.all()
crews = []
athletes = []
for result in results:
crews.append(result.crew.id)
for athlete in result.crew.athletes.all():
athletes.append(athlete.id)
if len(crews) != len(set(crews)):
raise ValidationError(
"Cannot have the same crew more than one time in a race"
)
#if len(athletes) != len(set(athletes)):
# raise ValidationError(
# "Cannot have the same athlete in different crews in a race"
# )
super(Race, self).save(*args, **kwargs)
def validate(self, verbose=False):
if len(self.results.all()) < 2:
if verbose:
print('False: Less than 2 results')
self.verified = False
self.save()
return False
l = self.results.all()[0].crew.size()
for result in self.results.all():
if result.crew.size() != l:
if verbose:
print('False: crew {c} has different crew size'.format(c=result.crew))
self.verified = False
self.save()
return False
if l not in [1,2,4,8]:
if verbose:
print('False: Crew size not in 1, 2, 4, or 8')
self.verified = False
self.save()
return False
results = self.results.all()
crews = []
athletes = []
for result in results:
crews.append(result.crew.id)
for athlete in result.crew.athletes.all():
if not athlete.dummy:
athletes.append(athlete.id)
if len(crews) != len(set(crews)):
if verbose:
print('False: Same crew competing twice')
self.verified = False
self.save()
return False
if len(athletes) != len(set(athletes)):
if verbose:
print('False: Duplicate athletes')
self.verified = False
self.save()
return False
self.verified = True
self.save()
def process(self):
if not self.verified:
if not self.validate():
return False
if self.processed:
return True
# validate the race
results = self.results.all().order_by('order')
crews = []
ranks = []
for result in results:
crew = result.crew
crewdict = {}
for athlete in crew.athletes.all():
crewdict[athlete.id] = trueskill.Rating(
athlete.trueskill_mu, athlete.trueskill_sigma)
crews.append(crewdict)
ranks.append(result.order)
rated_crews = trueskill.rate(crews, ranks)
for crew in rated_crews:
for id, rating in crew.items():
athlete = Athlete.objects.get(id=id)
athlete.trueskill_mu = rating.mu
athlete.trueskill_sigma = rating.sigma
athlete.save()
u = '{id},{f},{l},{mu},{sigma},{rid},{rname}'.format(
id = id,
f = athlete.first_name,
l = athlete.last_name,
mu = rating.mu,
sigma = rating.sigma,
rid = self.id,
rname = self.name,
)
dologging('ratings.csv',u)
self.processed = True
self.save()
return True
class raceForm(forms.ModelForm):
class Meta:
model = Race
fields = ['name','date','resulturl','crew_size','gender']
class Result(models.Model):
crew = models.ForeignKey(Crew, on_delete=models.CASCADE,
related_name='results')
race = models.ForeignKey(Race, on_delete=models.CASCADE,
related_name='results')
order = models.PositiveIntegerField()
class Meta:
unique_together = ('crew','order')
def __str__(self):
return u'{r}: {o} - {c}'.format(
r=self.race,
o=self.order,
c=self.crew,
)
def save(self, *args, **kwargs):
allresults = self.race.results.all()
athletes = []
for result in allresults:
for athlete in result.crew.athletes.all():
athletes.append(athlete.id)
if result.crew.id == self.crew.id:
raise ValidationError(
"Cannot have the same crew more than one time in a race"
)
if len(athletes) != len(set(athletes)):
print([item for item, count in collections.Counter(athletes).items() if count>1])
raise ValidationError(
"Cannot have the same athlete in different crews in a race"
)
super(Result,self).save(*args, **kwargs)
class resultForm(forms.ModelForm):
class Meta:
model = Result
fields = ['crew','race','order']

111
boatmovers/results.py Normal file
View File

@@ -0,0 +1,111 @@
import trueskill
from trueskill import Rating, rate
class Athlete:
def __init__(self, first_name, last_name, club, birth_year, mu=25, sigma=25./3.):
self.first_name = first_name
self.last_name = last_name
self.club = club
self.birth_year = birth_year
self.rating = Rating(mu, sigma)
def expose(self):
return trueskill.expose(self.rating)
def setrating(self, rating):
self.rating = rating
def __str__(self):
return u'{f} {l} {c} - {s:.2f}'.format(
f=self.first_name,
l=self.last_name,
c=self.club,
s=self.expose()
)
class Crew:
def __init__(self, athletes, name):
self.athletes = athletes
self.name = name
def size(self):
return len(self.athletes)
def __str__(self):
return u'{n}'.format(n=self.name)
class Result:
def __init__(self, crews, name, validated=False, processed=False):
self.crews = crews
self.name = name
self.verified = validated
self.processed = processed
def validate(self):
# crews need to be more than 2
if len(self.crews) < 2:
self.verified = False
return False
# crews need to be all same length
l = self.crews[0].size()
for crew in self.crews:
if crew.size() != l:
self.verified = False
return False
# crew length need to be 1, 2, 4 or 8
if l not in [1,2,4,8]:
self.verified = False
return False
# cannot have same crew multiple times in same race
if len(self.crews) != len(set(self.crews)):
self.verified = False
return False
# cannot have same athletes in different crews in same race
allathletes = []
for crew in self.crews:
for athlete in crew.athletes:
allathletes.append(athlete)
if len(allathletes) != len(set(allathletes)):
self.verified = False
return False
self.verified = True
return self.verified
def process(self):
if not self.verified:
if not self.validate():
return False
if self.processed:
return True
# validate the race
ratings = list([athlete.rating for athlete in crew.athletes] for crew in self.crews)
result = rate(ratings, ranks = list(range(len(self.crews))))
i = 0
j = 0
for c in result:
for rating in c:
self.crews[i].athletes[j].setrating(rating)
j += 1
i += 1
j = 0
self.processed = True
return True
def __str__(self):
s = self.name + ': '
for crew in self.crews:
s = s + str(crew) + ', '
return s[:-2]

153
boatmovers/scrapers.py Normal file
View File

@@ -0,0 +1,153 @@
import re
from bs4 import BeautifulSoup
import requests
from boatmovers.models import *
import pandas as pd
from django.core.exceptions import ValidationError
url_heineken = ''
def csv_reader(filename,raceid,clubcol='Ploeg',bankjes=['Slag'],uitslag='Pos',gender='m',
startorder=1):
race = Race.objects.get(id=raceid)
nr = race.crew_size
df = pd.read_csv(filename)
# replace column names if different
for row in df.itertuples():
order = row[df.columns.get_loc(uitslag)+1]
crewname = row[df.columns.get_loc(clubcol)+1]
crew = Crew(name=crewname)
crew.save()
for i in range(nr):
try:
naam = row[df.columns.get_loc(bankjes[i])+1].split(' ')
first_name = ' '.join(naam[:-1])
last_name = naam[-1]
dummy = False
except AttributeError:
try:
first_name = str(row.Slag)
last_name = ''
dummy=False
except TypeError:
first_name = 'Unknown'
last_name = 'Athlete'
dummy=True
athletes = Athlete.objects.filter(first_name = first_name,
last_name = last_name,
gender=gender)
if len(athletes) >= 1:
athlete = athletes[0]
else:
athlete = Athlete(first_name=first_name,
last_name=last_name,
club = crewname,
gender=gender,
dummy=dummy)
try:
athlete.save()
except ValidationError as e:
text, id = e.message.split(':')
athlete = Athlete.objects.get(id=id)
print(athlete)
crew.athletes.add(athlete)
result = Result(
crew = crew,
race = race,
order = order
)
try:
if order>=startorder:
result.save()
except ValidationError as e:
print(e)
print(' ')
def time_team_scraper(url,raceid,gender='m',startorder=1):
race = Race.objects.get(id=raceid)
nr = race.crew_size
r = requests.get(url)
soup = BeautifulSoup(r.content,features='lxml')
tbl = soup.find('table')
order = 1
str = re.search('(.*)results(.*)',url)
base = str.groups()[0]
for tr in tbl.findAll("tr"):
trs = tr.findAll("td")
for each in trs:
try:
link = each.find('a')['href']
name = each.find('a').contents[0]
except (TypeError, IndexError):
link = ''
name =''
if 'entry' in link:
print(order, name)
dfs = pd.read_html(base+link[3:])
df = dfs[0]
namen = df['naam']
crew = Crew(name=name)
crew.save()
for i in range(nr):
try:
names = df['naam'][i].split(' ')
first_name = ' '.join(names[:-1])
last_name = names[-1]
dummy=False
except AttributeError:
try:
first_name = str(df['naam'][i])
last_name = ''
dummy=False
except TypeError:
first_name = 'Unknown'
last_name = 'Athlete'
dummy=True
athletes = Athlete.objects.filter(first_name = first_name,
last_name = last_name)
if len(athletes) >= 1:
athlete = athletes[0]
else:
athlete = Athlete(first_name=first_name,
last_name=last_name,
club = name,
gender=gender,
dummy=dummy)
try:
athlete.save()
except ValidationError as e:
text, id = e.message.split(':')
athlete = Athlete.objects.get(id=id)
print(athlete)
crew.athletes.add(athlete)
result = Result(
crew = crew,
race = race,
order = order
)
if order >= startorder:
try:
result.save()
except ValidationError as e:
print(e)
order += 1
print('')

23
boatmovers/tasks.py Normal file
View File

@@ -0,0 +1,23 @@
import os
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
from YamJam import yamjam
CFG = yamjam()['rowsandallapp']
try:
os.environ.setdefault("DJANGO_SETTINGS_MODULE",CFG['settings_name'])
except KeyError:
os.environ.setdefault("DJANGO_SETTINGS_MODULE","rowsandall_app.settings")
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()
from boatmovers.models import Race
from rq import get_current_job
from django_rq import job
@job
def race_process(id):
job = get_current_job()
race = Race.objects.get(id=id)
return race.process()

View File

@@ -0,0 +1,16 @@
{% extends "boatmovers_base.html" %}
{% block main %}
<h1>
{{ athlete.first_name }} {{ athlete.last_name }}
</h1>
<p>
<table>
{% for result in results %}
<tr>
<td>{{ result.order }}</td><td>{{ result.race.name }}</td>
</tr>
{% endfor %}
</table>
</p>
{% endblock %}

View File

@@ -0,0 +1,124 @@
{% extends "boatmovers_base.html" %}
{% block main %}
<style>
.row {
display: flex;
}
.column {
flex: 50%;
border-width: 10px;
padding: 10px;
margin: 10px;
}
</style>
<h1>
Boat Movers Ranglijst
</h1>
<div>
<a href="/boatmovers/?filter=f">Dames</a>
<a href="/boatmovers/?filter=m">Heren</a>
<a href="/boatmovers/">Alles</a>
</div>
<div class="row">
<div class="column">
<p>
<table width="100%" class="listtable shortpadded">
<thead>
<tr>
<th>Rank</th>
<th>Score</th>
<th>Name</th><th>&nbsp;</th>
<th>Club</th>
<th>Gender</th>
</tr>
</thead>
<tbody>
{% for athlete in athletes %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ athlete.trueskill_exposed|floatformat:2 }}</td>
<td>{{ athlete.first_name }}</td>
<td>{{ athlete.last_name }}</td>
<td>{{ athlete.club }}</td>
<td>{{ athlete.gender }}</td>
<td><a href="/boatmovers/athlete/{{ athlete.id }}/">results</a></td>
{% if user.is_authenticated and user.is_staff %}
<td><a href="/admin/boatmovers/athlete/{{ athlete.id }}/change/">edit</a></td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</p>
</div>
<div class="column">
<p>
Deze ranglijst is een experimentele lijst gebaseerd op wedstrijduitslagen
die op het internet te vinden zijn. De focus is op Nederlandse Masters-wedstrijden
volgens KNRB-regels.
De inspiratie voor deze site kwam van <a href="https://rowingstats.com/">Rowing Stats</a>.
</p>
<p>
Zie onze <a href="faq">F.A.Q.</a> voor een beschrijving van hoe wij de ranglijst
berekenen.
</p>
<p>
Als je je naam hier wilt laten verwijderen, kan dat natuurlijk. Stuur ons
een emailtje op info@rowsandall.com.
</p>
<p>
Als je een uitslag mist, kun je ons dat laten weten door de "Nieuwe Race" link onder.
</p>
<p>
De ranglijst bevat nu resultaten van de volgende races:
</p>
<p>
<table>
{% for race in races %}
<tr>
<td>{{ race.date }}</td><td>{{ race.name }}</td>
<td>
<a href="race/{{ race.id }}">View Race</a>
</td>
</tr>
{% endfor %}
</table>
</p>
{% if user.is_authenticated and user.is_staff %}
{% if new_races %}
<p>
Niet verwerkte races:
</p>
<p>
<table>
{% for race in new_races %}
<tr>
<td>{{ race.date }}</td><td>{{ race.name }}</td>
<td>
<a href="race/{{ race.id }}">Manage Race</a>
</td>
</tr>
{% endfor %}
</table>
</p>
{% endif %}
<p>
<a href="athlete/add/">Nieuwe Roeier</a>
</p>
<p>
<a href="crew/add/">Nieuwe Ploeg</a>
</p>
{% endif %}
<p>
<a href="race/add/">Nieuwe Race</a>
</p>
{% if user.is_authenticated and user.is_staff %}
<p>
<a href="result/add/">Nieuwe uitslag</a>
</p>
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,7 @@
{% extends "boatmovers_base.html" %}
{% block main %}
<form action="" method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Create" />
</form>
{% endblock %}

View File

@@ -0,0 +1,4 @@
<form action="" method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Create" />
</form>

View File

@@ -0,0 +1,8 @@
{% extends "boatmovers_base.html" %}
{% block main %}
<p>Create new race</p>
<form action="" method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Create" />
</form>
{% endblock %}

View File

@@ -0,0 +1,7 @@
{% extends "boatmovers_base.html" %}
{% block main %}
<form action="" method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Create" />
</form>
{% endblock %}

View File

@@ -0,0 +1,41 @@
{% load leaflet_tags %}
{% load cookielaw_tags %}
{% block filters %}
{% endblock %}
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}Rowsandall Boatmovers{% endblock %}</title>
<link rel="stylesheet" href="/static/css/resetnew.css" />
<link rel="stylesheet" href="/static/fontawesome/css/fontawesome.min.css">
<link rel="stylesheet" href="/static/fontawesome/css/all.min.css">
<link rel="stylesheet" href="https://pro.fontawesome.com/releases/v5.12.0/css/all.css" integrity="sha384-ekOryaXPbeCpWQNxMwSWVvQ0+1VrStoPJq54shlYhR8HzQgig1v5fas6YgOqLoKz" crossorigin="anonymous">
<link rel="stylesheet" href="/static/css/styles2.css">
<link rel="stylesheet" href="/static/css/text2.css" />
<link rel="stylesheet" href="/static/css/rowsandall2.css" />
</head>
<body>
<div>
<a href="/boatmovers/">Ranglijst</a>
<a href="/boatmovers/faq/">F.A.Q.</a>
</div>
{% if WARNING_MESSAGE != '' %}
{{ WARNING_MESSAGE }}
{% endif %}
{% if messages %}
{% for message in messages %}
<p>
{{ message|safe }}
</p>
{% endfor %}
{% endif %}
{% block main %}
{% endblock %}
</body>
</html>
{% block scripts %} {% endblock %}

View File

@@ -0,0 +1,22 @@
{% extends "boatmovers_base.html" %}
{% block main %}
<h1>
{{ crew.name }}
</h1>
<p>
<table>
<tr>
<th>Athlete</th>
<th>&nbsp;</th>
<th>Score</th>
</tr>
{% for athlete in athletes %}
<tr>
<td>{{ athlete.first_name }}</td>
<td>{{ athlete.last_name }}</td>
<td>{{ athlete.trueskill_exposed|floatformat:2 }}</td>
</tr>
{% endfor %}
</table>
</p>
{% endblock %}

View File

@@ -0,0 +1,14 @@
{% extends "boatmovers_base.html" %}
{% block main %}
<h1>
{{ race.name }}
</h1>
<p>
<form method="post" enctype="multipart/form-data">{% csrf_token %}
{{ form.as_p }}
<input type="hidden" name="action" value="create" />
<input type="submit" value="Submit"/>
</form>
</p>
{% endblock %}

View File

@@ -0,0 +1,137 @@
{% extends "boatmovers_base.html" %}
{% block main %}
<h1>Hoe werkt de ranking?</h1>
<p>De ranking is gebaseerd op het “TrueSkill ranking system”. Voor iedere roeier worden twee variabelen
berekend. De eerste variabele (“mu”) geeft aan hoe “goed” je gemiddeld bent (in goede uitslagen van
wedstrijden). De tweede variabele (“sigma”) geeft de mate van onzekerheid in de schatting van “mu”.
De score op de site is berekend als “mu 3 sigma”. Vooral als je net begint, is dit een conservatieve
schatting.
</p>
<p>Iedere keer als er een wedstrijduitslag wordt verwerkt waar jij aan hebt deelgenomen, wordt je score
opnieuw berekend. Er wordt gekeken naar de (gemiddelde) scores van alle ploegen die aan de wedstrijd
hebben deelgenomen. Dan wordt er gekeken hoe waarschijnlijk deze uitslag is, gebaseerd op de scores
van de ploegen.</p>
<p>
Als je beter eindigt dan een ploeg met een hogere score voor de wedstrijd, zal jouw score stijgen.
Andersom, als je lager eindigt dan een ploeg met een lagere score, zal jouw score dalen. Hoeveel je
score stijgt of daalt, hangt af van hoe “onwaarschijnlijk” de uitslag is. Hoe onwaarschijnlijker je winst,
hoe meer je stijgt.</p>
<p>
Ook de onzekerheid in je “mu” zal dalen door aan wedstrijden deel te nemen. Als je begint, zal het dus
relatief makkelijk zijn om te stijgen in de ranking.</p>
<p>
<h1>Hoe kan ik stijgen op de ranking?</h1>
<p>Hoe vaker je aan wedstrijden deelneemt, hoe beter we weten hoe goed je bent. Zeker in het begin kun
je dus stijgen door veel wedstrijden te roeien. Je zult sneller stijgen door skiffwedstrijden te winnen dan
met wedstrijden in de 8. Dit komt doordat een skiff-uitslag de mate van onzekerheid in je skills sneller
doet dalen. Dit betekent niet dat je als 8-roeier niet bovenaan de ranking kunt staan. Je moet alleen
meer wedstrijden roeien voordat je “boven komt drijven”.</p>
<p>
Ook zul je sneller stijgen (en dalen!) door deel te nemen aan wedstrijden met een groot startveld.
Zodra je een paar wedstrijden hebt geroeid en het systeem een goed idee heeft van je niveau, is het
zaak om beter te worden en ploegen te verslaan die boven je staan. Het is dus verstandig om deel te
nemen aan goedbezochte wedstrijden.</p>
<h1>
Als het makkelijker is om te stijgen als je net begint, zijn dan ook mijn eerste races het belangrijkst
voor de plaats in de ranglijst?</h1>
<p>
Nee, dat is niet zo. TrueSkill legt meer nadruk op recente uitslagen dan aan oudere uitslagen. Als je dus
vaak tegen dezelfde ploegen start, zullen de meest recente winnaars hoger staan dan de oudere
winnaars.</p>
<h1>
Is dit een “ranglijst aller tijden” of een seizoensranking?</h1>
<p>Dit is een ranglijst aller tijden. Omdat veel roeiers in viertjes en achten starten, is een seizoen eigenlijk te
kort om skiffeurs te kunnen voorbijgaan op de ranking. Daarom laat ik de ranglijst oneindig doorlopen.
</p>
<h1>Als ik hoog sta in de ranking, kan ik dan hoog blijven door niet meer aan wedstrijden deel te nemen?</h1>
<p>Dat is inderdaad mogelijk. We gaan ervan uit dat je het leuker vindt om wedstrijden te roeien dan om
hoog in de ranking te staan. Ook hopen we dat de ranking mensen aanspoort om elkaar uit te dagen aan
wedstrijden mee te doen.</p>
<p>
Mocht blijken dat dit echt een probleem is, dan is het eenvoudig om een filter op laatste deelname aan
een wedstrijd toe te voegen. Persoonlijk vind ik het ook wel mooi als “legendarische” roeiers lang na
hun carriere hoog in de ranking blijven staan, ook al is er geen gelegenheid meer om tegen ze te starten.</p>
<h1>Ik roei altijd met dezelfde ploeg. Hoe kan het systeem een onderscheid in niveau bepalen tussen mijn
ploegleden onderling?</h1>
<p>Als alle leden van jouw ploeg altijd alleen in deze ene ploeg starten, zal je altijd dezelfde score houden.
Maar als een van jouw ploegleden ook wedstrijden start in een andere ploeg, dan zal jullie score gaan
verschillen.</p>
<h1>Is het eerlijk om voor ploegen het gemiddelde niveau te gebruiken?</h1>
<p>Onze aanname is dat dat de eenvoudigste manier is die waarschijnlijk vrij eerlijk is. Als roeien alleen
afhankelijk is van de power (ergometer score) die je in de benen hebt, klopt deze aanname 100 procent.
Maar bij het roeien is ook techniek belangrijk, dus er zal (gelukkig) altijd een verrassingsaspect zijn.
Waarom ontbreekt mijn ploeg in de uitslag?</p>
<p>In sommige uitslagen staan ploegen met roeiers met dezelfde naam. Dit is waarschijnlijk een foutje bij
de inschrijving, of het kan zijn dat je toevallig tegen een naamgenoot geroeid hebt.
Het systeem identificeert roeiers door hun voornaam/achternaam en geslacht. We hebben op dit
moment geen goed systeem om onderscheid te maken tussen twee verschillende roeiers die toevallig
allebei “Jan Visser” heten. Je zou de clubnaam kunnen gebruiken, maar soms wisselen roeiers van club.
Ook staan in de uitslagen ploegen geïdentificeerd als “Willem III/RIC/Hoop” en is het onmogelijk om te
zien welk ploeglid bij Willem III roeit en welk ploeglid bij De Hoop.</p>
<h1>Welke wedstrijden worden meegenomen?</h1>
<p>Op dit moment verwerken wij uitslagen van de volgende wedstrijden:
<ul>
<li>Heineken Roeivierkamp (einduitslag over alle afstanden)</li>
<li>Head of the River Amstel</li>
<li>Tweehead</li>
<li>Skiffhead</li>
<li>Spaarne Lenterace</li>
<li>Dutch Masters Open (alleen uitslagen van races die zonder startverschil worden geroeid)</li>
<li>Cottwich</li>
<li>Eemhead</li>
<li>Tromp Boat Races</li>
<li>Novembervieren</li>
</ul>
</p>
<p>Van deze wedstrijden nemen we uitslagen van veteranenvelden. We kijken niet naar het clubveld, of
open velden.</p>
<p>De reden dat we deze wedstrijden kiezen is dat het goedbezochte wedstrijden zijn met een online
uitslag die makkelijk te verwerken is (Time-Team, hoesnelwasik.nl). Het is lastig om wedstrijden te
verwerken waar niet de namen van alle ploegleden zijn te achterhalen. Vaak wordt er een PDF-je
gepubliceerd met alleen de naam van de slagroeier.</p>
<p>
Dit betekent niet dat we niet open staan voor het toevoegen van nieuwe wedstrijden. Volg de “Add
Race” link en vul de gegevens in. Wij krijgen dan een automatische email en gaan ernaar kijken.
Kan ik ook een onderling duel aangaan met iemand en dan de uitslag mee laten tellen?</p>
<p>
Het is belangrijk dat de uitslag van de wedstrijd onafhankelijk te controleren is. Het makkelijkst is dus af
te spreken allebei aan een van de meetellende wedstrijden te starten, of ons te vragen een (onder
KNRB-auspiciën geroeide) wedstrijd toe te voegen,
waarvan de uitslag online staat met alle ploegleden.</p>
<h1>Ik wil niet op deze ranking staan. Kun je mij verwijderen?</h1>
<p>Ja, dat kan op twee manieren:
<ol>
<li>Ik kan zorgen dat je naam niet in de ranking vermeld wordt. Je scores worden dan wel gebruikt
als je in ploegen start met andere mensen op de ranglijst. Je naam is dan nog wel zichtbaar als
mensen op die ploegen klikken in de uitslagenlijst. Aangezien deze uitslagen identiek zijn aan
wat al op het internet te vinden is, neem ik aan dat dit geen probleem is.
</li>
<li>Ik kan zorgen dat je uit de ranking verwijderd wordt en blijft. Bij het verwerken van uitslagen zal
ik dan jouw hele ploeg moeten verwijderen. De consequentie is dat je ploegmaten alleen
kunnen stijgen en dalen door wedstrijden zonder jou te roeien.
</li>
</ol>
</p>
<p>De enige informatie in het systeem is je voornaam, achternaam, geslacht en in welke ploegen je bent
gestart. Dit is informatie die in wedstrijduitslagen al beschikbaar is. Door in te schrijven voor deze
wedstrijden heb je al ingestemd met het openbaar maken van jouw naam en ploeggegevens in de
uitslag. Daarom vind ik het een redelijke aanname dat je geen bezwaar hebt tegen het opgenomen zijn
in deze ranking, maar als je het vraagt haal ik je er met alle plezier vanaf.</p>
{% endblock %}

View File

@@ -0,0 +1,63 @@
{% extends "boatmovers_base.html" %}
{% block main %}
<h1>
{{ race.name }}
</h1>
<p>
{{ race.date }}
</p>
<p>
<a target="_" href="{{ race.resulturl }}">Results</a>
</p>
<p>
<table>
<tr>
<th>Order</th>
<th>Crew</th><td></td>
</tr>
{% for result in results %}
<tr>
<td>{{ result.order }}</td>
<td><a href="/boatmovers/crew/{{ result.crew.id }}/">{{ result.crew.name }}</a></td>
{% if result.crew.id in duplicate_crews %}
<td>!</td>
{% elif result.crew.id in duplicate_athletes_crews %}
<td>!</td>
{% endif %}
</tr>
{% endfor %}
</table>
</p>
{% if user.is_authenticated and user.is_staff %}
{% if race.verified %}
<p>
Race has been verified
</p>
{% if race.processed %}
<p>
Race has been processed
</p>
{% else %}
<p>
Race is not processed. <a href="process/">Process Race</a>
</p>
{% endif %}
{% else %}
<p>
Race is not verified. <a href="verify/">Verify Race</a>
</p>
<p>
<a href="/boatmovers/race/{{ race.id }}/deleteresults/">Remove all results</a>
</p>
{% endif %}
{% if not race.verified and not race.processed %}
<p>
<a href="/boatmovers/result/add/">Add Result</a>
</p>
<p>
<a href="/boatmovers/race/{{ race.id }}/csv/">Add Result CSV</a>
</p>
{% endif %}
{% endif %}
{% endblock %}

3
boatmovers/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

25
boatmovers/urls.py Normal file
View File

@@ -0,0 +1,25 @@
from django.conf import settings
from django.conf.urls import url, include
from django.urls import path, re_path
from django.views.generic.base import TemplateView
import boatmovers.views as views
urlpatterns = [
url(r'athlete/add/$',views.AthleteCreateView.as_view(),name='athlete_add'),
url(r'athlete/(?P<id>\d+)/$',views.athlete_view,name='athlete_view'),
url(r'crew/add/$',views.CrewCreateView.as_view(),name='crew_add'),
url(r'race/add/$',views.RaceCreateView.as_view(),name='race_add'),
url(r'result/add/$',views.ResultCreateView.as_view(),name='result_add'),
url(r'race/(?P<id>\d+)/$',views.race_view,name='race_view'),
url(r'race/(?P<id>\d+)/csv/$',views.race_add_csv,name='race_add_csv'),
url(r'race/(?P<id>\d+)/verify/$',views.race_verify,name='race_verify'),
url(r'race/(?P<id>\d+)/process/$',views.race_process,name='race_process'),
url(r'race/(?P<id>\d+)/deleteresults/$',views.race_delete_results,
name='race_delete_results'),
url(r'crew/(?P<id>\d+)/$',views.crew_view,name='crew_view'),
url(r'^$',views.boatmovers_view,name='boatmovers'),
path(r'faq/', TemplateView.as_view(template_name='faq.html'), name='faq'),
#url(r'(?P<filter>\b[0-9A-Fa-f]+\b)/$',views.boatmovers_view,name='boatmovers')
]

215
boatmovers/views.py Normal file
View File

@@ -0,0 +1,215 @@
from django.shortcuts import render
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.contrib import messages
import collections
# Create your views here.
from django.views.generic.edit import CreateView
from boatmovers.models import Athlete, Crew, Race, Result
import boatmovers.tasks as tasks
from boatmovers.forms import CsvForm
from boatmovers.scrapers import *
#from rowers.rows import handle_uploaded_file
import django_rq
queue = django_rq.get_queue('high')
class AthleteCreateView(CreateView):
model = Athlete
fields = [
'first_name',
'last_name',
'birth_year',
'gender',
'club',
]
success_url = '/boatmovers/'
class CrewCreateView(CreateView):
model = Crew
fields = [
'name',
'athletes'
]
success_url = '/boatmovers/'
class RaceCreateView(CreateView):
model = Race
fields = [
'name',
'resulturl',
'date',
'crew_size',
'gender',
#'resultlist',
]
success_url = '/boatmovers/'
class ResultCreateView(CreateView):
model = Result
fields = [
'crew',
'race',
'order'
]
success_url = '/boatmovers/'
def athlete_view(request,id=0):
athlete = get_object_or_404(Athlete, pk=id)
crews = athlete.athlete_crews
resultslist = []
for crew in crews.values():
c = Crew.objects.get(id=crew['id'])
results = Result.objects.filter(crew=c)
for result in results:
resultslist.append(result)
return render(request,
'athlete.html',
{
'athlete':athlete,
'results':resultslist,
})
def boatmovers_view(request):
athletes = Athlete.objects.filter(trueskill_exposed__gt=0,
dummy=False).order_by('-trueskill_exposed','-birth_year','last_name','first_name')
filter = request.GET.get('filter','all')
if filter == 'm':
athletes = athletes.exclude(gender='f')
elif filter == 'f':
athletes = athletes.exclude(gender='m')
races = Race.objects.filter(verified=True,processed=True).order_by('-date')
new_races = Race.objects.filter(processed=False).order_by('date')
return render(request,
'boatmovers.html',
{'athletes':athletes,
'races': races,
'new_races': new_races}
)
def race_view(request,id=0):
race = get_object_or_404(Race, pk=id)
results = race.results.all().order_by('order')
crews = []
athletes = []
for result in results:
crews.append(result.crew.id)
for athlete in result.crew.athletes.all():
athletes.append(athlete.id)
# duplicates
duplicate_athletes = [item for item, count in collections.Counter(athletes).items() if count>1]
duplicate_crews = [item for item, count in collections.Counter(crews).items() if count>1]
duplicate_athletes_crews = []
for athlete_id in duplicate_athletes:
athlete = Athlete.objects.get(id=athlete_id)
crews = [crew.id for crew in athlete.athlete_crews.all()]
for crew in crews:
duplicate_athletes_crews.append(crew)
return render(request,
'race.html',
{
'race':race,
'results':results,
'duplicate_athletes':duplicate_athletes,
'duplicate_crews':duplicate_crews,
'duplicate_athletes_crews':duplicate_athletes_crews
}
)
def race_verify(request, id=0):
race = get_object_or_404(Race, pk=id)
outcome = race.validate()
return HttpResponseRedirect(reverse('race_view',kwargs={'id':race.id}))
def handle_uploaded_file(f):
with open('media/results.csv', 'wb+') as destination:
for chunk in f.chunks():
destination.write(chunk)
def race_add_csv(request, id=0):
race = get_object_or_404(Race, pk=id)
if race.verified or race.processed:
messages.error(request,"Cannot upload CSV file for processed or verified race")
url = reverse("race_view",kwargs={'id':id})
return HttpResponseRedirect(url)
form = CsvForm()
if request.method == 'POST':
form = CsvForm(request.POST, request.FILES)
if form.is_valid():
handle_uploaded_file(request.FILES['file'])
if race.crew_size == 1:
bankjes = ['Slag']
elif race.crew_size == 2:
bankjes = ['Slag','Boeg']
elif race.crew_size == 4:
bankjes = ['Slag','2','3','Boeg']
elif race.crew_size == 8:
bankjes = ['Slag','2','3','4','5','6','7','Boeg']
csv_reader('media/results.csv',race.id,bankjes=bankjes,gender=race.gender)
url = reverse('race_view',kwargs={'id':race.id})
return HttpResponseRedirect(url)
return render(request,
'csvform.html',
{
'race':race,
'form':form,
})
def race_process(request, id=0):
race = get_object_or_404(Race, pk=id)
job = queue.enqueue(tasks.race_process,race.id)
#outcome = race.process()
messages.info(request,"Your race is being processed. Reload to get the new status.")
return HttpResponseRedirect(reverse('race_view',kwargs={'id':race.id}))
def race_delete_results(request, id=0):
race = get_object_or_404(Race, pk=id)
results = race.results.all()
if not race.processed:
for result in results:
result.delete()
messages.info(request,'Results have been removed')
else:
messages.error(request,'Cannot remove processed results')
url = reverse('race_view',kwargs={'id':race.id})
return HttpResponseRedirect(url)
def crew_view(request, id=0):
crew = get_object_or_404(Crew, pk=id)
athletes = crew.athletes.all().order_by("-trueskill_exposed")
return render(request,
'crew.html',
{
'crew':crew,
'athletes':athletes
})

File diff suppressed because it is too large Load Diff

1089
ratings.csv Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -76,6 +76,7 @@ INSTALLED_APPS = [
'django_countries',
'rules',
'taggit',
'boatmovers',
]
AUTHENTICATION_BACKENDS = (
@@ -137,10 +138,7 @@ TEMPLATES = [
],
'libraries': {
'staticfiles': 'django.templatetags.static',
}
# 'loaders': [
# 'django.template.loaders.app_directories.Loader',
# ],
},
},
},
]

View File

@@ -79,7 +79,8 @@ urlpatterns += [
{'next_page': '/'},
name='logout',),
re_path(r'^rowers/', include('rowers.urls')),
# re_path(r'^survey/',include('survey.urls')),
re_path(r'^boatmovers/',include('boatmovers.urls')),
#re_path(r'^survey/',include('survey.urls')),
# re_path(r'^cvkbrno/',include('cvkbrno.urls')),
# re_path(r'^admin/rq/',include('django_rq_dashboard.urls')),
re_path(r'^call\_back', rowersviews.rower_process_callback),