Skip to content

Commit

Permalink
Add Endpoint for Viewing Lesson Completion Stats (TheOdinProject#967)
Browse files Browse the repository at this point in the history
* Add Endpoint for Viewing Lesson Completion Stats

To keep an eye on lesson completion stats, we need access to lesson
completion data so we can make visulizations.

* Fix authentication, it wasn't checking both the username and password it was instead just checking the password. Also fixes some unit test formatting

* Allow endpoint to be queried with a date range

* Add additional configuration keys to env

* Stub out accessing mailchimp api in unit test
  • Loading branch information
KevinMulhern authored Jan 31, 2019
1 parent c1d7084 commit 4a7827d
Show file tree
Hide file tree
Showing 18 changed files with 358 additions and 62 deletions.
9 changes: 9 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
API_USERNAME: 'development'
API_PASSWORD: 'qwerty123'
GITHUB_API_TOKEN: 1234
GITHUB_APP_ID: 1234
GITHUB_SECRET: 1234
GOOGLE_CLIENT_ID: 1234
GOOGLE_CLIENT_SECRET: 1234
MAILCHIMP_API_KEY: 1234
MAILCHIMP_LIST_ID: 1234
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
# Ignore application configuration
/config/application.yml
/config/database.yml
.env
.env.local

# secret token
.secret
Expand Down
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ group :development, :test do
gem 'shoulda-matchers', '~> 3.1'
gem 'rake', '~> 11.3'
gem 'rails-controller-testing', '~> 1.0'
gem 'figaro', '1.1'
gem 'dotenv-rails'
gem 'bundle-audit'
end

Expand Down
10 changes: 7 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ GEM
warden (~> 1.2.3)
diff-lcs (1.3)
docile (1.3.1)
dotenv (2.6.0)
dotenv-rails (2.6.0)
dotenv (= 2.6.0)
railties (>= 3.2, < 6.0)
equalizer (0.0.11)
erubi (1.7.1)
erubis (2.7.0)
Expand All @@ -114,8 +118,6 @@ GEM
faraday (0.12.2)
multipart-post (>= 1.2, < 3)
ffi (1.9.25)
figaro (1.1.0)
thor (~> 0.14)
font-awesome-rails (4.7.0.4)
railties (>= 3.2, < 6.0)
friendly_id (5.2.4)
Expand All @@ -140,6 +142,7 @@ GEM
concurrent-ruby (~> 1.0)
ice_nine (0.11.2)
jaro_winkler (1.5.1)
jaro_winkler (1.5.1-x86_64-darwin-17)
jquery-rails (4.2.2)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
Expand Down Expand Up @@ -365,6 +368,7 @@ GEM
PLATFORMS
ruby
x86_64-darwin-15
x86_64-darwin-17

DEPENDENCIES
acts_as_votable
Expand All @@ -376,8 +380,8 @@ DEPENDENCIES
database_cleaner (~> 1.5)
derailed
devise (~> 4.4.0)
dotenv-rails
factory_bot_rails (~> 4.11)
figaro (= 1.1)
font-awesome-rails (~> 4.7)
friendly_id (~> 5.1)
gibbon (~> 3.2.0)
Expand Down
36 changes: 36 additions & 0 deletions app/controllers/api/lesson_completions_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module Api
class LessonCompletionsController < ApplicationController
before_action :authenticate

def index
render json: serialized_lesson_completions
end

private

def serialized_lesson_completions
Course.order(:position).map do |course|
CourseSerializer.as_json(course, between_dates)
end
end

def between_dates
(DateTime.parse(start_date)..DateTime.parse(end_date))
end

def start_date
params.fetch(:start_date, '2013/01/01')
end

def end_date
params.fetch(:end_date, DateTime.now.to_s)
end

def authenticate
authenticate_or_request_with_http_basic do |username, password|
username == ENV.fetch('API_USERNAME') &&
password == ENV.fetch('API_PASSWORD')
end
end
end
end
27 changes: 27 additions & 0 deletions app/serializers/course_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class CourseSerializer
attr_reader :course, :between_dates
private :course, :between_dates

def initialize(course, between_dates = nil)
@course, @between_dates = course, between_dates
end

def self.as_json(course, between_dates = nil)
new(course, between_dates).as_json
end

def as_json(options = nil)
{
title: course.title,
sections: serialized_sections,
}
end

private

def serialized_sections
course.sections.map do |section|
SectionSerializer.as_json(section, between_dates)
end
end
end
25 changes: 25 additions & 0 deletions app/serializers/lesson_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
class LessonSerializer
attr_reader :lesson, :between_dates
private :lesson, :between_dates

def initialize(lesson, between_dates = nil)
@lesson, @between_dates = lesson, between_dates
end

def self.as_json(lesson, between_dates = nil)
new(lesson, between_dates).as_json
end

def as_json(options=nil)
{
title: lesson.title,
completions: completions.count,
}
end

private

def completions
lesson.lesson_completions.where(created_at: between_dates)
end
end
27 changes: 27 additions & 0 deletions app/serializers/section_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class SectionSerializer
attr_reader :section, :between_dates
private :section, :between_dates

def initialize(section, between_dates = nil)
@section, @between_dates = section, between_dates
end

def self.as_json(section, between_dates = nil)
new(section, between_dates).as_json
end

def as_json(options=nil)
{
title: section.title,
lessons: serialized_lessons
}
end

private

def serialized_lessons
section.lessons.map do |lesson|
LessonSerializer.as_json(lesson, between_dates)
end
end
end
4 changes: 1 addition & 3 deletions app/services/mailchimp_subscription.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ def initialize(options)
end

def self.create(options)
instance = new(options)
instance.create
instance
new(options).create
end

def create
Expand Down
4 changes: 4 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
get '/confirm_email' => 'users#send_confirmation_link'
end

namespace :api do
resources :lesson_completions, only: [:index]
end

get 'home' => 'static_pages#home'
get 'about' => 'static_pages#about'
get 'faq' => 'static_pages#faq'
Expand Down
78 changes: 78 additions & 0 deletions spec/controllers/api/lesson_completions_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
require 'rails_helper'

module Api
RSpec.describe LessonCompletionsController do
let(:params) { { start_date: start_date, end_date: end_date } }
let(:start_date) { '2019/01/01' }
let(:end_date) { '2019/01/31' }
let!(:course) { create(:course, title: 'Web Development 101', position: 1) }
let(:serialized_course) do
{
'title' => 'Web Development 101',
'sections' => [
{
'title' => 'Installations',
'lessons' => [
'title' => 'Overview',
'completions' => 1,
]
}
]
}
end
let(:username) { 'development' }
let(:password) { 'qwerty123' }
let(:authenticate_request) do
request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.
encode_credentials(username, password)
end
let(:between_dates) do
(DateTime.parse(start_date)..DateTime.parse(end_date))
end

before do
allow(CourseSerializer).to receive(:as_json).with(course, between_dates).
and_return(serialized_course)
end

describe '#index' do
context 'when authenticated' do

before do
authenticate_request
end

it 'returns a 200 status code' do
get :index, params: params
expect(response.status).to eql(200)
end

it 'renders serialized courses in json format' do
get :index, params: params
expect(JSON.parse(response.body)).to eql([serialized_course])
end

context 'when start and end dates are not present' do
let(:params) { { } }
let(:between_dates) do
(DateTime.parse('2013/01/01')..DateTime.parse(DateTime.now.to_s))
end

it 'uses the default dates' do
get :index, params: params

expect(CourseSerializer).to have_received(:as_json).
with(course, between_dates)
end
end
end

context 'when not authenticated' do
it 'does not allow access' do
get :index, params: params
expect(response.status).to eql(401)
end
end
end
end
end
2 changes: 1 addition & 1 deletion spec/rails_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,5 @@

config.include(Shoulda::Matchers::ActiveModel, type: :model)
config.include(Shoulda::Matchers::ActiveRecord, type: :model)
config.include(MailchimpHelper)
config.include FactoryBot::Syntax::Methods
end
40 changes: 40 additions & 0 deletions spec/serializers/course_serializer_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
require 'rails_helper'

RSpec.describe CourseSerializer do
subject { described_class.as_json(course, between_dates) }

let(:course) do
double(
'Section',
title: 'Web Development 101',
sections: sections,
)
end
let(:between_dates) do
(DateTime.parse('2019/01/01')..DateTime.parse('2019/12/31'))
end
let(:sections) { [section] }
let(:section) { double('Section') }
let(:serialized_section) do
{
title: 'Installations',
lessons: [{ title: 'Overview', completions: 1 }],
}
end

describe '#as_json' do
let(:serialized_course) do
{
title: 'Web Development 101',
sections: [serialized_section],
}
end

before do
allow(SectionSerializer).to receive(:as_json).
with(section, between_dates).and_return(serialized_section)
end

it { is_expected.to eql(serialized_course) }
end
end
32 changes: 32 additions & 0 deletions spec/serializers/lesson_serializer_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
require 'rails_helper'

RSpec.describe LessonSerializer do
subject { described_class.as_json(lesson, between_dates) }

let(:lesson) do
double('Lesson', title: 'Overview', lesson_completions: lesson_completions)
end
let(:between_dates) do
(DateTime.parse('2019/01/01')..DateTime.parse('2019/12/31'))
end
let(:lesson_completions) { [lesson_completion] }
let(:lesson_completion) do
double('LessonCompletion', created_at: '2019/01/10')
end

describe '#as_json' do
let(:serialized_lesson) do
{
title: 'Overview',
completions: 1
}
end

before do
allow(lesson_completions).to receive(:where).
with(created_at: between_dates).and_return([lesson_completion])
end

it { is_expected.to eql(serialized_lesson) }
end
end
Loading

0 comments on commit 4a7827d

Please sign in to comment.