mass refactor

This commit is contained in:
James Turk 2015-04-10 14:19:12 -04:00
parent b61d681685
commit c16b4caaa1
35 changed files with 241 additions and 284 deletions

3
fitnotes/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

40
fitnotes/importer.py Normal file
View File

@ -0,0 +1,40 @@
import sqlite3
from django.db import transaction
from lifting.models import Lift, Set
def _clean_name(name):
return name.lower()
def import_fitnotes_db(filename, user, fitnotes_to_lift):
# lift name => id
lift_ids = {_clean_name(l.name): l.id for l in Lift.objects.all()}
# build mapping FitNotes exercise id => our lift id
lift_id_mapping = {}
conn = sqlite3.connect(filename)
cur = conn.cursor()
for fnid, ename in cur.execute('SELECT _id, name FROM exercise WHERE exercise_type_id=0'):
cleaned = _clean_name(ename)
if cleaned not in fitnotes_to_lift:
lift_id_mapping[fnid] = cleaned
else:
lift_id_mapping[fnid] = lift_ids[fitnotes_to_lift[cleaned]]
with transaction.atomic():
Set.objects.filter(source='fitnotes').delete()
for fnid, date, weight_kg, reps in cur.execute(
'SELECT exercise_id, date, metric_weight, reps FROM training_log'):
# error if mapping wasn't found and there's a workout using it
if isinstance(lift_id_mapping[fnid], str):
raise ValueError('no known conversion for fitnotes exercise "{}"'.format(
lift_id_mapping[fnid]))
lift_id = lift_id_mapping[fnid]
Set.objects.create(lift_id=lift_id, date=date, weight_kg=weight_kg, reps=reps,
source='fitnotes', user=user)

View File

3
fitnotes/models.py Normal file
View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

62
fitnotes/tests.py Normal file
View File

@ -0,0 +1,62 @@
from django.test import TestCase
from django.contrib.auth.models import User
from inventory.models import Lift
from lifting.models import Set
from .importer import import_fitnotes_db
class TestFitnotesImport(TestCase):
# fitnotes.db has:
# April 1
# bench press 10 @ 45
# bench press 5 @ 95
# bench press 3 @ 135
# bench press 5 @ 155
# April 3
# squat 10 @ 45
# squat 5 @ 95
# squat 3 @ 135
# squat 2 @ 185
# squat 5 @ 225
def setUp(self):
self.user = User.objects.create_user('default', 'default@example.com', 'default')
self.bench = Lift.objects.create(name='bench press')
self.squat = Lift.objects.create(name='squat')
self.good_mapping = {'flat barbell bench press': 'bench press',
'barbell squat': 'squat'
}
self.bad_mapping = {'flat barbell bench press': 'bench press' }
def test_basic_import(self):
# ensure that the data comes in
import_fitnotes_db('fitnotes/testdata/example.fitnotes', self.user, self.good_mapping)
assert Set.objects.filter(lift=self.bench).count() == 4
assert Set.objects.filter(lift=self.squat).count() == 5
def test_double_import(self):
# two identical dbs, should be idempotent
import_fitnotes_db('fitnotes/testdata/example.fitnotes', self.user, self.good_mapping)
import_fitnotes_db('fitnotes/testdata/example.fitnotes', self.user, self.good_mapping)
assert Set.objects.filter(lift=self.bench).count() == 4
assert Set.objects.filter(lift=self.squat).count() == 5
def test_import_with_other_data(self):
Set.objects.create(lift=self.bench, weight_kg=100, reps=10, date='2014-01-01',
user=self.user)
import_fitnotes_db('fitnotes/testdata/example.fitnotes', self.user, self.good_mapping)
assert Set.objects.filter(lift=self.bench).count() == 5
def test_import_with_bad_mapping(self):
with self.assertRaises(ValueError):
import_fitnotes_db('fitnotes/testdata/example.fitnotes', self.user, self.bad_mapping)
assert Set.objects.filter(lift=self.bench).count() == 0
def test_bad_data_import(self):
# good db then bad db, should fail without screwing up existing data
import_fitnotes_db('fitnotes/testdata/example.fitnotes', self.user, self.good_mapping)
with self.assertRaises(Exception):
# baddata.fitnotes has all lift ids set to 9999
import_fitnotes_db('fitnotes/testdata/baddata.fitnotes', self.user, self.good_mapping)
assert Set.objects.count() == 9

