separate table for fa post favs

This commit is contained in:
Dylan Knutson
2025-08-19 01:22:56 +00:00
parent 7f521b30e9
commit 4830a4ce54
36 changed files with 364 additions and 305 deletions

View File

@@ -18,7 +18,7 @@ class Domain::PostsController < DomainController
visual_results
]
before_action :set_post!, only: %i[show]
before_action :set_user!, only: %i[user_favorite_posts user_created_posts]
before_action :set_user!, only: %i[user_created_posts]
before_action :set_post_group!, only: %i[posts_in_group]
class PostsIndexViewConfig < T::ImmutableStruct
@@ -65,29 +65,6 @@ class Domain::PostsController < DomainController
authorize @post
end
sig(:final) { void }
def user_favorite_posts
@posts_index_view_config =
PostsIndexViewConfig.new(
show_domain_filters: false,
show_creator_links: true,
index_type_header: "user_favorites",
)
@user = T.must(@user)
authorize @user
@posts = @user.faved_posts
@post_favs =
Domain::UserPostFav.where(user: @user, post: @posts).index_by(&:post_id)
# Apply pagination through posts_relation
@posts = posts_relation(@posts, skip_ordering: true)
authorize @posts
render :index
end
sig(:final) { void }
def user_created_posts
@posts_index_view_config =

View File

@@ -0,0 +1,29 @@
# typed: true
# frozen_string_literal: true
class Domain::UserPostFavsController < DomainController
before_action :set_user!, only: %i[favorites]
def self.param_config
DomainParamConfig.new(
post_id_param: :domain_post_id,
user_id_param: :domain_user_id,
post_group_id_param: :domain_post_group_id,
)
end
sig { void }
def favorites
@posts_index_view_config =
Domain::PostsController::PostsIndexViewConfig.new(
show_domain_filters: false,
show_creator_links: true,
index_type_header: "user_favorites",
)
user = T.cast(@user, Domain::User)
@user_post_favs =
user.user_post_favs.includes(:post).page(params[:page]).per(50)
authorize @user_post_favs
render :favorites
end
end

View File

@@ -42,7 +42,7 @@ module Stats::Helpers
records_array.map do |record|
Stats::DataPoint.new(
x: record.fav_id.to_f,
x: record.fa_fav_id.to_f,
y: T.must(record.explicit_time).to_f,
)
end

View File

@@ -102,11 +102,22 @@ class Domain::Post < ReduxApplicationRecord
inverse_of: :post,
dependent: :destroy
sig { params(klass: T.class_of(Domain::User)).void }
def self.has_faving_users!(klass)
self.class_has_faving_users = klass
sig do
params(
user_klass: T.class_of(Domain::User),
fav_klass: T.class_of(Domain::UserPostFav),
).void
end
def self.has_faving_users!(user_klass, fav_klass = Domain::UserPostFav)
self.class_has_faving_users = user_klass
has_many :user_post_favs,
class_name: fav_klass.name,
inverse_of: :post,
dependent: :destroy
has_many :faving_users,
class_name: klass.name,
class_name: user_klass.name,
through: :user_post_favs,
source: :user
end

View File

@@ -10,7 +10,7 @@ class Domain::Post::FaPost < Domain::Post
has_single_file!
has_single_creator! Domain::User::FaUser
has_faving_users! Domain::User::FaUser
has_faving_users! Domain::User::FaUser, Domain::UserPostFav::FaUserPostFav
after_initialize { self.state ||= "ok" if new_record? }

View File

@@ -170,13 +170,11 @@ class Domain::User < ReduxApplicationRecord
sig { params(post_ids: T::Array[Integer], log_entry: HttpLogEntry).void }
def upsert_new_favs(post_ids, log_entry:)
self.class.transaction do
type = self.class.fav_model_type.name
if post_ids.any?
post_ids.each_slice(1000) do |slice|
self.user_post_favs.upsert_all(
slice.map { |post_id| { post_id:, type: } },
slice.map { |post_id| { post_id: } },
unique_by: %i[user_id post_id],
update_only: %i[type],
)
end
end

View File

