|
# retoor <retoor@molodetz.nl>
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
from devplacepy.cache import TTLCache
|
|
from devplacepy.config import (
|
|
AWARD_DISPLAY_HOURS_DEFAULT,
|
|
AWARD_GIVE_COOLDOWN_HOURS_DEFAULT,
|
|
AWARD_RECEIVE_COOLDOWN_HOURS_DEFAULT,
|
|
)
|
|
from .core import db
|
|
from .pagination import build_pagination
|
|
from .settings import get_int_setting
|
|
from .core import get_table, _now_iso
|
|
from .users import get_users_by_uids
|
|
from .content import resolve_by_slug
|
|
from .soft_delete import soft_delete, soft_delete_in
|
|
|
|
AWARDS_PER_PAGE = 12
|
|
|
|
|
|
def _awards_table():
|
|
return get_table("awards")
|
|
|
|
|
|
def award_give_cooldown_hours() -> int:
|
|
return max(1, get_int_setting("award_give_cooldown_hours", AWARD_GIVE_COOLDOWN_HOURS_DEFAULT))
|
|
|
|
|
|
def award_receive_cooldown_hours() -> int:
|
|
return max(
|
|
1, get_int_setting("award_receive_cooldown_hours", AWARD_RECEIVE_COOLDOWN_HOURS_DEFAULT)
|
|
)
|
|
|
|
|
|
def award_display_hours() -> int:
|
|
return max(1, get_int_setting("award_display_hours", AWARD_DISPLAY_HOURS_DEFAULT))
|
|
|
|
|
|
def _cooldown_cutoff(hours: int) -> str:
|
|
return (datetime.now(timezone.utc) - timedelta(hours=hours)).isoformat()
|
|
|
|
|
|
def has_giver_cooldown(giver_uid: str) -> bool:
|
|
if not giver_uid or "awards" not in db.tables:
|
|
return False
|
|
cutoff = _cooldown_cutoff(award_give_cooldown_hours())
|
|
row = _awards_table().find_one(
|
|
giver_uid=giver_uid, deleted_at=None, created_at={">=": cutoff}
|
|
)
|
|
return row is not None
|
|
|
|
|
|
def has_receiver_cooldown(receiver_uid: str) -> bool:
|
|
if not receiver_uid or "awards" not in db.tables:
|
|
return False
|
|
cutoff = _cooldown_cutoff(award_receive_cooldown_hours())
|
|
row = _awards_table().find_one(
|
|
receiver_uid=receiver_uid, deleted_at=None, created_at={">=": cutoff}
|
|
)
|
|
return row is not None
|
|
|
|
|
|
def can_receive_award(receiver_uid: str) -> bool:
|
|
return not has_receiver_cooldown(receiver_uid)
|
|
|
|
|
|
def can_give_award(giver_uid: str, receiver_uid: str) -> bool:
|
|
if not giver_uid or not receiver_uid or giver_uid == receiver_uid:
|
|
return False
|
|
return not has_giver_cooldown(giver_uid) and not has_receiver_cooldown(receiver_uid)
|
|
|
|
|
|
def _published_filter():
|
|
return {"deleted_at": None, "generated_at": {">": ""}}
|
|
|
|
|
|
def count_published_awards(receiver_uid: str) -> int:
|
|
if not receiver_uid or "awards" not in db.tables:
|
|
return 0
|
|
return _awards_table().count(receiver_uid=receiver_uid, **_published_filter())
|
|
|
|
|
|
def _latest_published(receiver_uid: str):
|
|
if not receiver_uid or "awards" not in db.tables:
|
|
return None
|
|
rows = list(
|
|
_awards_table().find(
|
|
receiver_uid=receiver_uid,
|
|
deleted_at=None,
|
|
generated_at={">": ""},
|
|
order_by=["-generated_at"],
|
|
_limit=1,
|
|
)
|
|
)
|
|
return rows[0] if rows else None
|
|
|
|
|
|
def recompute_user_award_stats(receiver_uid: str) -> None:
|
|
if not receiver_uid or "users" not in db.tables:
|
|
return
|
|
count = count_published_awards(receiver_uid)
|
|
latest = _latest_published(receiver_uid)
|
|
users = get_table("users")
|
|
payload = {
|
|
"uid": receiver_uid,
|
|
"award_count": count,
|
|
"last_award_at": latest.get("generated_at") if latest else None,
|
|
"last_award_slug": latest.get("slug") if latest else None,
|
|
"last_award_uid": latest.get("uid") if latest else None,
|
|
}
|
|
users.update(payload, ["uid"])
|
|
|
|
|
|
_prominence_cache = TTLCache(ttl=15, max_size=500)
|
|
|
|
|
|
def award_is_prominent(user: dict | None) -> bool:
|
|
if not user or not user.get("last_award_at") or not user.get("last_award_uid"):
|
|
return False
|
|
cached = _prominence_cache.get(user["last_award_uid"])
|
|
if cached is not None:
|
|
return cached
|
|
prominent = _compute_prominence(user["last_award_uid"])
|
|
_prominence_cache.set(user["last_award_uid"], prominent)
|
|
return prominent
|
|
|
|
|
|
def _compute_prominence(award_uid: str) -> bool:
|
|
award = resolve_by_slug(_awards_table(), award_uid)
|
|
if not award or not award.get("generated_at"):
|
|
return False
|
|
try:
|
|
published = datetime.fromisoformat(award["generated_at"])
|
|
if published.tzinfo is None:
|
|
published = published.replace(tzinfo=timezone.utc)
|
|
except (ValueError, TypeError):
|
|
return False
|
|
window = timedelta(hours=award_display_hours())
|
|
return datetime.now(timezone.utc) - published <= window
|
|
|
|
|
|
def enrich_award(row: dict, givers: dict | None = None) -> dict:
|
|
item = dict(row)
|
|
giver_uid = row.get("giver_uid", "")
|
|
giver = (givers or {}).get(giver_uid) or get_users_by_uids([giver_uid]).get(giver_uid)
|
|
item["giver"] = giver
|
|
item["image_url"] = f"/awards/{row.get('slug', '')}/256"
|
|
item["thumb_url"] = f"/awards/{row.get('slug', '')}/64"
|
|
return item
|
|
|
|
|
|
def get_user_awards(receiver_uid: str, page: int = 1, per_page: int = AWARDS_PER_PAGE):
|
|
if not receiver_uid or "awards" not in db.tables:
|
|
return [], build_pagination(page, 0, per_page)
|
|
table = _awards_table()
|
|
total = table.count(receiver_uid=receiver_uid, **_published_filter())
|
|
offset = max(0, (page - 1) * per_page)
|
|
rows = list(
|
|
table.find(
|
|
receiver_uid=receiver_uid,
|
|
deleted_at=None,
|
|
generated_at={">": ""},
|
|
order_by=["-generated_at"],
|
|
_limit=per_page,
|
|
_offset=offset,
|
|
)
|
|
)
|
|
giver_uids = [row.get("giver_uid") for row in rows if row.get("giver_uid")]
|
|
givers = get_users_by_uids(giver_uids)
|
|
items = [enrich_award(row, givers) for row in rows]
|
|
return items, build_pagination(page, total, per_page)
|
|
|
|
|
|
def get_prominent_award(profile_user: dict) -> dict | None:
|
|
if not award_is_prominent(profile_user):
|
|
return None
|
|
award = resolve_by_slug(_awards_table(), profile_user.get("last_award_uid", ""))
|
|
if not award:
|
|
return None
|
|
return enrich_award(award)
|
|
|
|
|
|
def revoke_award(award_uid: str, admin_uid: str) -> dict | None:
|
|
table = _awards_table()
|
|
row = table.find_one(uid=award_uid)
|
|
if not row or row.get("deleted_at"):
|
|
return None
|
|
stamp = _now_iso()
|
|
attachment_uids = [
|
|
uid
|
|
for uid in (
|
|
row.get("attachment_uid_512"),
|
|
row.get("attachment_uid_256"),
|
|
row.get("attachment_uid_64"),
|
|
)
|
|
if uid
|
|
]
|
|
soft_delete("awards", admin_uid, stamp=stamp, uid=award_uid)
|
|
from devplacepy.attachments import soft_delete_attachments_for
|
|
|
|
soft_delete_attachments_for("award", [award_uid], admin_uid)
|
|
if attachment_uids:
|
|
soft_delete_in("attachments", "uid", attachment_uids, admin_uid, stamp=stamp)
|
|
recompute_user_award_stats(row.get("receiver_uid", ""))
|
|
return row |