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