Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added civicpatch/2dba439c07ca.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added civicpatch/3e2771d8c6d7.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added civicpatch/92a1a277f78b.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion civicpatch/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ RUN poetry install

FROM image-base AS post-install

RUN poetry run patchright install --with-deps chromium
RUN poetry run patchright install --with-deps chrome

COPY --chown=civicpatch_user:civicpatch_user ${PROJECT_NAME}/src/setup.py /app/src/setup.py
RUN poetry run post-install
Expand Down
Binary file added civicpatch/c8a27d7feb18.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions civicpatch/docker-compose.development.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,15 @@ services:
target: development
environment:
CIVICPATCH_ENV: test
# Integration tests
GOOGLE_GEMINI_TOKEN: ${GOOGLE_GEMINI_TOKEN}
OPENAI_TOKEN: ${OPENAI_TOKEN}
TOGETHER_AI_TOKEN: ${TOGETHER_AI_TOKEN}
volumes:
- ./data:/app/data
- ./data_source:/app/data_source
- ../shared:/app/src/shared:ro
- .:/app

civicpatch_prod:
build:
Expand Down
7 changes: 6 additions & 1 deletion civicpatch/mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,12 @@ docker compose -f docker-compose.development.yml run --rm civicpatch pytest -vvm

[tasks.evals]
run = """
docker compose -f docker-compose.development.yml run --rm civicpatch pytest -vvm evals tests/prompts
docker compose -f docker-compose.development.yml run --rm civicpatch_test pytest -vvm evals tests/prompts
"""

[tasks.evals_relevant]
run = """
docker compose -f docker-compose.development.yml run --rm civicpatch_test pytest -vvm evals_relevant tests/prompts
"""

[tasks.scratchtest]
Expand Down
1 change: 1 addition & 0 deletions civicpatch/pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ markers =
integration: mark a test as an integration test
unit: mark a test as a unit test
evals: mark a test related to evaluations
evals_relevant: mark a test related to relevant page evaluation
contracts: mark a test related to contracts

# Add default options for pytest
Expand Down
447 changes: 249 additions & 198 deletions civicpatch/src/frontend/build/bundle.js

Large diffs are not rendered by default.

