mirror of
https://github.com/ryankazokas/turbovault-app.git
synced 2026-04-16 23:22:53 +00:00
Adds types
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
# typed: true
|
||||
|
||||
class ApiToken < ApplicationRecord
|
||||
extend T::Sig
|
||||
|
||||
belongs_to :user
|
||||
|
||||
# Validations
|
||||
@@ -11,16 +15,19 @@ class ApiToken < ApplicationRecord
|
||||
scope :active, -> { where("expires_at IS NULL OR expires_at > ?", Time.current) }
|
||||
|
||||
# Instance methods
|
||||
sig { returns(T::Boolean) }
|
||||
def expired?
|
||||
expires_at.present? && expires_at < Time.current
|
||||
end
|
||||
|
||||
sig { void }
|
||||
def touch_last_used!
|
||||
update_column(:last_used_at, Time.current)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
sig { void }
|
||||
def generate_token
|
||||
self.token ||= SecureRandom.urlsafe_base64(32)
|
||||
end
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# typed: true
|
||||
|
||||
class Collection < ApplicationRecord
|
||||
extend T::Sig
|
||||
# Associations
|
||||
belongs_to :user
|
||||
belongs_to :parent_collection, class_name: "Collection", optional: true
|
||||
@@ -15,30 +18,36 @@ class Collection < ApplicationRecord
|
||||
scope :root_collections, -> { where(parent_collection_id: nil) }
|
||||
|
||||
# Instance methods
|
||||
sig { returns(Integer) }
|
||||
def game_count
|
||||
games.count
|
||||
end
|
||||
|
||||
sig { returns(Integer) }
|
||||
def total_game_count
|
||||
game_count + subcollections.sum(&:total_game_count)
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def root?
|
||||
parent_collection_id.nil?
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def subcollection?
|
||||
parent_collection_id.present?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
sig { void }
|
||||
def cannot_be_own_parent
|
||||
if parent_collection_id.present? && parent_collection_id == id
|
||||
errors.add(:parent_collection_id, "cannot be itself")
|
||||
end
|
||||
end
|
||||
|
||||
sig { void }
|
||||
def subcollection_depth_limit
|
||||
if parent_collection.present? && parent_collection.parent_collection.present?
|
||||
errors.add(:parent_collection_id, "cannot nest more than one level deep")
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# typed: true
|
||||
|
||||
class CollectionGame < ApplicationRecord
|
||||
extend T::Sig
|
||||
belongs_to :collection
|
||||
belongs_to :game
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# typed: true
|
||||
|
||||
class Game < ApplicationRecord
|
||||
extend T::Sig
|
||||
# Associations
|
||||
belongs_to :user
|
||||
belongs_to :platform
|
||||
@@ -54,12 +57,14 @@ class Game < ApplicationRecord
|
||||
scope :needs_igdb_review, -> { joins(:igdb_match_suggestions).where(igdb_match_suggestions: { status: "pending" }).distinct }
|
||||
|
||||
# Class methods
|
||||
sig { params(query: String).returns(T.untyped) }
|
||||
def self.search(query)
|
||||
where("title ILIKE ?", "%#{sanitize_sql_like(query)}%")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
sig { void }
|
||||
def set_date_added
|
||||
self.date_added ||= Date.current
|
||||
end
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# typed: true
|
||||
|
||||
class GameGenre < ApplicationRecord
|
||||
extend T::Sig
|
||||
belongs_to :game
|
||||
belongs_to :genre
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# typed: true
|
||||
|
||||
class Genre < ApplicationRecord
|
||||
extend T::Sig
|
||||
# Associations
|
||||
has_many :game_genres, dependent: :destroy
|
||||
has_many :games, through: :game_genres
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# typed: true
|
||||
|
||||
class IgdbGame < ApplicationRecord
|
||||
extend T::Sig
|
||||
# Associations
|
||||
has_many :igdb_match_suggestions
|
||||
has_many :games, foreign_key: :igdb_id, primary_key: :igdb_id
|
||||
@@ -12,10 +15,12 @@ class IgdbGame < ApplicationRecord
|
||||
scope :recent, -> { order(last_synced_at: :desc) }
|
||||
|
||||
# Instance methods
|
||||
sig { void }
|
||||
def increment_match_count!
|
||||
increment!(:match_count)
|
||||
end
|
||||
|
||||
sig { params(size: String).returns(T.nilable(String)) }
|
||||
def cover_image_url(size = "cover_big")
|
||||
return nil unless cover_url.present?
|
||||
# IGDB uses image IDs like "co1234"
|
||||
|
||||
@@ -24,7 +24,7 @@ class IgdbMatchSuggestion < ApplicationRecord
|
||||
def approve!
|
||||
transaction do
|
||||
update!(status: "approved", reviewed_at: Time.current)
|
||||
|
||||
|
||||
# Update the game with the matched IGDB ID
|
||||
game.update!(
|
||||
igdb_id: igdb_id,
|
||||
@@ -49,10 +49,10 @@ class IgdbMatchSuggestion < ApplicationRecord
|
||||
end
|
||||
|
||||
igdb_game_record.increment_match_count!
|
||||
|
||||
|
||||
# Map and assign IGDB genres to the game
|
||||
sync_genres_to_game if igdb_genres.present?
|
||||
|
||||
|
||||
# Reject all other pending suggestions for this game
|
||||
game.igdb_match_suggestions
|
||||
.where.not(id: id)
|
||||
@@ -131,13 +131,13 @@ class IgdbMatchSuggestion < ApplicationRecord
|
||||
igdb_genres.each do |igdb_genre_name|
|
||||
# Try exact match first
|
||||
local_genre = Genre.find_by("LOWER(name) = ?", igdb_genre_name.downcase)
|
||||
|
||||
|
||||
# Try mapped name
|
||||
if local_genre.nil? && genre_mappings[igdb_genre_name]
|
||||
mapped_name = genre_mappings[igdb_genre_name]
|
||||
local_genre = Genre.find_by("LOWER(name) = ?", mapped_name.downcase)
|
||||
end
|
||||
|
||||
|
||||
# Add genre to game if found and not already assigned
|
||||
if local_genre && !game.genres.include?(local_genre)
|
||||
game.genres << local_genre
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# typed: true
|
||||
|
||||
class Platform < ApplicationRecord
|
||||
extend T::Sig
|
||||
# Associations
|
||||
has_many :games, dependent: :restrict_with_error
|
||||
has_many :items, dependent: :restrict_with_error
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
# typed: true
|
||||
|
||||
class User < ApplicationRecord
|
||||
extend T::Sig
|
||||
|
||||
has_secure_password
|
||||
|
||||
# Associations
|
||||
@@ -10,7 +14,7 @@ class User < ApplicationRecord
|
||||
|
||||
# Validations
|
||||
validates :email, presence: true, uniqueness: { case_sensitive: false }, format: { with: URI::MailTo::EMAIL_REGEXP }
|
||||
validates :username, presence: true, uniqueness: { case_sensitive: false },
|
||||
validates :username, presence: true, uniqueness: { case_sensitive: false },
|
||||
length: { minimum: 3, maximum: 30 },
|
||||
format: { with: /\A[a-zA-Z0-9_]+\z/, message: "only allows letters, numbers, and underscores" }
|
||||
validates :password, length: { minimum: 8 }, if: -> { password.present? }
|
||||
@@ -20,22 +24,26 @@ class User < ApplicationRecord
|
||||
before_save :downcase_email
|
||||
|
||||
# Instance methods
|
||||
sig { returns(T::Boolean) }
|
||||
def generate_password_reset_token
|
||||
self.password_reset_token = SecureRandom.urlsafe_base64
|
||||
self.password_reset_sent_at = Time.current
|
||||
save!
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def password_reset_expired?
|
||||
password_reset_sent_at.nil? || password_reset_sent_at < 2.hours.ago
|
||||
end
|
||||
|
||||
sig { returns(String) }
|
||||
def theme_class
|
||||
"theme-#{theme}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
sig { void }
|
||||
def downcase_email
|
||||
self.email = email.downcase if email.present?
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user