cache get_alt_graph for 1 hour

pull/83/head
Aevann 2022-12-22 22:44:37 +02:00
parent 8554317588
commit 92cbe64ac0
9 changed files with 54 additions and 54 deletions

View File

@ -1,6 +1,6 @@
import random import random
from operator import * from operator import *
from typing import Callable, Union from typing import Union
import pyotp import pyotp
from sqlalchemy import Column, ForeignKey from sqlalchemy import Column, ForeignKey
@ -470,46 +470,6 @@ class User(Base):
def age(self): def age(self):
return int(time.time()) - self.created_utc return int(time.time()) - self.created_utc
@lazy
def get_alt_graph(self, db:scoped_session, alt_filter:Optional[Callable[[Query], Query]]=None, **kwargs) -> Query:
'''
Gets the full graph of alts (optionally filtering `Alt` objects by criteria using a callable,
such as by a date to only get alts from a certain date) as a query of users that can be filtered
further. This function filters alts marked as deleted by default, pass `include_deleted=True` to
disable this behavior and include delinked alts.
'''
if not alt_filter:
alt_filter = lambda q:q
if not kwargs.get('include_deleted', False):
deleted_filter = lambda q:q.filter(Alt.deleted == False)
else:
deleted_filter = lambda q:q
combined_filter = lambda q:deleted_filter(alt_filter(q))
alt_graph_cte = db.query(literal(self.id).label('user_id')).select_from(Alt).cte('alt_graph', recursive=True)
alt_graph_cte_inner = combined_filter(db.query(
case(
(Alt.user1 == alt_graph_cte.c.user_id, Alt.user2),
(Alt.user2 == alt_graph_cte.c.user_id, Alt.user1),
)
).select_from(Alt, alt_graph_cte).filter(
or_(alt_graph_cte.c.user_id == Alt.user1, alt_graph_cte.c.user_id == Alt.user2)
))
alt_graph_cte = alt_graph_cte.union(alt_graph_cte_inner)
return db.query(User).filter(User.id == alt_graph_cte.c.user_id)
@property
@lazy
def alts_patron(self):
for u in self.get_alt_graph(g.db):
if not u._deleted and u.patron: return True
return False
@property
@lazy @lazy
def follow_count(self): def follow_count(self):
return g.db.query(Follow).filter_by(user_id=self.id).count() return g.db.query(Follow).filter_by(user_id=self.id).count()

View File

