improve cache busting based on policy
This commit is contained in:
1
TODO.md
1
TODO.md
@@ -32,5 +32,6 @@
|
||||
- [ ] Make unified Static file job
|
||||
- [ ] Make unified Avatar file job
|
||||
- [ ] ko-fi domain icon
|
||||
- [ ] tumblr domain icon
|
||||
- [ ] Do PCA on user factors table to display a 2D plot of users
|
||||
- [ ] Use links found in descriptions to indicate re-scanning a post? (e.g. for comic next/prev links)
|
||||
|
||||
BIN
app/assets/images/domain-icons/ko-fi.png
Normal file
BIN
app/assets/images/domain-icons/ko-fi.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/assets/images/domain-icons/tumblr.png
Normal file
BIN
app/assets/images/domain-icons/tumblr.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
@@ -212,11 +212,15 @@ module Domain::DescriptionsHelper
|
||||
)
|
||||
end
|
||||
def props_for_user_hover_preview(user, visual_style, icon_size)
|
||||
cache_key = [
|
||||
user,
|
||||
policy(user),
|
||||
"popover_inline_link_domain_user",
|
||||
icon_size,
|
||||
]
|
||||
Rails
|
||||
.cache
|
||||
.fetch(
|
||||
[user, "popover_inline_link_domain_user", visual_style, icon_size],
|
||||
) do
|
||||
.fetch(cache_key) do
|
||||
num_posts =
|
||||
user.has_created_posts? ? user.user_post_creations.count : nil
|
||||
registered_at = domain_user_registered_at_string_for_view(user)
|
||||
@@ -227,7 +231,6 @@ module Domain::DescriptionsHelper
|
||||
avatar_thumb_size = icon_size == "large" ? "64-avatar" : "32-avatar"
|
||||
|
||||
{
|
||||
visualStyle: visual_style,
|
||||
iconSize: icon_size,
|
||||
linkText: user.name_for_view,
|
||||
userId: user.to_param,
|
||||
@@ -247,6 +250,10 @@ module Domain::DescriptionsHelper
|
||||
userNumFollowed: num_followed,
|
||||
}
|
||||
end
|
||||
.then do |props|
|
||||
props[:visualStyle] = visual_style
|
||||
props
|
||||
end
|
||||
end
|
||||
|
||||
sig do
|
||||
@@ -255,11 +262,11 @@ module Domain::DescriptionsHelper
|
||||
)
|
||||
end
|
||||
def props_for_post_hover_preview(post, link_text, visual_style)
|
||||
cache_key = [post, policy(post), "popover_inline_link_domain_post"]
|
||||
Rails
|
||||
.cache
|
||||
.fetch([post, "popover_inline_link_domain_post", visual_style]) do
|
||||
props = {
|
||||
visualStyle: visual_style,
|
||||
.fetch(cache_key) do
|
||||
{
|
||||
linkText: link_text,
|
||||
postId: post.to_param,
|
||||
postTitle: post.title,
|
||||
@@ -267,13 +274,16 @@ module Domain::DescriptionsHelper
|
||||
postThumbnailPath: thumbnail_for_post_path(post),
|
||||
postThumbnailAlt: "View on #{domain_name_for_model(post)}",
|
||||
postDomainIcon: domain_model_icon_path(post),
|
||||
}
|
||||
|
||||
if creator = post.primary_creator_for_view
|
||||
props[:creatorName] = creator.name_for_view
|
||||
props[:creatorAvatarPath] = user_avatar_path_for_view(creator)
|
||||
}.then do |props|
|
||||
if creator = post.primary_creator_for_view
|
||||
props[:creatorName] = creator.name_for_view
|
||||
props[:creatorAvatarPath] = user_avatar_path_for_view(creator)
|
||||
end
|
||||
props
|
||||
end
|
||||
|
||||
end
|
||||
.then do |props|
|
||||
props[:visualStyle] = visual_style
|
||||
props
|
||||
end
|
||||
end
|
||||
|
||||
@@ -60,6 +60,8 @@ module Domain::DomainsHelper
|
||||
"spreadshirt.de" => "spreadshirt.png",
|
||||
"spreadshirt.com" => "spreadshirt.png",
|
||||
"boosty.to" => "boosty.png",
|
||||
"tumblr.com" => "tumblr.png",
|
||||
"ko-fi.com" => "ko-fi.png",
|
||||
}.freeze,
|
||||
T::Hash[String, String],
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ module Domain::ModelHelper
|
||||
extend T::Sig
|
||||
extend T::Helpers
|
||||
include HelpersInterface
|
||||
include Pundit::Authorization
|
||||
abstract!
|
||||
|
||||
sig do
|
||||
@@ -15,7 +16,7 @@ module Domain::ModelHelper
|
||||
).returns(T.nilable(String))
|
||||
end
|
||||
def render_for_model(model, partial, as:, expires_in: 1.hour, cache_key: nil)
|
||||
cache_key ||= [model, partial]
|
||||
cache_key ||= [model, policy(model), partial]
|
||||
Rails
|
||||
.cache
|
||||
.fetch(cache_key, expires_in:) do
|
||||
|
||||
@@ -3,6 +3,7 @@ module Domain::UsersHelper
|
||||
extend T::Sig
|
||||
extend T::Helpers
|
||||
include HelpersInterface
|
||||
include Pundit::Authorization
|
||||
abstract!
|
||||
|
||||
sig { params(user: Domain::User).returns(T.nilable(String)) }
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import PostHoverPreview from './PostHoverPreview';
|
||||
import { anchorClassNamesForVisualStyle } from '../utils/hoverPreviewUtils';
|
||||
import {
|
||||
anchorClassNamesForVisualStyle,
|
||||
iconClassNamesForSize,
|
||||
} from '../utils/hoverPreviewUtils';
|
||||
|
||||
interface PostHoverPreviewWrapperProps {
|
||||
visualStyle: 'sky-link' | 'description-section-link';
|
||||
@@ -38,10 +41,14 @@ export const PostHoverPreviewWrapper: React.FC<
|
||||
>
|
||||
<a
|
||||
href={postPath}
|
||||
className={anchorClassNamesForVisualStyle(visualStyle)}
|
||||
className={anchorClassNamesForVisualStyle(visualStyle, true)}
|
||||
>
|
||||
{visualStyle === 'description-section-link' && (
|
||||
<i className="fa-regular fa-image h-4 w-4 flex-shrink-0"></i>
|
||||
<img
|
||||
src={postDomainIcon}
|
||||
alt={postTitle}
|
||||
className={iconClassNamesForSize('small')}
|
||||
/>
|
||||
)}
|
||||
{linkText}
|
||||
</a>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import { UserHoverPreview } from './UserHoverPreview';
|
||||
import { anchorClassNamesForVisualStyle } from '../utils/hoverPreviewUtils';
|
||||
import {
|
||||
anchorClassNamesForVisualStyle,
|
||||
iconClassNamesForSize,
|
||||
} from '../utils/hoverPreviewUtils';
|
||||
|
||||
type VisualStyle = 'sky-link' | 'description-section-link';
|
||||
type IconSize = 'small' | 'large';
|
||||
@@ -22,15 +25,6 @@ interface UserHoverPreviewWrapperProps {
|
||||
userNumFollowed?: number;
|
||||
}
|
||||
|
||||
function iconClassNamesForSize(size: IconSize) {
|
||||
switch (size) {
|
||||
case 'large':
|
||||
return 'h-8 w-8 flex-shrink-0 rounded-md';
|
||||
case 'small':
|
||||
return 'h-4 w-4 flex-shrink-0 rounded-md';
|
||||
}
|
||||
}
|
||||
|
||||
export const UserHoverPreviewWrapper: React.FC<
|
||||
UserHoverPreviewWrapperProps
|
||||
> = ({
|
||||
|
||||
@@ -11,6 +11,16 @@ import {
|
||||
* Utility functions for hover preview styling
|
||||
*/
|
||||
|
||||
export type IconSize = 'large' | 'small';
|
||||
export function iconClassNamesForSize(size: IconSize) {
|
||||
switch (size) {
|
||||
case 'large':
|
||||
return 'h-8 w-8 flex-shrink-0 rounded-md';
|
||||
case 'small':
|
||||
return 'h-5 w-5 flex-shrink-0 rounded-sm';
|
||||
}
|
||||
}
|
||||
|
||||
// Base preview container styling
|
||||
export const getPreviewContainerClassName = (
|
||||
maxWidth: string,
|
||||
|
||||
@@ -387,6 +387,8 @@ class Domain::Fa::Job::Base < Scraper::JobBase
|
||||
user.num_comments_given = user_page.num_comments_given
|
||||
user.num_journals = user_page.num_journals
|
||||
user.num_favorites = user_page.num_favorites
|
||||
user.num_watched_by = user_page.num_watched_by
|
||||
user.num_watching = user_page.num_watching
|
||||
user.profile_html =
|
||||
user_page.profile_html.encode("UTF-8", invalid: :replace, undef: :replace)
|
||||
if url = user_page.profile_thumb_url
|
||||
|
||||
@@ -113,6 +113,25 @@ class Domain::Fa::Job::UserPageJob < Domain::Fa::Job::Base
|
||||
unique_by: %i[from_id to_id],
|
||||
)
|
||||
user.scanned_follows_at = Time.current
|
||||
elsif recent_watching.any?
|
||||
# if there are watchers, find the ones we've already recorded. if
|
||||
# all of them are known, then we can assume watched users are up to date.
|
||||
num_recent_watching = recent_watching.count
|
||||
num_recent_watching_known =
|
||||
user
|
||||
.followed_users
|
||||
.where(url_name: recent_watching.map(&:url_name))
|
||||
.count
|
||||
|
||||
if (num_recent_watching == num_recent_watching_known) &&
|
||||
(user.followed_users.count == user_page.num_watching)
|
||||
logger.info(
|
||||
format_tags(
|
||||
"skipping UserFollowsJob, all watched users already known",
|
||||
),
|
||||
)
|
||||
user.scanned_follows_at = Time.current
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -148,6 +167,36 @@ class Domain::Fa::Job::UserPageJob < Domain::Fa::Job::Base
|
||||
unique_by: %i[from_id to_id],
|
||||
)
|
||||
user.scanned_followed_by_at = Time.current
|
||||
elsif recent_watchers.any?
|
||||
# if there are watchers, find the ones we've already recorded. if
|
||||
# all of them are known, then we can skip scanning favs the next time.
|
||||
known_watchers =
|
||||
Domain::User::FaUser.where(url_name: recent_watchers.map(&:url_name))
|
||||
if known_watchers.count == recent_watchers.count
|
||||
logger.info(
|
||||
format_tags("skipping followed by scan, all watchers already known"),
|
||||
)
|
||||
user.scanned_followed_by_at = Time.current
|
||||
end
|
||||
elsif recent_watchers.any?
|
||||
# if there are watchers, find the ones we've already recorded. if
|
||||
# all of them are known, then we can assume watched users are up to date.
|
||||
num_recent_watchers = recent_watchers.count
|
||||
num_recent_watchers_known =
|
||||
user
|
||||
.followed_by_users
|
||||
.where(url_name: recent_watchers.map(&:url_name))
|
||||
.count
|
||||
|
||||
if (num_recent_watchers == num_recent_watchers_known) &&
|
||||
(user.followed_by_users.count == user_page.num_watched_by)
|
||||
logger.info(
|
||||
format_tags(
|
||||
"skipping UserFollowsJob, all watched users already known",
|
||||
),
|
||||
)
|
||||
user.scanned_followed_by_at = Time.current
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -27,6 +27,8 @@ class Domain::Fa::Parser::UserPageHelper < Domain::Fa::Parser::Base
|
||||
@recent_watching = T.let(nil, T.nilable(T::Array[RecentUser]))
|
||||
@statistics = T.let(nil, T.nilable(Nokogiri::XML::Element))
|
||||
@main_about = T.let(nil, T.nilable(Nokogiri::XML::Element))
|
||||
@num_watched_by = T.let(nil, T.nilable(Integer))
|
||||
@num_watching = T.let(nil, T.nilable(Integer))
|
||||
|
||||
@elem = elem
|
||||
@page_version = page_version
|
||||
@@ -261,6 +263,34 @@ class Domain::Fa::Parser::UserPageHelper < Domain::Fa::Parser::Base
|
||||
@recent_watching ||= recent_users_for_section("Recently Watched")
|
||||
end
|
||||
|
||||
sig { returns(Integer) }
|
||||
def num_watched_by
|
||||
@num_watched_by ||=
|
||||
begin
|
||||
watchers_text =
|
||||
recent_users_section("Recent Watchers")
|
||||
.parent
|
||||
.css(".floatright")
|
||||
.first
|
||||
.text
|
||||
watchers_text.match(/Watched by (\d+)/)[1].to_i
|
||||
end
|
||||
end
|
||||
|
||||
sig { returns(Integer) }
|
||||
def num_watching
|
||||
@num_watching ||=
|
||||
begin
|
||||
watchers_text =
|
||||
recent_users_section("Recently Watched")
|
||||
.parent
|
||||
.css(".floatright")
|
||||
.first
|
||||
.text
|
||||
watchers_text.match(/Watching (\d+)/)[1].to_i
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
sig { params(section_name: String).returns(T::Array[RecentUser]) }
|
||||
@@ -289,6 +319,20 @@ class Domain::Fa::Parser::UserPageHelper < Domain::Fa::Parser::Base
|
||||
end
|
||||
end
|
||||
|
||||
sig { params(section_name: String).returns(Nokogiri::XML::Element) }
|
||||
def recent_users_section(section_name)
|
||||
case @page_version
|
||||
when VERSION_2
|
||||
@elem
|
||||
.css(".userpage-section-left")
|
||||
.find do |elem|
|
||||
elem.css(".section-header h2")&.first&.text&.strip == section_name
|
||||
end
|
||||
else
|
||||
unimplemented_version!
|
||||
end
|
||||
end
|
||||
|
||||
sig { params(legacy_name: Symbol, redux_idx: Integer).returns(Integer) }
|
||||
def stat_value(legacy_name, redux_idx)
|
||||
legacy_map = # if false # old mode?
|
||||
|
||||
@@ -8,12 +8,15 @@ class Domain::User::FaUser < Domain::User
|
||||
attr_json :artist_type, :string
|
||||
attr_json :mood, :string
|
||||
attr_json :profile_html, :string
|
||||
# num_* are those indicated on the user page
|
||||
attr_json :num_pageviews, :integer
|
||||
attr_json :num_submissions, :integer
|
||||
attr_json :num_comments_recieved, :integer
|
||||
attr_json :num_comments_given, :integer
|
||||
attr_json :num_journals, :integer
|
||||
attr_json :num_favorites, :integer
|
||||
attr_json :num_watched_by, :integer
|
||||
attr_json :num_watching, :integer
|
||||
attr_json_due_timestamp :scanned_gallery_at, 3.years
|
||||
attr_json_due_timestamp :scanned_page_at, 3.months
|
||||
attr_json_due_timestamp :scanned_follows_at, 3.months
|
||||
|
||||
@@ -1,54 +1,107 @@
|
||||
# typed: true
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ApplicationPolicy
|
||||
attr_reader :user, :record
|
||||
extend T::Sig
|
||||
|
||||
sig { returns(T.nilable(User)) }
|
||||
attr_reader :user
|
||||
|
||||
sig do
|
||||
returns(
|
||||
T.nilable(
|
||||
T.any(ReduxApplicationRecord, T.class_of(ReduxApplicationRecord)),
|
||||
),
|
||||
)
|
||||
end
|
||||
attr_reader :record
|
||||
|
||||
sig { returns(String) }
|
||||
def cache_key
|
||||
# find all the policy methods that the subclass defines up to ApplicationPolicy
|
||||
klass = T.let(self.class, T.class_of(ApplicationPolicy))
|
||||
methods = []
|
||||
|
||||
while klass < ApplicationPolicy
|
||||
methods.concat(klass.instance_methods(false))
|
||||
klass = T.cast(klass.superclass, T.class_of(ApplicationPolicy))
|
||||
end
|
||||
methods.uniq!
|
||||
methods.sort!
|
||||
method_values_string =
|
||||
methods.map { |method| "#{method}:#{send(method)}" }.join("/")
|
||||
"#{self.class.name}::#{Digest::SHA256.hexdigest(method_values_string)[0..16]}"
|
||||
end
|
||||
|
||||
sig do
|
||||
params(
|
||||
user: T.nilable(User),
|
||||
record:
|
||||
T.nilable(
|
||||
T.any(ReduxApplicationRecord, T.class_of(ReduxApplicationRecord)),
|
||||
),
|
||||
).void
|
||||
end
|
||||
def initialize(user, record)
|
||||
@user = user
|
||||
@record = record
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def index?
|
||||
false
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def show?
|
||||
false
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def create?
|
||||
false
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def new?
|
||||
create?
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def update?
|
||||
false
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def edit?
|
||||
update?
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def destroy?
|
||||
false
|
||||
end
|
||||
|
||||
class Scope
|
||||
extend T::Sig
|
||||
|
||||
sig { params(user: T.nilable(User), scope: T.untyped).void }
|
||||
def initialize(user, scope)
|
||||
@user = user
|
||||
@scope = scope
|
||||
end
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
def resolve
|
||||
raise NoMethodError, "You must define #resolve in #{self.class}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :user, :scope
|
||||
sig { returns(T.nilable(User)) }
|
||||
attr_reader :user
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
attr_reader :scope
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,50 +1,66 @@
|
||||
# typed: true
|
||||
# typed: strict
|
||||
class GlobalStatePolicy < ApplicationPolicy
|
||||
extend T::Sig
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def index?
|
||||
user.admin?
|
||||
user&.admin? || false
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def show?
|
||||
user.admin?
|
||||
user&.admin? || false
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def create?
|
||||
user.admin?
|
||||
user&.admin? || false
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def update?
|
||||
user.admin?
|
||||
user&.admin? || false
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def destroy?
|
||||
user.admin?
|
||||
user&.admin? || false
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def fa_cookies?
|
||||
user.admin?
|
||||
user&.admin? || false
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def edit_fa_cookies?
|
||||
user.admin?
|
||||
user&.admin? || false
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def update_fa_cookies?
|
||||
user.admin?
|
||||
user&.admin? || false
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def ib_cookies?
|
||||
user.admin?
|
||||
user&.admin? || false
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def edit_ib_cookies?
|
||||
user.admin?
|
||||
user&.admin? || false
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def update_ib_cookies?
|
||||
user.admin?
|
||||
user&.admin? || false
|
||||
end
|
||||
|
||||
class Scope < ApplicationPolicy::Scope
|
||||
extend T::Sig
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
def resolve
|
||||
scope.all
|
||||
end
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<%# Display post information with associated details %>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<%= link_to domain_post_path(post),
|
||||
<%= link_to Rails.application.routes.url_helpers.domain_post_path(post),
|
||||
class: "badge bg-primary",
|
||||
target: "_blank" do %>
|
||||
<i class="fa-solid fa-image me-1"></i><%= post.class.name %> #<%= post.id %>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<% attr = user.class.param_prefix_and_attribute[1] %>
|
||||
<%= link_to domain_user_path(user),
|
||||
<%= link_to Rails.application.routes.url_helpers.domain_user_path(user),
|
||||
class: "badge bg-primary",
|
||||
target: "_blank" do %>
|
||||
<i class="fa-solid fa-user me-1"></i><%= user.class.name %> #<%= user.id %>
|
||||
|
||||
@@ -74,6 +74,7 @@ ActiveSupport.on_load(:good_job_application_controller) do
|
||||
helper Domain::PostsHelper
|
||||
helper Domain::UsersHelper
|
||||
helper Domain::PostGroupsHelper
|
||||
helper Domain::DomainModelHelper
|
||||
helper GoodJobHelper
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,17 +2,21 @@
|
||||
|
||||
START_PROMETHEUS_EXPORTER =
|
||||
T.let(
|
||||
# do not start in test or console mode
|
||||
(
|
||||
!Rails.env.test? && !Rails.const_defined?("Console") &&
|
||||
Rails.const_defined?("Server")
|
||||
) ||
|
||||
# always start in sever mode
|
||||
Rails.const_defined?("Server") ||
|
||||
# always start in worker mode
|
||||
(Rails.env == "worker") ||
|
||||
# ran with args when in server mode, but no top level task
|
||||
(ARGV.any? && Rake.application.top_level_tasks.empty?),
|
||||
# never start in test mode
|
||||
!Rails.env.test? &&
|
||||
(
|
||||
(
|
||||
# and do not start in console mode
|
||||
!Rails.env.test? && !Rails.const_defined?("Console") &&
|
||||
Rails.const_defined?("Server")
|
||||
) ||
|
||||
# always start in sever mode
|
||||
Rails.const_defined?("Server") ||
|
||||
# always start in worker mode
|
||||
(Rails.env == "worker") ||
|
||||
# ran with args when in server mode, but no top level task
|
||||
(ARGV.any? && Rake.application.top_level_tasks.empty?)
|
||||
),
|
||||
T::Boolean,
|
||||
)
|
||||
|
||||
|
||||
3
sorbet/rbi/dsl/application_controller.rbi
generated
3
sorbet/rbi/dsl/application_controller.rbi
generated
@@ -34,9 +34,10 @@ class ApplicationController
|
||||
include ::Domain::UsersHelper
|
||||
include ::PathsHelper
|
||||
include ::Domain::DomainsHelper
|
||||
include ::Domain::DomainModelHelper
|
||||
include ::Pundit::Authorization
|
||||
include ::Domain::PostsHelper
|
||||
include ::Domain::DescriptionsHelper
|
||||
include ::Domain::DomainModelHelper
|
||||
include ::Domain::E621::PostsHelper
|
||||
include ::Domain::Fa::PostsHelper
|
||||
include ::Domain::Fa::UsersHelper
|
||||
|
||||
3
sorbet/rbi/dsl/devise_controller.rbi
generated
3
sorbet/rbi/dsl/devise_controller.rbi
generated
@@ -31,9 +31,10 @@ class DeviseController
|
||||
include ::Domain::UsersHelper
|
||||
include ::PathsHelper
|
||||
include ::Domain::DomainsHelper
|
||||
include ::Domain::DomainModelHelper
|
||||
include ::Pundit::Authorization
|
||||
include ::Domain::PostsHelper
|
||||
include ::Domain::DescriptionsHelper
|
||||
include ::Domain::DomainModelHelper
|
||||
include ::Domain::E621::PostsHelper
|
||||
include ::Domain::Fa::PostsHelper
|
||||
include ::Domain::Fa::UsersHelper
|
||||
|
||||
114
sorbet/rbi/dsl/domain/user/fa_user.rbi
generated
114
sorbet/rbi/dsl/domain/user/fa_user.rbi
generated
@@ -1816,6 +1816,96 @@ class Domain::User::FaUser
|
||||
sig { void }
|
||||
def num_submissions_will_change!; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def num_watched_by; end
|
||||
|
||||
sig { params(value: T.nilable(::Integer)).returns(T.nilable(::Integer)) }
|
||||
def num_watched_by=(value); end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def num_watched_by?; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def num_watched_by_before_last_save; end
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
def num_watched_by_before_type_cast; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def num_watched_by_came_from_user?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def num_watched_by_change; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def num_watched_by_change_to_be_saved; end
|
||||
|
||||
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
|
||||
def num_watched_by_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def num_watched_by_in_database; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def num_watched_by_previous_change; end
|
||||
|
||||
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
|
||||
def num_watched_by_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def num_watched_by_previously_was; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def num_watched_by_was; end
|
||||
|
||||
sig { void }
|
||||
def num_watched_by_will_change!; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def num_watching; end
|
||||
|
||||
sig { params(value: T.nilable(::Integer)).returns(T.nilable(::Integer)) }
|
||||
def num_watching=(value); end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def num_watching?; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def num_watching_before_last_save; end
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
def num_watching_before_type_cast; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def num_watching_came_from_user?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def num_watching_change; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def num_watching_change_to_be_saved; end
|
||||
|
||||
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
|
||||
def num_watching_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def num_watching_in_database; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def num_watching_previous_change; end
|
||||
|
||||
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
|
||||
def num_watching_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def num_watching_previously_was; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def num_watching_was; end
|
||||
|
||||
sig { void }
|
||||
def num_watching_will_change!; end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def page_scan_error; end
|
||||
|
||||
@@ -2021,6 +2111,12 @@ class Domain::User::FaUser
|
||||
sig { void }
|
||||
def restore_num_submissions!; end
|
||||
|
||||
sig { void }
|
||||
def restore_num_watched_by!; end
|
||||
|
||||
sig { void }
|
||||
def restore_num_watching!; end
|
||||
|
||||
sig { void }
|
||||
def restore_page_scan_error!; end
|
||||
|
||||
@@ -2180,6 +2276,18 @@ class Domain::User::FaUser
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_num_submissions?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def saved_change_to_num_watched_by; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_num_watched_by?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def saved_change_to_num_watching; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_num_watching?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def saved_change_to_page_scan_error; end
|
||||
|
||||
@@ -2848,6 +2956,12 @@ class Domain::User::FaUser
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_num_submissions?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_num_watched_by?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_num_watching?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_page_scan_error?; end
|
||||
|
||||
|
||||
2
sorbet/rbi/dsl/good_job/jobs_controller.rbi
generated
2
sorbet/rbi/dsl/good_job/jobs_controller.rbi
generated
@@ -30,6 +30,8 @@ class GoodJob::JobsController
|
||||
include ::Domain::UsersHelper
|
||||
include ::PathsHelper
|
||||
include ::Domain::DomainsHelper
|
||||
include ::Domain::DomainModelHelper
|
||||
include ::Pundit::Authorization
|
||||
include ::Domain::PostsHelper
|
||||
include ::Domain::PostGroupsHelper
|
||||
include ::GoodJobHelper
|
||||
|
||||
3
sorbet/rbi/dsl/rails/application_controller.rbi
generated
3
sorbet/rbi/dsl/rails/application_controller.rbi
generated
@@ -34,9 +34,10 @@ class Rails::ApplicationController
|
||||
include ::Domain::UsersHelper
|
||||
include ::PathsHelper
|
||||
include ::Domain::DomainsHelper
|
||||
include ::Domain::DomainModelHelper
|
||||
include ::Pundit::Authorization
|
||||
include ::Domain::PostsHelper
|
||||
include ::Domain::DescriptionsHelper
|
||||
include ::Domain::DomainModelHelper
|
||||
include ::Domain::E621::PostsHelper
|
||||
include ::Domain::Fa::PostsHelper
|
||||
include ::Domain::Fa::UsersHelper
|
||||
|
||||
@@ -34,9 +34,10 @@ class Rails::Conductor::BaseController
|
||||
include ::Domain::UsersHelper
|
||||
include ::PathsHelper
|
||||
include ::Domain::DomainsHelper
|
||||
include ::Domain::DomainModelHelper
|
||||
include ::Pundit::Authorization
|
||||
include ::Domain::PostsHelper
|
||||
include ::Domain::DescriptionsHelper
|
||||
include ::Domain::DomainModelHelper
|
||||
include ::Domain::E621::PostsHelper
|
||||
include ::Domain::Fa::PostsHelper
|
||||
include ::Domain::Fa::UsersHelper
|
||||
|
||||
3
sorbet/rbi/dsl/rails/health_controller.rbi
generated
3
sorbet/rbi/dsl/rails/health_controller.rbi
generated
@@ -34,9 +34,10 @@ class Rails::HealthController
|
||||
include ::Domain::UsersHelper
|
||||
include ::PathsHelper
|
||||
include ::Domain::DomainsHelper
|
||||
include ::Domain::DomainModelHelper
|
||||
include ::Pundit::Authorization
|
||||
include ::Domain::PostsHelper
|
||||
include ::Domain::DescriptionsHelper
|
||||
include ::Domain::DomainModelHelper
|
||||
include ::Domain::E621::PostsHelper
|
||||
include ::Domain::Fa::PostsHelper
|
||||
include ::Domain::Fa::UsersHelper
|
||||
|
||||
@@ -39,10 +39,6 @@ module HelpersInterface
|
||||
def number_to_human_size(size)
|
||||
end
|
||||
|
||||
sig { params(user: Domain::User).returns(Domain::UserPolicy) }
|
||||
def policy(user)
|
||||
end
|
||||
|
||||
sig do
|
||||
params(
|
||||
model: T.any(Domain::Post, Domain::User),
|
||||
|
||||
@@ -254,6 +254,162 @@ describe Domain::Fa::Job::UserPageJob do
|
||||
end
|
||||
end
|
||||
|
||||
context "the user has more than threshold watched users and they are all known" do
|
||||
let(:client_mock_config) do
|
||||
[
|
||||
{
|
||||
uri: "https://www.furaffinity.net/user/koul/",
|
||||
status_code: 200,
|
||||
content_type: "text/html",
|
||||
contents:
|
||||
SpecUtil.read_fixture_file(
|
||||
# this user page indicates:
|
||||
# - 14 watched
|
||||
# - 25 watchers
|
||||
"domain/fa/user_page/user_page_koul_over_threshold_watchers.html",
|
||||
),
|
||||
},
|
||||
]
|
||||
end
|
||||
|
||||
let(:user) { create(:domain_user_fa_user, url_name: "koul") }
|
||||
let(:recent_watched_user_url_names) do
|
||||
%w[
|
||||
spiritinchoco
|
||||
syrrvya
|
||||
knyazkolosok
|
||||
waspsalad
|
||||
moth-sprout
|
||||
suzamuri
|
||||
2d10
|
||||
lemithecat
|
||||
fr95
|
||||
floki.midnight
|
||||
dizzy.
|
||||
kilobytefox
|
||||
]
|
||||
end
|
||||
|
||||
it "does not mark scanned_follows_at if no followed users are known" do
|
||||
expect do
|
||||
perform_now({ user: })
|
||||
user.reload
|
||||
end.not_to change { user.scanned_follows_at }
|
||||
|
||||
expect(
|
||||
SpecUtil.enqueued_job_args(Domain::Fa::Job::UserFollowsJob),
|
||||
).to match([hash_including(user:, caused_by_entry: @log_entries[0])])
|
||||
end
|
||||
|
||||
it "does not mark scanned_follows_at if page indicates more followed than are recorded" do
|
||||
# all in recent are marked as followed
|
||||
recent_watched_user_url_names.each do |url_name|
|
||||
user.followed_users << create(
|
||||
:domain_user_fa_user,
|
||||
url_name:,
|
||||
name: url_name,
|
||||
)
|
||||
end
|
||||
|
||||
expect(user.followed_users.count).to eq(12)
|
||||
|
||||
# but the page indicates user watching 14
|
||||
expect do
|
||||
perform_now({ user: })
|
||||
user.reload
|
||||
end.not_to change { user.scanned_follows_at }
|
||||
|
||||
expect(user.followed_users.count).to eq(12)
|
||||
expect(
|
||||
SpecUtil.enqueued_job_args(Domain::Fa::Job::UserFollowsJob),
|
||||
).to match([hash_including(user:, caused_by_entry: @log_entries[0])])
|
||||
end
|
||||
|
||||
it "marks scanned_follows_at as recent if all followed users are known and number of followed matches page" do
|
||||
recent_watched_user_url_names.each do |url_name|
|
||||
user.followed_users << create(:domain_user_fa_user, url_name:)
|
||||
end
|
||||
|
||||
user.followed_users << create(:domain_user_fa_user, url_name: "test1")
|
||||
user.followed_users << create(:domain_user_fa_user, url_name: "test2")
|
||||
expect(user.followed_users.count).to eq(14)
|
||||
|
||||
expect do
|
||||
perform_now({ user: })
|
||||
user.reload
|
||||
end.to change { user.scanned_follows_at }.to be_within(3.seconds).of(
|
||||
Time.current,
|
||||
)
|
||||
|
||||
expect(
|
||||
SpecUtil.enqueued_job_args(Domain::Fa::Job::UserFollowsJob),
|
||||
).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "the user is watched by more users than the threshold" do
|
||||
let(:client_mock_config) do
|
||||
[
|
||||
{
|
||||
uri: "https://www.furaffinity.net/user/koul/",
|
||||
status_code: 200,
|
||||
content_type: "text/html",
|
||||
contents:
|
||||
SpecUtil.read_fixture_file(
|
||||
"domain/fa/user_page/user_page_koul_over_threshold_watchers.html",
|
||||
),
|
||||
},
|
||||
]
|
||||
end
|
||||
|
||||
let(:watched_by_user_url_names) do
|
||||
%w[
|
||||
skifmutt
|
||||
k92
|
||||
erri49
|
||||
spaghetti779
|
||||
nonuri
|
||||
mkyosh
|
||||
mystpaww
|
||||
retrorinx
|
||||
aurorethedire
|
||||
rasssss
|
||||
coyotesolo
|
||||
commissarisador
|
||||
]
|
||||
end
|
||||
|
||||
let(:user) { create(:domain_user_fa_user, url_name: "koul") }
|
||||
|
||||
it "does not mark scanned_followed_by_at as recent if over threshold" do
|
||||
expect do
|
||||
perform_now({ user: })
|
||||
user.reload
|
||||
end.not_to change { user.scanned_followed_by_at }
|
||||
end
|
||||
|
||||
it "marks scanned_followed_by_at as recent if all watched by users are known and number of watched by matches page" do
|
||||
watched_by_user_url_names.each do |url_name|
|
||||
user.followed_by_users << create(:domain_user_fa_user, url_name:)
|
||||
end
|
||||
|
||||
# 25 recorded, so make 25-12 more
|
||||
(25 - 12).times do |idx|
|
||||
user.followed_by_users << create(
|
||||
:domain_user_fa_user,
|
||||
url_name: "test#{idx}",
|
||||
)
|
||||
end
|
||||
|
||||
expect do
|
||||
perform_now({ user: })
|
||||
user.reload
|
||||
end.to change { user.scanned_followed_by_at }.to be_within(3.seconds).of(
|
||||
Time.current,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "the user has no recent favories" do
|
||||
let(:client_mock_config) do
|
||||
[
|
||||
|
||||
@@ -500,8 +500,26 @@ describe Domain::Fa::Parser::Page do
|
||||
assert_equal "Nothing special, just practice.", sub.description_html.strip
|
||||
end
|
||||
|
||||
it "parses num_watched_by and num_watching" do
|
||||
parser =
|
||||
get_parser_at(
|
||||
Rails.root.join(
|
||||
"test/fixtures/files/domain/fa/user_page/user_page_koul_over_threshold_watchers.html",
|
||||
),
|
||||
)
|
||||
assert_page_type parser, :probably_user_page?
|
||||
up = parser.user_page
|
||||
assert_equal 25, up.num_watched_by
|
||||
assert_equal 14, up.num_watching
|
||||
end
|
||||
|
||||
def get_parser(file, require_logged_in: true)
|
||||
path = File.join("domain/fa/parser/redux", file)
|
||||
get_parser_at(path, require_logged_in:)
|
||||
end
|
||||
|
||||
def get_parser_at(path, require_logged_in: true)
|
||||
path = path.to_s
|
||||
contents =
|
||||
SpecUtil.read_fixture_file(path) || raise("Couldn't open #{path}")
|
||||
parser =
|
||||
@@ -511,7 +529,7 @@ describe Domain::Fa::Parser::Page do
|
||||
)
|
||||
assert_equal Domain::Fa::Parser::Page::VERSION_2,
|
||||
parser.page_version,
|
||||
"parser version mismatch for file #{file}"
|
||||
"parser version mismatch for file #{path}"
|
||||
parser
|
||||
end
|
||||
end
|
||||
|
||||
1964
test/fixtures/files/domain/fa/user_page/user_page_koul_over_threshold_watchers.html
vendored
Normal file
1964
test/fixtures/files/domain/fa/user_page/user_page_koul_over_threshold_watchers.html
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user