@@ -10,7 +10,8 @@ class Domain::User::FaUser < Domain::User
# todo - set this to be the right fav model type
has_many :user_post_favs,
class_name: "Domain::UserPostFav",
-> { order(fa_fav_id: :desc) },
class_name: "Domain::UserPostFav::FaUserPostFav",
foreign_key: :user_id,
inverse_of: :user,
dependent: :destroy
@@ -132,17 +133,10 @@ class Domain::User::FaUser < Domain::User
).returns(Domain::UserPostFav)
end
def update_fav_model(post_id:, fav_id: nil, explicit_time: nil)
model =
self.user_post_favs.find_by(post_id:) ||
self.user_post_favs.build(
type: self.class.fav_model_type.name,
post_id:,
)
if model.is_a?(Domain::UserPostFav::FaUserPostFav)
model.fav_id = fav_id if fav_id.present?
model.explicit_time = explicit_time if explicit_time.present?
model.save!
end
model = self.user_post_favs.find_or_initialize_by(post_id:)
model.fa_fav_id = fav_id if fav_id.present?
model.explicit_time = explicit_time.in_time_zone if explicit_time.present?
model.save!
model
end

View File

@@ -1,32 +1,34 @@
# typed: strict
class Domain::UserPostFav::FaUserPostFav < Domain::UserPostFav
scope :with_explicit_time_and_id,
-> { where.not(explicit_time: nil).where.not(fav_id: nil) }
scope :with_inferred_time_and_id,
-> { where.not(inferred_time: nil).where.not(fav_id: nil) }
scope :with_fav_id, -> { where.not(fav_id: nil) }
attr_json :fav_id, :integer
attr_json :inferred_time, ActiveModelUtcTimeIntValue.new
attr_json :explicit_time, ActiveModelUtcTimeIntValue.new
validates :fav_id, uniqueness: true, if: :fav_id?
self.table_name = "domain_user_post_favs_fa"
belongs_to_with_counter_cache :user,
class_name: "Domain::User::FaUser",
inverse_of: :user_post_favs,
counter_cache: :user_post_favs_count
belongs_to :post,
class_name: "Domain::Post::FaPost",
inverse_of: :user_post_favs
scope :with_explicit_time_and_id,
-> { where.not(explicit_time: nil).where.not(fa_fav_id: nil) }
scope :with_inferred_time_and_id,
-> { where.not(inferred_time: nil).where.not(fa_fav_id: nil) }
scope :with_fa_fav_id, -> { where.not(fa_fav_id: nil) }
validates :fa_fav_id, uniqueness: true, if: :fa_fav_id?
sig(:final) { override.returns(T.nilable(FavedAt)) }
def faved_at
if explicit_time.present?
return FavedAt.new(time: explicit_time, type: FavedAtType::Explicit)
if (e = explicit_time)
return(FavedAt.new(time: e.to_time, type: FavedAtType::Explicit))
end
if inferred_time.present?
return(FavedAt.new(time: inferred_time, type: FavedAtType::Inferred))
if (i = inferred_time)
return(FavedAt.new(time: i.to_time, type: FavedAtType::Inferred))
end
regression_model =
@@ -34,13 +36,13 @@ class Domain::UserPostFav::FaUserPostFav < Domain::UserPostFav
name: "fa_fav_id_and_date",
model_type: "square_root",
)
if regression_model.nil? || fav_id.nil?
if regression_model.nil? || fa_fav_id.nil?
return(
FavedAt.new(time: post&.posted_at&.to_time, type: FavedAtType::PostedAt)
)
end
date_i = regression_model.predict(fav_id.to_f).to_i
date_i = regression_model.predict(fa_fav_id.to_f).to_i
FavedAt.new(time: Time.at(date_i), type: FavedAtType::InferredNow)
end
end

View File

@@ -0,0 +1,9 @@
# typed: strict
class Domain::UserPostFav::FaUserPostFavPolicy < ApplicationPolicy
extend T::Sig
sig { returns(T::Boolean) }
def favorites?
true
end
end

View File

@@ -58,7 +58,7 @@
<% end %>
<% end %>
<span class="flex-grow text-right">
<% user_post_fav = @post_favs && @post_favs[post.id] %>
<% user_post_fav = local_assigns[:user_post_fav] %>
<% if (faved_at = user_post_fav&.faved_at) && (time = faved_at.time) %>
<span class="flex items-center gap-1 justify-end">
<span