View File

@ -3,5 +3,5 @@ from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^edit/$', views.edit_profile, name='edit-profile'),
url(r'^fitnotes/$', views.fitnotes_upload),
]

18
fitnotes/views.py Normal file
View File

@ -0,0 +1,18 @@
from django.shortcuts import render
@login_required
def fitnotes_upload(request):
if request.method == 'POST':
form = FitnotesUploadForm(request.POST, request.FILES)
if form.is_valid():
_, fname = tempfile.mkstemp()
with open(fname, 'wb') as tmp:
for chunk in request.FILES['file'].chunks():
tmp.write(chunk)
try:
importers.import_fitnotes_db(fname, request.user)
finally:
os.remove(fname)
else:
form = FitnotesUploadForm()
return render(request, 'lifting/fitnotes.html', {'form': form})

0
inventory/__init__.py Normal file
View File

6
inventory/admin.py Normal file
View File

@ -0,0 +1,6 @@
from django.contrib import admin
from .models import Lift
@admin.register(Lift)
class LiftAdmin(admin.ModelAdmin):
pass

View File

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
]
operations = [
migrations.CreateModel(
name='Bar',
fields=[
('id', models.AutoField(verbose_name='ID', auto_created=True, serialize=False, primary_key=True)),
('name', models.CharField(max_length=100)),
('weight_kg', models.DecimalField(max_digits=7, decimal_places=3)),
],
),
migrations.CreateModel(
name='Lift',
fields=[
('id', models.AutoField(verbose_name='ID', auto_created=True, serialize=False, primary_key=True)),
('name', models.CharField(max_length=200)),
],
),
]

View File

22
inventory/models.py Normal file
View File

@ -0,0 +1,22 @@
from django.db import models
from common import to_lb
class Bar(models.Model):
name = models.CharField(max_length=100)
weight_kg = models.DecimalField(max_digits=7, decimal_places=3)
def __str__(self):
return '{} ({}lb / {}kg)'.format(self.name, self.weight_kg, self.weight_lb)
@property
def weight_lb(self):
return to_lb(self.weight_kg)
class Lift(models.Model):
name = models.CharField(max_length=200)
def __str__(self):
return self.name

3
inventory/tests.py Normal file
View File

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

View File

@ -0,0 +1 @@
default_app_config = 'lifting.apps.LiftingConfig'

View File

@ -1,15 +1,10 @@
from django.contrib import admin
from .models import Exercise, Set
@admin.register(Exercise)
class ExerciseAdmin(admin.ModelAdmin):
pass
from .models import Set
@admin.register(Set)
class SetAdmin(admin.ModelAdmin):
date_hierarchy = 'date'
readonly_fields = ('user', 'exercise', 'date')
list_filter = ('user__username', 'exercise')
fields = ('user', 'exercise', 'date', 'weight_kg', 'reps', 'source')
readonly_fields = ('user', 'lift', 'date')
list_filter = ('user__username', 'lift')
fields = ('user', 'lift', 'date', 'weight_kg', 'reps', 'source')

View File

@ -5,14 +5,14 @@ from django.contrib.auth.models import User
def create_profile(sender, created, instance, **kwargs):
from .models import Profile
from .models import LiftingOptions
if created:
Profile.objects.create(user=instance)
LiftingOptions.objects.create(user=instance)
class ProfileConfig(AppConfig):
name = 'profiles'
app_label = 'profiles'
class LiftingConfig(AppConfig):
name = 'lifting'
app_label = 'lifting'
def ready(self):
post_save.connect(create_profile, sender=User)

View File