@ -17,6 +17,7 @@ from files.helpers.settings import get_settings, toggle_setting
from files.helpers.useractions import * from files.helpers.useractions import *
from files.routes.routehelpers import check_for_alts from files.routes.routehelpers import check_for_alts
from files.routes.wrappers import * from files.routes.wrappers import *
from files.routes.routehelpers import get_alt_graph
from .front import frontlist from .front import frontlist
@ -261,7 +262,7 @@ def revert_actions(v:User, username):
send_repeatable_notification(user.id, f"@{v.username} (a site admin) has unbanned you!") send_repeatable_notification(user.id, f"@{v.username} (a site admin) has unbanned you!")
g.db.add(user) g.db.add(user)
for u in user.get_alt_graph(g.db): for u in get_alt_graph(user.id):
u.shadowbanned = None u.shadowbanned = None
u.unban_utc = 0 u.unban_utc = 0
u.ban_reason = None u.ban_reason = None
@ -678,7 +679,7 @@ def alt_votes_get(v):
@admin_level_required(PERMS['USER_LINK']) @admin_level_required(PERMS['USER_LINK'])
def admin_view_alts(v:User, username=None): def admin_view_alts(v:User, username=None):
u = get_user(username or request.values.get('username'), graceful=True) u = get_user(username or request.values.get('username'), graceful=True)
return render_template('admin/alts.html', v=v, u=u, alts=u.get_alt_graph(g.db) if u else None) return render_template('admin/alts.html', v=v, u=u, alts=get_alt_graph(u.id) if u else None)
@app.post('/@<username>/alts/') @app.post('/@<username>/alts/')
@limiter.limit(DEFAULT_RATELIMIT_SLOWER) @limiter.limit(DEFAULT_RATELIMIT_SLOWER)
@ -843,7 +844,7 @@ def unshadowban(user_id, v):
if not user.is_banned: user.ban_reason = None if not user.is_banned: user.ban_reason = None
g.db.add(user) g.db.add(user)
for alt in user.get_alt_graph(g.db): for alt in get_alt_graph(user.id):
alt.shadowbanned = None alt.shadowbanned = None
if not alt.is_banned: alt.ban_reason = None if not alt.is_banned: alt.ban_reason = None
g.db.add(alt) g.db.add(alt)
@ -920,7 +921,7 @@ def ban_user(user_id, v):
user.ban(admin=v, reason=reason, days=days) user.ban(admin=v, reason=reason, days=days)
if request.values.get("alts"): if request.values.get("alts"):
for x in user.get_alt_graph(g.db): for x in get_alt_graph(user.id):
if x.admin_level > v.admin_level: if x.admin_level > v.admin_level:
continue continue
x.ban(admin=v, reason=reason, days=days) x.ban(admin=v, reason=reason, days=days)
@ -1052,7 +1053,7 @@ def unban_user(user_id, v):
send_repeatable_notification(user.id, f"@{v.username} (a site admin) has unbanned you!") send_repeatable_notification(user.id, f"@{v.username} (a site admin) has unbanned you!")
g.db.add(user) g.db.add(user)
for x in user.get_alt_graph(g.db): for x in get_alt_graph(user.id):
if x.is_banned: send_repeatable_notification(x.id, f"@{v.username} (a site admin) has unbanned you!") if x.is_banned: send_repeatable_notification(x.id, f"@{v.username} (a site admin) has unbanned you!")
x.is_banned = None x.is_banned = None
x.unban_utc = 0 x.unban_utc = 0

View File

@ -12,6 +12,7 @@ from files.helpers.assetcache import assetcache_path
from files.helpers.config.const import * from files.helpers.config.const import *
from files.helpers.settings import get_settings from files.helpers.settings import get_settings
from files.helpers.sorting_and_time import make_age_string from files.helpers.sorting_and_time import make_age_string
from files.routes.routehelpers import get_alt_graph
from files.routes.routehelpers import get_formkey from files.routes.routehelpers import get_formkey
from files.__main__ import app, cache from files.__main__ import app, cache
@ -95,5 +96,5 @@ def inject_constants():
"HOUSE_JOIN_COST":HOUSE_JOIN_COST, "HOUSE_SWITCH_COST":HOUSE_SWITCH_COST, "IMAGE_FORMATS":IMAGE_FORMATS, "HOUSE_JOIN_COST":HOUSE_JOIN_COST, "HOUSE_SWITCH_COST":HOUSE_SWITCH_COST, "IMAGE_FORMATS":IMAGE_FORMATS,
"PAGE_SIZES":PAGE_SIZES, "THEMES":THEMES, "COMMENT_SORTS":COMMENT_SORTS, "SORTS":SORTS, "PAGE_SIZES":PAGE_SIZES, "THEMES":THEMES, "COMMENT_SORTS":COMMENT_SORTS, "SORTS":SORTS,
"TIME_FILTERS":TIME_FILTERS, "HOUSES":HOUSES, "TIERS_ID_TO_NAME":TIERS_ID_TO_NAME, "TIME_FILTERS":TIME_FILTERS, "HOUSES":HOUSES, "TIERS_ID_TO_NAME":TIERS_ID_TO_NAME,
"DEFAULT_CONFIG_VALUE":DEFAULT_CONFIG_VALUE, "IS_LOCALHOST":IS_LOCALHOST, "BACKGROUND_CATEGORIES":BACKGROUND_CATEGORIES, "PAGE_SIZE":PAGE_SIZE, "TAGLINE":TAGLINE, "HOLIDAY_EVENT":HOLIDAY_EVENT "DEFAULT_CONFIG_VALUE":DEFAULT_CONFIG_VALUE, "IS_LOCALHOST":IS_LOCALHOST, "BACKGROUND_CATEGORIES":BACKGROUND_CATEGORIES, "PAGE_SIZE":PAGE_SIZE, "TAGLINE":TAGLINE, "HOLIDAY_EVENT":HOLIDAY_EVENT, "get_alt_graph":get_alt_graph
} }

