An unverified before-and-after pairing does not necessarily make a useful after photo unsuitable for its gallery. Nor does a publishable after photo justify placing an arbitrary before photo beside it. I wrote a small module that calculates those two choices separately. It projects already-reviewed metadata into public lists; it does not inspect, classify or retouch pictures.
Separate selected inventory from the public projection
A synthetic Photo record contains an identifier, case and physical-space identifiers, a room category, a construction phase, displayed dimensions, and review and hold flags. Pair records name the two endpoints and declare whether the same view has been reviewed. The package includes no customer names, original photographs or real job records. Input inventory remains working material; public_photo_ids is a separate result for the current conditions.
This example deliberately requires reviewed to be exactly true before using a photo. It does not reproduce every review state of the original application. An integration must decide how selection, completed review and individual holds map into these declared inputs. Setting a boolean to true is not evidence that someone actually inspected the picture. The code checks a contract over supplied facts rather than discovering those facts.
Use editorial order for the gallery and reviewed relations for comparisons
gallery_order declares which photos belong in the ordinary gallery and in which order. The projection keeps only reviewed, unheld after photos belonging to the target case. It does not automatically promote every after photo in the inventory into the gallery. An after photo intended only for a comparison can stay out of gallery_order and remain comparison-only.
A comparison candidate must have the same case, a shared nonempty physical-space identifier, matching room categories, the before-to-after phase order and matching displayed orientation. It must also declare same_view_reviewed as true. A broad room name and two landscape frames cannot establish that the camera shows the same physical place or direction. A missing counterpart suppresses that comparison while leaving independently eligible gallery photos available.
Apply holds to the cover and both comparison endpoints
A hold on either endpoint suppresses the comparison. A held after photo also leaves the gallery and cover candidates. If preferred_cover is unavailable, the module chooses a remaining gallery entry. It does not promote a comparison-only image into a cover. When no photograph can be projected publicly, the result still retains the case identifier and a pending-photo empty state.
public_photo_ids is the deduplicated union of gallery and comparison identifiers. A lightbox or preload-data builder should use that result rather than the entire input inventory. Calculating an identifier list is distinct from controlling access to image bytes. The example does not block a direct file address, configure a media server or revoke previously distributed cache copies. Those are separate service responsibilities.
Explore the implementation
Complete independent synthetic implementation
"""Pure synthetic metadata projection, not visual review or media authorization."""
from dataclasses import dataclass
@dataclass(frozen=True)
class Photo:
id: str
case_id: str
space_id: str
room: str
phase: str
display_width: int
display_height: int
reviewed: bool = False
held: bool = False
@dataclass(frozen=True)
class Pair:
id: str
before: str
after: str
room: str
same_view_reviewed: bool = False
priority: int = 10
def _ids(values):
if any(not isinstance(v, str) or not v.strip() for v in values):
raise ValueError('Identifiers must be nonempty strings')
if len(set(values)) != len(values):
raise ValueError('Duplicate identifier')
def _orientation(photo):
return (photo.display_width > photo.display_height) - (photo.display_width < photo.display_height)
def project(*, case_id, photos, pairs, gallery_order, preferred_cover=None):
"""Project already-reviewed metadata. Never infer scene identity from pixels.
Gallery order is an explicit editorial input. A comparison-only after image
does not become a gallery image merely because it is used in a valid pair.
Width/height must describe the upright displayed frame, not raw EXIF data.
"""
if not isinstance(case_id, str) or not case_id.strip():
raise ValueError('A target case is required')
photos, pairs, gallery_order = tuple(photos), tuple(pairs), tuple(gallery_order)
if not all(isinstance(p, Photo) for p in photos) or not all(isinstance(p, Pair) for p in pairs):
raise ValueError('Expected Photo and Pair records')
_ids([p.id for p in photos]); _ids([p.id for p in pairs]); _ids(gallery_order)
lookup = {p.id: p for p in photos}
if any(k not in lookup for k in gallery_order):
raise ValueError('Gallery references a missing photo')
for p in photos:
if p.phase not in ('before', 'after'):
raise ValueError('Unknown construction phase')
if type(p.reviewed) is not bool or type(p.held) is not bool:
raise ValueError('Review and hold flags must be booleans')
if any(type(v) is not int or v <= 0 for v in (p.display_width, p.display_height)):
raise ValueError('Displayed dimensions must be positive integers')
if any(not isinstance(v, str) for v in (p.case_id, p.space_id, p.room)):
raise ValueError('Photo context must use string identifiers')
for p in pairs:
if type(p.same_view_reviewed) is not bool or type(p.priority) is not int:
raise ValueError('Invalid pair review or priority')
if not all(isinstance(v, str) for v in (p.before, p.after, p.room)):
raise ValueError('Invalid pair references')
def usable(p):
return p.reviewed and not p.held and p.case_id == case_id
gallery = [k for k in gallery_order if usable(lookup[k]) and lookup[k].phase == 'after']
accepted = []
for pair in sorted(pairs, key=lambda p: (p.priority, p.id)):
before, after = lookup.get(pair.before), lookup.get(pair.after)
if not pair.same_view_reviewed or before is None or after is None:
continue
if not usable(before) or not usable(after):
continue
if (before.phase, after.phase) != ('before', 'after'):
continue
if not before.space_id or before.space_id != after.space_id:
continue
if not pair.room or not before.room or not (pair.room == before.room == after.room):
continue
if _orientation(before) != _orientation(after):
continue
if any(p.room == pair.room for p in accepted):
continue
accepted.append(pair)
compare = list(dict.fromkeys(k for p in accepted for k in (p.before, p.after)))
public = list(dict.fromkeys(gallery + compare))
cover = preferred_cover if preferred_cover in gallery else next(iter(gallery), None)
return {'case_id': case_id, 'gallery_ids': gallery, 'pair_ids': [p.id for p in accepted],
'comparison_ids': compare, 'public_photo_ids': public, 'cover_id': cover,
'empty_state': 'after_photos_pending' if not public else None}
Make the choice among multiple pairs deterministic
When several candidates exist, the projection selects one eligible pair per room category by priority. If the preferred candidate becomes held, a later eligible candidate can take its place. An identifier provides a stable tie-breaker. That tie rule is a choice in this newly written example, not a claim that it copies the original renderer's sorting behavior.
The tests cover different cases and physical rooms, incorrect phase order, mismatched displayed orientation, unreviewed pairs and held photos. They also cover comparison-only after images, before-only cases, cover fallback, duplicate identifiers and invalid dimensions or flags. Nineteen unittest methods check these boundaries, including repeatability and input preservation. The purpose is to exercise distinct decisions that could otherwise be conflated in one public-visible flag.
The selector reads synthetic execution results
The six records below vary conditions over the same synthetic material: a reviewed pair, an unreviewed pair, a different physical space, a hold on the before image, a before-only case, and a comparison-only after image. Readers can compare cases where the comparison disappears while an eligible after photo stays in gallery_ids.
The selector displays saved Python results. It neither reads visitor pictures nor changes a review flag. Initial display and all six choices are checked for each language by executing the actual inline script with DOM stubs. That does not establish picture rendering, keyboard behavior or mobile layout in a real browser. The records intentionally contain no images because this package explains a metadata contract rather than a visual review tool.
Adapt the site vocabulary while retaining the boundary
Extract the ZIP and run the standard-library unittest suite and run_records.py to inspect the implementation and regenerate its records. Adapt case and physical-space identifiers, room categories, priorities and gallery order to the site's data. Adding automatic phase or scene inference before this function requires a separate decision about what the declared review inputs mean.
I would retain the separation between inventory and public projection, and apply holds consistently across every public path. The existing evidence-presentation module can still connect assets to explanatory claims, but it does not replace this selection logic. The package includes usage notes, tests, execution records and an MIT license, and the same tests are repeated after extraction into a separate directory.