@ -1,40 +0,0 @@
import sqlite3
from django.db import transaction
from lifting.models import Exercise, Set
def _clean_name(name):
return name.lower()
def import_fitnotes_db(filename, user):
# exercise names to db ids
exercises = {}
for e in Exercise.objects.all():
for n in e.names:
exercises[_clean_name(n)] = e.id
# build mapping FitNotes exercise id => our exercise id
exercise_id_mapping = {}
conn = sqlite3.connect(filename)
cur = conn.cursor()
for fnid, ename in cur.execute('SELECT _id, name FROM exercise WHERE exercise_type_id=0'):
cleaned = _clean_name(ename)
# map to an Exercise id or str
exercise_id_mapping[fnid] = exercises[cleaned] if cleaned in exercises else cleaned
with transaction.atomic():
Set.objects.filter(source='fitnotes').delete()
for fnid, date, weight_kg, reps in cur.execute(
'SELECT exercise_id, date, metric_weight, reps FROM training_log'):
# create Exercise if it wasn't found and there's a workout using it
if isinstance(exercise_id_mapping[fnid], str):
exercise_id_mapping[fnid] = Exercise.objects.create(
names=[exercise_id_mapping[fnid]]).id
exercise_id = exercise_id_mapping[fnid]
Set.objects.create(exercise_id=exercise_id, date=date, weight_kg=weight_kg, reps=reps,
source='fitnotes', user=user)

View File

