Browse files

An initial implementation of PointsBreakdown

Supports a single user and loading them from the database
  • Loading branch information...
1 parent c897135 commit b42fab3a8ab7b3e9262a4e609a2b0cf9b80daddb @skanev committed Jul 22, 2012
View
44 app/models/points_breakdown.rb
@@ -0,0 +1,44 @@
+class PointsBreakdown
+ def self.field(name, type)
+ @fields ||= []
+ @fields.append name
+
+ attr_reader name
+ define_method "#{name}=" do |value|
+ converted = case type.name
+ when 'Integer' then value.to_i
+ when 'String' then value
+ when 'Array' then value.tr('{}', '').split(',').map(&:to_i)
+ else raise "Don't know how to convert type #{type}"
+ end
+
+ instance_variable_set "@#{name}", converted
+ end
+ end
+
+ field :id, Integer
+ field :rank, Integer
+ field :vouchers, Integer
+ field :stars, Integer
+ field :name, String
+ field :tasks_breakdown, Array
+ field :quizzes_breakdown, Array
+
+ def initialize(hash = {})
+ hash.each do |key, value|
+ send "#{key}=", value
+ end
+ end
+
+ def total
+ vouchers + stars + tasks_breakdown.sum + quizzes_breakdown.sum
+ end
+
+ class << self
+ def find(user_id)
+ hash = ActiveRecord::Base.connection.execute("SELECT #{@fields.join(', ')} FROM points_breakdowns WHERE id = #{user_id.to_i}").first
+ raise "Cannot find user with id = #{user_id}" if hash.nil?
+ new hash
+ end
+ end
+end
View
85 db/migrate/20120721185730_add_points_breakdowns.rb
@@ -0,0 +1,85 @@
+class AddPointsBreakdowns < ActiveRecord::Migration
+ def up
+ execute <<-SQL
+ CREATE VIEW points_breakdowns AS
+ WITH users_tasks AS (
@ignisf
Collaborator
ignisf added a note Nov 22, 2012

Не е ли по-яко за всеки от долните няколко SELECT-а да си имаме отделно View (например за да ни е съвместима миграцията с SQLite, който не е и чувал за WITH)?

@skanev
Owner
skanev added a note Nov 22, 2012

Не, не е.

Съвместимостта със SQLite не е нещо, което ни интересува. Проекта трябва да се търкаля на Postgres в development.

@ignisf
Collaborator
ignisf added a note Nov 22, 2012

Okay

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ SELECT
+ users.id AS user_id,
+ tasks.id AS task_id,
+ GREATEST(COALESCE(points + adjustment, 0), 0) AS points
+ FROM users
+ INNER JOIN tasks ON TRUE
+ LEFT JOIN solutions ON (solutions.task_id = tasks.id AND solutions.user_id = users.id)
+ ORDER BY users.id, tasks.id
+ ), tasks_summary AS (
+ SELECT
+ user_id,
+ array_agg(points) AS breakdown,
+ SUM(points) AS points
+ FROM users_tasks
+ GROUP BY user_id
+ ), users_quizzes AS (
+ SELECT
+ users.id AS user_id,
+ quizzes.id AS quiz_id,
+ COALESCE(points, 0) AS points
+ FROM users
+ INNER JOIN quizzes ON TRUE
+ LEFT JOIN quiz_results ON (quiz_results.quiz_id = quizzes.id AND quiz_results.user_id = users.id)
+ ORDER BY users.id, quizzes.id
+ ), quizzes_summary AS (
+ SELECT
+ user_id,
+ array_agg(points) AS breakdown,
+ SUM(points) AS points
+ FROM users_quizzes
+ GROUP BY user_id
+ ), stars_summary AS (
+ SELECT
+ users.id AS user_id,
+ COUNT(posts.starred) AS points
+ FROM users
+ LEFT JOIN posts ON posts.user_id = users.id AND posts.starred
+ GROUP BY users.id
+ ), vouchers_summary AS (
+ SELECT
+ users.id AS user_id,
+ COUNT(vouchers.id) AS points
+ FROM users
+ LEFT JOIN vouchers ON vouchers.user_id = users.id
+ GROUP BY users.id
+ ), arrays_summary AS (
+ SELECT
+ users.id AS user_id,
+ COALESCE(tasks_summary.breakdown, '{}') AS tasks_breakdown,
+ COALESCE(tasks_summary.points, 0) AS tasks,
+ COALESCE(quizzes_summary.breakdown, '{}') AS quizzes_breakdown,
+ COALESCE(quizzes_summary.points, 0) AS quizzes
+ FROM users
+ LEFT JOIN tasks_summary ON tasks_summary.user_id = users.id
+ LEFT JOIN quizzes_summary ON quizzes_summary.user_id = users.id
+ )
+ SELECT
+ rank() OVER (ORDER BY arrays_summary.tasks + arrays_summary.quizzes + stars_summary.points + vouchers_summary.points DESC) AS rank,
+ users.id AS id,
+ users.name AS name,
+ arrays_summary.tasks_breakdown AS tasks_breakdown,
+ arrays_summary.tasks AS tasks,
+ arrays_summary.quizzes_breakdown AS quizzes_breakdown,
+ arrays_summary.quizzes AS quizzes,
+ stars_summary.points AS stars,
+ vouchers_summary.points AS vouchers,
+ arrays_summary.tasks + quizzes_summary.points + stars_summary.points + vouchers_summary.points AS total
+ FROM users
+ LEFT JOIN arrays_summary ON arrays_summary.user_id = users.id
+ LEFT JOIN quizzes_summary ON quizzes_summary.user_id = users.id
+ LEFT JOIN stars_summary ON stars_summary.user_id = users.id
+ LEFT JOIN vouchers_summary ON vouchers_summary.user_id = users.id
+ ORDER BY rank
+ SQL
+ end
+
+ def down
+ execute 'DROP VIEW points_breakdowns'
+ end
+end
View
4 spec/factories.rb
@@ -42,6 +42,10 @@
user
end
+ factory :starred_post, parent: :topic do
+ starred true
+ end
+
factory :voucher do
code { FactoryGirl.generate(:voucher_code) }
end
View
87 spec/models/points_breakdown_spec.rb
@@ -0,0 +1,87 @@
+require 'spec_helper'
+
+describe PointsBreakdown do
+ let(:user) { create :user }
+ let(:breakdown) { PointsBreakdown.find user.id }
+
+ it "knows the id of the user" do
+ breakdown.id.should eq user.id
+ end
+
+ it "knows the name of the user" do
+ breakdown.name.should eq user.name
+ end
+
+ it "knows how many points a user has from vouchers" do
+ create :voucher, user: user
+ breakdown.vouchers.should eq 1
+ end
+
+ it "knows how many points a user has from vouchers" do
+ create :starred_post, user: user
+ breakdown.stars.should eq 1
+ end
+
+ it "knows the points in each task" do
+ create :solution, user: user, points: 1
+ create :solution, user: user, points: 2
+
+ breakdown.tasks_breakdown.should eq [1, 2]
+ end
+
+ it "knows the points in each quiz" do
+ create :quiz_result, user: user, points: 1
+ create :quiz_result, user: user, points: 2
+
+ breakdown.quizzes_breakdown.should eq [1, 2]
+ end
+
+ it "knows the total points of the user" do
+ create :voucher, user: user
+ create :starred_post, user: user
+ create :solution, user: user, points: 1
+ create :quiz_result, user: user, points: 1
+
+ breakdown.total.should eq 4
+ end
+
+ it "knows the rank of the user" do
+ first = create :user
+ second = create :user
+ third = create :user
+
+ create :solution, user: first, points: 2
+ create :solution, user: second, points: 2
+ create :solution, user: third, points: 1
+
+ PointsBreakdown.find(first.id).rank.should eq 1
+ PointsBreakdown.find(second.id).rank.should eq 1
+ PointsBreakdown.find(third.id).rank.should eq 3
+ end
+
+ it "has reasonable defaults when the user has no points" do
+ breakdown.vouchers.should eq 0
+ breakdown.stars.should eq 0
+ breakdown.tasks_breakdown.should eq []
+ breakdown.quizzes_breakdown.should eq []
+ end
+
+ it "assigns 0 points to tasks without solutions" do
+ create :task
+ breakdown.tasks_breakdown.should eq [0]
+ end
+
+ it "does not assign negative points to tasks" do
+ create :solution, user: user, points: 1, adjustment: -2
+ breakdown.tasks_breakdown.should eq [0]
+ end
+
+ it "assigns 0 points to quizzes without results" do
+ create :quiz
+ breakdown.quizzes_breakdown.should eq [0]
+ end
+
+ it "raises an error the user does not exists" do
+ expect { PointsBreakdown.find 0 }.to raise_error 'Cannot find user with id = 0'
+ end
+end

0 comments on commit b42fab3

Please sign in to comment.