View File

@ -2,13 +2,17 @@ import time
import secrets import secrets
from random import randint from random import randint
from typing import Optional, Union from typing import Optional, Union, Callable
from sqlalchemy.orm import aliased, deferred, Query
from sqlalchemy.sql import case, literal
from sqlalchemy.sql.expression import or_
from flask import g, session from flask import g, session
from files.classes import Alt, Comment, User, Submission from files.classes import Alt, Comment, User, Submission
from files.helpers.config.const import * from files.helpers.config.const import *
from files.helpers.security import generate_hash, validate_hash from files.helpers.security import generate_hash, validate_hash
from files.__main__ import cache
def get_raw_formkey(u:User): def get_raw_formkey(u:User):
if not session.get("session_id"): if not session.get("session_id"):
@ -25,6 +29,38 @@ def validate_formkey(u:User, formkey:Optional[str]) -> bool:
if not formkey: return False if not formkey: return False
return validate_hash(get_raw_formkey(u), formkey) return validate_hash(get_raw_formkey(u), formkey)
@cache.memoize(timeout=3600)
def get_alt_graph(uid:int, alt_filter:Optional[Callable[[Query], Query]]=None, **kwargs) -> Query:
'''
Gets the full graph of alts (optionally filtering `Alt` objects by criteria using a callable,
such as by a date to only get alts from a certain date) as a query of users that can be filtered
further. This function filters alts marked as deleted by default, pass `include_deleted=True` to
disable this behavior and include delinked alts.
'''
if not alt_filter:
alt_filter = lambda q:q
if not kwargs.get('include_deleted', False):
deleted_filter = lambda q:q.filter(Alt.deleted == False)
else:
deleted_filter = lambda q:q
combined_filter = lambda q:deleted_filter(alt_filter(q))
alt_graph_cte = g.db.query(literal(uid).label('user_id')).select_from(Alt).cte('alt_graph', recursive=True)
alt_graph_cte_inner = combined_filter(g.db.query(
case(
(Alt.user1 == alt_graph_cte.c.user_id, Alt.user2),
(Alt.user2 == alt_graph_cte.c.user_id, Alt.user1),
)
).select_from(Alt, alt_graph_cte).filter(
or_(alt_graph_cte.c.user_id == Alt.user1, alt_graph_cte.c.user_id == Alt.user2)
))
alt_graph_cte = alt_graph_cte.union(alt_graph_cte_inner)
return g.db.query(User).filter(User.id == alt_graph_cte.c.user_id, User.id != uid).order_by(User.username).all()
def check_for_alts(current:User, include_current_session=True): def check_for_alts(current:User, include_current_session=True):
current_id = current.id current_id = current.id
ids = [x[0] for x in g.db.query(User.id).all()] ids = [x[0] for x in g.db.query(User.id).all()]
@ -65,7 +101,7 @@ def check_for_alts(current:User, include_current_session=True):
if include_current_session: if include_current_session:
session["history"] = list(past_accs) session["history"] = list(past_accs)
g.db.flush() g.db.flush()
for u in current.get_alt_graph(g.db): for u in get_alt_graph(current.id):
if u._alt_deleted: continue if u._alt_deleted: continue
if u.shadowbanned and current.id not in DONT_SHADOWBAN: if u.shadowbanned and current.id not in DONT_SHADOWBAN:
current.shadowbanned = u.shadowbanned current.shadowbanned = u.shadowbanned

View File

