# retoor <retoor@molodetz.nl>
from sqlalchemy import text
from devplacepy.database import CONSENTS_TABLE, _now_iso, db, get_table
from .agreements import Agreement
from .pending import has_gate_column, not_withdrawn, satisfied_clause
CONSENT_COLUMNS = (
"uid",
"owner_kind",
"owner_id",
"kind",
"version",
"state",
"granted_at",
"withdrawn_at",
"created_at",
"deleted_at",
"deleted_by",
)
def _gate_set_clause(agreement: Agreement) -> str:
users = get_table("users")
assignments = [f"{agreement.gate_column} = :version"]
if agreement.stamp_column and users.has_column(agreement.stamp_column):
assignments.append(f"{agreement.stamp_column} = :now")
return ", ".join(assignments)
def _insert_consent_sql(precondition: str) -> str:
columns = ", ".join(CONSENT_COLUMNS)
sql = (
f"INSERT INTO {CONSENTS_TABLE} ({columns}) "
"SELECT :row_uid, 'user', :uid, :kind, :version, 'granted', "
":now, '', :now, NULL, NULL"
)
if precondition:
return f"{sql} WHERE {precondition}"
return sql
def _consent_params(agreement: Agreement, uid: str, version: str, now: str) -> dict:
from devplacepy.utils import generate_uid
return {
"row_uid": generate_uid(),
"uid": uid,
"kind": agreement.kind,
"version": version,
"now": now,
}
def _claim_gate_column(agreement: Agreement, uid: str, version: str, now: str) -> bool:
claim = (
f"UPDATE users SET {_gate_set_clause(agreement)} "
"WHERE uid = :uid "
f"AND COALESCE({agreement.gate_column}, '') != :version "
f"AND {not_withdrawn('users.uid')}"
)
params = _consent_params(agreement, uid, version, now)
with db:
claimed = db.executable.execute(text(claim), params).rowcount
if claimed != 1:
return False
db.executable.execute(text(_insert_consent_sql("")), params)
return True
def _claim_ledger(agreement: Agreement, uid: str, version: str, now: str) -> bool:
precondition = (
f"{not_withdrawn(':uid')} AND {satisfied_clause(agreement, ':uid', '')}"
)
params = _consent_params(agreement, uid, version, now)
with db:
written = db.executable.execute(
text(_insert_consent_sql(precondition)), params
).rowcount
return written == 1
def _record(agreement: Agreement, user: dict, version: str) -> None:
from devplacepy.services.audit import record as audit
uid = user["uid"]
username = user.get("username") or ""
links = [audit.target("user", uid, username)]
if agreement.kind == "terms":
audit.record_system(
"terms.accept",
actor_kind="service",
actor_username="acceptance",
target_type="user",
target_uid=uid,
target_label=username,
new_value=version,
summary=f"{username} accepted terms version {version}",
links=links,
)
return
audit.record_system(
"consent.grant",
actor_kind="service",
actor_username="acceptance",
target_type="user",
target_uid=uid,
target_label=username,
new_value="granted",
metadata={"kind": agreement.kind},
summary=f"granted {agreement.kind} consent for {username}",
links=links,
)
def converge_user(agreement: Agreement, user: dict, version: str) -> bool:
uid = user.get("uid")
if not uid:
return False
now = _now_iso()
if has_gate_column(agreement):
won = _claim_gate_column(agreement, uid, version, now)
else:
won = _claim_ledger(agreement, uid, version, now)
if not won:
return False
_record(agreement, user, version)
return True