aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorstuppie2026-10-26 12:58:14 -0600
committerstuppie2026-10-26 12:58:14 -0600
commitc59786fc91bcf674e0352704cb23031f79ca901c (patch)
tree3141391e9daa5dbd61f8fd3300db4f65d221d5b3
parent550960e7f212aca61f884faeeba2c50881464954 (diff)
downloadgeneralresearch-c59786fc91bcf674e0352704cb23031f79ca901c.tar.gz
generalresearch-c59786fc91bcf674e0352704cb23031f79ca901c.zip
user manager: add set_last_activity method for last_X timestamps
-rw-r--r--generalresearch/managers/thl/user_manager/mysql_user_manager.py34
-rw-r--r--generalresearch/managers/thl/user_manager/user_manager.py13
-rw-r--r--generalresearch/models/thl/user.py18
3 files changed, 49 insertions, 16 deletions
diff --git a/generalresearch/managers/thl/user_manager/mysql_user_manager.py b/generalresearch/managers/thl/user_manager/mysql_user_manager.py
index 6b72edd..920cd22 100644
--- a/generalresearch/managers/thl/user_manager/mysql_user_manager.py
+++ b/generalresearch/managers/thl/user_manager/mysql_user_manager.py
@@ -4,8 +4,9 @@ import logging
import operator
from collections.abc import Collection
from datetime import UTC, datetime
+from enum import StrEnum
from threading import Lock
-from typing import TYPE_CHECKING, Any, Tuple
+from typing import TYPE_CHECKING, Any
from uuid import uuid4
import psycopg
@@ -25,6 +26,13 @@ logger = logging.getLogger()
logger.setLevel(logging.INFO)
+class LastActivityColumn(StrEnum):
+ LAST_SEEN = "last_seen"
+ LAST_TASK_COMPLETED_AT = "last_task_completed_at"
+ LAST_OFFERWALL_REQUESTED_AT = "last_offerwall_requested_at"
+ LAST_PROFILING_SUBMISSION_AT = "last_profiling_submission_at"
+
+
class MysqlUserManager(PostgresManager):
def __init__(
self,
@@ -37,18 +45,22 @@ class MysqlUserManager(PostgresManager):
self.product_id_exists_cache_lock = Lock()
super().__init__(pg_config=pg_config, permissions=permissions)
- def _set_last_seen(self, user: User, conn: Connection | None = None) -> None:
- # Don't call this directly. Use UserManager.set_last_seen()
+ def _set_last_activity(
+ self,
+ user: User,
+ column: LastActivityColumn,
+ conn: Connection | None = None,
+ ) -> None:
+ # Don't call this directly. Use UserManager.set_last_activity().
assert not self.is_read_replica
- now = datetime.now(tz=UTC)
+ column = LastActivityColumn(column)
+
with self.connection(conn) as conn, conn.cursor() as c:
c.execute(
- """
- UPDATE thl_user
- SET last_seen = %s
- WHERE id = %s
- """,
- params=[now, user.user_id],
+ sql.SQL("UPDATE thl_user SET {} = %s WHERE id = %s").format(
+ sql.Identifier(column)
+ ),
+ params=[getattr(user, column), user.user_id],
)
def _set_last_country_ip(
@@ -535,7 +547,7 @@ class MysqlUserManager(PostgresManager):
product_user_id=product_user_id,
email_startswith=email_startswith,
display_name_like=display_name_like,
- allow_global_search=allow_global_search
+ allow_global_search=allow_global_search,
)
if not any([product_user_id, email_startswith, display_name_like]):
# We're filtering on product, team, or business only. So use the
diff --git a/generalresearch/managers/thl/user_manager/user_manager.py b/generalresearch/managers/thl/user_manager/user_manager.py
index 6358e84..baa7c1f 100644
--- a/generalresearch/managers/thl/user_manager/user_manager.py
+++ b/generalresearch/managers/thl/user_manager/user_manager.py
@@ -3,7 +3,7 @@ from __future__ import annotations
import logging
import operator
from collections.abc import Collection
-from datetime import datetime
+from datetime import UTC, datetime
from threading import Lock
from typing import TYPE_CHECKING
@@ -17,6 +17,7 @@ from generalresearch.managers.thl.user_manager.exceptions import (
UserDoesntExistError,
)
from generalresearch.managers.thl.user_manager.mysql_user_manager import (
+ LastActivityColumn,
MysqlUserManager,
)
from generalresearch.managers.thl.user_manager.rate_limit import (
@@ -92,8 +93,16 @@ class UserManager:
self.get_user_cache_lock = Lock()
def set_last_seen(self, user: User) -> None:
+ return self.set_last_activity(user, LastActivityColumn.LAST_SEEN)
+
+ def set_last_activity(self, user: User, column: LastActivityColumn) -> None:
assert Permission.UPDATE in self.sql_permissions, "permission error"
- return self.mysql_user_manager._set_last_seen(user)
+ assert self.redis_user_manager is not None
+ column = LastActivityColumn(column)
+ key = f"user-activity:{user.user_id}:{column.value}"
+ if self.redis_user_manager.client.set(key, "1", nx=True, ex=60):
+ setattr(user, column, datetime.now(tz=UTC))
+ self.mysql_user_manager._set_last_activity(user, column)
def set_last_country_ip(
self, user: User, last_ip: str, last_country_iso: str | None = None
diff --git a/generalresearch/models/thl/user.py b/generalresearch/models/thl/user.py
index 59a14db..ba49104 100644
--- a/generalresearch/models/thl/user.py
+++ b/generalresearch/models/thl/user.py
@@ -77,11 +77,23 @@ class User(BaseModel):
description="When the user was created on the GRL platform.",
)
- # Note: due to cacheing, last_seen might be up to a day out of date!
+ # Note: due to cacheing, last_* timestamps might be up to a day out of date!
+ # Updates are rate-limited to once per minute
last_seen: AwareDatetimeISO | None = Field(
default=None,
- description="When the user was last seen on, or acting on any"
- "part of the GRL platform.",
+ description="When the user last started a task or returned from a marketplace redirect.",
+ )
+ last_task_completed_at: AwareDatetimeISO | None = Field(
+ default=None,
+ description="When the user most recently completed a task.",
+ )
+ last_offerwall_requested_at: AwareDatetimeISO | None = Field(
+ default=None,
+ description="When the user most recently requested an offerwall.",
+ )
+ last_profiling_submission_at: AwareDatetimeISO | None = Field(
+ default=None,
+ description="When the user most recently submitted profiling answers.",
)
# --- Prefetch Fields ---