@ -312,7 +312,7 @@ def transfer_currency(v:User, username:str, currency_name:Literal['coins', 'mars
if amount is None or amount <= 0: abort(400, f"Invalid number of {currency_name}") if amount is None or amount <= 0: abort(400, f"Invalid number of {currency_name}")
if amount < MIN_CURRENCY_TRANSFER: abort(400, f"You have to gift at least {MIN_CURRENCY_TRANSFER} {currency_name}") if amount < MIN_CURRENCY_TRANSFER: abort(400, f"You have to gift at least {MIN_CURRENCY_TRANSFER} {currency_name}")
tax = 0 tax = 0
if apply_tax and not v.patron and not receiver.patron and not v.alts_patron and not receiver.alts_patron: if apply_tax and not v.patron and not receiver.patron:
tax = math.ceil(amount*TAX_PCT) tax = math.ceil(amount*TAX_PCT)
reason = request.values.get("reason", "").strip() reason = request.values.get("reason", "").strip()

View File

@ -89,6 +89,8 @@ def vote_post_comment(target_id, new, v, cls, vote_cls):
if v.id == target.author.id: if v.id == target.author.id:
coin_delta = 0 coin_delta = 0
v_alts_id = [x.id for x in v.get_alt_graph(self.id) if not x._alt_deleted]
alt = False alt = False
if target.author.id in v.alt_ids or v.id in target.author.alt_ids: if target.author.id in v.alt_ids or v.id in target.author.alt_ids:
coin_delta = -1 coin_delta = -1

View File

@ -49,7 +49,7 @@
{% if v.admin_level >= PERMS['USER_LINK'] %} {% if v.admin_level >= PERMS['USER_LINK'] %}
<h2>Link Accounts</h2> <h2>Link Accounts</h2>
{% if u2 in u1.get_alt_graph(g.db) %} {% if u2 in get_alt_graph(u.id) %}
<p>Accounts are <a href="/@{{u1.username}}/alts">known alts</a> of each other.</p> <p>Accounts are <a href="/@{{u1.username}}/alts">known alts</a> of each other.</p>
{% else %} {% else %}

View File

@ -225,7 +225,7 @@
<span id="profile--alts">Alts:</span> <span id="profile--alts">Alts:</span>
{% endif %} {% endif %}
<ul id="profile--alts-list"> <ul id="profile--alts-list">
{% for account in u.get_alt_graph(g.db) if not account._alt_deleted and (v.can_see_shadowbanned or not account.shadowbanned) %} {% for account in get_alt_graph(u.id) if not account._alt_deleted and (v.can_see_shadowbanned or not account.shadowbanned) %}
<li><a href="{{account.url}}">@{{account.username}}</a>{% if account._is_manual %} [m]{% endif %}</li> <li><a href="{{account.url}}">@{{account.username}}</a>{% if account._is_manual %} [m]{% endif %}</li>
{% endfor %} {% endfor %}
</ul> </ul>
@ -489,7 +489,7 @@
<span id="profile-mobile--alts">Alts:</span> <span id="profile-mobile--alts">Alts:</span>
{% endif %} {% endif %}
<ul id="profile-mobile--alts-list"> <ul id="profile-mobile--alts-list">
{% for account in u.get_alt_graph(g.db) if not account._alt_deleted and (v.can_see_shadowbanned or not account.shadowbanned) %} {% for account in get_alt_graph(u.id) if not account._alt_deleted and (v.can_see_shadowbanned or not account.shadowbanned) %}
<li><a href="{{account.url}}">@{{account.username}}</a>{% if account._is_manual %} [m]{% endif %}</li> <li><a href="{{account.url}}">@{{account.username}}</a>{% if account._is_manual %} [m]{% endif %}</li>
{% endfor %} {% endfor %}
</ul> </ul>

View File

@ -30,7 +30,7 @@
{% endif %} {% endif %}
{% if v %} {% if v %}
<div id='tax' class="d-none">{% if v.patron or u.patron or v.alts_patron or u.alts_patron %}0{% else %}0.03{% endif %}</div> <div id='tax' class="d-none">{% if v.patron or u.patron %}0{% else %}0.03{% endif %}</div>
<script defer src="{{'js/userpage_v.js' | asset}}"></script> <script defer src="{{'js/userpage_v.js' | asset}}"></script>
<div id="username" class="d-none">{{u.username}}</div> <div id="username" class="d-none">{{u.username}}</div>
{% endif %} {% endif %}