@ -3,30 +3,36 @@ from __future__ import unicode_literals
from django.db import models, migrations
import django.contrib.postgres.fields
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('inventory', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Exercise',
name='LiftingOptions',
fields=[
('id', models.AutoField(verbose_name='ID', auto_created=True, primary_key=True, serialize=False)),
('names', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=200), size=None)),
('id', models.AutoField(verbose_name='ID', auto_created=True, serialize=False, primary_key=True)),
('lifting_units', models.CharField(default='i', choices=[('m', 'Metric (kg)'), ('i', 'Imperial (lb)')], max_length=1)),
('plate_pairs', django.contrib.postgres.fields.ArrayField(base_field=models.DecimalField(max_digits=7, decimal_places=3), default=['45', '45', '25', '10', '5', '5', '2.5', '1.25'], size=None)),
('user', models.OneToOneField(related_name='lifting_options', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Set',
fields=[
('id', models.AutoField(verbose_name='ID', auto_created=True, primary_key=True, serialize=False)),
('id', models.AutoField(verbose_name='ID', auto_created=True, serialize=False, primary_key=True)),
('date', models.DateField()),
('weight_kg', models.DecimalField(decimal_places=3, max_digits=7)),
('weight_kg', models.DecimalField(max_digits=7, decimal_places=3)),
('reps', models.PositiveIntegerField()),
('source', models.CharField(max_length=100)),
('exercise', models.ForeignKey(to='lifting.Exercise', related_name='sets')),
('lift', models.ForeignKey(related_name='sets', to='inventory.Lift')),
('user', models.ForeignKey(related_name='sets', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -1,22 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('lifting', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='set',
name='user',
field=models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='sets', default=None),
preserve_default=False,
),
]

View File

@ -1,30 +1,34 @@
from django.db import models
from django.contrib.auth.models import User
from django.contrib.postgres.fields import ArrayField
from inventory.models import Lift
SET_TYPES = (
('warmup', 'Warmup'),
('planned', 'Planned'),
)
UNITS = (
('m', 'Metric (kg)'),
('i', 'Imperial (lb)'),
)
class Exercise(models.Model):
names = ArrayField(models.CharField(max_length=200))
def display_name(self):
return self.names[0].title()
class LiftingOptions(models.Model):
user = models.OneToOneField(User, related_name='lifting_options')
def __str__(self):
return ', '.join(self.names)
lifting_units = models.CharField(max_length=1, choices=UNITS, default='i')
plate_pairs = ArrayField(models.DecimalField(max_digits=7, decimal_places=3),
default=['45','45','25','10','5','5','2.5','1.25'])
class Set(models.Model):
user = models.ForeignKey(User, related_name='sets')
date = models.DateField()
exercise = models.ForeignKey(Exercise, related_name='sets')
lift = models.ForeignKey(Lift, related_name='sets')
weight_kg = models.DecimalField(max_digits=7, decimal_places=3)
reps = models.PositiveIntegerField()
source = models.CharField(max_length=100)
def __str__(self):
return '{} - {} @ {}kg - {}'.format(self.exercise, self.reps, self.weight_kg, self.date)
return '{} - {} @ {}kg - {}'.format(self.lift, self.reps, self.weight_kg, self.date)

View File

@ -1,55 +1,10 @@
from django.test import TestCase
from django.contrib.auth.models import User
from lifting.models import Exercise, Set
from lifting.importers import import_fitnotes_db
from lifting.models import LiftingOptions
class TestFitnotesImport(TestCase):
# fitnotes.db has:
# April 1
# bench press 10 @ 45
# bench press 5 @ 95
# bench press 3 @ 135
# bench press 5 @ 155
# April 3
# squat 10 @ 45
# squat 5 @ 95
# squat 3 @ 135
# squat 2 @ 185
# squat 5 @ 225
class TestLiftingOptions(TestCase):
def setUp(self):
self.user = User.objects.create_user('default', 'default@example.com', 'default')
def test_basic_import(self):
# ensure that the data comes in
import_fitnotes_db('lifting/testdata/example.fitnotes', self.user)
assert Exercise.objects.count() == 2
bp = Exercise.objects.get(names__contains=["flat barbell bench press"])
squat = Exercise.objects.get(names__contains=["barbell squat"])
assert Set.objects.count() == 9
def test_double_import(self):
# two identical dbs, should be idempotent
import_fitnotes_db('lifting/testdata/example.fitnotes', self.user)
import_fitnotes_db('lifting/testdata/example.fitnotes', self.user)
assert Exercise.objects.count() == 2
assert Set.objects.count() == 9
def test_import_with_other_data(self):
Exercise.objects.create(names=['incline bench press'])
e = Exercise.objects.create(names=['flat barbell bench press'])
Set.objects.create(exercise=e, weight_kg=100, reps=10, date='2014-01-01', user=self.user)
import_fitnotes_db('lifting/testdata/example.fitnotes', self.user)
assert Exercise.objects.count() == 3
assert Set.objects.count() == 10
def test_bad_import(self):
# good db then bad db, should fail without screwing up existing data
import_fitnotes_db('lifting/testdata/example.fitnotes', self.user)
with self.assertRaises(Exception):
# baddata.fitnotes has all exercise ids set to 9999
import_fitnotes_db('lifting/testdata/baddata.fitnotes', self.user)
assert Exercise.objects.count() == 2
assert Set.objects.count() == 9
def test_signal(self):
u = User.objects.create_user(username='test', email='test@example.com', password='test')
assert LiftingOptions.objects.filter(user=u).count() == 1

View File

@ -10,5 +10,5 @@ urlpatterns = [
url(r'^lifts/$', views.lift_list, name='lift-list'),
url(r'^lifts/(?P<lift_id>\d+)/$', views.by_lift, name='lift-detail'),
url(r'^fitnotes/$', views.fitnotes_upload),
url(r'^edit/$', views.edit_profile, name='edit-profile'),
]

View File

@ -11,7 +11,8 @@ from django.views.generic import dates
from django.db.models import Count, Max
from . import importers
from .models import Set, Exercise
from .models import Set
from inventory.models import Lift
@login_required
@ -20,7 +21,7 @@ def month_lifts(request, year, month):
sets_by_day = defaultdict(set)
for workset in Set.objects.filter(user=request.user, date__year=year, date__month=month):
sets_by_day[workset.date.day].add(workset.exercise)
sets_by_day[workset.date.day].add(workset.lift)
date = datetime.date(year, month, 1)
# build calendar
@ -68,7 +69,7 @@ def day_lifts(request, year, month, day):
@login_required
def lift_list(request):
lifts = Exercise.objects.filter(sets__user=request.user).annotate(
lifts = Lift.objects.filter(sets__user=request.user).annotate(
total=Count('sets'), max_kg=Max('sets__weight_kg'),
last_date=Max('sets__date'),
).order_by('-last_date')
@ -77,8 +78,8 @@ def lift_list(request):
@login_required
def by_lift(request, lift_id):
lift = Exercise.objects.get(pk=lift_id)
sets = Set.objects.filter(user=request.user, exercise=lift).order_by('-date')
lift = Lift.objects.get(pk=lift_id)
sets = Set.objects.filter(user=request.user, lift=lift).order_by('-date')
return render(request, 'lifting/by_lift.html', {'lift': lift, 'sets': sets})
@ -87,18 +88,7 @@ class FitnotesUploadForm(forms.Form):
@login_required
def fitnotes_upload(request):
if request.method == 'POST':
form = FitnotesUploadForm(request.POST, request.FILES)
if form.is_valid():
_, fname = tempfile.mkstemp()
with open(fname, 'wb') as tmp:
for chunk in request.FILES['file'].chunks():
tmp.write(chunk)
try:
importers.import_fitnotes_db(fname, request.user)
finally:
os.remove(fname)
else:
form = FitnotesUploadForm()
return render(request, 'lifting/fitnotes.html', {'form': form})
def edit_profile(request):
form = request.user.profile
return render(request, 'profiles/edit.html', {'form': form})

View File

@ -1 +0,0 @@
default_app_config = 'profiles.apps.ProfileConfig'

View File

@ -1,23 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Profile',
fields=[
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)),
('lifting_units', models.CharField(max_length=1, choices=[('m', 'Metric (kg)'), ('i', 'Imperial (lbs)')])),
('user', models.OneToOneField(related_name='config', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -1,41 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
import django.contrib.postgres.fields
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('profiles', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Bar',
fields=[
('id', models.AutoField(auto_created=True, serialize=False, verbose_name='ID', primary_key=True)),
('name', models.CharField(max_length=100)),
('weight_kg', models.DecimalField(max_digits=7, decimal_places=3)),
('user', models.OneToOneField(related_name='bar', to=settings.AUTH_USER_MODEL)),
],
),
migrations.AddField(
model_name='profile',
name='plate_pairs',
field=django.contrib.postgres.fields.ArrayField(default=['45', '45', '25', '10', '5', '5', '2.5', '1.25'], base_field=models.DecimalField(max_digits=7, decimal_places=3), size=None),
),
migrations.AlterField(
model_name='profile',
name='lifting_units',
field=models.CharField(default='i', choices=[('m', 'Metric (kg)'), ('i', 'Imperial (lb)')], max_length=1),
),
migrations.AlterField(
model_name='profile',
name='user',
field=models.OneToOneField(related_name='profile', to=settings.AUTH_USER_MODEL),
),
]

View File

@ -1,31 +0,0 @@
from django.db import models
from django.contrib.auth.models import User
from django.contrib.postgres.fields import ArrayField
from common import to_lb
UNITS = (
('m', 'Metric (kg)'),
('i', 'Imperial (lb)'),
)
class Profile(models.Model):
user = models.OneToOneField(User, related_name='profile')
lifting_units = models.CharField(max_length=1, choices=UNITS, default='i')
plate_pairs = ArrayField(models.DecimalField(max_digits=7, decimal_places=3),
default=['45','45','25','10','5','5','2.5','1.25'])
class Bar(models.Model):
user = models.OneToOneField(User, related_name='bar')
name = models.CharField(max_length=100)
weight_kg = models.DecimalField(max_digits=7, decimal_places=3)
def __str__(self):
return '{} ({}lb / {}kg)'.format(self.name, self.weight_kg, self.weight_lb)
@property
def weight_lb(self):
return to_lb(self.weight_kg)

View File

@ -1,12 +0,0 @@
from django.test import TestCase
from django.contrib.auth.models import User
from .models import Profile
class TestProfile(TestCase):
def test_signal(self):
u = User.objects.create_user(username='test', email='test@example.com', password='test')
assert Profile.objects.filter(user=u).count() == 1

View File

@ -1,10 +0,0 @@
from django.shortcuts import render
from django import forms
from django.contrib.auth.decorators import login_required
@login_required
def edit_profile(request):
form = request.user.profile
return render(request, 'profiles/edit.html', {'form': form})

View File

@ -16,7 +16,7 @@
<table class="table day-workouts">
<thead>
<tr>
<th>Exercise</th>
<th>Lift</th>
<th>Weight</th>
<th>Reps</th>
<tr>
@ -24,7 +24,7 @@
<tbody>
{% for set in sets %}
<tr>
<td><a href="{% url 'lift-detail' set.exercise.id %}">{{set.exercise}}</a></td>
<td><a href="{% url 'lift-detail' set.lift.id %}">{{set.lift}}</a></td>
<td>{% mass_unit set.weight_kg %}</td>
<td>{{set.reps}}</td>
</tr>

View File

@ -14,7 +14,7 @@
<table class="table lifts-table">
<thead>
<tr>
<th>Exercise</th>
<th>Lift</th>
<th>Total Sets</th>
<th>Last Set</th>
<th>Max Weight</th>

View File

@ -21,8 +21,9 @@ INSTALLED_APPS = (
'django.contrib.messages',
'django.contrib.staticfiles',
'django_gravatar',
'profiles',
'inventory',
'lifting',
'fitnotes',
)
MIDDLEWARE_CLASSES = (