Jul 14 - Using CDN - Phase 4a

This commit is contained in:
2026-07-14 13:35:34 -04:00
parent 9d5a280ca6
commit 2d87a15b0d
4 changed files with 56 additions and 10 deletions
+15
View File
@@ -95,6 +95,13 @@ def _parse_datetime(value):
return None
def _media(key):
"""Absolute display URL for a storage key (presigned on R2, absolute-static
on local). '' for falsy keys. Used for iPad image rendering."""
from app.utils import storage
return storage.media_url(key, external=True) if key else ''
def _inspection_payload(inspection):
"""Serialize an Inspection to the dict returned in API responses."""
# Extract form responses from the notes JSON blob.
@@ -136,6 +143,14 @@ def _inspection_payload(inspection):
if inspection.completed_at else None,
'mobile_local_id': inspection.mobile_local_id,
'form_data': form_data,
# Absolute display URLs for image form fields (presigned on R2,
# absolute-static on local): {field_id: url}. The iPad prefers this
# over building ServerConfig + /static/ + value.
'form_media': {
fid: _media(v)
for fid, v in (form_data or {}).items()
if isinstance(v, str) and v.startswith('uploads/')
},
'form_schema': form_schema,
'inspector_notes': inspector_notes,
# ── Follow-up / re-inspection fields ──────────────────────────────
+15
View File
@@ -51,6 +51,13 @@ _UUID_RE = re.compile(
)
def _photo_urls(keys):
"""Map storage keys to absolute display URLs (presigned on R2, absolute-static
on local). Falsy keys are skipped. Used for iPad photo rendering."""
from app.utils import storage
return [storage.media_url(k, external=True) for k in keys if k]
def _issue_payload(issue):
"""Serialise an Issue to the dict returned in list/detail responses."""
facility = issue.resolved_facility
@@ -71,6 +78,14 @@ def _issue_payload(issue):
'photo_path': issue.photo_path or None,
'mobile_photo_paths': issue.mobile_photo_paths or [],
'result_photos': issue.result_photos or [],
# Absolute display URLs (presigned on R2, absolute-static on local) for
# the iPad, which loads photos off-origin. Relative keys above stay as
# keys. photo_urls order mirrors the iPad's evidence merge:
# [photo_path] + mobile_photo_paths.
'photo_urls': _photo_urls(
([issue.photo_path] if issue.photo_path else [])
+ (issue.mobile_photo_paths or [])),
'result_photo_urls': _photo_urls(issue.result_photos or []),
# Resolution details — set by web staff after fixing the issue.
'result_notes': issue.result_notes or None,
# Verification fields — set after a director/admin confirms fix.
+9 -6
View File
@@ -76,10 +76,12 @@ class LocalBackend:
def abs_local_path(self, key):
return os.path.normpath(os.path.join(self._static_folder(), key))
def media_url(self, key):
def media_url(self, key, external=False):
if not key:
return ''
return url_for('static', filename=key)
# external=True yields an absolute URL (scheme+host) for API responses
# consumed off-origin (the iPad); templates call with external=False.
return url_for('static', filename=key, _external=external)
def read(self, key):
with open(self.abs_local_path(key), 'rb') as fh:
@@ -164,13 +166,14 @@ class S3Backend:
# not exist) so transition code / callers can still reference it.
return self._local.abs_local_path(key)
def media_url(self, key):
def media_url(self, key, external=False):
if not key:
return ''
if self.fallback and not self._head(key):
# Object not in R2 yet — serve the local copy during transition.
if self._local.exists(key):
return self._local.media_url(key)
return self._local.media_url(key, external=external)
# Presigned R2 URLs are always absolute; the external flag is moot.
return self._client.generate_presigned_url(
'get_object',
Params={'Bucket': self.bucket, 'Key': key},
@@ -259,8 +262,8 @@ def save(file_obj, subfolder):
return get_backend().save(file_obj, subfolder)
def media_url(key):
return get_backend().media_url(key)
def media_url(key, external=False):
return get_backend().media_url(key, external=external)
def read(key):