Skip to content

Instantly share code, notes, and snippets.

@tatwell
Last active July 8, 2016 16:47
Show Gist options
  • Select an option

  • Save tatwell/c70e13b37c44e6ef4504 to your computer and use it in GitHub Desktop.

Select an option

Save tatwell/c70e13b37c44e6ef4504 to your computer and use it in GitHub Desktop.
Ruby Stack Overflow Script (with rspec test)

Ruby Stack Overflow

A simple script that finds overlooked questions that may deserve to be answered on Stack Overflow using the Stack Overflow API. It also includes a basic rspec test.

source "https://rubygems.org"
gem 'pry'
gem 'httparty'
gem 'rspec'
#
# ScoredQuestion Model
#
# Represents a StackOverflow question that has been returned by the API and that
# will be scored according to rubric in :score method.
#
class ScoredQuestion
attr_reader :json, :question, :owner
# Class Constants (tweak as suits you)
MIN_OWNER_REPUTATION = 25
MIN_OWNER_ACCEPT_RATE = 60
MAX_QUESTION_ANSWERS = 1
MIN_QUESTION_SCORE = 0
def initialize(question_json)
@json = question_json
@question = OpenStruct.new(@json)
@owner = OpenStruct.new(@json['owner'])
@owner.accept_rate = 0 if @owner.accept_rate.nil?
@owner.reputation = 0 if @owner.reputation.nil?
end
def interesting?
@question.is_answered == false &&
@owner.reputation >= MIN_OWNER_REPUTATION &&
@owner.accept_rate >= MIN_OWNER_ACCEPT_RATE &&
@question.answer_count <= MAX_QUESTION_ANSWERS &&
@question.score >= MIN_QUESTION_SCORE
end
def score
# Scores question based on a subjective formula of your device.
# TODO: include comments as factor since a lot of questions get resolved in
# comments before any answer submitted.
score = 0
score += age.to_f / (24 * HOUR_SECS) * 100
score += @owner.reputation / 20
score += @owner.accept_rate
score += @question.score * 20
score += score / ((@question.answer_count * 10) + 1)
score.to_i
end
def age
# in seconds
Time.now.to_i - @question.creation_date.to_i
end
def display_age
age_in_minutes = age / 60
age_in_hours = age_in_minutes / 60
if age_in_hours < 1
format('%d minute%s', age_in_minutes, age_in_minutes == 1 ? '' : 's')
else
format('%d hour%s', age_in_hours, age_in_hours == 1 ? '' : 's')
end
end
def to_s
[
@question.title,
@question.tags,
"#{@owner.display_name} (#{@owner.reputation} / #{@owner.accept_rate}%)",
"Score: #{self.score}",
"Answers: #{@question.answer_count}",
"Age: #{self.display_age}",
"Link: #{@question.link}"
]
end
end
#
# spec/scored_question_spec.rb
#
# Note: this is expected to be in spec subdirectory.
#
# To run:
# cd stackoverflow/ruby
# rspec spec/scored_question_spec.rb
#
#
require 'spec_helper'
require_relative '../stackoverflow'
require_relative '../scored_question'
describe ScoredQuestion do
describe '.new' do
let(:scored_question) { ScoredQuestion.new(@api_search_result) }
it { expect(scored_question.question.tags).to include('foo', 'bar') }
it { expect(scored_question.owner.user_id).to eq 1 }
it { expect(scored_question.owner.display_name).to eq 'foo' }
it { expect(scored_question.question.score).to eq 1 }
it { expect(scored_question.score).to be > 0 }
it { expect([true, false]).to include(scored_question.interesting?) }
end
describe '#interesting?' do
let(:interesting_question) { ScoredQuestion.new(@api_search_result) }
it { expect(interesting_question.question.is_answered).to be false }
it { expect(interesting_question.owner.reputation).to be >=
ScoredQuestion::MIN_OWNER_REPUTATION }
it { expect(interesting_question.owner.accept_rate).to be >=
ScoredQuestion::MIN_OWNER_ACCEPT_RATE }
it { expect(interesting_question.question.answer_count).to be <=
ScoredQuestion::MAX_QUESTION_ANSWERS }
it { expect(interesting_question.question.score).to be >=
ScoredQuestion::MIN_QUESTION_SCORE }
it { expect(interesting_question.interesting?).to be true }
let(:uninteresting_question) do
result = @api_search_result.dup
result['is_answered'] = true
ScoredQuestion.new(result)
end
it { expect(uninteresting_question.interesting?).to be false }
end
describe '#score' do
let(:scored_question) { ScoredQuestion.new(@api_search_result) }
it { expect(scored_question.score).to eq 170 }
end
end
RSpec.configure do |config|
config.before(:example) {
@api_search_result = {
tags: ['foo', 'bar'],
'owner' => {
user_id: 1,
display_name: 'foo',
link: 'http://stackoverflow.com/users/1/foo',
user_type: 'registered',
reputation: ScoredQuestion::MIN_OWNER_REPUTATION,
accept_rate: ScoredQuestion::MIN_OWNER_ACCEPT_RATE,
profile_image: 'https://www.gravatar.com/avatar/foo?s=1&d=identicon&r=PG&f=1',
},
question_id: 1,
link: 'http://stackoverflow.com/questions/100/test-foo',
title: 'Can you foo?',
is_answered: false,
view_count: 10,
answer_count: 0,
score: 1,
creation_date: Time.now.to_i - 3600,
last_activity_date: Time.now.to_i - 60
}
}
end
#
# stackoverflow.rb
#
# Requests questions from the Stack Overflow API and filters and scores them
# based on criteria defined in script.
#
# For Advanced Search API, see https://api.stackexchange.com/docs/advanced-search
#
# API KEY
# Either paste your key directly below or export to your environment, like so:
# $ export STACKOVERFLOW_API_KEY="my-api-key"
#
# USAGE
# ruby stackoverflow.rb
# > usage
#
require 'pry'
require 'httparty'
require_relative 'scored_question'
# For extra requests, put your API key here
API_KEY = ENV['STACKOVERFLOW_API_KEY']
# Constants
API_URL = 'https://api.stackexchange.com/2.2'
HOUR_SECS = 60 * 60
#
# Functions
#
def api_search_request(request_params)
default_params = {
site: 'stackoverflow',
sort: 'activity',
order: 'desc'
}
params = default_params.merge(request_params)
params['key'] = API_KEY if API_KEY
url = format('%s/search/advanced', API_URL)
puts "making request to #{url}"
response = HTTParty.get(url, { query: params })
response['questions'] = response['items'].map { |question| ScoredQuestion.new(question) }
response
end
def search_recent_open_questions(tag='ruby', exclude_tag='.net')
questions = []
# Param constants
max_page = 5
max_age = 72 # hours
page_size = 100
params = {
tagged: tag,
notttagged: exclude_tag,
closed: 'False',
accepted: 'False',
fromdate: Time.now.to_i - (max_age * HOUR_SECS),
page: 1,
pagesize: page_size
}
while params[:page] <= max_page
response = api_search_request(params)
questions += response['questions']
params[:page] += 1
params[:page] = max_age + 1 unless response['has_more']
end
puts format('Search complete: %s requests remaining on quote',
response.fetch('quota_remaining', '?'))
return questions
end
def filter_interesting_questions(tag='ruby', exclude_tag='.net')
questions = search_recent_open_questions(tag, exclude_tag)
puts "filtering #{questions.length} results"
interesting_questions = questions.keep_if { |question| question.interesting? }
interesting_questions = interesting_questions.sort_by(&:score).reverse
# Return enumerator object: http://ruby-doc.org/core-2.1.1/Enumerator.html
interesting_questions.each
end
#
# Interface
#
def usage
puts <<USAGE
USAGE:
pry(main)> questions = filter_interesting_questions('tag1;tag2')
pry(main)> questions.count
pry(main)> questions.first
pry(main)> filter('tag1;tag2')
pry(main)> $questions.next.to_s
SAMPLE TAGS:
ruby, ruby-on-rails, activerecord, rspec
USAGE
end
# Global here allows us to skip assignment in calling filter.
$questions = nil
def filter(tags, exclude_tags='')
$questions = filter_interesting_questions(tags)
puts format('Filtered %d questions for tags: %s', $questions.count, tags.split(';'))
puts 'To inspect results: $questions.next.to_s'
end
# Pry provides a quasi interactive interface.
# Only interrupts if file called directly
binding.pry if __FILE__ == $0
#
# spec/stackoverflow_spec.rb
#
# Note: this is expected to be in spec subdirectory.
#
# To run:
# cd stackoverflow/ruby
# rspec spec/stackoverflow_spec.rb
#
#
require 'spec_helper'
require 'json'
require_relative '../stackoverflow'
require_relative '../scored_question'
describe 'Tests stackoverflow script' do
context 'handling API requests' do
context 'when there are more then 5 pages of results' do
before(:each) do
allow(HTTParty).to receive(:get).and_return({
'items' => [@api_search_result],
'has_more' => true
})
end
it 'stops at 5th page' do
results = search_recent_open_questions('foo')
expect(results.count).to eq 5
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment