Commit e89bc7c

mo khan <mo@mokhan.ca>
2015-05-19 14:55:04
create a workout record for each recors in the sqlite db.
1 parent b769c08
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)