moving helper functions to scripts folder

This commit is contained in:
matt committed 2026-10-21 04:14:10 -07:00
1 parent e7fad500f9
commit dc39b6988c
9 files changed
+514 -49

No files matched your search

+53 -13
View File
@@ -3,6 +3,15 @@ from database import *
from werkzeug.utils import secure_filename
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = os.path.join(app.root_path, 'photos')
app.config['URL_PREFIX'] = '/uploads' # public URL prefix
# make sure the folder exists
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
# helper to build a public URL
def public_url(filename: str) -> str:
return f"{app.config['URL_PREFIX']}/{filename}"
# Initialise database on startup
initialize_db()
@@ -179,29 +188,60 @@ def setting_detail(name: str):
# ----------------------------- Photos -----------------------------
@app.route('/api/providers/<provider_id>/photos', methods=['POST'])
# photo upload endpoint
@app.route('/api/providers/<int:provider_id>/photos', methods=['POST'])
def upload_photos(provider_id):
if 'photos' not in request.files:
return jsonify({'error': 'No files uploaded'}), 400
# Secure filename + save to persistent storage
upload_folder = f'{os.path.dirname(__file__)}/photos'
os.makedirs(upload_folder, exist_ok=True)
files = request.files.getlist('photos')
if not files:
return jsonify({'error': 'No files in list'}), 400
photo_paths = []
for file in request.files.getlist('photos'):
if file.filename == '':
allowed_ext = {'.jpg', '.jpeg', '.png', '.gif'}
saved_filenames = []
for f in files:
if f.filename == '':
continue
filename = secure_filename(f.filename)
ext = os.path.splitext(filename)[1].lower()
if ext not in allowed_ext:
return jsonify({'error': f'Unsupported file type: {ext}'}), 400
# rename to avoid collisions
new_name = f"{uuid.uuid4().hex}{ext}"
f.save(os.path.join(app.config['UPLOAD_FOLDER'], new_name))
saved_filenames.append(public_url(new_name))
filename = secure_filename(file.filename)
file.save(os.path.join(upload_folder, filename))
photo_paths.append(f"/uploads/{filename}")
# Merge with any pre‑existing photos
existing = db.fetch(f"SELECT photos FROM providers WHERE id=?", (provider_id,))[0][0]
try:
photos = json.loads(existing)
except Exception:
photos = []
photos.extend(saved_filenames)
# Store paths in DB (e.g., PostgreSQL JSON field)
db.execute("UPDATE providers SET photos = %s WHERE id = %s",
(json.dumps(photo_paths), provider_id))
(json.dumps(photos), provider_id))
return jsonify({'success': True, 'paths': photo_paths}), 200
return jsonify({'success': True, 'photos': photos}), 200
# Endpoint to change the main photo
@app.route('/api/providers/<int:provider_id>/main_photo', methods=['PUT'])
def set_main_photo(provider_id):
data = request.get_json()
url = data.get('url')
if not url:
return jsonify({'error': 'Missing url'}), 400
# Validate that the URL belongs to this provider
photos = db.fetch(f"SELECT photos FROM providers WHERE id=?", (provider_id,))[0][0]
if json.loads(photos).count(url) == 0:
return jsonify({'error': 'URL not part of this provider'}), 400
db.execute("UPDATE providers SET main_photo = %s WHERE id = %s",
(url, provider_id))
return jsonify({'success': True, 'main_photo': url})
# Run the app if this file is executed directly
if __name__ == "__main__":
+3 -1
View File
@@ -52,7 +52,9 @@ def initialize_db() -> None:
age INTEGER,
photo TEXT,
rates TEXT,
private INTEGER NOT NULL DEFAULT 0
private INTEGER NOT NULL DEFAULT 0,
photos TEXT DEFAULT '[]',
main_photo TEXT
);
"""
)
+302 -25
View File
@@ -1,37 +1,314 @@
<html>
<head>
</head>
<?php
/**
* photos.php - Service Provider Photo Gallery Implementation
* Requirements:
* 1. Upload multiple images via PHP Frontend
* 2. Display gallery with thumbnail selection
* 3. Use Flask API for storage and metadata
* 4. Store photos in file system (not database BLOBs)
*/
require_once __DIR__ . '/config.php';
<body>
// Validate provider ID
if (!isset($_GET['id']) || empty($_GET['id'])) {
die('Provider ID missing. Use: photos.php?id=123');
}
$provider_id = $_GET['id'];
<div class="photo-grid">
<div class="photo-item">
<input type="radio" name="main_photo" value="1" checked>
<img src="photo1.jpg" />
</div>
<div class="photo-item">
<input type="radio" name="main_photo" value="2">
<img src="photo2.jpg" />
</div>
</div>
// Fetch existing photos from API
$api_url = BASE_API_URL . '/providers/' . urlencode($provider_id) . "/photos";
$existing_photos = [];
try {
$response = file_get_contents($api_url);
$existing_photos = json_decode($response, true);
if (isset($existing_photos['error'])) {
trigger_error($existing_photos['error'], E_USER_WARNING);
$existing_photos = [];
}
} catch (Exception $e) {
trigger_error('API error: ' . $e->getMessage(), E_USER_WARNING);
}
<input type="file" id="photo-upload" name="photos[]" multiple accept="image/*" />
// Check upload permissions
$has_upload_perm = true; // Add authorization logic here
// Generate unique token for upload (security)
$upload_token = bin2hex(random_bytes(16));
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Photos for Service Provider #<?php echo htmlspecialchars($provider_id); ?></title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
line-height: 1.5;
color: #333;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
h1 {
margin-bottom: 30px;
border-bottom: 1px solid #eee;
padding-bottom: 15px;
}
.upload-section {
display: flex;
align-items: center;
gap: 15px;
margin: 20px 0;
}
.upload-btn {
padding: 10px 20px;
background: #3a86ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.upload-btn:hover {
background: #1d66cc;
}
#preview-container {
display: none;
gap: 10px;
margin-top: 10px;
}
.photo-item {
position: relative;
width: 80px;
height: 80px;
margin: 5px;
}
.photo-item img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 4px;
}
.main-indicator {
position: absolute;
top: 2px;
right: 2px;
width: 16px;
height: 16px;
background: #3a86ff;
border-radius: 50%;
}
#gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
margin-top: 30px;
}
.loading {
text-align: center;
padding: 20px;
color: #666;
}
.no-photos {
text-align: center;
padding: 50px;
color: #777;
}
.error {
color: #e74c3c;
margin: 20px 0;
}
</style>
</head>
<body>
<h1>Photos for Service Provider #<?php echo htmlspecialchars($provider_id); ?></h1>
<?php if (isset($existing_photos['error'])): ?>
<div class="error">
<?php echo htmlspecialchars($existing_photos['error']); ?>
</div>
<?php endif; ?>
<?php if ($has_upload_perm): ?>
<div class="upload-section">
<input type="file" id="photo-upload" name="photos[]" multiple accept="image/*">
<button id="upload-btn" class="upload-btn">Upload Photos</button>
</div>
<div id="preview-container"></div>
<?php else: ?>
<p>Upload permissions disabled. Contact administrator.</p>
<?php endif; ?>
<div class="loading" id="loading">
Loading photos...
</div>
<div id="gallery">
<?php if (empty($existing_photos)): ?>
<div class="no-photos">
<h2>No photos uploaded yet</h2>
<p>Upload images to display them here</p>
</div>
<?php else: ?>
<?php foreach ($existing_photos as $photo): ?>
<div class="photo-item">
<input type="radio" name="main_photo" value="<?php echo htmlspecialchars($photo['path']); ?>" <?php echo $photo['is_main'] ? 'checked' : ''; ?> class="main-radio">
<img src="<?php echo htmlspecialchars($photo['path']); ?>" alt="Service provider photo">
<?php if ($photo['is_main']): ?>
<div class="main-indicator"></div>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<script>
document.getElementById('photo-upload').addEventListener('change', function(e) {
const files = Array.from(e.target.files);
const preview = document.getElementById('preview-container');
preview.innerHTML = '';
document.addEventListener('DOMContentLoaded', () => {
const photoInput = document.getElementById('photo-upload');
const previewContainer = document.getElementById('preview-container');
const gallery = document.getElementById('gallery');
const loading = document.getElementById('loading');
files.forEach(file => {
const img = document.createElement('img');
img.src = URL.createObjectURL(file);
img.classList.add('preview-thumb');
preview.appendChild(img);
// Show preview on file selection
photoInput.addEventListener('change', function(e) {
if (this.files.length > 0) {
previewContainer.style.display = 'flex';
}
previewContainer.innerHTML = '';
for (let i = 0; i < this.files.length; i++) {
const file = this.files[i];
const reader = new FileReader();
reader.onload = function(e) {
const preview = document.createElement('div');
preview.className = 'photo-item';
preview.innerHTML = `
<input type="radio" name="main_photo" value="${file.name}"
class="main-radio"
style="position: absolute; top: 2px; left: 2px;">
<img src="${e.target.result}"
alt="Photo preview">
<div style="position: absolute; top: 2px; right: 2px;
width: 16px; height: 16px;
background: #3a86ff;
border-radius: 50%;"></div>
`;
previewContainer.appendChild(preview);
};
reader.readAsDataURL(file);
}
});
// Handle upload button
document.getElementById('upload-btn').addEventListener('click', async () => {
const fileInput = document.getElementById('photo-upload');
if (!fileInput.files.length) {
alert('Please select at least one photo to upload');
return;
}
const formData = new FormData();
for (let i = 0; i < fileInput.files.length; i++) {
formData.append('photos[]', fileInput.files[i]);
}
try {
const response = await fetch(
BASE_API_URL . '/providers/<?php echo urlencode($provider_id); ?>/photos',
{
method: 'POST',
body: formData,
headers: {
'X-Upload-Token': '<?php echo $upload_token; ?>'
}
}
);
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
const data = await response.json();
// Add new photos to gallery
data.paths.forEach(path => {
const newPhoto = document.createElement('div');
newPhoto.className = 'photo-item';
newPhoto.innerHTML = `
<input type="radio" name="main_photo" value="${path}"
class="main-radio"
style="position: absolute; top: 2px; left: 2px;">
<img src="${path}"
alt="New photo"
style="object-fit: cover;">
<div style="position: absolute; top: 2px; right: 2px;
width: 16px; height: 16px;
background: #3a86ff;
border-radius: 50%;"></div>
`;
gallery.appendChild(newPhoto);
});
// Reset form
fileInput.value = '';
previewContainer.style.display = 'none';
// Update main photo selection (if any)
const mainRadios = gallery.querySelectorAll('.main-radio');
if (mainRadios.length > 0) {
mainRadios[0].checked = true;
}
} catch (error) {
console.error('Upload failed:', error);
alert('Upload failed: ' + error.message);
}
});
// Handle main photo selection
gallery.addEventListener('change', async (e) => {
if (e.target.className === 'main-radio') {
const mainPhoto = e.target.value;
const providerId = '<?php echo $provider_id; ?>';
try {
const response = await fetch(
BASE_API_URL . '/providers/${providerId}/main_photo',
{
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
photo_path: mainPhoto
})
}
);
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
// Update visuals
gallery.querySelectorAll('.main-indicator').forEach(ind => {
ind.style.display = 'none';
});
e.target.parentElement.querySelector('.main-indicator').style.display = 'block';
} catch (error) {
console.error('Setting main photo failed:', error);
}
}
});
// Hide loading message
loading.style.display = 'none';
});
</script>
</body>
</body>
</html>
File renamed without changes.
File renamed without changes.
@@ -33,7 +33,39 @@ function callApi(string $method, string $path, $body = null)
}
// ------------------------------------------------------------------
// 1️⃣ Get provider details
// ------------------------------------------------------------------
// Helper: upload photos (multipart)
// Returns the JSON response from the API (or null on error)
function uploadPhotosApi(int $provider_id, array $files): ?array
{
$url = BASE_API_URL . "/api/providers/{$provider_id}/photos";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Build multipart array
$multipart = [];
foreach ($files['tmp_name'] as $i => $tmp) {
if ($files['name'][$i] === '') continue;
$multipart[] = [
'name' => 'photos',
'contents' => fopen($tmp, 'r'),
'filename' => $files['name'][$i]
];
}
curl_setopt($ch, CURLOPT_POSTFIELDS, $multipart);
// Let PHP set the correct Content‑Type boundary
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: multipart/form-data']);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return ($httpCode < 400) ? json_decode($response, true) : null;
}
// Get provider details
$provider = callApi('GET', '/providers?id=' . $provider_id);
if (!$provider) {
echo '<h1>Error</h1><p>Could not load provider details.</p>';
@@ -43,14 +75,14 @@ if (is_array($provider) && count($provider) > 0 && isset($provider[0])) {
$provider = $provider[0];
}
// 2️⃣ Get rate‑option list (comma‑separated string → array)
// Get rate‑option list (comma‑separated string → array)
$rateSetting = callApi('GET', '/settings/rate_options');
$rateOptions = [];
if ($rateSetting && isset($rateSetting['value'])) {
$rateOptions = array_map('trim', explode(',', $rateSetting['value']));
}
// 3️⃣ Decode the API’s rate string (JSON → array of arrays)
// Decode the API’s rate string (JSON → array of arrays)
$ratesData = $provider['rates'] ?? '[]';
$decoded = [];
if (is_string($ratesData)) {
@@ -62,7 +94,7 @@ if (is_string($ratesData)) {
$decoded = $ratesData; // already an array – legacy path
}
// 4️⃣ Build two helper arrays
// Build two helper arrays
$selectedRateKeys = []; // lowercase keys of selected rates
$ratesAssoc = []; // key → value
@@ -75,7 +107,7 @@ foreach ($decoded as $entry) {
}
}
// 5️⃣ Find orphan rates (present in $decoded but not in $rateOptions)
// Find orphan rates (present in $decoded but not in $rateOptions)
$orphanRates = [];
$orphanKeys = [];
foreach ($decoded as $entry) {
@@ -90,8 +122,8 @@ foreach ($decoded as $entry) {
}
// ------------------------------------------------------------------
// 6️⃣ Handle form POST
// --- Handle form POST -------------------------------------------------
// Handle form POST
/*
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Base payload
$payload = [
@@ -107,14 +139,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Rebuild the rates array from the form.
$finalRates = [];
// 1️⃣ Add the selected valid rate options (value may be changed by the user)
// Add the selected valid rate options (value may be changed by the user)
$selectedRateKeysRaw = $_POST['rate_selected'] ?? [];
foreach ($selectedRateKeysRaw as $opt) { // $opt is the original rate name
$newVal = $_POST['rate_value'][$opt] ?? '0'; // the updated value
$finalRates[] = [$opt => $newVal];
}
// 2️⃣ Add orphan rates that the user chose **not** to delete
// Add orphan rates that the user chose **not** to delete
$orphanDeleteSet = array_flip(array_map('strtolower', $_POST['orphan_delete'] ?? []));
foreach ($orphanRates as $key => $value) { // $key is already lower‑cased
if (!isset($orphanDeleteSet[strtolower($key)])) {
@@ -122,7 +154,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
}
// 3️⃣ Encode the new array as JSON
// Encode the new array as JSON
//$payload['rates'] = !empty($finalRates) ? json_encode($finalRates) : null;
$payload['rates'] = !empty($finalRates) ? $finalRates : null;
@@ -135,6 +167,68 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
exit;
}
echo '<h1>Error</h1><p>Failed to update provider.</p>';
}*/
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
/* -------- 1. Build the base payload -------- */
$payload = [
'name' => $_POST['name'] ?? '',
'age' => (int)($_POST['age'] ?? 0),
'photo' => $_POST['photo'] ?? '',
'phone' => $_POST['phone'] ?? '',
'private' => isset($_POST['private']),
];
/* -------- 2. Re‑build the rates array -------- */
$finalRates = [];
$selectedRateKeysRaw = $_POST['rate_selected'] ?? [];
foreach ($selectedRateKeysRaw as $opt) {
$newVal = $_POST['rate_value'][$opt] ?? '0';
$finalRates[] = [$opt => $newVal];
}
// orphan rates that the user chose *not* to delete
$orphanDeleteSet = array_flip(array_map('strtolower', $_POST['orphan_delete'] ?? []));
foreach ($orphanRates as $key => $value) {
if (!isset($orphanDeleteSet[$key])) {
$finalRates[] = [$key => $value];
}
}
$payload['rates'] = !empty($finalRates) ? $finalRates : null;
/* -------- 3. Call the API to update provider -------- */
$result = callApi('PUT', "/providers/{$provider_id}", $payload);
if (!$result) {
echo '<h1>Error</h1><p>Failed to update provider.</p>';
exit;
}
/* -------- 4. Upload photos if any were chosen -------- */
if (!empty($_FILES['photos']['name'][0])) {
$uploadRes = uploadPhotosApi($provider_id, $_FILES['photos']);
if ($uploadRes && isset($uploadRes['photos'])) {
// Update main photo if the user selected one
$selectedMain = $_POST['main_photo'] ?? null;
if ($selectedMain) {
callApi('PUT', "/api/providers/{$provider_id}/main_photo",
['url' => $selectedMain]);
}
} else {
// Photo upload failed – you may want to show a message
echo '<p style="color:red;">Photo upload failed.</p>';
}
}
/* -------- 5. Set the chosen main photo from the new uploads -------- */
// If a main photo was chosen from the new uploads, send it
if (!empty($_POST['main_photo'])) {
callApi('PUT', "/api/providers/{$provider_id}/main_photo",
['url' => $_POST['main_photo']]);
}
/* -------- 6. Redirect back to the list -------- */
header('Location: list_providers.php');
exit;
}
?>
@@ -158,6 +252,30 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
<h1>Edit Provider</h1>
<form method="post" id="editForm">
<!-- Basic fields -->
<?php if (!empty($provider['photos'])): ?>
<fieldset>
<legend>Existing Photos</legend>
<div style="display:flex; flex-wrap:wrap;">
<?php foreach ($provider['photos'] as $idx => $url): ?>
<div style="margin:4px; text-align:center;">
<img src="<?= htmlspecialchars($url) ?>" style="max-width:100px; height:auto;">
<br>
<input type="radio" name="main_photo" value="<?= htmlspecialchars($url) ?>"
<?= ($provider['main_photo'] ?? '') === $url ? 'checked' : '' ?>>
<label>Thumbnail</label>
</div>
<?php endforeach; ?>
</div>
</fieldset>
<?php endif; ?>
<fieldset>
<legend>Upload New Photos</legend>
<label>
<input type="file" name="photos[]" multiple accept="image/*">
</label>
<p>You can select several images at once.</p>
</fieldset>
<label>Name: <input type="text" name="name" required value="<?php echo htmlspecialchars($provider['name'] ?? ''); ?>"></label>
<label>Age: <input type="number" name="age" required value="<?php echo htmlspecialchars($provider['age'] ?? ''); ?>"></label>
<label>Photo URL: <input type="text" name="photo" value="<?php echo htmlspecialchars($provider['photo'] ?? ''); ?>"></label>
File renamed without changes.
File renamed without changes.
+28
View File
@@ -0,0 +1,28 @@
<?php
require_once __DIR__.'/config.php';
function uploadPhotos($provider_id, $files) {
$url = BASE_API_URL . "/api/providers/$provider_id/photos";
$c = curl_init($url);
curl_setopt($c, CURLOPT_POST, true);
curl_setopt($c, CURLOPT_HTTPHEADER, ['Content-Type: multipart/form-data']);
// Build multipart array
$multipart = [];
foreach ($files['tmp_name'] as $i => $tmp) {
$multipart[] = [
'name' => 'photos',
'contents' => fopen($tmp, 'r'),
'filename' => $files['name'][$i],
];
}
curl_setopt($c, CURLOPT_POSTFIELDS, $multipart);
curl_setopt($c, CURLOPT_RETURNTRANSFER, true);
$res = curl_exec($c);
$http = curl_getinfo($c, CURLINFO_HTTP_CODE);
curl_close($c);
return $http < 400 ? json_decode($res, true) : null;
}
?>