116 changes: 56 additions & 60 deletions civicpatch/src/frontend/components/edit-people/editable-people-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const TRACKED_FIELDS = [

function EditablePeopleList({ jurisdiction_ocdid, people = [] }) {
// Assign _tempKey to people if not present
const [localPeople, setPeople] = useState(
const [currentPeople, setCurrentPeople] = useState(
people.map(person => ({
...person,
_tempKey: person._tempKey || genKey(),
Expand All @@ -29,15 +29,22 @@ function EditablePeopleList({ jurisdiction_ocdid, people = [] }) {
const [error, setError] = useState(null);
const [openPullRequests, setOpenPullRequests] = useState([]);
const [selectedOpenPullRequest, setSelectedOpenPullRequest] = useState(null);
const [dirty, setDirty] = useState(false);
const [notice, setNotice] = useState(null);
const [isLoading, setIsLoading] = useState(false);

const selectedPeople = currentPeople.filter(p => p._selected).map(p => p._tempKey);
const dirty = currentPeople.some(person => person._dirty);

const peopleToSubmit = currentPeople
.filter(p => !p._deleted)
.map(({ _dirty, _changes, _tempKey, _selected, _deleted, _isNew, ...person }) => person);

const {
refs: cardRefs,
focusedIdx,
setFocusedIdx,
handleKeyDown,
} = useRovingFocusList(localPeople.length);
} = useRovingFocusList(currentPeople.length);

useEffect(() => {
if (!jurisdiction_ocdid) return;
Expand Down Expand Up @@ -71,8 +78,8 @@ function EditablePeopleList({ jurisdiction_ocdid, people = [] }) {


function toggleSelect(key) {
setPeople(localPeople =>
localPeople.map(person =>
setCurrentPeople(currentPeople =>
currentPeople.map(person =>
person._tempKey === key
? { ...person, _selected: !person._selected }
: person
Expand All @@ -96,7 +103,7 @@ function EditablePeopleList({ jurisdiction_ocdid, people = [] }) {
...person,
_tempKey: person._tempKey || genKey(),
}));
setPeople(peopleWithKeys);
setCurrentPeople(peopleWithKeys);
setOriginalPeople(peopleWithKeys); // Save as baseline for change tracking
}

Expand Down Expand Up @@ -127,14 +134,14 @@ function EditablePeopleList({ jurisdiction_ocdid, people = [] }) {
}

function handleBulkDelete() {
markAsDeleted(selected);
markAsDeleted(selectedPeople);
}

function handleDelete(key) {
// If it's a new person that hasn't been submitted yet, just remove it from the list
const person = localPeople.find(p => p._tempKey === key);
const person = currentPeople.find(p => p._tempKey === key);
if (person?._isNew) {
setPeople(localPeople => localPeople.filter(p => p._tempKey !== key));
setCurrentPeople(currentPeople => currentPeople.filter(p => p._tempKey !== key));
return;
}

Expand All @@ -143,7 +150,11 @@ function EditablePeopleList({ jurisdiction_ocdid, people = [] }) {

function handleAdd() {
const default_office_name = "Council Member";
const first_person = originalPeople.lenggth > 0 ? originalPeople[0] : null;
const last_person = originalPeople.length > 0 ? originalPeople[originalPeople.length - 1] : null;
const default_office_division_ocdid = originalPeople.length > 0 ? originalPeople[0].office?.division_ocdid : null;
const default_link = last_person?.urls?.[0] || null;
const default_source_url = last_person?.source_urls?.[0] || null;

const newPerson = {
_tempKey: genKey(),
Expand All @@ -155,7 +166,7 @@ function EditablePeopleList({ jurisdiction_ocdid, people = [] }) {
other_names: [],
phones: [],
emails: [],
urls: [],
urls: default_link ? [default_link] : [],
start_date: null,
end_date: null,
office: {
Expand All @@ -165,18 +176,18 @@ function EditablePeopleList({ jurisdiction_ocdid, people = [] }) {
image: null,
jurisdiction_ocdid: jurisdiction_ocdid,
cdn_image: null,
source_urls: [],
source_urls: [default_source_url],
updated_at: new Date().toISOString().replace(/\.\d{3}Z$/, '+00:00'), // Correctly formatted updated_at
};
setPeople(localPeople => [newPerson, ...localPeople]);
setCurrentPeople(currentPeople => [newPerson, ...currentPeople]);
}

function reorderPersonToBottom(key) {
setPeople(localPeople => {
const index = localPeople.findIndex(p => p._tempKey === key);
if (index === -1) return localPeople;
const person = localPeople[index];
const without = localPeople.filter(p => p._tempKey !== key);
setCurrentPeople(currentPeople => {
const index = currentPeople.findIndex(p => p._tempKey === key);
if (index === -1) return currentPeople;
const person = currentPeople[index];
const without = currentPeople.filter(p => p._tempKey !== key);
return [...without, person];
});
}
Expand All @@ -188,12 +199,11 @@ function EditablePeopleList({ jurisdiction_ocdid, people = [] }) {
} else {
assignPeople(people);
}
setDirty(false);
} else {
const originalPerson = originalPeople.find(p => p._tempKey === tempKey);
if (originalPerson) {
setPeople(localPeople =>
localPeople.map(person =>
setCurrentPeople(currentPeople =>
currentPeople.map(person =>
person._tempKey === tempKey ? { ...originalPerson } : person
)
);
Expand All @@ -202,36 +212,33 @@ function EditablePeopleList({ jurisdiction_ocdid, people = [] }) {
}

function handleMerge() {
if (selected.length < 2) return;
const selectedPeople = localPeople.filter(p => selected.includes(p._tempKey));
if (selectedPeople.length < 2) return;

const singleValueFields = ["name", "image", "start_date", "end_date", "image", "cdn_image", "updated_at"];
const arrayFields = ["other_names", "phones", "emails", "urls", "source_urls"];
const peopleToMerge = currentPeople.filter(p => selectedPeople.includes(p._tempKey));

// Use the first selected person as the base for merging
const basePersonIndex = localPeople.findIndex(p => p._tempKey === selected[0]);
const basePersonIndex = currentPeople.findIndex(p => p._tempKey === selectedPeople[0]);
if (basePersonIndex === -1) return;
const basePerson = { ...localPeople[basePersonIndex] };
const basePerson = { ...currentPeople[basePersonIndex] };

// Merge single value fields (pick first non-null, fallback to null)
for (const field of singleValueFields) {
basePerson[field] = selectedPeople.map(p => p[field]).find(v => v != null && v !== "") || null;
basePerson[field] = peopleToMerge.map(p => p[field]).find(v => v != null && v !== "") || null;
}

// Merge array fields (combine, dedupe)
for (const field of arrayFields) {
basePerson[field] = Array.from(
new Set(
selectedPeople
peopleToMerge
.flatMap(p => Array.isArray(p[field]) ? p[field] : (p[field] ? [p[field]] : []))
.filter(Boolean)
)
);
}

// Special merge for office.name
const officeNames = selectedPeople
const officeNames = peopleToMerge
.map(p => p.office?.name)
.filter(Boolean);
if (officeNames.length > 0) {
Expand All @@ -242,19 +249,18 @@ function EditablePeopleList({ jurisdiction_ocdid, people = [] }) {
};
}

basePerson.jurisdiction_ocdid = selectedPeople[0].jurisdiction_ocdid;
// Keep the original _tempKey of the base person

// Remove all selected except the base, and replace base with merged
setPeople(p => {
const filtered = p.filter(person => !selected.includes(person._tempKey) || person._tempKey === basePerson._tempKey);
filtered[basePersonIndex] = basePerson;
return filtered;
// Remove all selected except the base (keep merged in same position), and replace base with merged
setCurrentPeople(p => {
const withoutMerged = p.filter(person => !selectedPeople.includes(person._tempKey) || person._tempKey === basePerson._tempKey);
const withChanges = withoutMerged.map(person => person._tempKey === basePerson._tempKey ? { ...basePerson, _dirty: true, _changes: TRACKED_FIELDS } : person);
const resetSelected = withChanges.map(person => ({ ...person, _selected: false }));
return resetSelected;
});
setDirty(true);
}

function submitChanges(branchName, data) {
setIsLoading(true);

const url = [
`/api/api_proxy/jobs/people/pull_request/`,
encodeURIComponent(branchName),
Expand All @@ -270,9 +276,7 @@ function EditablePeopleList({ jurisdiction_ocdid, people = [] }) {
body: JSON.stringify({ jurisdiction_ocdid, data }),
})
.then(r => {
console.log("Submit response:", r);
if (!r.ok) {
console.log("rethrowing...")
throw new Error(`Failed to submit changes: ${r.status} ${r.statusText}`);
}
return r.json();
Expand All @@ -281,15 +285,15 @@ function EditablePeopleList({ jurisdiction_ocdid, people = [] }) {
console.error("Error submitting changes:", e);
throw e;
})
.finally(() => setIsLoading(false));
}

function handleSubmit() {
setDirty(false);
setPeople([]);
setCurrentPeople([]);

submitChanges(
selectedOpenPullRequest, // TODO: if not available needs to open a new PR instead
localPeople.map(({ _dirty, _changes, _tempKey, _selected, _deleted, _isNew, ...person }) => person)
peopleToSubmit
)
.then(() => {
setNotice(
Expand All @@ -305,8 +309,8 @@ function EditablePeopleList({ jurisdiction_ocdid, people = [] }) {
}

function updatePerson(key, updates) {
setPeople(localPeople =>
localPeople.map(person => {
setCurrentPeople(currentPeople =>
currentPeople.map(person => {
if (person._tempKey === key) {
const originalPerson = originalPeople.find(p => p._tempKey === key);
const nextPerson = { ...person, ...updates };
Expand All @@ -322,15 +326,8 @@ function updatePerson(key, updates) {
return person;
})
);
setDirty(true);
}

const selected = localPeople.filter(p => p._selected).map(p => p._tempKey);

const updatedPeople = localPeople
.filter(p => !p._deleted)
.map(({ _dirty, _changes, _tempKey, _selected, _deleted, _isNew, ...person }) => person);

if (loading) return html`<p>Loading people...</p>`;

function truncateBranchName(branchName, maxLength = 24) {
Expand All @@ -346,7 +343,7 @@ function updatePerson(key, updates) {
<diff-preview
.original=${yaml.dump(originalPeople.map(({ _tempKey, ...person }) => person))}
.updated=${yaml.dump(
updatedPeople
peopleToSubmit
)}
></diff-preview>
<div style="margin-top:2rem;">
Expand All @@ -360,7 +357,7 @@ function updatePerson(key, updates) {
max-height: 300px;
overflow: auto;
">${yaml.dump(
updatedPeople
peopleToSubmit
)}</pre>
</div>
</div>
Expand Down Expand Up @@ -461,16 +458,16 @@ function updatePerson(key, updates) {
<button
@click=${handleMerge}
style="margin-right: 1rem;"
?disabled=${selected.length < 2}
?disabled=${selectedPeople.length < 2}
>
Merge (${selected.length})
Merge (${selectedPeople.length})
</button>
<button
@click=${handleBulkDelete}
style="margin-right: 1rem;"
?disabled=${selected.length === 0}
?disabled=${selectedPeople.length === 0}
>
Delete (${selected.length})
Delete (${selectedPeople.length})
</button>
<button
@click=${() => handleReset()}
Expand Down Expand Up @@ -510,7 +507,7 @@ function updatePerson(key, updates) {
width: 100%;
"
>
${localPeople.map(
${currentPeople.map(
(person, idx) => html`
<div role="listitem">
<person-card
Expand All @@ -519,7 +516,6 @@ function updatePerson(key, updates) {
@focus=${() => setFocusedIdx(idx)}
@keydown=${e => handleCardKeyDown(e, idx, person._tempKey) }
.person=${person}
.selected=${selected.includes(person._tempKey)}
.onSelect=${() => toggleSelect(person._tempKey)}
.onDelete=${() => handleDelete(person._tempKey)}
.onChange=${(field, value) => updatePerson(person._tempKey, { [field]: value })}
Expand Down
Loading
Loading