# retoor 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