Skip to content

Commit 8ba4dec

Browse files
authored
Reviews Endpoint Initial Scaffold (#296)
1 parent ced9841 commit 8ba4dec

25 files changed

+420
-1
lines changed

Gemfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ gem 'opentelemetry-sdk'
6969
gem 'opentelemetry-exporter-otlp'
7070
gem 'opentelemetry-instrumentation-all'
7171

72+
# for review imports from NRDBc
73+
gem 'reverse_markdown'
74+
7275
group :development, :test do
7376
gem "brakeman", "~> 5.2"
7477
gem "bundler-audit", "~> 0.9.0"

Gemfile.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,8 @@ GEM
450450
responders (3.1.1)
451451
actionpack (>= 5.2)
452452
railties (>= 5.2)
453+
reverse_markdown (2.1.1)
454+
nokogiri
453455
rexml (3.3.2)
454456
strscan
455457
rspec (3.12.0)
@@ -572,6 +574,7 @@ DEPENDENCIES
572574
rack-cors (= 2.0.0)
573575
rails (>= 7.0.7.1)
574576
responders
577+
reverse_markdown
575578
rspec-rails
576579
rspec_api_documentation
577580
rubocop

app/controllers/reviews_controller.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# frozen_string_literal: true
2+
3+
# Controller for the Review resource.
4+
class ReviewsController < ApplicationController
5+
def index
6+
reviews = ReviewResource.all(params)
7+
respond_with(reviews)
8+
end
9+
10+
def show
11+
reviews = ReviewResource.find(params)
12+
respond_with(reviews)
13+
end
14+
end

app/models/card.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ def restrictions
4949
class_name: 'RawPrinting',
5050
primary_key: :id
5151

52+
has_many :reviews
5253
has_many :printings,
5354
primary_key: :id
5455

app/models/review.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
class Review < ApplicationRecord
2+
belongs_to :card
3+
has_many :review_comments
4+
has_many :review_votes
5+
6+
def votes
7+
review_votes.count
8+
end
9+
10+
def comments
11+
review_comments
12+
end
13+
end

app/models/review_comment.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
class ReviewComment < ApplicationRecord
2+
belongs_to :review
3+
end

app/models/review_vote.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
class ReviewVote < ApplicationRecord
2+
belongs_to :review
3+
end

app/resources/card_resource.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ class CardResource < ApplicationResource # rubocop:disable Metrics/ClassLength
110110
end
111111
end
112112
has_many :rulings
113+
has_many :reviews
113114
belongs_to :side
114115

115116
many_to_many :decklists do

app/resources/review_resource.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# frozen_string_literal: true
2+
3+
# Resource for the Review object (currently imported from NRDBc)
4+
class ReviewResource < ApplicationResource
5+
primary_endpoint '/reviews', %i[index show]
6+
7+
attribute :id, :integer
8+
attribute :username, :string do
9+
@object.user_id
10+
end
11+
attribute :body, :string
12+
attribute :card, :string do
13+
@object.card.title
14+
end
15+
attribute :card_id, :string
16+
attribute :created_at, :datetime
17+
attribute :updated_at, :datetime
18+
attribute :votes, :integer
19+
20+
belongs_to :card
21+
22+
attribute :comments, :array do
23+
@object.comments.map do |comment|
24+
{
25+
id: comment.id,
26+
body: comment.body,
27+
user: comment.user_id,
28+
created_at: comment.created_at,
29+
updated_at: comment.updated_at
30+
}
31+
end
32+
end
33+
end

config/routes.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
resources :illustrators, only: %i[index show]
2020
resources :printings, only: %i[index show]
2121
resources :restrictions, only: %i[index show]
22+
resources :reviews, only: %i[index show]
2223
resources :rulings, only: %i[index show]
2324
resources :sides, only: %i[index show]
2425
resources :snapshots, only: %i[index show]
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
class CreateReviews < ActiveRecord::Migration[7.1]
2+
def change
3+
create_table :reviews do |t|
4+
t.text :ruling
5+
t.string :username
6+
t.text :card_id, null: false
7+
8+
t.timestamps
9+
end
10+
11+
add_foreign_key :reviews, :cards
12+
end
13+
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
class CreateReviewComments < ActiveRecord::Migration[7.1]
2+
def change
3+
create_table :review_comments do |t|
4+
t.text :body
5+
t.string :username
6+
t.references :review, null: false, foreign_key: true
7+
8+
t.timestamps
9+
end
10+
end
11+
end
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
class CreateReviewVotes < ActiveRecord::Migration[7.1]
2+
def change
3+
create_table :review_votes do |t|
4+
t.string :username
5+
t.references :review, null: false, foreign_key: true
6+
7+
t.timestamps
8+
end
9+
end
10+
end
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
class RenameReviewRulingToBody < ActiveRecord::Migration[7.1]
2+
def change
3+
change_table :reviews do |t|
4+
t.rename :ruling, :body
5+
end
6+
end
7+
end
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
class RenameReviewUsernameToUserId < ActiveRecord::Migration[7.1]
2+
def change
3+
change_table :reviews do |t|
4+
t.remove :username
5+
end
6+
add_reference :reviews, :user, type: :string
7+
8+
change_table :review_comments do |t|
9+
t.remove :username
10+
end
11+
add_reference :review_comments, :user, type: :string
12+
13+
change_table :review_votes do |t|
14+
# temporary, actual usernames not available yet, using placeholder values instead
15+
t.rename :username, :user_id
16+
end
17+
end
18+
end

db/schema.rb

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
#
1111
# It's strongly recommended that you check this file into your version control system.
1212

13-
ActiveRecord::Schema[7.1].define(version: 2024_06_22_192959) do
13+
ActiveRecord::Schema[7.1].define(version: 2024_07_21_065003) do
1414
# These are extensions that must be enabled in order to support this database
1515
enable_extension "pgcrypto"
1616
enable_extension "plpgsql"
@@ -281,6 +281,33 @@
281281
t.datetime "updated_at", null: false
282282
end
283283

284+
create_table "review_comments", force: :cascade do |t|
285+
t.text "body"
286+
t.bigint "review_id", null: false
287+
t.datetime "created_at", null: false
288+
t.datetime "updated_at", null: false
289+
t.string "user_id"
290+
t.index ["review_id"], name: "index_review_comments_on_review_id"
291+
t.index ["user_id"], name: "index_review_comments_on_user_id"
292+
end
293+
294+
create_table "review_votes", force: :cascade do |t|
295+
t.string "user_id"
296+
t.bigint "review_id", null: false
297+
t.datetime "created_at", null: false
298+
t.datetime "updated_at", null: false
299+
t.index ["review_id"], name: "index_review_votes_on_review_id"
300+
end
301+
302+
create_table "reviews", force: :cascade do |t|
303+
t.text "body"
304+
t.text "card_id", null: false
305+
t.datetime "created_at", null: false
306+
t.datetime "updated_at", null: false
307+
t.string "user_id"
308+
t.index ["user_id"], name: "index_reviews_on_user_id"
309+
end
310+
284311
create_table "rulings", force: :cascade do |t|
285312
t.string "card_id", null: false
286313
t.string "question"
@@ -352,6 +379,9 @@
352379
add_foreign_key "restrictions_cards_restricted", "restrictions"
353380
add_foreign_key "restrictions_cards_universal_faction_cost", "cards"
354381
add_foreign_key "restrictions_cards_universal_faction_cost", "restrictions"
382+
add_foreign_key "review_comments", "reviews"
383+
add_foreign_key "review_votes", "reviews"
384+
add_foreign_key "reviews", "cards"
355385
add_foreign_key "rulings", "cards"
356386
add_foreign_key "snapshots", "card_pools"
357387
add_foreign_key "snapshots", "formats"

lib/tasks/reviews.rake

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# frozen_string_literal: true
2+
3+
require 'json'
4+
require 'net/http'
5+
require 'optparse'
6+
require 'uri'
7+
require 'reverse_markdown'
8+
namespace :reviews do
9+
desc 'Imports review from NRDBc, currently storing usernames as strings instead of references'
10+
11+
def text_to_id(text)
12+
text.downcase
13+
.unicode_normalize(:nfd)
14+
.gsub(/\P{ASCII}/, '')
15+
.gsub(/'s(\p{Space}|\z)/, 's\1')
16+
.split(/[\p{Space}\p{Punct}]+/)
17+
.reject { |s| s&.strip&.empty? }
18+
.join('_')
19+
end
20+
21+
def retrieve_reviews
22+
url = URI('https://netrunnerdb.com/api/2.0/public/reviews')
23+
24+
http = Net::HTTP.new(url.host, url.port)
25+
http.use_ssl = true
26+
request = Net::HTTP::Get.new(url)
27+
28+
response = http.request(request)
29+
30+
return JSON.parse(response.body) if response.is_a?(Net::HTTPSuccess)
31+
32+
raise "Failed to retrieve reviews! Status code: #{response.code}"
33+
end
34+
35+
def purge_tables
36+
# Only do this in a transaction
37+
raise 'Called DB purge outside of a transaction!' unless Review.connection.transaction_open?
38+
39+
puts 'Purging Review Tables'
40+
ReviewVote.delete_all
41+
ReviewComment.delete_all
42+
Review.delete_all
43+
end
44+
45+
task import: :environment do
46+
puts 'Importing Reviews from NetrunnerDB Classic'
47+
reviews_body = retrieve_reviews
48+
49+
card_ids = Card.all.pluck(:id).to_set
50+
Review.transaction do
51+
purge_tables
52+
puts 'Starting import'
53+
reviews_body['data'].each do |review|
54+
card_name = review['title']
55+
rev_body = ReverseMarkdown.convert review['ruling']
56+
username = review['user']
57+
comments = review['comments']
58+
59+
card_id = text_to_id(card_name)
60+
if card_ids.include? card_id
61+
r = Review.new
62+
r.card_id = card_id
63+
r.user_id = username
64+
r.body = rev_body
65+
r.created_at = DateTime.parse(review['date_create'])
66+
r.updated_at = DateTime.parse(review['date_update'])
67+
r.save!
68+
69+
# Hack for votes: generate filler entries in the join table
70+
ReviewVote.transaction do
71+
review['votes'].times do
72+
vote = ReviewVote.new
73+
vote.user_id = 'TBD_Future_Problem'
74+
vote.review = r
75+
vote.save!
76+
end
77+
end
78+
79+
# Generate Comments for each deck
80+
ReviewComment.transaction do
81+
comments.each do |comment|
82+
c = ReviewComment.new
83+
c.user_id = comment['user']
84+
c.body = ReverseMarkdown.convert comment['comment']
85+
c.review = r
86+
c.created_at = comment['date_create']
87+
c.updated_at = comment['date_update']
88+
c.save!
89+
end
90+
end
91+
else
92+
puts "Missing Card entry with title: #{card_name}"
93+
end
94+
end
95+
end
96+
end
97+
end

spec/acceptance/cards_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
* Decklists
2121
* Faction
2222
* Printings
23+
* Reviews
2324
* Rulings
2425
* Side
2526
EXPLANATION

spec/acceptance/reviews_spec.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
require 'rspec_api_documentation/dsl'
5+
6+
resource 'Reviews' do
7+
header 'Content-Type', 'application/json'
8+
header 'Host', 'api-preview.netrunnderdb.com'
9+
10+
explanation <<~EXPLANATION
11+
Card reviews have the following relationships
12+
13+
* Card
14+
EXPLANATION
15+
16+
get '/api/v3/public/reviews' do
17+
example_request 'All Reviews' do
18+
expect(status).to eq 200
19+
end
20+
end
21+
22+
get '/api/v3/public/reviews/:id' do
23+
parameter :id, type: :string, required: true
24+
25+
let(:id) { '1' }
26+
example_request 'Get A Single Review' do
27+
expect(status).to eq 200
28+
end
29+
end
30+
31+
get '/api/v3/public/reviews?filter[card_id]=:query' do
32+
parameter :query, type: :string, required: true
33+
34+
let(:query) { 'endurance' }
35+
example_request 'Filter on a single card id' do
36+
expect(status).to eq 200
37+
end
38+
end
39+
end

0 commit comments

Comments
 (0)