Commit a7918b01 by Rocky Duan

initial import

parents
*.gem
*.rbc
.bundle
.config
coverage
InstalledFiles
lib/bundler/man
pkg
rdoc
spec/reports
test/tmp
test/version_tmp
tmp
# YARD artifacts
.yardoc
_yardoc
doc/
Gemfile.lock
*.sqlite3
.*.swp
.*.swo
.*.swm
rvm use 1.9.3
source :rubygems
gem 'sinatra'
gem 'activerecord'
gem 'yajl-ruby'
gem 'ancestry'
gem 'sqlite3'
gem 'ampex'
group :test do
gem 'rspec'
gem 'rack-test', :require => "rack/test"
end
comment_as_a_service
====================
An independent comment system which supports voting and nested comments. It also supports features including instructor endorsement for education-aimed discussion platforms.
require 'rubygems'
require 'active_record'
require 'yaml'
require 'logger'
desc "Load the environment"
task :environment do
env = ENV["SINATRA_ENV"] || "development"
databases = YAML.load_file("config/database.yml")
ActiveRecord::Base.establish_connection(databases[env])
end
namespace :db do
desc "Migrate the database"
task :migrate => :environment do
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Migration.verbose = true
ActiveRecord::Migrator.migrate("db/migrate")
end
task :seed => :environment do
require_relative 'models/comment.rb'
require_relative 'models/comment_thread.rb'
require_relative 'models/vote.rb'
Comment.delete_all
CommentThread.delete_all
Vote.delete_all
comment_thread = CommentThread.create! :commentable_type => "questions", :commentable_id => 1
5.times do
comment_thread.root_comments.create :body => "top comment", :title => "top #{rand(10)}", :user_id => 1, :course_id => 1, :comment_thread_id => comment_thread.id
end
50.times do
Comment.all.reject{|c| c.is_root?}.sample.children.create :body => "comment body", :title => "comment title #{rand(50)}", :user_id => 1, :course_id => 1, :comment_thread_id => comment_thread.id
end
Comment.all.reject{|c| c.is_root?}.each do |c|
(1..20).each do |id|
Vote.create! :value => ["up", "down"].sample, :comment_id => c.id, :user_id => id
end
end
end
end
require 'rubygems'
require 'yajl'
require 'active_record'
require 'sinatra'
require_relative 'models/comment'
require_relative 'models/comment_thread'
require_relative 'models/vote'
env_index = ARGV.index("-e")
env_arg = ARGV[env_index + 1] if env_index
env = env_arg || ENV["SINATRA_ENV"] || "development"
databases = YAML.load_file("config/database.yml")
ActiveRecord::Base.establish_connection(databases[env])
# retrive all comments of a commentable object
get '/api/v1/commentables/:commentable_type/:commentable_id/comments' do |commentable_type, commentable_id|
comment_thread = CommentThread.find_or_create_by_commentable_type_and_commentable_id(commentable_type, commentable_id)
comment_thread.json_comments
end
# create a new top-level comment
post '/api/v1/commentables/:commentable_type/:commentable_id/comments' do |commentable_type, commentable_id|
comment_thread = CommentThread.find_or_create_by_commentable_type_and_commentable_id(commentable_type, commentable_id)
comment_params = params.select {|key, value| %w{body title user_id course_id}.include? key}
comment_params.merge :comment_thread_id => comment_thread.id
comment = comment_thread.root_comments.create(comment_params)
if comment.valid?
comment.to_json
else
error 400, comment.errors.to_json
end
end
# delete a commentable object and its associated comments
delete '/api/v1/commentables/:commentable_type/:commentable_id' do |commentable_type, commentable_id|
comment_thread = CommentThread.find_by_commentable_type_and_commentable_id(commentable_type, commentable_id)
if comment_thread.nil?
error 400, {:error => "commentable object does not exist"}.to_json
else
comment_thread.destroy
comment_thread.to_json
end
end
# create a new subcomment (reply to comment) only if the comment is NOT a super comment
post '/api/v1/comments/:comment_id' do |comment_id|
comment = Comment.find_by_id(comment_id)
if comment.nil? or comment.is_root?
error 400, {:error => "invalid comment id"}.to_json
else
comment_params = params.select {|key, value| %w{body title user_id course_id}.include? key}
comment_params.merge :comment_thread_id => comment.root.comment_thread.id
sub_comment = comment.children.create(comment_params)
if comment.valid?
comment.to_json
else
error 400, comment.errors.to_json
end
end
end
# delete the comment and the associated sub comments only if the comment is NOT the super comment
delete '/api/v1/comments/:comment_id' do |comment_id|
comment = Comment.find_by_id(comment_id)
if comment.nil? or comment.is_root?
error 400, {:error => "invalid comment id"}.to_json
else
comment.destroy
comment.to_json
end
end
# update the body / title (or both) of a comment provided the comment is NOT the super comment
put '/api/v1/comments/:comment_id' do |comment_id|
comment = Comment.find_by_id(comment_id)
if comment.nil? or comment.is_root?
error 400, {:error => "invalid comment id"}.to_json
else
comment_params = params.select {|key, value| %w{body title}.include? key}
if comment.update_attributes(comment_params)
comment.to_json
else
error 400, comment.errors.to_json
end
end
end
# create or update the vote on the comment by the user
put '/api/v1/votes/comments/:comment_id/users/:user_id' do |comment_id, user_id|
if not %w{up down}.include? params["value"]
error 400, {:error => "value must be up or down"}.to_json
else
comment = Comment.find_by_id(comment_id)
if comment.nil?
error 400, {:error => "invalid comment id"}.to_json
else
vote = Vote.create_or_update :user_id => user_id, :comment_id => comment_id, :value => params["value"]
if vote
vote.to_json
else
error 400, vote.errors.to_json
end
end
end
end
# undo the vote on the comment by the user
delete '/api/v1/votes/comments/:comment_id/users/:user_id' do |comment_id, user_id|
vote = Vote.find_by_comment_id_and_user_id(comment_id, user_id)
if vote.nil?
error 400, {:error => "vote does not exist"}.to_json
else
vote.destroy
vote.to_json
end
end
if env.to_s == "development"
get '/api/v1/clean' do
Comment.delete_all
CommentThread.delete_all
Vote.delete_all
{}.to_json
end
end
development:
adapter: sqlite3
database: db/development.sqlite3
pool: 100
test:
adapter: sqlite3
database: db/test.sqlite3
pool: 100
# Taken and modified from gem 'acts_as_commentable_with_threading':
# https://github.com/elight/acts_as_commentable_with_threading/blob/master/lib/generators/acts_as_commentable_with_threading_migration/templates/migration.rb
class CreateComments < ActiveRecord::Migration
def self.up
create_table :comments do |t|
t.text :body
t.text :title, :default => ""
t.string :ancestry
t.integer :user_id
t.integer :course_id
t.integer :comment_thread_id
t.timestamps
end
add_index :comments, :user_id
add_index :comments, :course_id
add_index :comments, :ancestry
end
def self.down
drop_table :comments
end
end
class CreateCommentThreads < ActiveRecord::Migration
def self.up
create_table :comment_threads do |t|
t.string :commentable_type
t.string :commentable_id
t.timestamps
end
end
def self.down
drop_table :comment_threads
end
end
class CreateVotes < ActiveRecord::Migration
def self.up
create_table :votes do |t|
t.integer :user_id
t.integer :comment_id
t.string :value
t.timestamps
end
add_index :votes, :comment_id
add_index :votes, :user_id
add_index :votes, [:comment_id, :user_id]
end
def self.down
drop_table :votes
end
end
require 'active_record'
require 'ancestry'
class Comment < ActiveRecord::Base
attr_accessible :body, :title, :user_id, :course_id, :comment_thread_id
has_ancestry
has_many :votes
belongs_to :comment_thread
def self.hash_tree(nodes)
nodes.map do |node, sub_nodes|
{
:id => node.id,
:body => node.body,
:title => node.title,
:user_id => node.user_id,
:course_id => node.course_id,
:created_at => node.created_at,
:updated_at => node.updated_at,
:comment_thread_id => node.comment_thread_id,
:children => hash_tree(sub_nodes).compact,
:votes => {:up => Vote.comment_id(node.id).up.count, :down => Vote.comment_id(node.id).down.count},
}
end
end
def to_hash_tree
self.class.hash_tree(self.subtree.arrange)
end
end
require 'active_record'
class CommentThread < ActiveRecord::Base
has_one :super_comment, :class_name => "Comment", :dependent => :destroy
# Ensures that each thread is associated with a commentable object
validates_presence_of :commentable_type, :commentable_id
# Ensures that there is only one thread for each commentable object
validates_uniqueness_of :commentable_id, :scope => :commentable_type
# Helper class method to create a new thread with the corresponding super comment
#def self.find_or_build(commentable_type, commentable_id)
# comment_thread = CommentThread.find_or_create_by_commentable_type_and_commentable
# Create a super comment which does not hold anything itself, but points to all comments of the thread
after_create :create_super_comment
def create_super_comment
comment = Comment.create! :comment_thread_id => self.id
end
def root_comments
super_comment.children
end
def comments
super_comment.descendants
end
def json_comments
super_comment.to_hash_tree.first[:children].to_json
end
end
require 'active_record'
# Adapted from "Service-Oriented Design with Ruby and Rails"
class Vote < ActiveRecord::Base
attr_accessible :value, :user_id, :comment_id
belongs_to :comment
validates_inclusion_of :value, :in => %w{up down}
validates_uniqueness_of :user_id, :scope => :comment_id
validates_presence_of :comment_id, :user_id
scope :up, :conditions => ["value = ?", "up"]
scope :down, :conditions => ["value = ?", "down"]
scope :user_id, lambda {|user_id| {:conditions => ["user_id = ?", user_id]}}
scope :comment_id, lambda {|comment_id| {:conditions => ["comment_id = ?", comment_id]}}
def self.create_or_update(attributes)
vote = Vote.find_by_comment_id_and_user_id(attributes[:comment_id], attributes[:user_id])
if vote
vote.value = attributes[:value]
vote.save
vote
else
Vote.create(attributes)
end
end
end
require 'spec_helper'
require 'yajl'
describe "app" do
describe "comments" do
before :each do
Comment.delete_all
CommentThread.delete_all
end
describe "POST on /api/v1/commentables/:commentable_type/:commentable_id/comments" do
it "should create a top-level comment with correct body, title, user_id, and course_id" do
post "/api/v1/commentables/questions/1/comments", :body => "comment body", :title => "comment title", :user_id => 1, :course_id => 1
last_response.should be_ok
comment = CommentThread.first.root_comments.first
comment.should_not be_nil
comment.body.should == "comment body"
comment.title.should == "comment title"
comment.user_id.should == 1
comment.user_id.should == 1
end
end
describe "POST on /api/v1/comments/:comment_id" do
before :each do
CommentThread.create! :commentable_type => "questions", :commentable_id => 1
CommentThread.first.root_comments.create :body => "top comment", :title => "top", :user_id => 1, :course_id => 1, :comment_thread_id => CommentThread.first.id
end
it "should create a sub comment with correct body, title, user_id, and course_id" do
post "/api/v1/comments/#{CommentThread.first.root_comments.first.id}",
:body => "comment body", :title => "comment title", :user_id => 1, :course_id => 1
last_response.should be_ok
comment = CommentThread.first.root_comments.first.children.first
comment.should_not be_nil
comment.body.should == "comment body"
comment.title.should == "comment title"
comment.user_id.should == 1
comment.user_id.should == 1
end
it "should not create a sub comment for the super comment" do
post "/api/v1/comments/#{CommentThread.first.super_comment.id}",
:body => "comment body", :title => "comment title", :user_id => 1, :course_id => 1
last_response.status.should == 400
end
end
describe "GET on /api/v1/commentables/:commentable_type/:commentable_id/comments" do
it "should create a corresponding comment thread with a super comment" do
get "/api/v1/commentables/questions/1/comments"
last_response.should be_ok
comment_thread = CommentThread.first
comment_thread.should_not be_nil
comment_thread.super_comment.should_not be_nil
end
it "should create a corresponding comment thread with correct type and id" do
get "/api/v1/commentables/questions/1/comments"
last_response.should be_ok
comment_thread = CommentThread.first
comment_thread.commentable_type.should == 'questions'
comment_thread.commentable_id.should == '1'
end
it "returns an empty array when there are no comments" do
get "/api/v1/commentables/questions/1/comments"
last_response.should be_ok
comments = Yajl::Parser.parse last_response.body
comments.length.should == 0
end
it "retrieves all comments with their votes in a nested structure in json format" do
comment_thread = CommentThread.create! :commentable_type => "questions", :commentable_id => 1
comment = []
sub_comment = []
comment << (comment_thread.root_comments.create :body => "top comment", :title => "top 0", :user_id => 1, :course_id => 1, :comment_thread_id => comment_thread.id)
sub_comment << (comment[0].children.create :body => "comment body", :title => "comment title 0", :user_id => 1, :course_id => 1, :comment_thread_id => comment_thread.id)
sub_comment << (comment[0].children.create :body => "comment body", :title => "comment title 1", :user_id => 1, :course_id => 1, :comment_thread_id => comment_thread.id)
Vote.create! :value => "up", :comment_id => comment[0].id, :user_id => 1
Vote.create! :value => "up", :comment_id => comment[0].id, :user_id => 2
Vote.create! :value => "up", :comment_id => comment[0].id, :user_id => 3
Vote.create! :value => "up", :comment_id => comment[0].id, :user_id => 4
Vote.create! :value => "down", :comment_id => comment[0].id, :user_id => 5
Vote.create! :value => "down", :comment_id => comment[0].id, :user_id => 6
Vote.create! :value => "down", :comment_id => comment[0].id, :user_id => 7
get "/api/v1/commentables/questions/1/comments"
last_response.should be_ok
comments = Yajl::Parser.parse last_response.body
comments.length.should == 1
c = comments[0]
c["title"].should == "top 0"
c["id"].should == comment[0].id
c["votes"]["up"].should == 4
c["votes"]["down"].should == 3
c["comment_thread_id"].should == comment_thread.id
c["created_at"].should_not be_nil
c["updated_at"].should_not be_nil
c["children"].length.should == 2
c["children"][0]["title"].should == "comment title 0"
c["children"][0]["id"].should == sub_comment[0].id
c["children"][0]["created_at"].should_not be_nil
c["children"][0]["updated_at"].should_not be_nil
end
end
describe "DELETE on /api/v1/commentables/:commentable_type/:commentable_id" do
before :each do
comment_thread = CommentThread.create! :commentable_type => "questions", :commentable_id => 1
comment = []
sub_comment = []
comment << (comment_thread.root_comments.create :body => "top comment", :title => "top 0", :user_id => 1, :course_id => 1, :comment_thread_id => comment_thread.id)
sub_comment << (comment[0].children.create :body => "comment body", :title => "comment title 0", :user_id => 1, :course_id => 1, :comment_thread_id => comment_thread.id)
comment << (comment_thread.root_comments.create :body => "top comment", :title => "top 1", :user_id => 1, :course_id => 1, :comment_thread_id => comment_thread.id)
sub_comment << (comment[1].children.create :body => "comment body", :title => "comment title 1", :user_id => 1, :course_id => 1, :comment_thread_id => comment_thread.id)
end
it "should return error when called on a nonexisted thread" do
delete "/api/v1/commentables/i_do_not_exist/1"
last_response.status.should == 400
end
it "deletes all comments associated with a thread when called on the thread" do
delete "/api/v1/commentables/questions/1"
last_response.should be_ok
CommentThread.count.should == 0
Comment.count.should == 0
end
it "deletes the comment and all sub comments when called on the comment" do
comment_thread = CommentThread.first
comment = comment_thread.root_comments.first
delete "/api/v1/comments/#{comment.id}"
last_response.should be_ok
comment_thread.root_comments.count.should == 1
comment_thread.comments.count.should == 2
comment_thread.root_comments.first.title.should == "top 1"
comment_thread.root_comments.first.children.first.title.should == "comment title 1"
end
it "should not delete the super comment" do
comment_thread = CommentThread.first
comment = comment_thread.super_comment
delete "/api/v1/comments/#{comment.id}"
last_response.status.should == 400
end
end
describe "PUT on /api/v1/comments/comment_id" do
before :each do
comment_thread = CommentThread.create! :commentable_type => "questions", :commentable_id => 1
comment_thread.root_comments.create :body => "top comment", :title => "top 0", :user_id => 1, :course_id => 1, :comment_thread_id => comment_thread.id
end
it "should update body and title" do
comment = CommentThread.first.comments.first
put "/api/v1/comments/#{comment.id}", :body => "new body", :title => "new title"
last_response.should be_ok
comment = CommentThread.first.comments.first
comment.body.should == "new body"
comment.title.should == "new title"
end
it "should not update the super comment" do
comment = CommentThread.first.super_comment
put "/api/v1/comments/#{comment.id}", :body => "new body", :title => "new title"
last_response.status.should == 400
end
it "should not update user_id nor course_id" do
comment = CommentThread.first.comments.first
put "/api/v1/comments/#{comment.id}", :user_id => 100, :course_id => 100
last_response.should be_ok
comment = CommentThread.first.comments.first
comment.user_id.should == 1
comment.course_id.should == 1
end
end
end
describe "votings" do
before :each do
CommentThread.delete_all
Comment.delete_all
Vote.delete_all
end
describe "PUT on /api/v1/votes/comments/:comment_id/users/:user_id" do
before :each do
CommentThread.create! :commentable_type => "questions", :commentable_id => 1
CommentThread.first.root_comments.create :body => "top comment", :title => "top", :user_id => 1, :course_id => 1
end
it "votes up on a comment" do
comment = CommentThread.first.comments.first
put "/api/v1/votes/comments/#{comment.id}/users/1", :value => "up"
last_response.should be_ok
vote = Vote.first
vote.should_not be_nil
vote.user_id.should == 1
vote.comment_id.should == comment.id
vote.value.should == "up"
end
it "votes down on a comment" do
comment = CommentThread.first.comments.first
put "/api/v1/votes/comments/#{comment.id}/users/1", :value => "down"
last_response.should be_ok
vote = Vote.first
vote.should_not be_nil
vote.user_id.should == 1
vote.comment_id.should == comment.id
vote.value.should == "down"
end
it "rejects invalid vote value" do
comment = CommentThread.first.comments.first
put "/api/v1/votes/comments/#{comment.id}/users/1", :value => "up_or_down"
last_response.status.should == 400
end
it "rejects nonexisted comment id" do
comment = CommentThread.first.comments.first
put "/api/v1/votes/comments/#{comment.id ** 2}/users/1", :value => "up"
last_response.status.should == 400
end
it "change vote on comment" do
comment = CommentThread.first.comments.first
Vote.create! :value => "up", :user_id => 1, :comment_id => comment.id
put "/api/v1/votes/comments/#{comment.id}/users/1", :value => "down"
last_response.should be_ok
Vote.first.value.should == "down"
end
end
describe "DELETE on /api/v1/votes/comments/:comment_id/users/:user_id" do
before :each do
CommentThread.create! :commentable_type => "questions", :commentable_id => 1
CommentThread.first.root_comments.create :body => "top comment", :title => "top", :user_id => 1, :course_id => 1, :comment_thread_id => CommentThread.first.id
end
it "deletes vote" do
comment = CommentThread.first.comments.first
Vote.create! :value => "up", :user_id => 1, :comment_id => comment.id
delete "/api/v1/votes/comments/#{comment.id}/users/1"
last_response.should be_ok
Vote.count.should == 0
end
it "returns 400 for nonexisted vote" do
comment = CommentThread.first.comments.first
delete "/api/v1/votes/comments/#{comment.id}/users/1"
last_response.status.should == 400
end
end
end
end
require File.join(File.dirname(__FILE__), '..', 'app')
require 'sinatra'
require 'rack/test'
# setup test environment
set :environment, :test
set :run, false
set :raise_errors, true
set :logging, false
def app
Sinatra::Application
end
RSpec.configure do |config|
config.include Rack::Test::Methods
end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment