Merge branch 'main' into feature/wardrobe-v2
This commit is contained in:
commit
f13481783d
25 changed files with 2099 additions and 986 deletions
1
Gemfile
1
Gemfile
|
|
@ -87,4 +87,5 @@ gem "shell", "~> 0.8.1"
|
||||||
|
|
||||||
# For automated tests.
|
# For automated tests.
|
||||||
gem 'rspec-rails', '~> 8.0', '>= 8.0.2', group: [:development, :test]
|
gem 'rspec-rails', '~> 8.0', '>= 8.0.2', group: [:development, :test]
|
||||||
|
gem 'rails-controller-testing', group: [:test]
|
||||||
gem "webmock", "~> 3.24", group: [:test]
|
gem "webmock", "~> 3.24", group: [:test]
|
||||||
|
|
|
||||||
|
|
@ -341,6 +341,10 @@ GEM
|
||||||
activesupport (= 8.1.2)
|
activesupport (= 8.1.2)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 8.1.2)
|
railties (= 8.1.2)
|
||||||
|
rails-controller-testing (1.0.5)
|
||||||
|
actionpack (>= 5.0.1.rc1)
|
||||||
|
actionview (>= 5.0.1.rc1)
|
||||||
|
activesupport (>= 5.0.1.rc1)
|
||||||
rails-dom-testing (2.3.0)
|
rails-dom-testing (2.3.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
minitest
|
minitest
|
||||||
|
|
@ -505,6 +509,7 @@ DEPENDENCIES
|
||||||
rack-attack (~> 6.7)
|
rack-attack (~> 6.7)
|
||||||
rack-mini-profiler (~> 4.0, >= 4.0.1)
|
rack-mini-profiler (~> 4.0, >= 4.0.1)
|
||||||
rails (~> 8.0, >= 8.0.1)
|
rails (~> 8.0, >= 8.0.1)
|
||||||
|
rails-controller-testing
|
||||||
rails-i18n (~> 8.0, >= 8.0.1)
|
rails-i18n (~> 8.0, >= 8.0.1)
|
||||||
rdiscount (~> 2.2, >= 2.2.7.1)
|
rdiscount (~> 2.2, >= 2.2.7.1)
|
||||||
rspec-rails (~> 8.0, >= 8.0.2)
|
rspec-rails (~> 8.0, >= 8.0.2)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,14 @@
|
||||||
body.users-top_contributors
|
body.users-top_contributors
|
||||||
text-align: center
|
text-align: center
|
||||||
|
|
||||||
|
.timeframe-nav
|
||||||
|
margin: 1em 0
|
||||||
|
display: flex
|
||||||
|
justify-content: center
|
||||||
|
gap: 1em
|
||||||
|
list-style: none
|
||||||
|
padding: 0
|
||||||
|
|
||||||
#top-contributors
|
#top-contributors
|
||||||
border:
|
border:
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,10 @@ class PetsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def destination
|
def destination
|
||||||
case (params[:destination] || params[:origin])
|
if request.get?
|
||||||
when 'wardrobe' then wardrobe_path
|
wardrobe_path
|
||||||
else root_path
|
else
|
||||||
|
root_path
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,10 @@ class UsersController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def top_contributors
|
def top_contributors
|
||||||
@users = User.top_contributors.paginate :page => params[:page], :per_page => 20
|
valid_timeframes = User::VALID_TIMEFRAMES.map(&:to_s)
|
||||||
|
@timeframe = params[:timeframe].presence_in(valid_timeframes) || 'all_time'
|
||||||
|
@users = User.top_contributors_for(@timeframe.to_sym)
|
||||||
|
.paginate(page: params[:page], per_page: 20)
|
||||||
end
|
end
|
||||||
|
|
||||||
def edit
|
def edit
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
module OutfitsHelper
|
module OutfitsHelper
|
||||||
def destination_tag(value)
|
|
||||||
hidden_field_tag 'destination', value, :id => nil
|
|
||||||
end
|
|
||||||
|
|
||||||
def latest_contribution_description(contribution)
|
def latest_contribution_description(contribution)
|
||||||
user = contribution.user
|
user = contribution.user
|
||||||
contributed = contribution.contributed
|
contributed = contribution.contributed
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,51 @@ class User < ApplicationRecord
|
||||||
|
|
||||||
scope :top_contributors, -> { order('points DESC').where('points > 0') }
|
scope :top_contributors, -> { order('points DESC').where('points > 0') }
|
||||||
|
|
||||||
|
VALID_TIMEFRAMES = [:all_time, :this_year, :this_month, :this_week]
|
||||||
|
|
||||||
|
scope :top_contributors_for, ->(timeframe = :all_time) {
|
||||||
|
case timeframe.to_sym
|
||||||
|
when :all_time
|
||||||
|
top_contributors # Use existing efficient scope
|
||||||
|
else
|
||||||
|
top_contributors_by_period(timeframe)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
def self.top_contributors_by_period(timeframe)
|
||||||
|
start_date = case timeframe.to_sym
|
||||||
|
when :this_week then 1.week.ago
|
||||||
|
when :this_month then 1.month.ago
|
||||||
|
when :this_year then 1.year.ago
|
||||||
|
else raise ArgumentError, "Invalid timeframe: #{timeframe}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Build the CASE statement dynamically from Contribution::POINT_VALUES
|
||||||
|
point_case = Contribution::POINT_VALUES.map { |type, points|
|
||||||
|
"WHEN #{connection.quote(type)} THEN #{points}"
|
||||||
|
}.join("\n ")
|
||||||
|
|
||||||
|
select(
|
||||||
|
'users.*',
|
||||||
|
"COALESCE(SUM(
|
||||||
|
CASE contributions.contributed_type
|
||||||
|
#{point_case}
|
||||||
|
END
|
||||||
|
), 0) AS period_points"
|
||||||
|
)
|
||||||
|
.joins('INNER JOIN contributions ON contributions.user_id = users.id')
|
||||||
|
.where('contributions.created_at >= ?', start_date)
|
||||||
|
.group('users.id')
|
||||||
|
.having('period_points > 0')
|
||||||
|
.order('period_points DESC, users.id ASC')
|
||||||
|
end
|
||||||
|
|
||||||
|
# Virtual attribute reader for dynamically calculated points (from time-period queries).
|
||||||
|
# Falls back to the denormalized `points` column when not calculated.
|
||||||
|
def period_points
|
||||||
|
attributes['period_points'] || points
|
||||||
|
end
|
||||||
|
|
||||||
after_update :sync_name_with_auth_user!, if: :saved_change_to_name?
|
after_update :sync_name_with_auth_user!, if: :saved_change_to_name?
|
||||||
after_update :log_trade_activity, if: -> user {
|
after_update :log_trade_activity, if: -> user {
|
||||||
(user.saved_change_to_owned_closet_hangers_visibility? &&
|
(user.saved_change_to_owned_closet_hangers_visibility? &&
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
- html_options = {} unless defined? html_options
|
- html_options = {} unless defined? html_options
|
||||||
- html_options[:id] ||= "outfit-viewer-#{SecureRandom.hex(8)}"
|
- viewer_id = html_options[:id] ||= "outfit-viewer-#{SecureRandom.hex(8)}"
|
||||||
= content_tag "outfit-viewer", **html_options do
|
= content_tag "outfit-viewer", **html_options do
|
||||||
.loading-indicator= render partial: "hanger_spinner"
|
.loading-indicator= render partial: "hanger_spinner"
|
||||||
|
|
||||||
- outfit.visible_layers.each do |swf_asset|
|
- outfit.visible_layers.each do |swf_asset|
|
||||||
%outfit-layer{
|
%outfit-layer{
|
||||||
|
id: "#{viewer_id}-layer-#{swf_asset.id}",
|
||||||
data: {
|
data: {
|
||||||
"asset-id": swf_asset.id,
|
"asset-id": swf_asset.id,
|
||||||
"zone": swf_asset.zone.label,
|
"zone": swf_asset.zone.label,
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,7 @@
|
||||||
%h1= t 'app_name'
|
%h1= t 'app_name'
|
||||||
%h2= t '.tagline'
|
%h2= t '.tagline'
|
||||||
|
|
||||||
= form_tag load_pet_path, method: 'POST', class: 'primary load-pet-to-wardrobe' do
|
= form_tag load_pet_path, method: 'GET', class: 'primary load-pet-to-wardrobe' do
|
||||||
= hidden_field_tag 'destination', 'wardrobe'
|
|
||||||
%fieldset
|
%fieldset
|
||||||
%legend= t '.load_pet'
|
%legend= t '.load_pet'
|
||||||
= pet_name_tag class: 'main-pet-name'
|
= pet_name_tag class: 'main-pet-name'
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,13 @@
|
||||||
- title t('.title')
|
- title t('.title')
|
||||||
|
|
||||||
|
%ul.timeframe-nav
|
||||||
|
- ['all_time', 'this_year', 'this_month', 'this_week'].each do |tf|
|
||||||
|
%li
|
||||||
|
- if @timeframe == tf
|
||||||
|
%strong= t(".timeframes.#{tf}")
|
||||||
|
- else
|
||||||
|
= link_to t(".timeframes.#{tf}"), top_contributors_path(timeframe: tf)
|
||||||
|
|
||||||
= will_paginate @users
|
= will_paginate @users
|
||||||
%table#top-contributors
|
%table#top-contributors
|
||||||
%thead
|
%thead
|
||||||
|
|
@ -11,5 +20,5 @@
|
||||||
%tr
|
%tr
|
||||||
%th{:scope => 'row'}= @users.offset + rank + 1
|
%th{:scope => 'row'}= @users.offset + rank + 1
|
||||||
%td= link_to user.name, user_contributions_path(user)
|
%td= link_to user.name, user_contributions_path(user)
|
||||||
%td= user.points
|
%td= user.period_points
|
||||||
= will_paginate @users
|
= will_paginate @users
|
||||||
|
|
|
||||||
|
|
@ -640,6 +640,11 @@ en-MEEP:
|
||||||
rank: Reep
|
rank: Reep
|
||||||
user: Meepit
|
user: Meepit
|
||||||
points: Peeps
|
points: Peeps
|
||||||
|
timeframes:
|
||||||
|
all_time: All Meep
|
||||||
|
this_year: Meeps Year
|
||||||
|
this_month: Meeps Month
|
||||||
|
this_week: Meeps Week
|
||||||
|
|
||||||
update:
|
update:
|
||||||
success: Settings successfully meeped.
|
success: Settings successfully meeped.
|
||||||
|
|
|
||||||
|
|
@ -783,6 +783,11 @@ en:
|
||||||
rank: Rank
|
rank: Rank
|
||||||
user: User
|
user: User
|
||||||
points: Points
|
points: Points
|
||||||
|
timeframes:
|
||||||
|
all_time: All Time
|
||||||
|
this_year: This Year
|
||||||
|
this_month: This Month
|
||||||
|
this_week: This Week
|
||||||
|
|
||||||
update:
|
update:
|
||||||
success: Settings successfully saved.
|
success: Settings successfully saved.
|
||||||
|
|
|
||||||
|
|
@ -505,6 +505,11 @@ es:
|
||||||
rank: Puesto
|
rank: Puesto
|
||||||
user: Usuario
|
user: Usuario
|
||||||
points: Puntos
|
points: Puntos
|
||||||
|
timeframes:
|
||||||
|
all_time: Todo el Tiempo
|
||||||
|
this_year: Este Año
|
||||||
|
this_month: Este Mes
|
||||||
|
this_week: Esta Semana
|
||||||
update:
|
update:
|
||||||
success: Ajustes guardados correctamente.
|
success: Ajustes guardados correctamente.
|
||||||
invalid: "No hemos podido guardar los ajustes: %{errors}"
|
invalid: "No hemos podido guardar los ajustes: %{errors}"
|
||||||
|
|
|
||||||
|
|
@ -499,6 +499,11 @@ pt:
|
||||||
rank: Rank
|
rank: Rank
|
||||||
user: Usuário
|
user: Usuário
|
||||||
points: Pontos
|
points: Pontos
|
||||||
|
timeframes:
|
||||||
|
all_time: Todo o Tempo
|
||||||
|
this_year: Este Ano
|
||||||
|
this_month: Este Mês
|
||||||
|
this_week: Esta Semana
|
||||||
update:
|
update:
|
||||||
success: Configurações salvas com sucesso
|
success: Configurações salvas com sucesso
|
||||||
invalid: "Não foi possível salvar as configurações: %{errors}"
|
invalid: "Não foi possível salvar as configurações: %{errors}"
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ OpenneoImpressItems::Application.routes.draw do
|
||||||
get '/alt-styles', to: redirect('/rainbow-pool/styles')
|
get '/alt-styles', to: redirect('/rainbow-pool/styles')
|
||||||
|
|
||||||
# Loading and modeling pets!
|
# Loading and modeling pets!
|
||||||
post '/pets/load' => 'pets#load', :as => :load_pet
|
match '/pets/load' => 'pets#load', :as => :load_pet, via: [:get, :post]
|
||||||
get '/modeling' => 'pets#bulk', :as => :bulk_pets
|
get '/modeling' => 'pets#bulk', :as => :bulk_pets
|
||||||
|
|
||||||
# Contributions to our modeling database!
|
# Contributions to our modeling database!
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
class AddIndexToContributionsUserIdAndCreatedAt < ActiveRecord::Migration[8.1]
|
||||||
|
def change
|
||||||
|
add_index :contributions, [:user_id, :created_at],
|
||||||
|
name: 'index_contributions_on_user_id_and_created_at'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -10,28 +10,28 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.0].define(version: 2024_04_08_120359) do
|
ActiveRecord::Schema[8.1].define(version: 2024_04_08_120359) do
|
||||||
create_table "users", id: { type: :integer, unsigned: true }, charset: "utf8mb3", collation: "utf8mb3_general_ci", force: :cascade do |t|
|
create_table "users", id: { type: :integer, unsigned: true }, charset: "utf8mb3", collation: "utf8mb3_general_ci", force: :cascade do |t|
|
||||||
t.string "name", limit: 30, null: false
|
t.datetime "created_at", precision: nil
|
||||||
t.string "encrypted_password", limit: 64
|
t.datetime "current_sign_in_at", precision: nil
|
||||||
|
t.string "current_sign_in_ip"
|
||||||
t.string "email", limit: 50
|
t.string "email", limit: 50
|
||||||
|
t.string "encrypted_password", limit: 64
|
||||||
|
t.integer "failed_attempts", default: 0
|
||||||
|
t.datetime "last_sign_in_at", precision: nil
|
||||||
|
t.string "last_sign_in_ip"
|
||||||
|
t.datetime "locked_at", precision: nil
|
||||||
|
t.string "name", limit: 30, null: false
|
||||||
|
t.string "neopass_email"
|
||||||
t.string "password_salt", limit: 32
|
t.string "password_salt", limit: 32
|
||||||
|
t.string "provider"
|
||||||
|
t.datetime "remember_created_at"
|
||||||
|
t.datetime "reset_password_sent_at", precision: nil
|
||||||
t.string "reset_password_token"
|
t.string "reset_password_token"
|
||||||
t.integer "sign_in_count", default: 0
|
t.integer "sign_in_count", default: 0
|
||||||
t.datetime "current_sign_in_at", precision: nil
|
|
||||||
t.datetime "last_sign_in_at", precision: nil
|
|
||||||
t.string "current_sign_in_ip"
|
|
||||||
t.string "last_sign_in_ip"
|
|
||||||
t.integer "failed_attempts", default: 0
|
|
||||||
t.string "unlock_token"
|
|
||||||
t.datetime "locked_at", precision: nil
|
|
||||||
t.datetime "created_at", precision: nil
|
|
||||||
t.datetime "updated_at", precision: nil
|
|
||||||
t.datetime "reset_password_sent_at", precision: nil
|
|
||||||
t.datetime "remember_created_at"
|
|
||||||
t.string "provider"
|
|
||||||
t.string "uid"
|
t.string "uid"
|
||||||
t.string "neopass_email"
|
t.string "unlock_token"
|
||||||
|
t.datetime "updated_at", precision: nil
|
||||||
t.index ["email"], name: "index_users_on_email", unique: true
|
t.index ["email"], name: "index_users_on_email", unique: true
|
||||||
t.index ["provider", "uid"], name: "index_users_on_provider_and_uid", unique: true
|
t.index ["provider", "uid"], name: "index_users_on_provider_and_uid", unique: true
|
||||||
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
||||||
|
|
|
||||||
205
db/schema.rb
205
db/schema.rb
|
|
@ -10,50 +10,50 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.0].define(version: 2025_02_16_041650) do
|
ActiveRecord::Schema[8.1].define(version: 2026_01_21_031001) do
|
||||||
create_table "alt_styles", charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
create_table "alt_styles", charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||||
t.integer "species_id", null: false
|
|
||||||
t.integer "color_id", null: false
|
|
||||||
t.integer "body_id", null: false
|
t.integer "body_id", null: false
|
||||||
|
t.integer "color_id", null: false
|
||||||
t.datetime "created_at", precision: nil, null: false
|
t.datetime "created_at", precision: nil, null: false
|
||||||
t.datetime "updated_at", precision: nil, null: false
|
|
||||||
t.string "series_name"
|
|
||||||
t.string "thumbnail_url", null: false
|
|
||||||
t.string "full_name"
|
t.string "full_name"
|
||||||
|
t.string "series_name"
|
||||||
|
t.integer "species_id", null: false
|
||||||
|
t.string "thumbnail_url", null: false
|
||||||
|
t.datetime "updated_at", precision: nil, null: false
|
||||||
t.index ["color_id"], name: "index_alt_styles_on_color_id"
|
t.index ["color_id"], name: "index_alt_styles_on_color_id"
|
||||||
t.index ["species_id"], name: "index_alt_styles_on_species_id"
|
t.index ["species_id"], name: "index_alt_styles_on_species_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "auth_servers", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
create_table "auth_servers", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||||
t.string "short_name", limit: 10, null: false
|
|
||||||
t.string "name", limit: 40, null: false
|
|
||||||
t.text "icon", size: :long, null: false
|
|
||||||
t.text "gateway", size: :long, null: false
|
t.text "gateway", size: :long, null: false
|
||||||
|
t.text "icon", size: :long, null: false
|
||||||
|
t.string "name", limit: 40, null: false
|
||||||
t.string "secret", limit: 64, null: false
|
t.string "secret", limit: 64, null: false
|
||||||
|
t.string "short_name", limit: 10, null: false
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "campaigns", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
create_table "campaigns", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||||
t.integer "progress", default: 0, null: false
|
|
||||||
t.integer "goal", null: false
|
|
||||||
t.boolean "active", null: false
|
t.boolean "active", null: false
|
||||||
t.datetime "created_at", precision: nil, null: false
|
|
||||||
t.datetime "updated_at", precision: nil, null: false
|
|
||||||
t.boolean "advertised", default: true, null: false
|
t.boolean "advertised", default: true, null: false
|
||||||
|
t.datetime "created_at", precision: nil, null: false
|
||||||
t.text "description", size: :long, null: false
|
t.text "description", size: :long, null: false
|
||||||
t.string "purpose", default: "our hosting costs this year", null: false
|
t.integer "goal", null: false
|
||||||
t.string "theme_id", default: "hug", null: false
|
|
||||||
t.text "thanks", size: :long
|
|
||||||
t.string "name"
|
t.string "name"
|
||||||
|
t.integer "progress", default: 0, null: false
|
||||||
|
t.string "purpose", default: "our hosting costs this year", null: false
|
||||||
|
t.text "thanks", size: :long
|
||||||
|
t.string "theme_id", default: "hug", null: false
|
||||||
|
t.datetime "updated_at", precision: nil, null: false
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "closet_hangers", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
create_table "closet_hangers", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||||
t.integer "item_id"
|
|
||||||
t.integer "user_id"
|
|
||||||
t.integer "quantity"
|
|
||||||
t.datetime "created_at", precision: nil
|
t.datetime "created_at", precision: nil
|
||||||
t.datetime "updated_at", precision: nil
|
t.integer "item_id"
|
||||||
t.boolean "owned", default: true, null: false
|
|
||||||
t.integer "list_id"
|
t.integer "list_id"
|
||||||
|
t.boolean "owned", default: true, null: false
|
||||||
|
t.integer "quantity"
|
||||||
|
t.datetime "updated_at", precision: nil
|
||||||
|
t.integer "user_id"
|
||||||
t.index ["item_id", "owned"], name: "index_closet_hangers_on_item_id_and_owned"
|
t.index ["item_id", "owned"], name: "index_closet_hangers_on_item_id_and_owned"
|
||||||
t.index ["list_id"], name: "index_closet_hangers_on_list_id"
|
t.index ["list_id"], name: "index_closet_hangers_on_list_id"
|
||||||
t.index ["user_id", "list_id", "item_id", "owned", "created_at"], name: "index_closet_hangers_test_20131226"
|
t.index ["user_id", "list_id", "item_id", "owned", "created_at"], name: "index_closet_hangers_test_20131226"
|
||||||
|
|
@ -63,84 +63,85 @@ ActiveRecord::Schema[8.0].define(version: 2025_02_16_041650) do
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "closet_lists", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
create_table "closet_lists", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||||
t.string "name"
|
|
||||||
t.text "description", size: :long
|
|
||||||
t.integer "user_id"
|
|
||||||
t.boolean "hangers_owned", null: false
|
|
||||||
t.datetime "created_at", precision: nil
|
t.datetime "created_at", precision: nil
|
||||||
|
t.text "description", size: :long
|
||||||
|
t.boolean "hangers_owned", null: false
|
||||||
|
t.string "name"
|
||||||
t.datetime "updated_at", precision: nil
|
t.datetime "updated_at", precision: nil
|
||||||
|
t.integer "user_id"
|
||||||
t.integer "visibility", default: 1, null: false
|
t.integer "visibility", default: 1, null: false
|
||||||
t.index ["user_id"], name: "index_closet_lists_on_user_id"
|
t.index ["user_id"], name: "index_closet_lists_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "colors", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
create_table "colors", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||||
t.boolean "basic"
|
t.boolean "basic"
|
||||||
t.boolean "standard"
|
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
t.string "pb_item_name"
|
t.string "pb_item_name"
|
||||||
t.string "pb_item_thumbnail_url"
|
t.string "pb_item_thumbnail_url"
|
||||||
|
t.boolean "standard"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "contributions", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
create_table "contributions", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||||
t.string "contributed_type", limit: 8, null: false
|
|
||||||
t.integer "contributed_id", null: false
|
t.integer "contributed_id", null: false
|
||||||
t.integer "user_id", null: false
|
t.string "contributed_type", limit: 8, null: false
|
||||||
t.datetime "created_at", precision: nil, null: false
|
t.datetime "created_at", precision: nil, null: false
|
||||||
|
t.integer "user_id", null: false
|
||||||
t.index ["contributed_id", "contributed_type"], name: "index_contributions_on_contributed_id_and_contributed_type"
|
t.index ["contributed_id", "contributed_type"], name: "index_contributions_on_contributed_id_and_contributed_type"
|
||||||
|
t.index ["user_id", "created_at"], name: "index_contributions_on_user_id_and_created_at"
|
||||||
t.index ["user_id"], name: "index_contributions_on_user_id"
|
t.index ["user_id"], name: "index_contributions_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "donation_features", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
create_table "donation_features", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||||
|
t.datetime "created_at", precision: nil, null: false
|
||||||
t.integer "donation_id", null: false
|
t.integer "donation_id", null: false
|
||||||
t.integer "outfit_id"
|
t.integer "outfit_id"
|
||||||
t.datetime "created_at", precision: nil, null: false
|
|
||||||
t.datetime "updated_at", precision: nil, null: false
|
t.datetime "updated_at", precision: nil, null: false
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "donations", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
create_table "donations", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||||
t.integer "amount", null: false
|
t.integer "amount", null: false
|
||||||
|
t.integer "campaign_id", null: false
|
||||||
t.string "charge_id", null: false
|
t.string "charge_id", null: false
|
||||||
t.integer "user_id"
|
t.datetime "created_at", precision: nil, null: false
|
||||||
|
t.string "donor_email"
|
||||||
t.string "donor_name"
|
t.string "donor_name"
|
||||||
t.string "secret"
|
t.string "secret"
|
||||||
t.datetime "created_at", precision: nil, null: false
|
|
||||||
t.datetime "updated_at", precision: nil, null: false
|
t.datetime "updated_at", precision: nil, null: false
|
||||||
t.string "donor_email"
|
t.integer "user_id"
|
||||||
t.integer "campaign_id", null: false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "item_outfit_relationships", charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
create_table "item_outfit_relationships", charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||||
|
t.datetime "created_at", precision: nil
|
||||||
|
t.boolean "is_worn"
|
||||||
t.integer "item_id"
|
t.integer "item_id"
|
||||||
t.integer "outfit_id"
|
t.integer "outfit_id"
|
||||||
t.boolean "is_worn"
|
|
||||||
t.datetime "created_at", precision: nil
|
|
||||||
t.datetime "updated_at", precision: nil
|
t.datetime "updated_at", precision: nil
|
||||||
t.index ["item_id"], name: "index_item_outfit_relationships_on_item_id"
|
t.index ["item_id"], name: "index_item_outfit_relationships_on_item_id"
|
||||||
t.index ["outfit_id", "is_worn"], name: "index_item_outfit_relationships_on_outfit_id_and_is_worn"
|
t.index ["outfit_id", "is_worn"], name: "index_item_outfit_relationships_on_outfit_id_and_is_worn"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "items", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
create_table "items", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||||
t.text "zones_restrict", size: :medium, null: false
|
t.text "cached_compatible_body_ids", default: ""
|
||||||
t.text "thumbnail_url", size: :long, null: false
|
t.string "cached_occupied_zone_ids", default: ""
|
||||||
|
t.boolean "cached_predicted_fully_modeled", default: false, null: false
|
||||||
t.string "category", limit: 50
|
t.string "category", limit: 50
|
||||||
t.string "type", limit: 50
|
|
||||||
t.integer "rarity_index", limit: 2
|
|
||||||
t.integer "price", limit: 3, null: false
|
|
||||||
t.integer "weight_lbs", limit: 2
|
|
||||||
t.text "species_support_ids", size: :long
|
|
||||||
t.datetime "created_at", precision: nil
|
t.datetime "created_at", precision: nil
|
||||||
t.datetime "updated_at", precision: nil
|
t.text "description", size: :medium, null: false
|
||||||
|
t.integer "dyeworks_base_item_id"
|
||||||
t.boolean "explicitly_body_specific", default: false, null: false
|
t.boolean "explicitly_body_specific", default: false, null: false
|
||||||
|
t.boolean "is_manually_nc", default: false, null: false
|
||||||
t.integer "manual_special_color_id"
|
t.integer "manual_special_color_id"
|
||||||
t.column "modeling_status_hint", "enum('done','glitchy')"
|
t.column "modeling_status_hint", "enum('done','glitchy')"
|
||||||
t.boolean "is_manually_nc", default: false, null: false
|
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
t.text "description", size: :medium, null: false
|
t.integer "price", limit: 3, null: false
|
||||||
t.string "rarity", default: "", null: false
|
t.string "rarity", default: "", null: false
|
||||||
t.integer "dyeworks_base_item_id"
|
t.integer "rarity_index", limit: 2
|
||||||
t.string "cached_occupied_zone_ids", default: ""
|
t.text "species_support_ids", size: :long
|
||||||
t.text "cached_compatible_body_ids", default: ""
|
t.text "thumbnail_url", size: :long, null: false
|
||||||
t.boolean "cached_predicted_fully_modeled", default: false, null: false
|
t.string "type", limit: 50
|
||||||
|
t.datetime "updated_at", precision: nil
|
||||||
|
t.integer "weight_lbs", limit: 2
|
||||||
|
t.text "zones_restrict", size: :medium, null: false
|
||||||
t.index ["dyeworks_base_item_id"], name: "index_items_on_dyeworks_base_item_id"
|
t.index ["dyeworks_base_item_id"], name: "index_items_on_dyeworks_base_item_id"
|
||||||
t.index ["modeling_status_hint", "created_at", "id"], name: "items_modeling_status_hint_and_created_at_and_id"
|
t.index ["modeling_status_hint", "created_at", "id"], name: "items_modeling_status_hint_and_created_at_and_id"
|
||||||
t.index ["modeling_status_hint", "created_at"], name: "items_modeling_status_hint_and_created_at"
|
t.index ["modeling_status_hint", "created_at"], name: "items_modeling_status_hint_and_created_at"
|
||||||
|
|
@ -150,9 +151,9 @@ ActiveRecord::Schema[8.0].define(version: 2025_02_16_041650) do
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "login_cookies", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
create_table "login_cookies", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||||
t.integer "user_id", null: false
|
|
||||||
t.integer "series", null: false
|
t.integer "series", null: false
|
||||||
t.integer "token", null: false
|
t.integer "token", null: false
|
||||||
|
t.integer "user_id", null: false
|
||||||
t.index ["user_id", "series"], name: "login_cookies_user_id_and_series"
|
t.index ["user_id", "series"], name: "login_cookies_user_id_and_series"
|
||||||
t.index ["user_id"], name: "login_cookies_user_id"
|
t.index ["user_id"], name: "login_cookies_user_id"
|
||||||
end
|
end
|
||||||
|
|
@ -164,34 +165,34 @@ ActiveRecord::Schema[8.0].define(version: 2025_02_16_041650) do
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "nc_mall_records", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t|
|
create_table "nc_mall_records", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t|
|
||||||
t.integer "item_id", null: false
|
t.datetime "created_at", null: false
|
||||||
t.integer "price", null: false
|
|
||||||
t.integer "discount_price"
|
|
||||||
t.datetime "discount_begins_at"
|
t.datetime "discount_begins_at"
|
||||||
t.datetime "discount_ends_at"
|
t.datetime "discount_ends_at"
|
||||||
t.datetime "created_at", null: false
|
t.integer "discount_price"
|
||||||
|
t.integer "item_id", null: false
|
||||||
|
t.integer "price", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.index ["item_id"], name: "index_nc_mall_records_on_item_id", unique: true
|
t.index ["item_id"], name: "index_nc_mall_records_on_item_id", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "neopets_connections", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
create_table "neopets_connections", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||||
t.integer "user_id"
|
|
||||||
t.string "neopets_username"
|
|
||||||
t.datetime "created_at", precision: nil, null: false
|
t.datetime "created_at", precision: nil, null: false
|
||||||
|
t.string "neopets_username"
|
||||||
t.datetime "updated_at", precision: nil, null: false
|
t.datetime "updated_at", precision: nil, null: false
|
||||||
|
t.integer "user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "outfits", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
create_table "outfits", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||||
t.integer "pet_state_id"
|
|
||||||
t.integer "user_id"
|
|
||||||
t.datetime "created_at", precision: nil
|
|
||||||
t.datetime "updated_at", precision: nil
|
|
||||||
t.string "name"
|
|
||||||
t.boolean "starred", default: false, null: false
|
|
||||||
t.string "image"
|
|
||||||
t.string "image_layers_hash"
|
|
||||||
t.boolean "image_enqueued", default: false, null: false
|
|
||||||
t.bigint "alt_style_id"
|
t.bigint "alt_style_id"
|
||||||
|
t.datetime "created_at", precision: nil
|
||||||
|
t.string "image"
|
||||||
|
t.boolean "image_enqueued", default: false, null: false
|
||||||
|
t.string "image_layers_hash"
|
||||||
|
t.string "name"
|
||||||
|
t.integer "pet_state_id"
|
||||||
|
t.boolean "starred", default: false, null: false
|
||||||
|
t.datetime "updated_at", precision: nil
|
||||||
|
t.integer "user_id"
|
||||||
t.index ["alt_style_id"], name: "index_outfits_on_alt_style_id"
|
t.index ["alt_style_id"], name: "index_outfits_on_alt_style_id"
|
||||||
t.index ["pet_state_id"], name: "index_outfits_on_pet_state_id"
|
t.index ["pet_state_id"], name: "index_outfits_on_pet_state_id"
|
||||||
t.index ["user_id"], name: "index_outfits_on_user_id"
|
t.index ["user_id"], name: "index_outfits_on_user_id"
|
||||||
|
|
@ -199,40 +200,40 @@ ActiveRecord::Schema[8.0].define(version: 2025_02_16_041650) do
|
||||||
|
|
||||||
create_table "parents_swf_assets", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
create_table "parents_swf_assets", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||||
t.integer "parent_id", null: false
|
t.integer "parent_id", null: false
|
||||||
t.integer "swf_asset_id", null: false
|
|
||||||
t.string "parent_type", limit: 8, null: false
|
t.string "parent_type", limit: 8, null: false
|
||||||
|
t.integer "swf_asset_id", null: false
|
||||||
t.index ["parent_id", "parent_type"], name: "index_parents_swf_assets_on_parent_id_and_parent_type"
|
t.index ["parent_id", "parent_type"], name: "index_parents_swf_assets_on_parent_id_and_parent_type"
|
||||||
t.index ["parent_id", "swf_asset_id"], name: "unique_parents_swf_assets", unique: true
|
t.index ["parent_id", "swf_asset_id"], name: "unique_parents_swf_assets", unique: true
|
||||||
t.index ["swf_asset_id"], name: "parents_swf_assets_swf_asset_id"
|
t.index ["swf_asset_id"], name: "parents_swf_assets_swf_asset_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "pet_loads", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
create_table "pet_loads", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||||
t.string "pet_name", limit: 20, null: false
|
|
||||||
t.text "amf", size: :long, null: false
|
t.text "amf", size: :long, null: false
|
||||||
t.datetime "created_at", precision: nil, null: false
|
t.datetime "created_at", precision: nil, null: false
|
||||||
|
t.string "pet_name", limit: 20, null: false
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "pet_states", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
create_table "pet_states", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||||
t.integer "pet_type_id", null: false
|
|
||||||
t.text "swf_asset_ids", size: :medium, null: false
|
|
||||||
t.boolean "female"
|
|
||||||
t.integer "mood_id"
|
|
||||||
t.boolean "unconverted"
|
|
||||||
t.boolean "labeled", default: false, null: false
|
|
||||||
t.boolean "glitched", default: false, null: false
|
|
||||||
t.string "artist_neopets_username"
|
t.string "artist_neopets_username"
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
|
t.boolean "female"
|
||||||
|
t.boolean "glitched", default: false, null: false
|
||||||
|
t.boolean "labeled", default: false, null: false
|
||||||
|
t.integer "mood_id"
|
||||||
|
t.integer "pet_type_id", null: false
|
||||||
|
t.text "swf_asset_ids", size: :medium, null: false
|
||||||
|
t.boolean "unconverted"
|
||||||
t.datetime "updated_at"
|
t.datetime "updated_at"
|
||||||
t.index ["pet_type_id"], name: "pet_states_pet_type_id"
|
t.index ["pet_type_id"], name: "pet_states_pet_type_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "pet_types", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
create_table "pet_types", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||||
t.integer "color_id", null: false
|
|
||||||
t.integer "species_id", null: false
|
|
||||||
t.datetime "created_at", precision: nil, null: false
|
|
||||||
t.integer "body_id", limit: 2, null: false
|
|
||||||
t.string "image_hash", limit: 8
|
|
||||||
t.string "basic_image_hash"
|
t.string "basic_image_hash"
|
||||||
|
t.integer "body_id", limit: 2, null: false
|
||||||
|
t.integer "color_id", null: false
|
||||||
|
t.datetime "created_at", precision: nil, null: false
|
||||||
|
t.string "image_hash", limit: 8
|
||||||
|
t.integer "species_id", null: false
|
||||||
t.index ["body_id", "color_id", "species_id"], name: "pet_types_body_id_and_color_id_and_species_id"
|
t.index ["body_id", "color_id", "species_id"], name: "pet_types_body_id_and_color_id_and_species_id"
|
||||||
t.index ["body_id"], name: "pet_types_body_id"
|
t.index ["body_id"], name: "pet_types_body_id"
|
||||||
t.index ["color_id", "species_id"], name: "pet_types_color_id_and_species_id"
|
t.index ["color_id", "species_id"], name: "pet_types_color_id_and_species_id"
|
||||||
|
|
@ -252,50 +253,50 @@ ActiveRecord::Schema[8.0].define(version: 2025_02_16_041650) do
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "swf_assets", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
create_table "swf_assets", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||||
t.string "type", limit: 7, null: false
|
t.integer "body_id", limit: 2, null: false
|
||||||
|
t.datetime "converted_at", precision: nil
|
||||||
|
t.datetime "created_at", precision: nil, null: false
|
||||||
|
t.boolean "has_image", default: false, null: false
|
||||||
|
t.boolean "image_manual", default: false, null: false
|
||||||
|
t.boolean "image_requested", default: false, null: false
|
||||||
|
t.string "known_glitches", limit: 128, default: ""
|
||||||
|
t.text "manifest", size: :long
|
||||||
|
t.timestamp "manifest_cached_at"
|
||||||
|
t.datetime "manifest_loaded_at"
|
||||||
|
t.integer "manifest_status_code"
|
||||||
|
t.string "manifest_url"
|
||||||
t.integer "remote_id", limit: 3, null: false
|
t.integer "remote_id", limit: 3, null: false
|
||||||
|
t.datetime "reported_broken_at", precision: nil
|
||||||
|
t.string "type", limit: 7, null: false
|
||||||
t.text "url", size: :long, null: false
|
t.text "url", size: :long, null: false
|
||||||
t.integer "zone_id", null: false
|
t.integer "zone_id", null: false
|
||||||
t.text "zones_restrict", size: :medium, null: false
|
t.text "zones_restrict", size: :medium, null: false
|
||||||
t.datetime "created_at", precision: nil, null: false
|
|
||||||
t.integer "body_id", limit: 2, null: false
|
|
||||||
t.boolean "has_image", default: false, null: false
|
|
||||||
t.boolean "image_requested", default: false, null: false
|
|
||||||
t.datetime "reported_broken_at", precision: nil
|
|
||||||
t.datetime "converted_at", precision: nil
|
|
||||||
t.boolean "image_manual", default: false, null: false
|
|
||||||
t.text "manifest", size: :long
|
|
||||||
t.timestamp "manifest_cached_at"
|
|
||||||
t.string "known_glitches", limit: 128, default: ""
|
|
||||||
t.string "manifest_url"
|
|
||||||
t.datetime "manifest_loaded_at"
|
|
||||||
t.integer "manifest_status_code"
|
|
||||||
t.index ["body_id"], name: "swf_assets_body_id_and_object_id"
|
t.index ["body_id"], name: "swf_assets_body_id_and_object_id"
|
||||||
t.index ["type", "remote_id"], name: "swf_assets_type_and_id"
|
t.index ["type", "remote_id"], name: "swf_assets_type_and_id"
|
||||||
t.index ["zone_id"], name: "idx_swf_assets_zone_id"
|
t.index ["zone_id"], name: "idx_swf_assets_zone_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "users", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
create_table "users", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||||
t.string "name", limit: 30, null: false
|
|
||||||
t.integer "auth_server_id", limit: 1, null: false
|
t.integer "auth_server_id", limit: 1, null: false
|
||||||
t.integer "remote_id", null: false
|
|
||||||
t.integer "points", default: 0, null: false
|
|
||||||
t.boolean "beta", default: false, null: false
|
t.boolean "beta", default: false, null: false
|
||||||
t.string "remember_token"
|
|
||||||
t.datetime "remember_created_at", precision: nil
|
|
||||||
t.integer "owned_closet_hangers_visibility", default: 1, null: false
|
|
||||||
t.integer "wanted_closet_hangers_visibility", default: 1, null: false
|
|
||||||
t.integer "contact_neopets_connection_id"
|
t.integer "contact_neopets_connection_id"
|
||||||
t.timestamp "last_trade_activity_at"
|
t.timestamp "last_trade_activity_at"
|
||||||
t.boolean "support_staff", default: false, null: false
|
t.string "name", limit: 30, null: false
|
||||||
|
t.integer "owned_closet_hangers_visibility", default: 1, null: false
|
||||||
|
t.integer "points", default: 0, null: false
|
||||||
|
t.datetime "remember_created_at", precision: nil
|
||||||
|
t.string "remember_token"
|
||||||
|
t.integer "remote_id", null: false
|
||||||
t.boolean "shadowbanned", default: false, null: false
|
t.boolean "shadowbanned", default: false, null: false
|
||||||
|
t.boolean "support_staff", default: false, null: false
|
||||||
|
t.integer "wanted_closet_hangers_visibility", default: 1, null: false
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "zones", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
create_table "zones", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||||
t.integer "depth"
|
t.integer "depth"
|
||||||
t.integer "type_id"
|
|
||||||
t.string "label", null: false
|
t.string "label", null: false
|
||||||
t.string "plain_label", null: false
|
t.string "plain_label", null: false
|
||||||
|
t.integer "type_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
add_foreign_key "alt_styles", "colors"
|
add_foreign_key "alt_styles", "colors"
|
||||||
|
|
|
||||||
72
db/seeds/top_contributors_sample_data.rb
Normal file
72
db/seeds/top_contributors_sample_data.rb
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
# Sample contributions for testing Top Contributors feature
|
||||||
|
# Run with: rails runner db/seeds/top_contributors_sample_data.rb
|
||||||
|
|
||||||
|
puts "Creating sample contributions for Top Contributors testing..."
|
||||||
|
|
||||||
|
# Find or create test users
|
||||||
|
users = []
|
||||||
|
5.times do |i|
|
||||||
|
name = "TestContributor#{i + 1}"
|
||||||
|
user = User.find_or_create_by!(name: name) do |u|
|
||||||
|
# Create a corresponding auth_user record
|
||||||
|
auth_user = AuthUser.create!(
|
||||||
|
name: name,
|
||||||
|
email: "test#{i + 1}@example.com",
|
||||||
|
password: 'password123',
|
||||||
|
)
|
||||||
|
u.remote_id = auth_user.id
|
||||||
|
u.auth_server_id = 1
|
||||||
|
end
|
||||||
|
users << user
|
||||||
|
end
|
||||||
|
|
||||||
|
# Get some existing items/pet types to contribute
|
||||||
|
items = Item.limit(10).to_a
|
||||||
|
pet_types = PetType.limit(5).to_a
|
||||||
|
swf_assets = SwfAsset.limit(5).to_a
|
||||||
|
|
||||||
|
if items.empty? || pet_types.empty?
|
||||||
|
puts "WARNING: No items or pet types found. Create some first or contributions will be limited."
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create contributions with different time periods
|
||||||
|
# User 1: Heavy contributor this week
|
||||||
|
if items.any?
|
||||||
|
3.times { Contribution.create!(user: users[0], contributed: items.sample, created_at: 2.days.ago) }
|
||||||
|
5.times { Contribution.create!(user: users[0], contributed: items.sample, created_at: 5.days.ago) }
|
||||||
|
end
|
||||||
|
|
||||||
|
# User 2: Heavy contributor this month, but not this week
|
||||||
|
if items.any? && pet_types.any?
|
||||||
|
2.times { Contribution.create!(user: users[1], contributed: items.sample, created_at: 15.days.ago) }
|
||||||
|
1.times { Contribution.create!(user: users[1], contributed: pet_types.sample, created_at: 20.days.ago) }
|
||||||
|
end
|
||||||
|
|
||||||
|
# User 3: Heavy contributor this year, but not this month
|
||||||
|
if pet_types.any?
|
||||||
|
3.times { Contribution.create!(user: users[2], contributed: pet_types.sample, created_at: 3.months.ago) }
|
||||||
|
end
|
||||||
|
|
||||||
|
# User 4: Old contributor (only in all-time)
|
||||||
|
if items.any?
|
||||||
|
users[3].update!(points: 500) # Set points directly for all-time view
|
||||||
|
2.times { Contribution.create!(user: users[3], contributed: items.sample, created_at: 2.years.ago) }
|
||||||
|
end
|
||||||
|
|
||||||
|
# User 5: Mixed contributions across all periods
|
||||||
|
if items.any? && pet_types.any?
|
||||||
|
Contribution.create!(user: users[4], contributed: items.sample, created_at: 1.day.ago)
|
||||||
|
Contribution.create!(user: users[4], contributed: pet_types.sample, created_at: 10.days.ago)
|
||||||
|
Contribution.create!(user: users[4], contributed: items.sample, created_at: 2.months.ago)
|
||||||
|
end
|
||||||
|
if swf_assets.any?
|
||||||
|
Contribution.create!(user: users[4], contributed: swf_assets.sample, created_at: 4.days.ago)
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "Created sample contributions:"
|
||||||
|
puts "- #{users[0].name}: #{users[0].contributions.count} contributions (focus: this week)"
|
||||||
|
puts "- #{users[1].name}: #{users[1].contributions.count} contributions (focus: this month)"
|
||||||
|
puts "- #{users[2].name}: #{users[2].contributions.count} contributions (focus: this year)"
|
||||||
|
puts "- #{users[3].name}: #{users[3].contributions.count} contributions (focus: all-time, #{users[3].points} points)"
|
||||||
|
puts "- #{users[4].name}: #{users[4].contributions.count} contributions (mixed periods)"
|
||||||
|
puts "\nTest the feature at: http://localhost:3000/users/top-contributors"
|
||||||
|
|
@ -442,13 +442,12 @@
|
||||||
mode: "755"
|
mode: "755"
|
||||||
state: directory
|
state: directory
|
||||||
|
|
||||||
- name: Remove 10min cron job to run `rails nc_mall:sync`
|
- name: Create 2min cron job to run `rails items:auto_model`
|
||||||
become_user: impress
|
become_user: impress
|
||||||
cron:
|
cron:
|
||||||
state: absent
|
name: "Impress: auto-model items"
|
||||||
name: "Impress: sync NC Mall data"
|
minute: "*/2"
|
||||||
minute: "*/10"
|
job: "bash -c 'source /etc/profile && source ~/.bash_profile && cd /srv/impress/current && bin/rails items:auto_model'"
|
||||||
job: "bash -c 'source /etc/profile && source ~/.bash_profile && cd /srv/impress/current && bin/rails nc_mall:sync'"
|
|
||||||
|
|
||||||
- name: Create 10min cron job to run `rails neopets:import`
|
- name: Create 10min cron job to run `rails neopets:import`
|
||||||
become_user: impress
|
become_user: impress
|
||||||
|
|
|
||||||
119
spec/controllers/users_controller_spec.rb
Normal file
119
spec/controllers/users_controller_spec.rb
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
require_relative '../rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe UsersController, type: :controller do
|
||||||
|
include Devise::Test::ControllerHelpers
|
||||||
|
|
||||||
|
describe 'GET #top_contributors' do
|
||||||
|
let!(:user1) { create_user('Alice', 100) }
|
||||||
|
let!(:user2) { create_user('Bob', 50) }
|
||||||
|
let!(:user3) { create_user('Charlie', 0) }
|
||||||
|
|
||||||
|
context 'without timeframe parameter' do
|
||||||
|
it 'defaults to all_time timeframe' do
|
||||||
|
get :top_contributors
|
||||||
|
expect(assigns(:timeframe)).to eq('all_time')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns users ordered by points' do
|
||||||
|
get :top_contributors
|
||||||
|
users = assigns(:users)
|
||||||
|
expect(users.to_a.map(&:id)).to eq([user1.id, user2.id])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'paginates results' do
|
||||||
|
get :top_contributors, params: { page: 1 }
|
||||||
|
users = assigns(:users)
|
||||||
|
expect(users).to respond_to(:total_pages)
|
||||||
|
expect(users).to respond_to(:current_page)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with valid timeframe parameter' do
|
||||||
|
it 'accepts all_time' do
|
||||||
|
get :top_contributors, params: { timeframe: 'all_time' }
|
||||||
|
expect(assigns(:timeframe)).to eq('all_time')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'accepts this_year' do
|
||||||
|
get :top_contributors, params: { timeframe: 'this_year' }
|
||||||
|
expect(assigns(:timeframe)).to eq('this_year')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'accepts this_month' do
|
||||||
|
get :top_contributors, params: { timeframe: 'this_month' }
|
||||||
|
expect(assigns(:timeframe)).to eq('this_month')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'accepts this_week' do
|
||||||
|
get :top_contributors, params: { timeframe: 'this_week' }
|
||||||
|
expect(assigns(:timeframe)).to eq('this_week')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'calls User.top_contributors_for with the timeframe' do
|
||||||
|
expect(User).to receive(:top_contributors_for).with(:this_week).and_call_original
|
||||||
|
get :top_contributors, params: { timeframe: 'this_week' }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with invalid timeframe parameter' do
|
||||||
|
it 'defaults to all_time' do
|
||||||
|
get :top_contributors, params: { timeframe: 'invalid' }
|
||||||
|
expect(assigns(:timeframe)).to eq('all_time')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not raise an error' do
|
||||||
|
expect {
|
||||||
|
get :top_contributors, params: { timeframe: 'invalid' }
|
||||||
|
}.not_to raise_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with pagination' do
|
||||||
|
before do
|
||||||
|
# Create 25 users to test pagination (per_page is 20)
|
||||||
|
25.times do |i|
|
||||||
|
create_user("User#{i}", 100 - i)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'paginates with 20 users per page' do
|
||||||
|
get :top_contributors
|
||||||
|
expect(assigns(:users).size).to eq(20)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'supports page parameter' do
|
||||||
|
get :top_contributors, params: { page: 2 }
|
||||||
|
expect(assigns(:users).current_page).to eq(2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'works with timeframe and pagination together' do
|
||||||
|
get :top_contributors, params: { timeframe: 'all_time', page: 2 }
|
||||||
|
expect(assigns(:timeframe)).to eq('all_time')
|
||||||
|
expect(assigns(:users).current_page).to eq(2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'renders the correct template' do
|
||||||
|
it 'renders the top_contributors template' do
|
||||||
|
get :top_contributors
|
||||||
|
expect(response).to render_template('top_contributors')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns HTTP success' do
|
||||||
|
get :top_contributors
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper methods
|
||||||
|
def create_user(name, points = 0)
|
||||||
|
auth_user = AuthUser.create!(
|
||||||
|
name: name,
|
||||||
|
email: "#{name.downcase}@example.com",
|
||||||
|
password: 'password123',
|
||||||
|
password_confirmation: 'password123'
|
||||||
|
)
|
||||||
|
User.create!(name: name, remote_id: auth_user.id, auth_server_id: 1, points: points)
|
||||||
|
end
|
||||||
|
end
|
||||||
293
spec/models/user_spec.rb
Normal file
293
spec/models/user_spec.rb
Normal file
|
|
@ -0,0 +1,293 @@
|
||||||
|
require_relative '../rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe User do
|
||||||
|
describe '.top_contributors_for' do
|
||||||
|
let!(:user1) { create_user('Alice') }
|
||||||
|
let!(:user2) { create_user('Bob') }
|
||||||
|
let!(:user3) { create_user('Charlie') }
|
||||||
|
|
||||||
|
context 'with all_time timeframe' do
|
||||||
|
it 'uses the denormalized points column' do
|
||||||
|
user1.update!(points: 100)
|
||||||
|
user2.update!(points: 50)
|
||||||
|
user3.update!(points: 0)
|
||||||
|
|
||||||
|
results = User.top_contributors_for(:all_time)
|
||||||
|
expect(results.map(&:id)).to eq([user1.id, user2.id])
|
||||||
|
expect(results.first.points).to eq(100)
|
||||||
|
expect(results.second.points).to eq(50)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'excludes users with zero points' do
|
||||||
|
user1.update!(points: 100)
|
||||||
|
user2.update!(points: 0)
|
||||||
|
|
||||||
|
results = User.top_contributors_for(:all_time)
|
||||||
|
expect(results).not_to include(user2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'orders by points descending' do
|
||||||
|
user1.update!(points: 50)
|
||||||
|
user2.update!(points: 100)
|
||||||
|
user3.update!(points: 75)
|
||||||
|
|
||||||
|
results = User.top_contributors_for(:all_time)
|
||||||
|
expect(results.map(&:id)).to eq([user2.id, user3.id, user1.id])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with this_week timeframe' do
|
||||||
|
let(:item) { create_item }
|
||||||
|
|
||||||
|
before do
|
||||||
|
# Create contributions from this week
|
||||||
|
create_contribution(user1, item, 3.days.ago) # 3 points
|
||||||
|
create_contribution(user1, item, 2.days.ago) # 3 points
|
||||||
|
|
||||||
|
# Create contributions from last month (should be excluded)
|
||||||
|
create_contribution(user2, item, 1.month.ago) # 3 points (excluded)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'calculates points from contributions in the last week' do
|
||||||
|
results = User.top_contributors_for(:this_week)
|
||||||
|
expect(results.first).to eq(user1)
|
||||||
|
expect(results.first.period_points).to eq(6)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'excludes users with no recent contributions' do
|
||||||
|
results = User.top_contributors_for(:this_week)
|
||||||
|
expect(results).not_to include(user2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'excludes contributions older than one week' do
|
||||||
|
create_contribution(user3, item, 8.days.ago)
|
||||||
|
|
||||||
|
results = User.top_contributors_for(:this_week)
|
||||||
|
expect(results).not_to include(user3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with this_month timeframe' do
|
||||||
|
let(:item) { create_item }
|
||||||
|
let(:pet_type) { create_pet_type }
|
||||||
|
|
||||||
|
before do
|
||||||
|
# User 1: contributions from this month
|
||||||
|
create_contribution(user1, item, 15.days.ago) # 3 points
|
||||||
|
create_contribution(user1, pet_type, 20.days.ago) # 15 points
|
||||||
|
|
||||||
|
# User 2: contributions older than one month
|
||||||
|
create_contribution(user2, item, 35.days.ago) # 3 points (excluded)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'calculates points from contributions in the last month' do
|
||||||
|
results = User.top_contributors_for(:this_month)
|
||||||
|
expect(results.first).to eq(user1)
|
||||||
|
expect(results.first.period_points).to eq(18)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'excludes contributions older than one month' do
|
||||||
|
results = User.top_contributors_for(:this_month)
|
||||||
|
expect(results).not_to include(user2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with this_year timeframe' do
|
||||||
|
let(:item) { create_item }
|
||||||
|
|
||||||
|
before do
|
||||||
|
# User 1: contributions from this year
|
||||||
|
create_contribution(user1, item, 3.months.ago) # 3 points
|
||||||
|
create_contribution(user1, item, 6.months.ago) # 3 points
|
||||||
|
|
||||||
|
# User 2: contributions older than one year
|
||||||
|
create_contribution(user2, item, 13.months.ago) # 3 points (excluded)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'calculates points from contributions in the last year' do
|
||||||
|
results = User.top_contributors_for(:this_year)
|
||||||
|
expect(results.first).to eq(user1)
|
||||||
|
expect(results.first.period_points).to eq(6)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'excludes contributions older than one year' do
|
||||||
|
results = User.top_contributors_for(:this_year)
|
||||||
|
expect(results).not_to include(user2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'point value calculations' do
|
||||||
|
let(:item) { create_item }
|
||||||
|
let(:pet_type) { create_pet_type }
|
||||||
|
let(:alt_style) { create_alt_style }
|
||||||
|
|
||||||
|
it 'assigns 3 points for Item contributions' do
|
||||||
|
create_contribution(user1, item, 1.day.ago)
|
||||||
|
|
||||||
|
results = User.top_contributors_for(:this_week)
|
||||||
|
expect(results.first.period_points).to eq(3)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'assigns 15 points for PetType contributions' do
|
||||||
|
create_contribution(user1, pet_type, 1.day.ago)
|
||||||
|
|
||||||
|
results = User.top_contributors_for(:this_week)
|
||||||
|
expect(results.first.period_points).to eq(15)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'assigns 30 points for AltStyle contributions' do
|
||||||
|
create_contribution(user1, alt_style, 1.day.ago)
|
||||||
|
|
||||||
|
results = User.top_contributors_for(:this_week)
|
||||||
|
expect(results.first.period_points).to eq(30)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sums multiple contribution types correctly' do
|
||||||
|
create_contribution(user1, item, 1.day.ago) # 3 points
|
||||||
|
create_contribution(user1, pet_type, 2.days.ago) # 15 points
|
||||||
|
create_contribution(user1, alt_style, 3.days.ago) # 30 points
|
||||||
|
|
||||||
|
results = User.top_contributors_for(:this_week)
|
||||||
|
expect(results.first.period_points).to eq(48)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'ordering and filtering' do
|
||||||
|
let(:item) { create_item }
|
||||||
|
|
||||||
|
before do
|
||||||
|
# Create various contributions
|
||||||
|
3.times { create_contribution(user1, item, 1.day.ago) } # 9 points
|
||||||
|
5.times { create_contribution(user2, item, 2.days.ago) } # 15 points
|
||||||
|
2.times { create_contribution(user3, item, 3.days.ago) } # 6 points
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'orders by period_points descending' do
|
||||||
|
results = User.top_contributors_for(:this_week)
|
||||||
|
expect(results.map(&:id)).to eq([user2.id, user1.id, user3.id])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'uses user.id as secondary sort for tied scores' do
|
||||||
|
# Create two users with same points
|
||||||
|
user4 = create_user('Dave')
|
||||||
|
user5 = create_user('Eve')
|
||||||
|
|
||||||
|
create_contribution(user4, item, 1.day.ago) # 3 points
|
||||||
|
create_contribution(user5, item, 1.day.ago) # 3 points
|
||||||
|
|
||||||
|
results = User.top_contributors_for(:this_week).where(id: [user4.id, user5.id])
|
||||||
|
# Should be ordered by user.id ASC when points are tied
|
||||||
|
expect(results.first.id).to be < results.second.id
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'excludes users with zero contributions in period' do
|
||||||
|
# user3 has no contributions this week
|
||||||
|
user4 = create_user('Dave')
|
||||||
|
|
||||||
|
results = User.top_contributors_for(:this_week)
|
||||||
|
expect(results).not_to include(user4)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with invalid timeframe' do
|
||||||
|
it 'raises ArgumentError' do
|
||||||
|
expect { User.top_contributors_by_period(:invalid) }.
|
||||||
|
to raise_error(ArgumentError, /Invalid timeframe/)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#period_points' do
|
||||||
|
let(:user) { create_user('Alice') }
|
||||||
|
|
||||||
|
context 'when period_points attribute is set' do
|
||||||
|
it 'returns the calculated period_points' do
|
||||||
|
# Simulate a query that sets period_points
|
||||||
|
user_with_period = User.select('users.*, 42 AS period_points').find(user.id)
|
||||||
|
expect(user_with_period.period_points).to eq(42)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when period_points attribute is not set' do
|
||||||
|
it 'falls back to denormalized points column' do
|
||||||
|
user.update!(points: 100)
|
||||||
|
expect(user.period_points).to eq(100)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper methods
|
||||||
|
def create_user(name)
|
||||||
|
auth_user = AuthUser.create!(
|
||||||
|
name: name,
|
||||||
|
email: "#{name.downcase}@example.com",
|
||||||
|
password: 'password123',
|
||||||
|
password_confirmation: 'password123'
|
||||||
|
)
|
||||||
|
User.create!(name: name, remote_id: auth_user.id, auth_server_id: 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_contribution(user, contributed, created_at)
|
||||||
|
Contribution.create!(
|
||||||
|
user: user,
|
||||||
|
contributed: contributed,
|
||||||
|
created_at: created_at
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_item
|
||||||
|
# Create a minimal item for testing
|
||||||
|
Item.create!(
|
||||||
|
name: "Test Item #{SecureRandom.hex(4)}",
|
||||||
|
description: "Test item",
|
||||||
|
thumbnail_url: "http://example.com/thumb.png",
|
||||||
|
rarity: "",
|
||||||
|
price: 0,
|
||||||
|
zones_restrict: ""
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_swf_asset
|
||||||
|
# Create a minimal swf_asset for testing
|
||||||
|
zone = Zone.first || Zone.create!(id: 1, label: "Test Zone", plain_label: "Test Zone", type_id: 1)
|
||||||
|
SwfAsset.create!(
|
||||||
|
type: 'object',
|
||||||
|
remote_id: SecureRandom.random_number(100000),
|
||||||
|
url: "http://example.com/test.swf",
|
||||||
|
zone_id: zone.id,
|
||||||
|
body_id: 0
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_pet_type
|
||||||
|
# Use find_or_create_by to avoid duplicate key errors
|
||||||
|
species = Species.find_or_create_by!(name: "Test Species #{SecureRandom.hex(4)}")
|
||||||
|
color = Color.find_or_create_by!(name: "Test Color #{SecureRandom.hex(4)}")
|
||||||
|
PetType.create!(
|
||||||
|
species_id: species.id,
|
||||||
|
color_id: color.id,
|
||||||
|
body_id: 0
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_pet_state
|
||||||
|
pet_type = create_pet_type
|
||||||
|
PetState.create!(
|
||||||
|
pet_type: pet_type,
|
||||||
|
swf_asset_ids: []
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_alt_style
|
||||||
|
# Use find_or_create_by to avoid duplicate key errors
|
||||||
|
species = Species.find_or_create_by!(name: "Test Species #{SecureRandom.hex(4)}")
|
||||||
|
color = Color.find_or_create_by!(name: "Test Color #{SecureRandom.hex(4)}")
|
||||||
|
AltStyle.create!(
|
||||||
|
species_id: species.id,
|
||||||
|
color_id: color.id,
|
||||||
|
body_id: 0,
|
||||||
|
series_name: "Test Series",
|
||||||
|
thumbnail_url: "http://example.com/thumb.png"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
BIN
vendor/cache/rails-controller-testing-1.0.5.gem
vendored
Normal file
BIN
vendor/cache/rails-controller-testing-1.0.5.gem
vendored
Normal file
Binary file not shown.
1789
vendor/javascript/idiomorph.js
vendored
1789
vendor/javascript/idiomorph.js
vendored
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue