Skip to content

Commit 1e7093e

Browse files
Core 1190 ephemeral books (#651)
* Add support for ephemeral books * Add ancillaries consumer * Small fix for not showing corrupted build artifacts This shouldn't happen for real, but it does happen when running integration tests, so let's fix it! * Filter ABL on REX consumer by default * Remove extra print statement Oops * Use more specific prefix for identifying super documents * Ensure build artifacts frame does not get too large
1 parent b472835 commit 1e7093e

File tree

8 files changed

+170
-11
lines changed

8 files changed

+170
-11
lines changed

backend/app/app/api/endpoints/abl.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ async def get_abl_info(
2626
version: Optional[str] = None,
2727
code_version: Optional[str] = None,
2828
):
29+
if consumer is None:
30+
consumer = "REX"
31+
elif consumer == "ANY":
32+
consumer = None
2933
return abl_service.get_abl_info_database(
3034
db, consumer, repo_name, version, code_version
3135
)

backend/app/app/service/abl.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,8 @@ def update_versions_by_consumer(
159159

160160

161161
def guess_consumer(book_slug: str) -> str:
162-
if book_slug.endswith("-ancillary") or book_slug.endswith("-ancillaries"):
163-
return "ancillary"
162+
if book_slug.startswith("super--"):
163+
return "ancillaries"
164164
return "REX"
165165

166166

backend/app/app/service/jobs.py

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import logging
33
from datetime import datetime
44
from typing import Dict, Generator, List, Optional, Union, cast
5-
from uuid import UUID
5+
from uuid import NAMESPACE_OID, UUID, uuid5
66

77
from lxml import etree
88
from sqlalchemy.exc import IntegrityError
@@ -75,6 +75,20 @@ def add_books_to_commit(
7575
db.add(db_book)
7676

7777

78+
def get_or_add_book(db: Session, book: Book):
79+
existing_book = cast(
80+
Optional[Book],
81+
db.query(Book)
82+
.filter(Book.uuid == book.uuid, Book.commit_id == book.commit_id)
83+
.first(),
84+
)
85+
if existing_book is not None:
86+
return existing_book
87+
db.add(book)
88+
db.flush()
89+
return book
90+
91+
7892
def add_books_to_job(
7993
db: Session,
8094
job: JobSchema,
@@ -190,11 +204,38 @@ async def insert_job():
190204

191205
def update(self, db_session: Session, job: JobSchema, job_in: JobUpdate):
192206
if isinstance(job_in.artifact_urls, list):
193-
book_job_by_book_slug = {b.book.slug: b for b in job.books}
194-
for artifact_url in job_in.artifact_urls:
195-
book_job_by_book_slug[
196-
artifact_url.slug
197-
].artifact_url = artifact_url.url
207+
artifact_url_by_book_slug = {
208+
art.slug: art.url for art in job_in.artifact_urls
209+
}
210+
job_books = {b.book.slug for b in job.books}
211+
# books that are created during the build
212+
ephemeral_book_slugs = {
213+
artifact_url.slug
214+
for artifact_url in job_in.artifact_urls
215+
if artifact_url.slug not in job_books
216+
}
217+
if ephemeral_book_slugs:
218+
for book_slug in ephemeral_book_slugs:
219+
book_uuid = str(uuid5(NAMESPACE_OID, book_slug))
220+
style = (
221+
"super"
222+
if book_slug.startswith("super--")
223+
else "unknown"
224+
)
225+
book = Book(
226+
uuid=book_uuid,
227+
commit_id=job.books[0].book.commit_id,
228+
edition=0,
229+
slug=book_slug,
230+
style=style,
231+
)
232+
book = get_or_add_book(db_session, book)
233+
add_books_to_job(db_session, job, [book])
234+
db_session.flush()
235+
for book_job in job.books:
236+
artifact_url = artifact_url_by_book_slug.get(book_job.book.slug)
237+
if artifact_url:
238+
book_job.artifact_url = artifact_url
198239
elif job_in.artifact_urls is not None:
199240
job.books[0].artifact_url = job_in.artifact_urls
200241
return super().update(db_session, job, job_in, JobUpdate)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""add ancillaries consumer
2+
3+
Revision ID: a853887ac719
4+
Revises: aa75305665c6
5+
Create Date: 2025-09-03 19:22:17.308206
6+
7+
"""
8+
9+
from datetime import datetime, timezone
10+
11+
import sqlalchemy as sa
12+
from alembic import op
13+
14+
# revision identifiers, used by Alembic.
15+
revision = "a853887ac719"
16+
down_revision = "aa75305665c6"
17+
branch_labels = None
18+
depends_on = None
19+
20+
21+
utcnow = datetime.now(timezone.utc)
22+
consumer_table = sa.table(
23+
"consumer",
24+
sa.column("id", sa.Integer),
25+
sa.column("name", sa.String),
26+
sa.column("created_at", sa.DateTime),
27+
sa.column("updated_at", sa.DateTime),
28+
)
29+
approved_book_table = sa.table(
30+
"approved_book",
31+
sa.column("book_id", sa.Integer()),
32+
sa.column("consumer_id", sa.Integer()),
33+
sa.column("code_version_id", sa.Integer()),
34+
sa.column("created_at", sa.DateTime()),
35+
sa.column("updated_at", sa.DateTime()),
36+
)
37+
38+
39+
new_consumers = [
40+
{"id": 2, "name": "ancillaries", "created_at": utcnow, "updated_at": utcnow}
41+
]
42+
43+
44+
def upgrade():
45+
bind = op.get_bind()
46+
insert = consumer_table.insert().values(new_consumers)
47+
bind.execute(insert)
48+
49+
50+
def downgrade():
51+
bind = op.get_bind()
52+
new_consumer_ids = {c["id"] for c in new_consumers}
53+
delete_abl_entries = approved_book_table.delete().where(
54+
approved_book_table.c.consumer_id.in_(new_consumer_ids)
55+
)
56+
bind.execute(delete_abl_entries)
57+
delete = consumer_table.delete().where(
58+
consumer_table.c.id.in_(new_consumer_ids)
59+
)
60+
bind.execute(delete)

backend/app/tests/integration/api/endpoints/test_jobs.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,55 @@ def test_jobs_cru(
9595

9696
# AND: We get the error as a string
9797
assert error_text == error_text_from_server
98+
99+
100+
@pytest.mark.integration
101+
@pytest.mark.parametrize(
102+
"status_id, job_type_id, version, repo_name, repo_owner, book_name",
103+
[
104+
("1", "4", None, "tiny-book", "openstax", "book-slug1"),
105+
("1", "4", None, "tiny-book", "openstax", "book-slug1"),
106+
],
107+
)
108+
def test_ephemeral_book(
109+
testclient,
110+
api_url,
111+
status_id,
112+
job_type_id,
113+
version,
114+
repo_owner,
115+
repo_name,
116+
book_name,
117+
):
118+
# GIVEN: An api url to the jobs endpoint
119+
# AND: Data for job is ready to be submitted.
120+
data = {
121+
"status_id": status_id,
122+
"job_type_id": job_type_id,
123+
"version": version,
124+
"repository": {"name": repo_name, "owner": repo_owner},
125+
"book": book_name,
126+
}
127+
128+
# WHEN: A POST request is made to the url with data
129+
response = testclient.post(f"{api_url}/{ENDPOINT}/", json=data)
130+
131+
# THEN: A 200 code is returned
132+
assert response.status_code == 200
133+
job = response.json()
134+
job_id = job["id"]
135+
136+
# WHEN: A PUT request is made to the url with data
137+
# In the first case the book is newly added, it is reused in the second
138+
data = {
139+
"status_id": "5",
140+
"artifact_urls": [
141+
{"slug": "super-cool-book", "url": "http://example.com"}
142+
],
143+
"worker_version": "dev",
144+
}
145+
job_update_response = testclient.put(
146+
f"{api_url}/{ENDPOINT}/{job_id}", json=data
147+
)
148+
# THEN: A 200 code is returned
149+
assert job_update_response.status_code == 200

frontend/specs/abl.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ describe("fetchABL", () => {
194194
const url = fetchSpy.mock.lastCall?.[0];
195195
expect(fetchSpy).toHaveBeenCalledTimes(1);
196196
expect(errors[0]).toBeUndefined();
197-
expect(url).toBe("/api/abl/");
197+
expect(url).toBe("/api/abl/?consumer=ANY");
198198
expect(value).toStrictEqual([]);
199199
});
200200
it("sends errors to error store", async () => {

frontend/src/components/BuildArtifacts.svelte

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<div id="build-artifacts-frame">
88
<h3>Build Artifacts</h3>
99
<ul>
10-
{#each selectedJob.artifact_urls as artifact}
10+
{#each selectedJob.artifact_urls.filter(art => art.slug && art.url) as artifact}
1111
<li>
1212
<a href={artifact.url} target="_blank" rel="noreferrer"
1313
>{artifact.slug}</a
@@ -22,6 +22,8 @@
2222
padding: 10px;
2323
margin: 10px;
2424
background-color: rgba(255, 128, 0, 0.25);
25+
max-height: 15rem;
26+
overflow: scroll;
2527
}
2628
2729
#build-artifacts-frame h3:nth-of-type(1) {

frontend/src/ts/abl.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export function getLatestCodeVersionForJob(
6868
export async function fetchABL(): Promise<ApprovedBookWithDate[]> {
6969
let abl: ApprovedBookWithDate[];
7070
try {
71-
abl = await RequireAuth.fetchJson("/api/abl/");
71+
abl = await RequireAuth.fetchJson("/api/abl/?consumer=ANY");
7272
} catch (error) {
7373
handleError(error);
7474
abl = [];

0 commit comments

Comments
 (0)