This commit is contained in:
parent
411a2a96d0
commit
482bb5afa3
|
|
@ -0,0 +1,118 @@
|
|||
<?php
|
||||
// allusers.php - inline editing table for all users
|
||||
include_once("peter_db.php");
|
||||
$peter_db = new peter_db();
|
||||
$c = $peter_db->getConnection();
|
||||
mysqli_set_charset($c, 'utf8');
|
||||
|
||||
// Fetch departments
|
||||
$dept_opts = [];
|
||||
$dept_q = "SELECT id, name FROM gavi_departments ORDER BY name";
|
||||
$dept_r = mysqli_query($c, $dept_q);
|
||||
while ($row = mysqli_fetch_assoc($dept_r)) { $dept_opts[] = $row; }
|
||||
|
||||
$users = [];
|
||||
$q = "SELECT cu.id, cu.name, cu.email, cu.goo, cud.department_id
|
||||
FROM conf_users cu
|
||||
LEFT JOIN conf_user_departments cud ON cud.user_id = cu.id
|
||||
ORDER BY cu.name";
|
||||
$r = mysqli_query($c, $q);
|
||||
while ($row = mysqli_fetch_assoc($r)) { $users[] = $row; }
|
||||
|
||||
// Handle AJAX updates
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$uid = isset($_POST['id']) ? intval($_POST['id']) : 0;
|
||||
$field = isset($_POST['field']) ? $_POST['field'] : '';
|
||||
$value = isset($_POST['value']) ? trim($_POST['value']) : '';
|
||||
|
||||
if ($uid > 0 && in_array($field, ['name','email','goo'])) {
|
||||
$stmt = mysqli_prepare($c, "UPDATE conf_users SET $field = ? WHERE id = ?");
|
||||
mysqli_stmt_bind_param($stmt, "si", $value, $uid);
|
||||
mysqli_stmt_execute($stmt);
|
||||
echo json_encode(['ok' => true]);
|
||||
exit;
|
||||
}
|
||||
if ($uid > 0 && $field === 'department_id') {
|
||||
$dept = intval($value);
|
||||
if ($dept > 0) {
|
||||
$stmt = mysqli_prepare($c, "INSERT INTO conf_user_departments (user_id, department_id) VALUES (?, ?) ON DUPLICATE KEY UPDATE department_id = VALUES(department_id)");
|
||||
mysqli_stmt_bind_param($stmt, "ii", $uid, $dept);
|
||||
mysqli_stmt_execute($stmt);
|
||||
} else {
|
||||
$stmt = mysqli_prepare($c, "DELETE FROM conf_user_departments WHERE user_id = ?");
|
||||
mysqli_stmt_bind_param($stmt, "i", $uid);
|
||||
mysqli_stmt_execute($stmt);
|
||||
}
|
||||
echo json_encode(['ok' => true]);
|
||||
exit;
|
||||
}
|
||||
echo json_encode(['ok' => false, 'err' => 'invalid']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$MY_TITLE = "All Users";
|
||||
$MY_CRUMB = "All Users";
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div class="bg-white p-4 rounded shadow">
|
||||
<table class="min-w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b">
|
||||
<th class="text-left py-2 pr-3">Name</th>
|
||||
<th class="text-left py-2 pr-3">Email</th>
|
||||
<th class="text-left py-2 pr-3">GOO</th>
|
||||
<th class="text-left py-2 pr-3">Department</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($users as $u) { ?>
|
||||
<tr class="border-b" data-id="<?= $u['id'] ?>">
|
||||
<td contenteditable="true" data-field="name" class="py-1 pr-3"><?= htmlspecialchars($u['name']) ?></td>
|
||||
<td contenteditable="true" data-field="email" class="py-1 pr-3"><?= htmlspecialchars($u['email']) ?></td>
|
||||
<td contenteditable="true" data-field="goo" class="py-1 pr-3"><?= htmlspecialchars($u['goo']) ?></td>
|
||||
<td class="py-1 pr-3">
|
||||
<select data-field="department_id" class="border rounded px-2 py-1">
|
||||
<option value="0">-- None --</option>
|
||||
<?php foreach ($dept_opts as $d) { ?>
|
||||
<option value="<?= $d['id'] ?>" <?= ($u['department_id'] == $d['id']) ? 'selected' : '' ?>><?= htmlspecialchars($d['name']) ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
const rows = document.querySelectorAll('tbody tr');
|
||||
function postUpdate(id, field, value) {
|
||||
const form = new FormData();
|
||||
form.append('id', id);
|
||||
form.append('field', field);
|
||||
form.append('value', value);
|
||||
fetch(window.location.href, { method: 'POST', body: form })
|
||||
.then(r => r.json())
|
||||
.then(j => { if(!j.ok){ alert('Save failed'); } })
|
||||
.catch(() => alert('Save failed'));
|
||||
}
|
||||
rows.forEach(row => {
|
||||
const id = row.getAttribute('data-id');
|
||||
row.querySelectorAll('[contenteditable]').forEach(cell => {
|
||||
cell.addEventListener('blur', () => {
|
||||
postUpdate(id, cell.dataset.field, cell.innerText.trim());
|
||||
});
|
||||
});
|
||||
row.querySelectorAll('select[data-field]').forEach(sel => {
|
||||
sel.addEventListener('change', () => {
|
||||
postUpdate(id, sel.dataset.field, sel.value);
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?php
|
||||
$CONTENT = ob_get_clean();
|
||||
include 'layout.php';
|
||||
|
|
@ -0,0 +1,251 @@
|
|||
#!/usr/bin/env python3
|
||||
import datetime as dt
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import html
|
||||
import unicodedata
|
||||
|
||||
try:
|
||||
import pymysql
|
||||
except ImportError: # pragma: no cover - depends on local env
|
||||
pymysql = None
|
||||
|
||||
try:
|
||||
from bs4 import BeautifulSoup
|
||||
except ImportError: # pragma: no cover - depends on local env
|
||||
BeautifulSoup = None
|
||||
|
||||
|
||||
CONFIG_FILE = "peter_db.php"
|
||||
|
||||
|
||||
def load_db_config():
|
||||
if not os.path.exists(CONFIG_FILE):
|
||||
raise FileNotFoundError(f"Missing {CONFIG_FILE}")
|
||||
with open(CONFIG_FILE, "r", encoding="utf-8") as fh:
|
||||
text = fh.read()
|
||||
def pick(key):
|
||||
match = re.search(rf"\\$this->{key}\\s*=\\s*'([^']*)'", text)
|
||||
return match.group(1) if match else None
|
||||
cfg = {
|
||||
"host": pick("DBServer"),
|
||||
"user": pick("DBUser"),
|
||||
"password": pick("DBPass"),
|
||||
"database": pick("DBName"),
|
||||
}
|
||||
missing = [k for k, v in cfg.items() if not v]
|
||||
if missing:
|
||||
raise ValueError(f"Missing DB config values: {', '.join(missing)}")
|
||||
return cfg
|
||||
|
||||
|
||||
def get_conn():
|
||||
from secretdir import dbhost, dbuser,dbpassword
|
||||
if pymysql is None:
|
||||
raise RuntimeError("pymysql is not installed. Install it with: pip install pymysql")
|
||||
#cfg = load_db_config()
|
||||
return pymysql.connect(
|
||||
host=dbhost,
|
||||
user=dbuser,
|
||||
password=dbpassword,
|
||||
database="db",
|
||||
charset="utf8mb4",
|
||||
cursorclass=pymysql.cursors.DictCursor,
|
||||
)
|
||||
|
||||
|
||||
def parse_day(input_str):
|
||||
input_str = input_str.strip()
|
||||
for fmt in ("%m/%d/%y", "%m/%d/%Y", "%Y-%m-%d"):
|
||||
try:
|
||||
d = dt.datetime.strptime(input_str, fmt).date()
|
||||
if d.year < 100:
|
||||
d = d.replace(year=2000 + d.year)
|
||||
return d
|
||||
except ValueError:
|
||||
continue
|
||||
raise ValueError("Expected a date like 1/22/26 or 2026-01-22")
|
||||
|
||||
|
||||
def json_filename(day):
|
||||
return f"sessions_{day.strftime('%Y-%m-%d')}.json"
|
||||
|
||||
|
||||
def normalize_value(val):
|
||||
if isinstance(val, (dt.datetime, dt.date)):
|
||||
if isinstance(val, dt.date) and not isinstance(val, dt.datetime):
|
||||
return val.strftime("%Y-%m-%d")
|
||||
return val.strftime("%Y-%m-%d %H:%M:%S")
|
||||
return val
|
||||
|
||||
|
||||
def sql_literal(val):
|
||||
if val is None:
|
||||
return "NULL"
|
||||
if isinstance(val, (dt.datetime, dt.date)):
|
||||
if isinstance(val, dt.date) and not isinstance(val, dt.datetime):
|
||||
return f"'{val.strftime('%Y-%m-%d')}'"
|
||||
return f"'{val.strftime('%Y-%m-%d %H:%M:%S')}'"
|
||||
if isinstance(val, (int, float)) and not isinstance(val, bool):
|
||||
return str(val)
|
||||
text = str(val)
|
||||
if pymysql is not None:
|
||||
text = pymysql.converters.escape_string(text)
|
||||
return "'" + text + "'"
|
||||
|
||||
|
||||
def clean_desc_for_export(text):
|
||||
if text is None:
|
||||
return None
|
||||
if BeautifulSoup is None:
|
||||
raise RuntimeError("beautifulsoup4 is not installed. Install it with: pip install beautifulsoup4")
|
||||
allowed = ["b", "i", "em", "strong", "ul", "ol", "li", "p", "br"]
|
||||
s = unicodedata.normalize("NFKD", str(text))
|
||||
# Remove script/style blocks entirely.
|
||||
s = re.sub(r"(?is)<(script|style)[^>]*>.*?</\\1>", "", s)
|
||||
soup = BeautifulSoup(s, "html.parser")
|
||||
# Drop disallowed tags, keep allowed tags without attributes.
|
||||
for tag in soup.find_all(True):
|
||||
if tag.name not in allowed:
|
||||
tag.unwrap()
|
||||
else:
|
||||
tag.attrs = {}
|
||||
s = "".join(str(x) for x in soup.contents)
|
||||
s = html.unescape(s)
|
||||
s = s.replace("\u00a0", " ")
|
||||
s = s.replace("\u200b", "")
|
||||
'''s = re.sub(r"[ \\t\\f\\v]+", " ", s)
|
||||
s = re.sub(r"\\s*<br>\\s*", "<br>", s)
|
||||
s = re.sub(r"(?:<br>\\s*){3,}", "<br><br>", s)
|
||||
s = re.sub(r"\\s*</p>\\s*", "</p>", s)
|
||||
s = re.sub(r"\\s*<p>\\s*", "<p>", s)'''
|
||||
return s.strip()
|
||||
|
||||
|
||||
def db_dump_day():
|
||||
day = parse_day(input("Enter day (e.g., 1/22/26): "))
|
||||
fname = json_filename(day)
|
||||
conn = get_conn()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT * FROM conf_sessions WHERE DATE(starttime) = %s ORDER BY starttime, id",
|
||||
(day.strftime("%Y-%m-%d"),),
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
finally:
|
||||
conn.close()
|
||||
with open(fname, "w", encoding="utf-8") as fh:
|
||||
json.dump(rows, fh, indent=2, sort_keys=True, default=str)
|
||||
print(f"Wrote {len(rows)} session(s) to {fname}")
|
||||
|
||||
|
||||
def db_reload_day_json():
|
||||
day = parse_day(input("Enter day (e.g., 1/22/26): "))
|
||||
fname = json_filename(day)
|
||||
if not os.path.exists(fname):
|
||||
raise FileNotFoundError(f"Missing {fname}")
|
||||
with open(fname, "r", encoding="utf-8") as fh:
|
||||
rows = json.load(fh)
|
||||
if not isinstance(rows, list):
|
||||
raise ValueError("JSON file must be a list of session rows")
|
||||
conn = get_conn()
|
||||
updated = 0
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
for row in rows:
|
||||
if "id" not in row:
|
||||
continue
|
||||
ses_id = row["id"]
|
||||
cur.execute("SELECT * FROM conf_sessions WHERE id = %s", (ses_id,))
|
||||
current = cur.fetchone()
|
||||
if not current:
|
||||
print(f"Skipping missing id {ses_id}")
|
||||
continue
|
||||
changes = {}
|
||||
for key, new_val in row.items():
|
||||
if key == "id" or key not in current:
|
||||
continue
|
||||
cur_val = current[key]
|
||||
norm_new = normalize_value(new_val)
|
||||
norm_cur = normalize_value(cur_val)
|
||||
if norm_new != norm_cur:
|
||||
changes[key] = new_val
|
||||
if changes:
|
||||
set_sql = ", ".join([f"`{k}`=%s" for k in changes.keys()])
|
||||
params = list(changes.values()) + [ses_id]
|
||||
cur.execute(f"UPDATE conf_sessions SET {set_sql} WHERE id=%s", params)
|
||||
updated += 1
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
print(f"Updated {updated} session(s) from {fname}")
|
||||
|
||||
|
||||
def db_export_sessions_sql():
|
||||
if pymysql is None:
|
||||
raise RuntimeError("pymysql is not installed. Install it with: pip install pymysql")
|
||||
conn = get_conn()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT * FROM conf_sessions WHERE id > 1462 ORDER BY id")
|
||||
rows = cur.fetchall()
|
||||
columns = [col[0] for col in cur.description]
|
||||
finally:
|
||||
conn.close()
|
||||
if not rows:
|
||||
print("No sessions found with id > 1462")
|
||||
return
|
||||
if "id" in columns:
|
||||
columns = [c for c in columns if c != "id"]
|
||||
os.makedirs("data", exist_ok=True)
|
||||
out_path = os.path.join("data", "sessions.sql")
|
||||
with open(out_path, "w", encoding="utf-8") as fh:
|
||||
for row in rows:
|
||||
values = []
|
||||
for col in columns:
|
||||
val = row.get(col)
|
||||
if col == "desc":
|
||||
val = clean_desc_for_export(val)
|
||||
values.append(sql_literal(val))
|
||||
col_sql = ", ".join([f"`{c}`" for c in columns])
|
||||
val_sql = ", ".join(values)
|
||||
fh.write(f"INSERT INTO conf_sessions ({col_sql}) VALUES ({val_sql});\n")
|
||||
print(f"Wrote {len(rows)} INSERT statements to {out_path}")
|
||||
|
||||
|
||||
def main():
|
||||
actions = [
|
||||
("Dump day's sessions from db", db_dump_day),
|
||||
("Reload day's session from json", db_reload_day_json),
|
||||
("Export sessions id>1462 to SQL inserts", db_export_sessions_sql),
|
||||
("Reload ops.py", "__RELOAD__"),
|
||||
("Quit", None),
|
||||
]
|
||||
while True:
|
||||
print("\nOps Menu")
|
||||
for i, (label, _) in enumerate(actions, 1):
|
||||
print(f"{i}. {label}")
|
||||
choice = input("Choose an option: ").strip()
|
||||
if not choice.isdigit():
|
||||
print("Please enter a number.")
|
||||
continue
|
||||
idx = int(choice) - 1
|
||||
if idx < 0 or idx >= len(actions):
|
||||
print("Invalid choice.")
|
||||
continue
|
||||
label, action = actions[idx]
|
||||
if action is None:
|
||||
return
|
||||
if action == "__RELOAD__":
|
||||
os.execv(sys.executable, [sys.executable] + sys.argv)
|
||||
try:
|
||||
action()
|
||||
except Exception as exc:
|
||||
print(f"Error: {exc}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
// user.php - edit a single user (conf_users) and their department mapping (conf_user_departments)
|
||||
|
||||
include_once("peter_db.php");
|
||||
$peter_db = new peter_db();
|
||||
$c = $peter_db->getConnection();
|
||||
mysqli_set_charset($c, 'utf8');
|
||||
|
||||
$user_id = isset($_REQUEST['id']) ? intval($_REQUEST['id']) : 0;
|
||||
$message = '';
|
||||
|
||||
// Fetch department options
|
||||
$dept_opts = [];
|
||||
$dept_q = "SELECT id, parent, name FROM gavi_departments ORDER BY name";
|
||||
$dept_r = mysqli_query($c, $dept_q);
|
||||
while ($row = mysqli_fetch_assoc($dept_r)) { $dept_opts[] = $row; }
|
||||
|
||||
// Helper: fetch user record
|
||||
function fetch_user($c, $uid) {
|
||||
$sql = "SELECT id, goo, email, name FROM conf_users WHERE id = ?";
|
||||
$stmt = mysqli_prepare($c, $sql);
|
||||
mysqli_stmt_bind_param($stmt, "i", $uid);
|
||||
mysqli_stmt_execute($stmt);
|
||||
$res = mysqli_stmt_get_result($stmt);
|
||||
return mysqli_fetch_assoc($res);
|
||||
}
|
||||
|
||||
// Helper: fetch mapping
|
||||
function fetch_dept($c, $uid) {
|
||||
$sql = "SELECT department_id FROM conf_user_departments WHERE user_id = ?";
|
||||
$stmt = mysqli_prepare($c, $sql);
|
||||
mysqli_stmt_bind_param($stmt, "i", $uid);
|
||||
mysqli_stmt_execute($stmt);
|
||||
$res = mysqli_stmt_get_result($stmt);
|
||||
$row = mysqli_fetch_assoc($res);
|
||||
return $row ? intval($row['department_id']) : null;
|
||||
}
|
||||
|
||||
// Process save
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $user_id > 0) {
|
||||
$name = isset($_POST['name']) ? trim($_POST['name']) : '';
|
||||
$email = isset($_POST['email']) ? trim($_POST['email']) : '';
|
||||
$goo = isset($_POST['goo']) ? trim($_POST['goo']) : '';
|
||||
$dept = isset($_POST['department_id']) ? intval($_POST['department_id']) : 0;
|
||||
|
||||
// Update conf_users
|
||||
$upd = mysqli_prepare($c, "UPDATE conf_users SET name = ?, email = ?, goo = ? WHERE id = ?");
|
||||
mysqli_stmt_bind_param($upd, "sssi", $name, $email, $goo, $user_id);
|
||||
mysqli_stmt_execute($upd);
|
||||
|
||||
// Upsert department mapping
|
||||
if ($dept > 0) {
|
||||
$ins = mysqli_prepare($c, "INSERT INTO conf_user_departments (user_id, department_id) VALUES (?, ?) ON DUPLICATE KEY UPDATE department_id = VALUES(department_id)");
|
||||
mysqli_stmt_bind_param($ins, "ii", $user_id, $dept);
|
||||
mysqli_stmt_execute($ins);
|
||||
} else {
|
||||
$del = mysqli_prepare($c, "DELETE FROM conf_user_departments WHERE user_id = ?");
|
||||
mysqli_stmt_bind_param($del, "i", $user_id);
|
||||
mysqli_stmt_execute($del);
|
||||
}
|
||||
|
||||
$message = "Saved changes.";
|
||||
}
|
||||
|
||||
$user = $user_id ? fetch_user($c, $user_id) : null;
|
||||
$user_dept = $user_id ? fetch_dept($c, $user_id) : null;
|
||||
|
||||
$MY_TITLE = "Edit User";
|
||||
$MY_CRUMB = "Edit User";
|
||||
|
||||
if (!$user) {
|
||||
$CONTENT = "<p>No user found. Provide ?id=USER_ID in the query string.</p>";
|
||||
include 'layout.php';
|
||||
exit();
|
||||
}
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<?php if ($message) { ?>
|
||||
<div class="mb-4 p-3 bg-green-100 text-green-800 rounded border border-green-200"><?= htmlspecialchars($message) ?></div>
|
||||
<?php } ?>
|
||||
<form method="post" class="space-y-4 bg-white p-4 rounded shadow">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Name</label>
|
||||
<input name="name" value="<?= htmlspecialchars($user['name']) ?>" class="w-full border rounded px-3 py-2" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Email</label>
|
||||
<input name="email" value="<?= htmlspecialchars($user['email']) ?>" class="w-full border rounded px-3 py-2" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">GOO</label>
|
||||
<input name="goo" value="<?= htmlspecialchars($user['goo'] ?? '') ?>" class="w-full border rounded px-3 py-2" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Department</label>
|
||||
<select name="department_id" class="w-full border rounded px-3 py-2">
|
||||
<option value="0">-- None --</option>
|
||||
<?php foreach ($dept_opts as $d) {
|
||||
$dept_name = trim($d['name'] ?? '');
|
||||
if ($dept_name === '') { $dept_name = '(Unnamed Department)'; }
|
||||
?>
|
||||
<option value="<?= intval($d['id']) ?>" <?= ($user_dept === intval($d['id'])) ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($dept_name) ?> (<?= intval($d['id']) ?>)
|
||||
</option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="pt-2">
|
||||
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
<?php
|
||||
$CONTENT = ob_get_clean();
|
||||
include 'layout.php';
|
||||
Loading…
Reference in New Issue