Merge branch 'release/v18.5.16'
This commit is contained in:
0
boatmovers/__init__.py
Normal file
0
boatmovers/__init__.py
Normal file
36
boatmovers/admin.py
Normal file
36
boatmovers/admin.py
Normal 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
6
boatmovers/apps.py
Normal 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
5
boatmovers/forms.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django import forms
|
||||
|
||||
class CsvForm(forms.Form):
|
||||
file = forms.FileField(label='CSV File')
|
||||
# comment = forms.CharField(required=False)
|
||||
58
boatmovers/migrations/0001_initial.py
Normal file
58
boatmovers/migrations/0001_initial.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
17
boatmovers/migrations/0002_alter_athlete_unique_together.py
Normal file
17
boatmovers/migrations/0002_alter_athlete_unique_together.py
Normal 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')},
|
||||
),
|
||||
]
|
||||
28
boatmovers/migrations/0003_auto_20220622_1753.py
Normal file
28
boatmovers/migrations/0003_auto_20220622_1753.py
Normal 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')},
|
||||
),
|
||||
]
|
||||
22
boatmovers/migrations/0004_auto_20220622_1835.py
Normal file
22
boatmovers/migrations/0004_auto_20220622_1835.py
Normal 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')},
|
||||
),
|
||||
]
|
||||
19
boatmovers/migrations/0005_athlete_gender.py
Normal file
19
boatmovers/migrations/0005_athlete_gender.py
Normal 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,
|
||||
),
|
||||
]
|
||||
23
boatmovers/migrations/0006_auto_20220624_0811.py
Normal file
23
boatmovers/migrations/0006_auto_20220624_0811.py
Normal 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),
|
||||
),
|
||||
]
|
||||
24
boatmovers/migrations/0007_auto_20220624_0820.py
Normal file
24
boatmovers/migrations/0007_auto_20220624_0820.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
23
boatmovers/migrations/0008_auto_20220624_1135.py
Normal file
23
boatmovers/migrations/0008_auto_20220624_1135.py
Normal 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)'),
|
||||
),
|
||||
]
|
||||
18
boatmovers/migrations/0009_alter_race_crew_size.py
Normal file
18
boatmovers/migrations/0009_alter_race_crew_size.py
Normal 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)'),
|
||||
),
|
||||
]
|
||||
17
boatmovers/migrations/0010_remove_race_resultlist.py
Normal file
17
boatmovers/migrations/0010_remove_race_resultlist.py
Normal 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',
|
||||
),
|
||||
]
|
||||
18
boatmovers/migrations/0011_alter_race_processed.py
Normal file
18
boatmovers/migrations/0011_alter_race_processed.py
Normal 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),
|
||||
),
|
||||
]
|
||||
33
boatmovers/migrations/0012_auto_20220625_1328.py
Normal file
33
boatmovers/migrations/0012_auto_20220625_1328.py
Normal 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')},
|
||||
),
|
||||
]
|
||||
18
boatmovers/migrations/0013_alter_crew_athletes.py
Normal file
18
boatmovers/migrations/0013_alter_crew_athletes.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
18
boatmovers/migrations/0014_athlete_dummy.py
Normal file
18
boatmovers/migrations/0014_athlete_dummy.py
Normal 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),
|
||||
),
|
||||
]
|
||||
17
boatmovers/migrations/0015_alter_athlete_unique_together.py
Normal file
17
boatmovers/migrations/0015_alter_athlete_unique_together.py
Normal 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')},
|
||||
),
|
||||
]
|
||||
18
boatmovers/migrations/0016_race_gender.py
Normal file
18
boatmovers/migrations/0016_race_gender.py
Normal 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),
|
||||
),
|
||||
]
|
||||
0
boatmovers/migrations/__init__.py
Normal file
0
boatmovers/migrations/__init__.py
Normal file
256
boatmovers/models.py
Normal file
256
boatmovers/models.py
Normal 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
111
boatmovers/results.py
Normal 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
153
boatmovers/scrapers.py
Normal 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
23
boatmovers/tasks.py
Normal 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()
|
||||
16
boatmovers/templates/athlete.html
Normal file
16
boatmovers/templates/athlete.html
Normal 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 %}
|
||||
124
boatmovers/templates/boatmovers.html
Normal file
124
boatmovers/templates/boatmovers.html
Normal 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> </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 %}
|
||||
7
boatmovers/templates/boatmovers/athlete_form.html
Normal file
7
boatmovers/templates/boatmovers/athlete_form.html
Normal 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 %}
|
||||
4
boatmovers/templates/boatmovers/crew_form.html
Normal file
4
boatmovers/templates/boatmovers/crew_form.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<form action="" method="post">{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" value="Create" />
|
||||
</form>
|
||||
8
boatmovers/templates/boatmovers/race_form.html
Normal file
8
boatmovers/templates/boatmovers/race_form.html
Normal 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 %}
|
||||
7
boatmovers/templates/boatmovers/result_form.html
Normal file
7
boatmovers/templates/boatmovers/result_form.html
Normal 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 %}
|
||||
41
boatmovers/templates/boatmovers_base.html
Normal file
41
boatmovers/templates/boatmovers_base.html
Normal 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 %}
|
||||
22
boatmovers/templates/crew.html
Normal file
22
boatmovers/templates/crew.html
Normal file
@@ -0,0 +1,22 @@
|
||||
{% extends "boatmovers_base.html" %}
|
||||
{% block main %}
|
||||
<h1>
|
||||
{{ crew.name }}
|
||||
</h1>
|
||||
<p>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Athlete</th>
|
||||
<th> </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 %}
|
||||
14
boatmovers/templates/csvform.html
Normal file
14
boatmovers/templates/csvform.html
Normal 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 %}
|
||||
137
boatmovers/templates/faq.html
Normal file
137
boatmovers/templates/faq.html
Normal 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 %}
|
||||
63
boatmovers/templates/race.html
Normal file
63
boatmovers/templates/race.html
Normal 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
3
boatmovers/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
25
boatmovers/urls.py
Normal file
25
boatmovers/urls.py
Normal 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
215
boatmovers/views.py
Normal 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
|
||||
})
|
||||
2144
cvkbrno/views.py
2144
cvkbrno/views.py
File diff suppressed because it is too large
Load Diff
1089
ratings.csv
Normal file
1089
ratings.csv
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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',
|
||||
# ],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user