Commit e89bc7c
Changed files (36)
app
controllers
models
views
training_sessions
db
spec
controllers
factories
fixtures
models
routing
support
app/controllers/training_sessions_controller.rb
@@ -0,0 +1,10 @@
+class TrainingSessionsController < ApplicationController
+ def index
+ @training_sessions = current_user.training_sessions
+ end
+
+ def upload
+ ProcessBackupJob.perform_later(current_user, params[:backup])
+ redirect_to dashboard_path, notice: t('.success')
+ end
+end
app/jobs/process_backup_job.rb
@@ -0,0 +1,42 @@
+class ProcessBackupJob < ActiveJob::Base
+ WORKOUTS_SQL="SELECT * FROM WORKOUTS;"
+ queue_as :default
+
+ def perform(user, backup_file)
+ tmp_dir do |dir|
+ `unzip #{backup_file} -d #{dir}`
+ database(dir) do |db|
+ db.execute(WORKOUTS_SQL) do |row|
+ user.training_sessions.create_workout_from(map_from(row))
+ end
+ end
+ end
+ end
+
+ private
+
+ def tmp_dir
+ Dir.mktmpdir do |dir|
+ yield dir
+ end
+ end
+
+ def database(dir)
+ yield SQLite3::Database.new("#{dir}/stronglifts.db")
+ end
+
+ def map_from(row)
+ WorkoutRow.new(
+ id: row[0],
+ date: DateTime.parse(row[1]),
+ workout: row[2],
+ exercise_1: JSON.parse(row[3]),
+ exercise_2: JSON.parse(row[4]),
+ exercise_3: JSON.parse(row[5]),
+ note: row[6],
+ body_weight: row[7],
+ arm_work: row[8].present? ? JSON.parse(row[8]) : nil,
+ temp: row[9]
+ )
+ end
+end
app/models/exercise.rb
@@ -0,0 +1,4 @@
+class Exercise < ActiveRecord::Base
+ has_many :exercise_workouts
+ has_many :workouts, through: :exercise_workouts
+end
app/models/exercise_session.rb
@@ -0,0 +1,5 @@
+class ExerciseSession < ActiveRecord::Base
+ belongs_to :training_session
+ belongs_to :exercise_workout
+ has_one :exercise, through: :exercise_workout
+end
app/models/exercise_workout.rb
@@ -0,0 +1,4 @@
+class ExerciseWorkout < ActiveRecord::Base
+ belongs_to :exercise
+ belongs_to :workout
+end
app/models/program.rb
@@ -0,0 +1,4 @@
+class Program < ActiveRecord::Base
+ has_many :exercises, through: :workouts
+ has_many :workouts
+end
app/models/training_session.rb
@@ -0,0 +1,29 @@
+class TrainingSession < ActiveRecord::Base
+ belongs_to :user
+ belongs_to :workout
+ has_many :exercise_sessions
+ attr_accessor :occurred_at
+
+ def self.create_workout_from(workout_row)
+ program = Program.find_by(name: "StrongLifts 5×5")
+ workout = program.workouts.find_by(name: workout_row.workout)
+ transaction do
+ session = create!(workout: workout, occurred_at: workout_row.date)
+
+ workout.exercise_workouts.each_with_index do |exercise_workout, index|
+ exercise_row = workout_row.exercises[index]
+ session.exercise_sessions.create!(
+ exercise_workout: exercise_workout,
+ sets: [
+ exercise_row['set1'].to_i > 0 ? exercise_row['set1'] : 0,
+ exercise_row['set2'].to_i > 0 ? exercise_row['set2'] : 0,
+ exercise_row['set3'].to_i > 0 ? exercise_row['set3'] : 0,
+ exercise_row['set4'].to_i > 0 ? exercise_row['set4'] : 0,
+ exercise_row['set5'].to_i > 0 ? exercise_row['set5'] : 0,
+ ]
+ )
+ end
+ session
+ end
+ end
+end
app/models/user.rb
@@ -1,5 +1,6 @@
class User < ActiveRecord::Base
has_secure_password
+ has_many :training_sessions
USERNAME_REGEX=/\A[-a-z0-9_.]*\z/i
validates :username, presence: true, format: { with: USERNAME_REGEX }, uniqueness: true
app/models/workout.rb
@@ -0,0 +1,9 @@
+class Workout < ActiveRecord::Base
+ belongs_to :program
+ has_many :exercise_workouts
+ has_many :exercises, through: :exercise_workouts
+
+ def add_exercise(exercise, sets:, repetitions:)
+ exercise_workouts.create!(exercise: exercise, sets: sets, repetitions: repetitions)
+ end
+end
app/models/workout_row.rb
@@ -0,0 +1,19 @@
+class WorkoutRow
+ attr_accessor :id, :date, :workout
+ attr_accessor :exercise_1, :exercise_2, :exercise_3
+ attr_accessor :note, :body_weight, :arm_work, :temp
+
+ def initialize(attributes = {})
+ attributes.each do |attribute|
+ send("#{attribute.first}=", attribute.last)
+ end
+ end
+
+ def exercises
+ @exercises ||= [
+ exercise_1,
+ exercise_2,
+ exercise_3,
+ ]
+ end
+end
app/views/training_sessions/index.html.erb
@@ -0,0 +1,43 @@
+<div class="row">
+ <!-- Side Bar -->
+ <div class="large-4 small-12 columns">
+ <div class="hide-for-small panel">
+ <div class="row">
+ <%= form_tag(upload_training_sessions_path, method: :post) do %>
+ <div class="small-12 columns">
+ <label><%= t('.backup_file') %>
+ <%= file_field_tag :backup %>
+ </label>
+ </div> <!-- /.small-12 -->
+ <div class="small-12 columns">
+ <%= submit_tag t('.upload_backup_button'), class: 'button' %>
+ </div> <!-- /.small-12 -->
+ <% end %>
+ </div>
+ </div>
+ <a href="#">
+ <div class="panel callout radius">
+ <h6><%= @training_sessions.count %> training sessions completed</h6>
+ </div>
+ </a>
+ </div>
+ <!-- End Side Bar -->
+
+ <!-- Thumbnails -->
+ <div class="large-8 columns">
+ <div class="row">
+ <% @training_sessions.each do |training_session| %>
+ <div class="large-4 small-6 columns">
+ <img src="http://placehold.it/1000x1000&text=Thumbnail">
+
+ <div class="panel">
+ <h5><%= training_session.created_at %></h5>
+ <h6 class="subheader"><%= training_session.created_at %></h6>
+ </div>
+ </div>
+ <% end %>
+ </div>
+ <!-- End Thumbnails -->
+ </div>
+ </div>
+</div>
bin/bootstrap-vagrant-user.sh
bin/bootstrap.sh
bin/rails
@@ -1,16 +1,4 @@
#!/usr/bin/env ruby
-#
-# This file was generated by Bundler.
-#
-# The application 'rails' is installed as part of a gem, and
-# this file is here to facilitate running it.
-#
-
-require 'pathname'
-ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
- Pathname.new(__FILE__).realpath)
-
-require 'rubygems'
-require 'bundler/setup'
-
-load Gem.bin_path('railties', 'rails')
+APP_PATH = File.expand_path('../../config/application', __FILE__)
+require_relative '../config/boot'
+require 'rails/commands'
bin/rake
@@ -1,16 +1,8 @@
#!/usr/bin/env ruby
-#
-# This file was generated by Bundler.
-#
-# The application 'rake' is installed as part of a gem, and
-# this file is here to facilitate running it.
-#
-
-require 'pathname'
-ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
- Pathname.new(__FILE__).realpath)
-
-require 'rubygems'
-require 'bundler/setup'
-
-load Gem.bin_path('rake', 'rake')
+begin
+ load File.expand_path("../spring", __FILE__)
+rescue LoadError
+end
+require_relative '../config/boot'
+require 'rake'
+Rake.application.run
bin/rspec
@@ -1,4 +1,8 @@
#!/usr/bin/env ruby
+begin
+ load File.expand_path("../spring", __FILE__)
+rescue LoadError
+end
#
# This file was generated by Bundler.
#
bin/setup
@@ -1,16 +1,29 @@
#!/usr/bin/env ruby
-#
-# This file was generated by Bundler.
-#
-# The application 'setup' is installed as part of a gem, and
-# this file is here to facilitate running it.
-#
-
require 'pathname'
-ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
- Pathname.new(__FILE__).realpath)
-require 'rubygems'
-require 'bundler/setup'
+# path to your application root.
+APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
+
+Dir.chdir APP_ROOT do
+ # This script is a starting point to setup your application.
+ # Add necessary setup steps to this file:
+
+ puts "== Installing dependencies =="
+ system "gem install bundler --conservative"
+ system "bundle check || bundle install"
+
+ # puts "\n== Copying sample files =="
+ # unless File.exist?("config/database.yml")
+ # system "cp config/database.yml.sample config/database.yml"
+ # end
+
+ puts "\n== Preparing database =="
+ system "bin/rake db:setup"
+
+ puts "\n== Removing old logs and tempfiles =="
+ system "rm -f log/*"
+ system "rm -rf tmp/cache"
-load Gem.bin_path('factory_girl_rails', 'setup')
+ puts "\n== Restarting application server =="
+ system "touch tmp/restart.txt"
+end
bin/spring
@@ -1,16 +1,15 @@
#!/usr/bin/env ruby
-#
-# This file was generated by Bundler.
-#
-# The application 'spring' is installed as part of a gem, and
-# this file is here to facilitate running it.
-#
-require 'pathname'
-ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
- Pathname.new(__FILE__).realpath)
+# This file loads spring without using Bundler, in order to be fast.
+# It gets overwritten when you run the `spring binstub` command.
-require 'rubygems'
-require 'bundler/setup'
+unless defined?(Spring)
+ require "rubygems"
+ require "bundler"
-load Gem.bin_path('spring', 'spring')
+ if match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m)
+ Gem.paths = { "GEM_PATH" => [Bundler.bundle_path.to_s, *Gem.path].uniq }
+ gem "spring", match[1]
+ require "spring/binstub"
+ end
+end
config/locales/en.yml
@@ -39,3 +39,9 @@ en:
password: "Password"
login_button: "Login"
register_link: "Create an account"
+ training_sessions:
+ index:
+ backup_file: 'File'
+ upload_backup_button: "Upload"
+ upload:
+ success: 'Our minions have been summoned to unpack your backup.'
config/routes.rb
@@ -2,6 +2,11 @@ Rails.application.routes.draw do
root "sessions#new"
resources :sessions, only: [:new, :create, :destroy]
resources :registrations, only: [:new, :create]
- get "/dashboard" => "sessions#new", as: :dashboard
+ resources :training_sessions, only: [:index] do
+ collection do
+ post :upload
+ end
+ end
+ get "/dashboard" => "training_sessions#index", as: :dashboard
get "/terms" => "static_pages#terms"
end
db/migrate/20150519150012_create_workouts.rb
@@ -0,0 +1,10 @@
+class CreateWorkouts < ActiveRecord::Migration
+ def change
+ create_table :workouts, id: :uuid do |t|
+ t.uuid :user_id, null: false
+
+ t.timestamps null: false
+ end
+ add_index :workouts, :user_id
+ end
+end
db/migrate/20150521220401_rename_workouts_to_training_sessions.rb
@@ -0,0 +1,5 @@
+class RenameWorkoutsToTrainingSessions < ActiveRecord::Migration
+ def change
+ rename_table :workouts, :training_sessions
+ end
+end
db/migrate/20150521222240_create_exercise_workouts.rb
@@ -0,0 +1,27 @@
+class CreateExerciseWorkouts < ActiveRecord::Migration
+ def change
+ create_table :workouts, id: :uuid do |t|
+ t.uuid :program_id, null: false
+ t.string :name, null: false
+ t.timestamps null: false
+ end
+
+ create_table :exercises, id: :uuid do |t|
+ t.string :name, null: false
+ t.timestamps null: false
+ end
+
+ create_table :exercise_workouts, id: :uuid do |t|
+ t.uuid :exercise_id, null: false
+ t.uuid :workout_id, null: false
+ t.integer :sets, null: false
+ t.integer :repetitions, null: false
+ t.timestamps null: false
+ end
+
+ create_table :programs, id: :uuid do |t|
+ t.string :name, null: false
+ t.timestamps null: false
+ end
+ end
+end
db/migrate/20150521231555_create_exercise_sessions.rb
@@ -0,0 +1,9 @@
+class CreateExerciseSessions < ActiveRecord::Migration
+ def change
+ create_table :exercise_sessions, id: :uuid do |t|
+ t.uuid :training_session_id, null: false
+ t.uuid :exercise_workout_id, null: false
+ t.timestamps null: false
+ end
+ end
+end
db/migrate/20150521234926_add_training_id_to_training_session.rb
@@ -0,0 +1,5 @@
+class AddTrainingIdToTrainingSession < ActiveRecord::Migration
+ def change
+ add_column :training_sessions, :workout_id, :uuid, null: false
+ end
+end
db/migrate/20150522004907_add_sets_to_exercise_sessions.rb
@@ -0,0 +1,5 @@
+class AddSetsToExerciseSessions < ActiveRecord::Migration
+ def change
+ add_column :exercise_sessions, :sets, :text, array: true, default: []
+ end
+end
db/schema.rb
@@ -11,16 +11,61 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20150124163233) do
+ActiveRecord::Schema.define(version: 20150522004907) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
enable_extension "uuid-ossp"
+ create_table "exercise_sessions", id: :uuid, default: "uuid_generate_v4()", force: :cascade do |t|
+ t.uuid "training_session_id", null: false
+ t.uuid "exercise_workout_id", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.text "sets", default: [], array: true
+ end
+
+ create_table "exercise_workouts", id: :uuid, default: "uuid_generate_v4()", force: :cascade do |t|
+ t.uuid "exercise_id", null: false
+ t.uuid "workout_id", null: false
+ t.integer "sets", null: false
+ t.integer "repetitions", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ create_table "exercises", id: :uuid, default: "uuid_generate_v4()", force: :cascade do |t|
+ t.string "name", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ create_table "programs", id: :uuid, default: "uuid_generate_v4()", force: :cascade do |t|
+ t.string "name", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ create_table "training_sessions", id: :uuid, default: "uuid_generate_v4()", force: :cascade do |t|
+ t.uuid "user_id", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.uuid "workout_id", null: false
+ end
+
+ add_index "training_sessions", ["user_id"], name: "index_training_sessions_on_user_id", using: :btree
+
create_table "users", id: :uuid, default: "uuid_generate_v4()", force: :cascade do |t|
t.string "username", null: false
t.string "email", null: false
t.string "password_digest"
end
+ create_table "workouts", id: :uuid, default: "uuid_generate_v4()", force: :cascade do |t|
+ t.uuid "program_id", null: false
+ t.string "name", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
end
spec/controllers/training_sessions_controller_spec.rb
@@ -0,0 +1,44 @@
+require "rails_helper"
+
+describe TrainingSessionsController do
+ let(:user) { create(:user) }
+
+ before :each do
+ http_login(user)
+ end
+
+ describe "#index" do
+ include_context "stronglifts_program"
+ let!(:training_session_a) { create(:training_session, user: user, workout: workout_a) }
+ let!(:training_session_b) { create(:training_session, user: user, workout: workout_b) }
+
+ it "loads all my training sessions" do
+ get :index
+ expect(assigns(:training_sessions)).to match_array([training_session_a, training_session_b])
+ end
+ end
+
+ describe "#upload" do
+ let(:backup_file) { Rails.root.join("spec", "fixtures", "stronglifts.backup").to_s }
+
+ before :each do
+ allow(ProcessBackupJob).to receive(:perform_later)
+ end
+
+ it "uploads a new backup" do
+ post :upload, backup: backup_file
+ expect(ProcessBackupJob).to have_received(:perform_later).with(user, backup_file)
+ end
+
+ it "redirects to the dashboard" do
+ post :upload, backup: backup_file
+ expect(response).to redirect_to(dashboard_path)
+ end
+
+ it 'displays a friendly message' do
+ post :upload, backup: backup_file
+ translation = I18n.translate("training_sessions.upload.success")
+ expect(flash[:notice]).to eql(translation)
+ end
+ end
+end
spec/factories/training_sessions.rb
@@ -0,0 +1,5 @@
+FactoryGirl.define do
+ factory :training_session do
+ association :user
+ end
+end
spec/fixtures/backup.stronglifts
Binary file
spec/jobs/process_backup_job_spec.rb
@@ -0,0 +1,16 @@
+require 'rails_helper'
+
+describe ProcessBackupJob, type: :job do
+ include_context "stronglifts_program"
+ let(:user) { create(:user) }
+
+ describe "#perform" do
+ let(:backup_file) { Rails.root.join("spec", "fixtures", "backup.stronglifts").to_s }
+
+ it "adds each workout to the list of training sessions for the user" do
+ subject.perform(user, backup_file)
+
+ expect(user.training_sessions.count).to eql(31)
+ end
+ end
+end
spec/models/training_session_spec.rb
@@ -0,0 +1,68 @@
+require "rails_helper"
+
+describe TrainingSession, type: :model do
+ describe ".create_workout_from" do
+ include_context "stronglifts_program"
+ let(:user) { create(:user) }
+
+ let(:row) do
+ [
+ 31,
+ "2015-05-13 06:10:21",
+ "A",
+ "{\"weight\":{\"lastWeightKg\":90,\"lastWeightLb\":200},\"success\":false,\"set1\":-1,\"set2\":-1,\"set3\":-1,\"set4\":-1,\"set5\":-1,\"messageDismissed\":false,\"workoutType\":0,\"warmup\":{\"exerciseType\":1,\"targetWeight\":200,\"warmupSets\":[{\"completed\":false},{\"completed\":false},{\"completed\":false},{\"completed\":false},{\"completed\":false}]}}",
+ "{\"weight\":{\"lastWeightKg\":65,\"lastWeightLb\":145},\"success\":true,\"set1\":5,\"set2\":5,\"set3\":5,\"set4\":5,\"set5\":5,\"messageDismissed\":false,\"workoutType\":0,\"warmup\":{\"exerciseType\":2,\"targetWeight\":145,\"warmupSets\":[{\"completed\":true},{\"completed\":true},{\"completed\":true},{\"completed\":true}]}}",
+ "{\"weight\":{\"lastWeightKg\":60,\"lastWeightLb\":130},\"success\":false,\"set1\":5,\"set2\":4,\"set3\":4,\"set4\":4,\"set5\":4,\"messageDismissed\":false,\"workoutType\":0,\"warmup\":{\"exerciseType\":3,\"targetWeight\":130,\"warmupSets\":[{\"completed\":true}]}}",
+ "",
+ "209LB",
+ "{\"set1\":8,\"set2\":4,\"set3\":4,\"messageDismissed\":false,\"exercise\":0,\"weight\":{\"lastWeightKg\":0,\"lastWeightLb\":0}}",
+ 0
+ ]
+ end
+
+ let(:workout_row) do
+ WorkoutRow.new(
+ id: row[0],
+ date: DateTime.parse(row[1]),
+ workout: row[2],
+ exercise_1: JSON.parse(row[3]),
+ exercise_2: JSON.parse(row[4]),
+ exercise_3: JSON.parse(row[5]),
+ note: row[6],
+ body_weight: row[7],
+ arm_work: JSON.parse(row[8])
+ )
+ end
+
+ it 'creates a new workout' do
+ training_session = user.training_sessions.create_workout_from(workout_row)
+
+ expect(training_session).to be_persisted
+ expect(training_session.occurred_at).to eql(workout_row.date)
+ expect(training_session.workout).to eql(workout_a)
+ expect(training_session.exercise_sessions.count).to eql(3)
+ expect(training_session.exercise_sessions.map { |x| x.exercise.name }).to match_array(["Squat", "Bench Press", "Barbell Row"])
+
+ squat_session = training_session.exercise_sessions.find_by(exercise_workout: squat_workout)
+ expect(squat_session.sets[0]).to eql('0')
+ expect(squat_session.sets[1]).to eql('0')
+ expect(squat_session.sets[2]).to eql('0')
+ expect(squat_session.sets[3]).to eql('0')
+ expect(squat_session.sets[4]).to eql('0')
+
+ bench_session = training_session.exercise_sessions.find_by(exercise_workout: bench_workout)
+ expect(bench_session.sets[0]).to eql("5")
+ expect(bench_session.sets[1]).to eql("5")
+ expect(bench_session.sets[2]).to eql("5")
+ expect(bench_session.sets[3]).to eql("5")
+ expect(bench_session.sets[4]).to eql("5")
+
+ row_session = training_session.exercise_sessions.find_by(exercise_workout: row_workout)
+ expect(row_session.sets[0]).to eql("5")
+ expect(row_session.sets[1]).to eql("4")
+ expect(row_session.sets[2]).to eql("4")
+ expect(row_session.sets[3]).to eql("4")
+ expect(row_session.sets[4]).to eql("4")
+ end
+ end
+end
spec/routing/dashboard_routing_spec.rb
@@ -1,7 +1,7 @@
require "rails_helper"
-RSpec.describe "/dashboard", type: :routing do
+describe "/dashboard", type: :routing do
it "routes to the items listing" do
- expect(get: "/dashboard").to route_to("sessions#new")
+ expect(get: "/dashboard").to route_to("training_sessions#index")
end
end
spec/support/stronglifts_program.rb
@@ -0,0 +1,12 @@
+shared_context "stronglifts_program" do
+ let!(:program) { Program.create!(name: "StrongLifts 5×5") }
+ let!(:squat) { Exercise.new(name: "Squat") }
+ let!(:workout_a) { program.workouts.create name: "A" }
+ let!(:squat_workout) { workout_a.add_exercise(squat, sets: 5, repetitions: 5) }
+ let!(:bench_workout) { workout_a.add_exercise(Exercise.new(name: "Bench Press"), sets: 5, repetitions: 5) }
+ let!(:row_workout) { workout_a.add_exercise(Exercise.new(name: "Barbell Row"), sets: 5, repetitions: 5) }
+ let!(:workout_b) { program.workouts.create name: "B" }
+ let!(:squat_workout_b) { workout_b.add_exercise(squat, sets: 5, repetitions: 5) }
+ let!(:overhead_press_workout) { workout_b.add_exercise(Exercise.new(name: "Overhead Press"), sets: 5, repetitions: 5) }
+ let!(:deadlift_workout) { workout_b.add_exercise(Exercise.new(name: "Deadlift"), sets: 1, repetitions: 5) }
+end
Gemfile
@@ -5,6 +5,7 @@ ruby '2.2.2'
gem 'rails', '4.2.0'
# Use postgresql as the database for Active Record
gem 'pg'
+gem 'sqlite3'
# Use SCSS for stylesheets
gem 'sass-rails', '~> 5.0'
# Use Uglifier as compressor for JavaScript assets
Gemfile.lock
@@ -235,6 +235,7 @@ GEM
actionpack (>= 3.0)
activesupport (>= 3.0)
sprockets (>= 2.8, < 4.0)
+ sqlite3 (1.3.10)
teaspoon (0.9.1)
railties (>= 3.2.5, < 5)
term-ansicolor (1.3.0)
@@ -303,6 +304,7 @@ DEPENDENCIES
spring
spring-commands-rspec
spring-commands-teaspoon
+ sqlite3
teaspoon
turbolinks
uglifier (>= 1.3.0)