View File

@@ -1,25 +1,6 @@
<% content_for :head do %>
<style>
.grid-cell {
padding: 0.25rem;
border-right: 1px solid #e2e8f0;
}
.grid-cell:last-child {
padding-left: 0;
padding-right: 1rem;
border-right: none;
}
.grid-cell:first-child {
padding-left: 1rem;
}
.grid-row:hover .grid-cell {
background-color: #f1f5f9;
}
</style>
<% end %>
<div class="w-full max-w-2xl mx-auto mt-4 text-center sm:mt-6">
<% index_type_header_partial = "domain/posts/index_type_headers/#{@posts_index_view_config.index_type_header}" %>
<%= render partial: index_type_header_partial, locals: { user: @user, params: params, posts: @posts } %>
<%= render partial: index_type_header_partial, locals: { user: @user, params: params, relation: @posts } %>
</div>
<% if @posts_index_view_config.show_domain_filters %>
<%= render partial: "domain_filter_controls" %>

View File

@@ -1,5 +1,5 @@
<h1 class="text-2xl">
<%= link_to user.name_for_view, domain_user_path(user), class: "text-blue-600 hover:text-blue-800" %>'s favorited posts
<%= page_str(params) %>
(<%= posts.total_count %> total)
(<%= relation.total_count %> total)
</h1>

View File

@@ -0,0 +1,14 @@
<div class="w-full max-w-2xl mx-auto mt-4 text-center sm:mt-6">
<% index_type_header_partial = "domain/posts/index_type_headers/#{@posts_index_view_config.index_type_header}" %>
<%= render partial: index_type_header_partial, locals: { user: @user, params: params, relation: @user_post_favs } %>
</div>
<% if @posts_index_view_config.show_domain_filters %>
<%= render partial: "domain_filter_controls" %>
<% end %>
<%= render partial: "shared/pagination_controls", locals: { collection: @user_post_favs } %>
<div class="mx-auto flex flex-wrap justify-center">
<% @user_post_favs.each do |user_post_fav| %>
<%= render partial: "domain/posts/as_gallery_item", locals: { post: user_post_fav.post, user_post_fav: user_post_fav } %>
<% end %>
</div>
<%= render partial: "shared/pagination_controls", locals: { collection: @user_post_favs } %>

View File

@@ -1,10 +1,9 @@
<%# nasty hack, otherwise postgres uses a bad query plan %>
<% if user.is_a?(Domain::User::FaUser) || user.is_a?(Domain::User::InkbunnyUser) %>
<% fav_posts = user.faved_posts.includes(:creator).limit(5) %>
<% post_favs = user.user_post_favs.includes(post: :creator).limit(5) %>
<% else %>
<% fav_posts = user.faved_posts.limit(5) %>
<% post_favs = user.user_post_favs.limit(5) %>
<% end%>
<% post_favs = Domain::UserPostFav.where(user: user, post: fav_posts).index_by(&:post_id) %>
<section class="animated-shadow-sky sky-section">
<h2 class="section-header">
<span class="font-medium text-slate-900">Favorited Posts</span>
@@ -12,20 +11,20 @@
<%= link_to "#{user.user_post_favs.size} total", domain_user_favorites_path(user), class: "blue-link" %>
</span>
</h2>
<% if fav_posts.any? %>
<% fav_posts.each do |post| %>
<% if post_favs.any? %>
<% post_favs.each do |user_post_fav| %>
<% post = user_post_fav.post %>
<div class="flex flex-col px-4 py-2 group">
<span class="flex gap-2 group-hover:flex-grow-1">
<%= render(
partial: "domain/has_description_html/inline_link_domain_post",
locals: {
post: post,
post:,
visual_style: "sky-link",
domain_icon: false
}
) %>
<span class="whitespace-nowrap flex-grow text-right text-slate-500">
<% user_post_fav = post_favs[post.id] %>
<% if (faved_at = user_post_fav&.faved_at) && (time = faved_at.time) %>
<%= time_ago_in_words(faved_at.time) %> ago
<% else %>