You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by ar...@apache.org on 2026/03/27 17:34:37 UTC
(tooling-trusted-releases) branch arm updated (574295b6 -> f6a4841c)
This is an automated email from the ASF dual-hosted git repository.
arm pushed a change to branch arm
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
omit 574295b6 #931 - tidy up file planner code
new f6a4841c #931 - tidy up file planner code
This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version. This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:
* -- * -- B -- O -- O -- O (574295b6)
\
N -- N -- N refs/heads/arm (f6a4841c)
You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.
Any revisions marked "omit" are not gone; other references still
refer to them. Any revisions marked "discard" are gone forever.
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
atr/get/finish.py | 108 ++----------------------------------------------------
1 file changed, 4 insertions(+), 104 deletions(-)
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org
(tooling-trusted-releases) 01/01: #931 - tidy up file planner code
Posted by ar...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
arm pushed a commit to branch arm
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
commit f6a4841cc6916704a47c4a7d59186470a91da623
Author: Alastair McFarlane <ar...@apache.org>
AuthorDate: Fri Mar 27 17:22:56 2026 +0000
#931 - tidy up file planner code
---
atr/get/compose.py | 293 ++++++++++++++++++-
atr/get/finish.py | 108 +------
atr/post/compose.py | 9 +-
atr/post/finish.py | 46 ---
atr/shared/__init__.py | 2 +
atr/shared/{finish.py => compose.py} | 19 +-
atr/shared/finish.py | 23 +-
atr/shared/web.py | 316 ---------------------
atr/static/js/ts/create-a-jwt.js | 1 +
atr/static/js/ts/create-a-jwt.js.map | 2 +-
atr/static/js/ts/finish-selected-move.js.map | 1 -
.../ts/{finish-selected-move.js => move-files.js} | 2 +-
atr/static/js/ts/move-files.js.map | 1 +
.../ts/{finish-selected-move.ts => move-files.ts} | 0
tests/e2e/announce/test_get.py | 13 -
tests/e2e/compose/conftest.py | 2 +-
tests/e2e/compose/test_get.py | 13 +
tests/e2e/sbom/test_post.py | 8 +-
18 files changed, 326 insertions(+), 533 deletions(-)
diff --git a/atr/get/compose.py b/atr/get/compose.py
index e5d552d6..906ba499 100644
--- a/atr/get/compose.py
+++ b/atr/get/compose.py
@@ -14,19 +14,35 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
+import pathlib
+from typing import TYPE_CHECKING, Literal
-from typing import Literal
-
+import aiofiles.os
import asfquart.base as base
+import htpy
+from quart_wtf import utils
import atr.blueprints.get as get
import atr.db as db
+import atr.db.interaction as interaction
+import atr.form as form
+import atr.htm as htm
import atr.mapping as mapping
+import atr.models.results as results
import atr.models.safe as safe
import atr.models.sql as sql
+import atr.paths as paths
+import atr.post as post
import atr.shared as shared
+import atr.shared.draft as draft
+import atr.storage as storage
+import atr.template as template
+import atr.util as util
import atr.web as web
+if TYPE_CHECKING:
+ from collections.abc import Sequence
+
@get.typed
async def selected(
@@ -49,4 +65,275 @@ async def selected(
).demand(base.ASFQuartException("Release does not exist", errorcode=404))
if release.phase != sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT:
return await mapping.release_as_redirect(session, release)
- return await shared.web.check(session, release)
+
+ base_path = paths.release_directory(release)
+
+ # TODO: This takes 180ms for providers
+ # We could cache it
+ all_paths = [path async for path in util.paths_recursive(base_path)]
+ all_paths.sort()
+
+ async with storage.read(session) as read:
+ ragp = read.as_general_public()
+ info = await ragp.releases.path_info(release, all_paths)
+
+ user_ssh_keys: Sequence[sql.SSHKey] = []
+ asf_id: str | None = None
+ server_domain: str | None = None
+ server_host: str | None = None
+
+ asf_id = session.uid
+ server_domain = session.app_host.split(":", 1)[0]
+ server_host = session.app_host
+ async with db.session() as data:
+ user_ssh_keys = await data.ssh_key(asf_uid=session.uid).all()
+
+ async with db.session() as data:
+ quarantined_pending = await data.quarantined(release_key=release.key, status=sql.QuarantineStatus.PENDING).all()
+ quarantined_failed = await data.quarantined(release_key=release.key, status=sql.QuarantineStatus.FAILED).all()
+
+ clear_quarantine_forms: dict[int, htm.Element] = {}
+ for q in quarantined_failed:
+ if q.id is None:
+ continue
+ clear_quarantine_forms[q.id] = form.render(
+ model_cls=draft.ClearQuarantineForm,
+ action=util.as_url(
+ post.draft.quarantine_clear,
+ project_key=release.safe_project_key,
+ version_key=release.safe_version_key,
+ ),
+ form_classes=".d-inline-block.m-0",
+ submit_classes="btn-sm btn-outline-secondary",
+ submit_label="Dismiss",
+ empty=True,
+ defaults={"quarantined_id": str(q.id)},
+ )
+
+ # Get the number of ongoing tasks for the current revision
+ ongoing_tasks_count = 0
+ match await interaction.latest_info(release.safe_project_key, release.safe_version_key):
+ case (revision_number, revision_editor, revision_timestamp):
+ ongoing_tasks_count = await interaction.tasks_ongoing(
+ release.safe_project_key,
+ release.safe_version_key,
+ revision_number,
+ )
+ case None:
+ revision_number = None
+ revision_editor = None
+ revision_timestamp = None
+
+ delete_form = form.render(
+ model_cls=form.Empty,
+ action=util.as_url(selected, project_key=release.safe_project_key, version_key=release.safe_version_key),
+ submit_label="Delete this draft",
+ submit_classes="btn btn-danger",
+ empty=True,
+ confirm="Are you sure you want to delete this draft? This cannot be undone.",
+ )
+
+ delete_file_forms: dict[str, htm.Element] = {}
+ for path in all_paths:
+ delete_file_forms[str(path)] = form.render(
+ model_cls=draft.DeleteFileForm,
+ action=util.as_url(
+ post.draft.delete_file, project_key=release.safe_project_key, version_key=release.safe_version_key
+ ),
+ form_classes=".d-inline-block.m-0",
+ submit_classes="btn-sm btn-outline-danger",
+ submit_label="Delete",
+ empty=True,
+ defaults={"file_path": str(path)},
+ # TODO: Add a static check for the confirm syntax
+ confirm=(
+ "Are you sure you want to delete this file? "
+ "This will also delete any associated metadata files. "
+ "This cannot be undone."
+ ),
+ )
+
+ recheck_form = form.render(
+ model_cls=form.Empty,
+ action=util.as_url(
+ post.draft.recheck, project_key=release.safe_project_key, version_key=release.safe_version_key
+ ),
+ submit_label="Recheck with fresh cache",
+ submit_classes="btn btn-primary",
+ )
+ cache_reset_form = form.render(
+ model_cls=form.Empty,
+ action=util.as_url(
+ post.draft.cache_reset, project_key=release.safe_project_key, version_key=release.safe_version_key
+ ),
+ submit_label="Recheck with global cache",
+ submit_classes="btn btn-primary",
+ submit_disabled=release.check_cache_key is None,
+ )
+
+ has_files = await util.has_files(release)
+
+ blocker_errors = False
+ if revision_number is not None:
+ blocker_errors = await interaction.has_blocker_checks(release, revision_number)
+
+ checks_summary_html = shared.web.render_checks_summary(info, release.safe_project_key, release.safe_version_key)
+ move_file_html = _render_move_section(10)
+
+ csrf_token = utils.generate_csrf()
+ # Should be already validated, but check again
+ latest_revision_dir = paths.release_directory(release)
+ source_files_rel, target_dirs = await _sources_and_targets(latest_revision_dir)
+ safe_source_files_rel = [util.validate_path(f).as_posix() for f in sorted(source_files_rel)]
+ safe_target_dirs = [util.validate_path(d).as_posix() for d in sorted(target_dirs)]
+ scripts = htpy.fragment[
+ htpy.script(id="file-data", type="application/json")[util.json_for_script_element(safe_source_files_rel)],
+ htpy.script(id="dir-data", type="application/json")[util.json_for_script_element(safe_target_dirs)],
+ htpy.script(
+ id="main-script-data",
+ src=util.static_url("js/ts/move-files.js"),
+ **{"data-csrf-token": csrf_token},
+ )[""],
+ ]
+
+ return await template.render(
+ "check-selected.html",
+ project_key=release.project.key,
+ version_key=release.version,
+ release=release,
+ paths=all_paths,
+ info=info,
+ revision_editor=revision_editor,
+ revision_time=revision_timestamp,
+ revision_number=revision_number,
+ ongoing_tasks_count=ongoing_tasks_count,
+ quarantined_pending=quarantined_pending,
+ quarantined_failed=quarantined_failed,
+ clear_quarantine_forms=clear_quarantine_forms,
+ delete_form=delete_form,
+ delete_file_forms=delete_file_forms,
+ asf_id=asf_id,
+ server_domain=server_domain,
+ server_host=server_host,
+ user_ssh_keys=user_ssh_keys,
+ format_datetime=util.format_datetime,
+ models=sql,
+ recheck_form=recheck_form,
+ cache_reset_form=cache_reset_form,
+ csrf_input=str(form.csrf_input()),
+ has_files=has_files,
+ blocker_errors=blocker_errors,
+ checks_summary_html=checks_summary_html,
+ move_file_html=move_file_html,
+ scripts=str(scripts),
+ )
+
+
+def _render_move_section(max_files_to_show: int = 10) -> htm.Element:
+ """Render the move files section with JavaScript interaction."""
+ section = htm.Block()
+
+ section.h2["Move items to a different directory"]
+ section.p[
+ "You may ",
+ htm.strong["optionally"],
+ " move files between your directories here if you want change their location for the final release. "
+ "Note that files with associated metadata (e.g. ",
+ htm.code[".asc"],
+ " or ",
+ htm.code[".sha512"],
+ " files) are treated as a single unit and will be moved together if any one of them is selected for movement.",
+ ]
+
+ section.append(htm.div("#move-error-alert.alert.alert-danger.d-none", role="alert", **{"aria-live": "assertive"}))
+
+ left_card = htm.Block(htm.div, classes=".card.mb-4")
+ left_card.div(".card-header.bg-light")[htm.h3(".mb-0")["Select items to move"]]
+ left_card.div(".card-body")[
+ htpy.input(
+ "#file-filter.form-control.mb-2",
+ type="text",
+ placeholder="Search for an item to move...",
+ ),
+ htm.table(".table.table-sm.table-striped.border.mt-3")[htm.tbody("#file-list-table-body")],
+ htm.div("#file-list-more-info.text-muted.small.mt-1"),
+ htpy.button(
+ "#select-files-toggle-button.btn.btn-outline-secondary.w-100.mt-2",
+ type="button",
+ )["Select these files"],
+ ]
+
+ right_card = htm.Block(htm.div, classes=".card.mb-4")
+ right_card.div(".card-header.bg-light")[
+ htm.h3(".mb-0")[
+ htm.span("#selected-file-name-title")["Select a destination for the file"],
+ htm.span(".text-muted.small")[" (enter a new directory name to create it)"],
+ ]
+ ]
+ right_card.div(".card-body")[
+ htpy.input(
+ "#dir-filter-input.form-control.mb-2",
+ type="text",
+ placeholder="Search for a directory to move to...",
+ ),
+ htm.table(".table.table-sm.table-striped.border.mt-3")[htm.tbody("#dir-list-table-body")],
+ htm.div("#dir-list-more-info.text-muted.small.mt-1"),
+ ]
+
+ section.form(".atr-canary")[
+ htm.div(".row")[
+ htm.div(".col-lg-6")[left_card.collect()],
+ htm.div(".col-lg-6")[right_card.collect()],
+ ],
+ htm.div(".mb-3")[
+ htpy.label(".form-label", for_="maxFilesInput")["Items to show per list:"],
+ htpy.input(
+ "#max-files-input.form-control.form-control-sm.w-25",
+ type="number",
+ value=str(max_files_to_show),
+ min="1",
+ ),
+ ],
+ htm.div("#current-move-selection-info.text-muted")["Please select a file and a destination."],
+ htm.div[htpy.button("#confirm-move-button.btn.btn-success.mt-2", type="button")["Move to selected directory"]],
+ ]
+
+ return section.collect()
+
+
+async def _sources_and_targets(latest_revision_dir: pathlib.Path) -> tuple[list[pathlib.Path], set[pathlib.Path]]:
+ source_items_rel: list[pathlib.Path] = []
+ target_dirs: set[pathlib.Path] = {pathlib.Path(".")}
+
+ async for item_rel_path in util.paths_recursive_all(latest_revision_dir):
+ current_parent = item_rel_path.parent
+ source_items_rel.append(item_rel_path)
+
+ while True:
+ target_dirs.add(current_parent)
+ if current_parent == pathlib.Path("."):
+ break
+ current_parent = current_parent.parent
+
+ item_abs_path = latest_revision_dir / item_rel_path
+ if await aiofiles.os.path.isfile(item_abs_path):
+ pass
+ elif await aiofiles.os.path.isdir(item_abs_path):
+ target_dirs.add(item_rel_path)
+
+ return source_items_rel, target_dirs
+
+
+def _warnings_from_vote_result(vote_task: sql.Task | None) -> list[str]:
+ # TODO: Replace this with a schema.Strict model
+ # But we'd still need to do some of this parsing and validation
+ # We should probably rethink how to send data through tasks
+
+ if (not vote_task) or (not vote_task.result):
+ return ["No vote task result found."]
+
+ vote_task_result = vote_task.result
+ if not isinstance(vote_task_result, results.VoteInitiate):
+ return ["Vote task result is not a results.VoteInitiate instance."]
+
+ return vote_task_result.mail_send_warnings
diff --git a/atr/get/finish.py b/atr/get/finish.py
index 70b2c07c..831b7d42 100644
--- a/atr/get/finish.py
+++ b/atr/get/finish.py
@@ -23,10 +23,8 @@ from typing import Literal
import aiofiles.os
import asfquart.base as base
-import htpy
import markupsafe
import quart
-import quart_wtf.utils as utils
import atr.analysis as analysis
import atr.blueprints.get as get
@@ -71,9 +69,7 @@ async def selected(
"""
await session.check_access(project_key)
try:
- (release, source_files_rel, target_dirs, deletable_dirs, rc_analysis, tasks) = await _get_page_data(
- project_key, version_key
- )
+ (release, deletable_dirs, rc_analysis, tasks) = await _get_page_data(project_key, version_key)
except ValueError:
async with db.session() as data:
release_fallback = await data.release(
@@ -105,8 +101,6 @@ async def selected(
return await _render_page(
release=release,
- source_files_rel=source_files_rel,
- target_dirs=target_dirs,
deletable_dirs=deletable_dirs,
rc_analysis=rc_analysis,
distribution_tasks=tasks,
@@ -156,9 +150,7 @@ async def _deletable_choices(
async def _get_page_data(
project_key: safe.ProjectKey, version_key: safe.VersionKey
-) -> tuple[
- sql.Release, list[pathlib.Path], set[pathlib.Path], list[tuple[str, str]], RCTagAnalysisResult, Sequence[sql.Task]
-]:
+) -> tuple[sql.Release, list[tuple[str, str]], RCTagAnalysisResult, Sequence[sql.Task]]:
"""Get all the data needed to render the finish page."""
async with db.session() as data:
via = sql.validate_instrumented_attribute
@@ -189,11 +181,11 @@ async def _get_page_data(
raise ValueError("Release is not in preview phase")
latest_revision_dir = paths.release_directory(release)
- source_files_rel, target_dirs = await _sources_and_targets(latest_revision_dir)
+ _, target_dirs = await _sources_and_targets(latest_revision_dir)
deletable_dirs = await _deletable_choices(latest_revision_dir, target_dirs)
rc_analysis_result = await _analyse_rc_tags(latest_revision_dir)
- return release, source_files_rel, target_dirs, deletable_dirs, rc_analysis_result, tasks
+ return release, deletable_dirs, rc_analysis_result, tasks
def _render_delete_directory_form(deletable_dirs: list[tuple[str, str]]) -> htm.Element:
@@ -293,79 +285,8 @@ def _render_distribution_tasks(release: sql.Release, tasks: Sequence[sql.Task])
return block.collect()
-def _render_move_section(max_files_to_show: int = 10) -> htm.Element:
- """Render the move files section with JavaScript interaction."""
- section = htm.Block()
-
- section.h2["Move items to a different directory"]
- section.p[
- "You may ",
- htm.strong["optionally"],
- " move files between your directories here if you want change their location for the final release. "
- "Note that files with associated metadata (e.g. ",
- htm.code[".asc"],
- " or ",
- htm.code[".sha512"],
- " files) are treated as a single unit and will be moved together if any one of them is selected for movement.",
- ]
-
- section.append(htm.div("#move-error-alert.alert.alert-danger.d-none", role="alert", **{"aria-live": "assertive"}))
-
- left_card = htm.Block(htm.div, classes=".card.mb-4")
- left_card.div(".card-header.bg-light")[htm.h3(".mb-0")["Select items to move"]]
- left_card.div(".card-body")[
- htpy.input(
- "#file-filter.form-control.mb-2",
- type="text",
- placeholder="Search for an item to move...",
- ),
- htm.table(".table.table-sm.table-striped.border.mt-3")[htm.tbody("#file-list-table-body")],
- htm.div("#file-list-more-info.text-muted.small.mt-1"),
- htpy.button(
- "#select-files-toggle-button.btn.btn-outline-secondary.w-100.mt-2",
- type="button",
- )["Select these files"],
- ]
-
- right_card = htm.Block(htm.div, classes=".card.mb-4")
- right_card.div(".card-header.bg-light")[
- htm.h3(".mb-0")[htm.span("#selected-file-name-title")["Select a destination for the file"]]
- ]
- right_card.div(".card-body")[
- htpy.input(
- "#dir-filter-input.form-control.mb-2",
- type="text",
- placeholder="Search for a directory to move to...",
- ),
- htm.table(".table.table-sm.table-striped.border.mt-3")[htm.tbody("#dir-list-table-body")],
- htm.div("#dir-list-more-info.text-muted.small.mt-1"),
- ]
-
- section.form(".atr-canary")[
- htm.div(".row")[
- htm.div(".col-lg-6")[left_card.collect()],
- htm.div(".col-lg-6")[right_card.collect()],
- ],
- htm.div(".mb-3")[
- htpy.label(".form-label", for_="maxFilesInput")["Items to show per list:"],
- htpy.input(
- "#max-files-input.form-control.form-control-sm.w-25",
- type="number",
- value=str(max_files_to_show),
- min="1",
- ),
- ],
- htm.div("#current-move-selection-info.text-muted")["Please select a file and a destination."],
- htm.div[htpy.button("#confirm-move-button.btn.btn-success.mt-2", type="button")["Move to selected directory"]],
- ]
-
- return section.collect()
-
-
async def _render_page(
release: sql.Release,
- source_files_rel: list,
- target_dirs: set,
deletable_dirs: list[tuple[str, str]],
rc_analysis: RCTagAnalysisResult,
distribution_tasks: Sequence[sql.Task],
@@ -405,9 +326,6 @@ async def _render_page(
page.append(_render_dist_warning())
page.append(_render_distribution_buttons(release))
- # Move files section
- page.append(_render_move_section(max_files_to_show=10))
-
# Delete directory form
if deletable_dirs:
page.append(_render_delete_directory_form(deletable_dirs))
@@ -442,24 +360,6 @@ async def _render_page(
"""
page.style[markupsafe.Markup(page_styles)]
- # JavaScript data
- # TODO: Add htm.script
- csrf_token = utils.generate_csrf()
- # Should be already validated, but check again
- safe_source_files_rel = [util.validate_path(f).as_posix() for f in sorted(source_files_rel)]
- safe_target_dirs = [util.validate_path(d).as_posix() for d in sorted(target_dirs)]
- page.append(
- htpy.script(id="file-data", type="application/json")[util.json_for_script_element(safe_source_files_rel)]
- )
- page.append(htpy.script(id="dir-data", type="application/json")[util.json_for_script_element(safe_target_dirs)])
- page.append(
- htpy.script(
- id="main-script-data",
- src=util.static_url("js/ts/finish-selected-move.js"),
- **{"data-csrf-token": csrf_token},
- )[""]
- )
-
content = page.collect()
return await template.blank(
diff --git a/atr/post/compose.py b/atr/post/compose.py
index 640a5e6e..5c4aaca4 100644
--- a/atr/post/compose.py
+++ b/atr/post/compose.py
@@ -15,7 +15,6 @@
# specific language governing permissions and limitations
# under the License.
-from collections.abc import Awaitable, Callable
from typing import Literal
import quart
@@ -34,7 +33,7 @@ async def selected(
_compose: Literal["compose"],
project_key: safe.ProjectKey,
version_key: safe.VersionKey,
- move_form: shared.finish.MoveFileForm,
+ move_form: shared.compose.MoveFileForm,
) -> tuple[web.QuartResponse, int] | web.WerkzeugResponse:
"""
URL: /compose/<project_key>/<version_key>
@@ -46,11 +45,11 @@ async def selected(
async def _move_file_to_revision(
- move_form: shared.finish.MoveFileForm,
+ move_form: shared.compose.MoveFileForm,
session: web.Committer,
project_key: safe.ProjectKey,
version_key: safe.VersionKey,
- respond: Callable[[int, str], Awaitable[tuple[web.QuartResponse, int] | web.WerkzeugResponse]],
+ respond: shared.compose.Respond,
) -> tuple[web.QuartResponse, int] | web.WerkzeugResponse:
source_files_rel = move_form.source_files
target_dir_rel = move_form.target_directory
@@ -91,7 +90,7 @@ async def _move_file_to_revision(
def _respond_helper(
session: web.Committer, project_key: safe.ProjectKey, version_key: safe.VersionKey, wants_json: bool
-) -> Callable[[int, str], Awaitable[tuple[web.QuartResponse, int] | web.WerkzeugResponse]]:
+) -> shared.compose.Respond:
"""Create a response helper function for the compose route."""
import atr.get as get
diff --git a/atr/post/finish.py b/atr/post/finish.py
index afabcc1f..f370486e 100644
--- a/atr/post/finish.py
+++ b/atr/post/finish.py
@@ -45,8 +45,6 @@ async def selected(
match finish_form:
case shared.finish.DeleteEmptyDirectoryForm() as delete_form:
return await _delete_empty_directory(delete_form, session, project_key, version_key, respond)
- case shared.finish.MoveFileForm() as move_form:
- return await _move_file_to_revision(move_form, session, project_key, version_key, respond)
case shared.finish.RemoveRCTagsForm():
return await _remove_rc_tags(session, project_key, version_key, respond)
@@ -74,50 +72,6 @@ async def _delete_empty_directory(
return await respond(200, f"Deleted empty directory '{dir_to_delete_rel}'.")
-async def _move_file_to_revision(
- move_form: shared.finish.MoveFileForm,
- session: web.Committer,
- project_key: safe.ProjectKey,
- version_key: safe.VersionKey,
- respond: shared.finish.Respond,
-) -> tuple[web.QuartResponse, int] | web.WerkzeugResponse:
- source_files_rel = move_form.source_files
- target_dir_rel = move_form.target_directory
- try:
- async with storage.write(session) as write:
- wacp = await write.as_project_committee_member(project_key)
- creation_error, moved_files_names, skipped_files_names = await wacp.release.move_file(
- project_key, version_key, source_files_rel, target_dir_rel
- )
-
- if creation_error is not None:
- return await respond(409, creation_error)
-
- response_messages = []
- if moved_files_names:
- response_messages.append(f"Moved {', '.join(moved_files_names)}")
- if skipped_files_names:
- response_messages.append(f"Skipped {', '.join(skipped_files_names)} (already in target directory)")
-
- if not response_messages:
- if not source_files_rel:
- return await respond(400, "No source files specified for move.")
- msg = f"No files were moved. {', '.join(skipped_files_names)} already in '{target_dir_rel}'."
- return await respond(200, msg)
-
- return await respond(200, ". ".join(response_messages) + ".")
-
- except FileNotFoundError:
- log.exception("File not found during move operation in new revision")
- return await respond(400, "Error: Source file not found during move operation.")
- except OSError as e:
- log.exception("Error moving file in new revision")
- return await respond(500, f"Error moving file: {e}")
- except Exception as e:
- log.exception("Unexpected error during file move")
- return await respond(500, f"ERROR: {e!s}")
-
-
async def _remove_rc_tags(
session: web.Committer,
project_key: safe.ProjectKey,
diff --git a/atr/shared/__init__.py b/atr/shared/__init__.py
index 4090d591..2e639a6b 100644
--- a/atr/shared/__init__.py
+++ b/atr/shared/__init__.py
@@ -18,6 +18,7 @@
from typing import Final
import atr.shared.announce as announce
+import atr.shared.compose as compose
import atr.shared.distribution as distribution
import atr.shared.draft as draft
import atr.shared.finish as finish
@@ -66,6 +67,7 @@ algorithms: Final[dict[int, str]] = {
__all__ = [
"algorithms",
"announce",
+ "compose",
"distribution",
"draft",
"finish",
diff --git a/atr/shared/finish.py b/atr/shared/compose.py
similarity index 77%
copy from atr/shared/finish.py
copy to atr/shared/compose.py
index 1a7f835f..eb0daf72 100644
--- a/atr/shared/finish.py
+++ b/atr/shared/compose.py
@@ -16,7 +16,7 @@
# under the License.
from collections.abc import Awaitable, Callable
-from typing import Annotated, Literal
+from typing import Literal
import pydantic
@@ -26,14 +26,7 @@ import atr.web as web
type Respond = Callable[[int, str], Awaitable[tuple[web.QuartResponse, int] | web.WerkzeugResponse]]
-type DELETE_DIR = Literal["DELETE_DIR"]
type MOVE_FILE = Literal["MOVE_FILE"]
-type REMOVE_RC_TAGS = Literal["REMOVE_RC_TAGS"]
-
-
-class DeleteEmptyDirectoryForm(form.Form):
- variant: DELETE_DIR = form.value(DELETE_DIR)
- directory_to_delete: safe.RelPath = form.label("Directory to delete", widget=form.Widget.SELECT)
class MoveFileForm(form.Form):
@@ -52,13 +45,3 @@ class MoveFileForm(form.Form):
if source.parent == target_dir_path:
raise ValueError(f"Target directory cannot be the same as the source directory for {source.name}.")
return self
-
-
-class RemoveRCTagsForm(form.Empty):
- variant: REMOVE_RC_TAGS = form.value(REMOVE_RC_TAGS)
-
-
-type FinishForm = Annotated[
- DeleteEmptyDirectoryForm | MoveFileForm | RemoveRCTagsForm,
- form.DISCRIMINATOR,
-]
diff --git a/atr/shared/finish.py b/atr/shared/finish.py
index 1a7f835f..7b511368 100644
--- a/atr/shared/finish.py
+++ b/atr/shared/finish.py
@@ -18,8 +18,6 @@
from collections.abc import Awaitable, Callable
from typing import Annotated, Literal
-import pydantic
-
import atr.form as form
import atr.models.safe as safe
import atr.web as web
@@ -27,7 +25,6 @@ import atr.web as web
type Respond = Callable[[int, str], Awaitable[tuple[web.QuartResponse, int] | web.WerkzeugResponse]]
type DELETE_DIR = Literal["DELETE_DIR"]
-type MOVE_FILE = Literal["MOVE_FILE"]
type REMOVE_RC_TAGS = Literal["REMOVE_RC_TAGS"]
@@ -36,29 +33,11 @@ class DeleteEmptyDirectoryForm(form.Form):
directory_to_delete: safe.RelPath = form.label("Directory to delete", widget=form.Widget.SELECT)
-class MoveFileForm(form.Form):
- variant: MOVE_FILE = form.value(MOVE_FILE)
- source_files: form.RelPathList = form.label("Files to move")
- target_directory: safe.RelPath = form.label("Target directory")
-
- @pydantic.model_validator(mode="after")
- def validate_move(self) -> "MoveFileForm":
- if not self.source_files:
- raise ValueError("Please select at least one file to move.")
-
- target_dir_path = self.target_directory.as_path()
- for source_path in self.source_files:
- source = source_path.as_path()
- if source.parent == target_dir_path:
- raise ValueError(f"Target directory cannot be the same as the source directory for {source.name}.")
- return self
-
-
class RemoveRCTagsForm(form.Empty):
variant: REMOVE_RC_TAGS = form.value(REMOVE_RC_TAGS)
type FinishForm = Annotated[
- DeleteEmptyDirectoryForm | MoveFileForm | RemoveRCTagsForm,
+ DeleteEmptyDirectoryForm | RemoveRCTagsForm,
form.DISCRIMINATOR,
]
diff --git a/atr/shared/web.py b/atr/shared/web.py
index 1e87d1d2..49c11f7e 100644
--- a/atr/shared/web.py
+++ b/atr/shared/web.py
@@ -14,219 +14,13 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-import pathlib
-from typing import TYPE_CHECKING
-import aiofiles.os
import htpy
-import quart_wtf.utils as utils
-import atr.db as db
-import atr.db.interaction as interaction
-import atr.form as form
-import atr.get as get
import atr.htm as htm
-import atr.models.results as results
import atr.models.safe as safe
-import atr.models.sql as sql
-import atr.paths as paths
-import atr.post as post
-import atr.shared.draft as draft
-import atr.storage as storage
import atr.storage.types as types
-import atr.template as template
import atr.util as util
-import atr.web as web
-
-if TYPE_CHECKING:
- from collections.abc import Sequence
-
-
-async def check(
- session: web.Committer | None,
- release: sql.Release,
- task_mid: str | None = None,
- vote_form: htm.Element | None = None,
- resolve_form: htm.Element | None = None,
- archive_url: str | None = None,
- vote_task: sql.Task | None = None,
- can_vote: bool = False,
- can_resolve: bool = False,
-) -> web.WerkzeugResponse | str:
- base_path = paths.release_directory(release)
-
- # TODO: This takes 180ms for providers
- # We could cache it
- all_paths = [path async for path in util.paths_recursive(base_path)]
- all_paths.sort()
-
- async with storage.read(session) as read:
- ragp = read.as_general_public()
- info = await ragp.releases.path_info(release, all_paths)
-
- user_ssh_keys: Sequence[sql.SSHKey] = []
- asf_id: str | None = None
- server_domain: str | None = None
- server_host: str | None = None
-
- if session is not None:
- asf_id = session.uid
- server_domain = session.app_host.split(":", 1)[0]
- server_host = session.app_host
- async with db.session() as data:
- user_ssh_keys = await data.ssh_key(asf_uid=session.uid).all()
-
- async with db.session() as data:
- quarantined_pending = await data.quarantined(release_key=release.key, status=sql.QuarantineStatus.PENDING).all()
- quarantined_failed = await data.quarantined(release_key=release.key, status=sql.QuarantineStatus.FAILED).all()
-
- clear_quarantine_forms: dict[int, htm.Element] = {}
- if session is not None:
- for q in quarantined_failed:
- if q.id is None:
- continue
- clear_quarantine_forms[q.id] = form.render(
- model_cls=draft.ClearQuarantineForm,
- action=util.as_url(
- post.draft.quarantine_clear,
- project_key=release.safe_project_key,
- version_key=release.safe_version_key,
- ),
- form_classes=".d-inline-block.m-0",
- submit_classes="btn-sm btn-outline-secondary",
- submit_label="Dismiss",
- empty=True,
- defaults={"quarantined_id": str(q.id)},
- )
-
- # Get the number of ongoing tasks for the current revision
- ongoing_tasks_count = 0
- match await interaction.latest_info(release.safe_project_key, release.safe_version_key):
- case (revision_number, revision_editor, revision_timestamp):
- ongoing_tasks_count = await interaction.tasks_ongoing(
- release.safe_project_key,
- release.safe_version_key,
- revision_number,
- )
- case None:
- revision_number = None
- revision_editor = None
- revision_timestamp = None
-
- delete_form = form.render(
- model_cls=form.Empty,
- action=util.as_url(
- get.compose.selected, project_key=release.safe_project_key, version_key=release.safe_version_key
- ),
- submit_label="Delete this draft",
- submit_classes="btn btn-danger",
- empty=True,
- confirm="Are you sure you want to delete this draft? This cannot be undone.",
- )
-
- delete_file_forms: dict[str, htm.Element] = {}
- for path in all_paths:
- delete_file_forms[str(path)] = form.render(
- model_cls=draft.DeleteFileForm,
- action=util.as_url(
- post.draft.delete_file, project_key=release.safe_project_key, version_key=release.safe_version_key
- ),
- form_classes=".d-inline-block.m-0",
- submit_classes="btn-sm btn-outline-danger",
- submit_label="Delete",
- empty=True,
- defaults={"file_path": str(path)},
- # TODO: Add a static check for the confirm syntax
- confirm=(
- "Are you sure you want to delete this file? "
- "This will also delete any associated metadata files. "
- "This cannot be undone."
- ),
- )
-
- recheck_form = form.render(
- model_cls=form.Empty,
- action=util.as_url(
- post.draft.recheck, project_key=release.safe_project_key, version_key=release.safe_version_key
- ),
- submit_label="Recheck with fresh cache",
- submit_classes="btn btn-primary",
- )
- cache_reset_form = form.render(
- model_cls=form.Empty,
- action=util.as_url(
- post.draft.cache_reset, project_key=release.safe_project_key, version_key=release.safe_version_key
- ),
- submit_label="Recheck with global cache",
- submit_classes="btn btn-primary",
- submit_disabled=release.check_cache_key is None,
- )
-
- vote_task_warnings = _warnings_from_vote_result(vote_task)
- has_files = await util.has_files(release)
-
- blocker_errors = False
- if revision_number is not None:
- blocker_errors = await interaction.has_blocker_checks(release, revision_number)
-
- checks_summary_html = render_checks_summary(info, release.safe_project_key, release.safe_version_key)
- move_file_html = _render_move_section(10)
-
- csrf_token = utils.generate_csrf()
- # Should be already validated, but check again
- latest_revision_dir = paths.release_directory(release)
- source_files_rel, target_dirs = await _sources_and_targets(latest_revision_dir)
- safe_source_files_rel = [util.validate_path(f).as_posix() for f in sorted(source_files_rel)]
- safe_target_dirs = [util.validate_path(d).as_posix() for d in sorted(target_dirs)]
- scripts = htpy.fragment[
- htpy.script(id="file-data", type="application/json")[util.json_for_script_element(safe_source_files_rel)],
- htpy.script(id="dir-data", type="application/json")[util.json_for_script_element(safe_target_dirs)],
- htpy.script(
- id="main-script-data",
- src=util.static_url("js/ts/finish-selected-move.js"),
- **{"data-csrf-token": csrf_token},
- )[""],
- ]
-
- return await template.render(
- "check-selected.html",
- project_key=release.project.key,
- version_key=release.version,
- release=release,
- paths=all_paths,
- info=info,
- revision_editor=revision_editor,
- revision_time=revision_timestamp,
- revision_number=revision_number,
- ongoing_tasks_count=ongoing_tasks_count,
- quarantined_pending=quarantined_pending,
- quarantined_failed=quarantined_failed,
- clear_quarantine_forms=clear_quarantine_forms,
- delete_form=delete_form,
- delete_file_forms=delete_file_forms,
- asf_id=asf_id,
- server_domain=server_domain,
- server_host=server_host,
- user_ssh_keys=user_ssh_keys,
- format_datetime=util.format_datetime,
- models=sql,
- task_mid=task_mid,
- vote_form=vote_form,
- vote_task=vote_task,
- archive_url=archive_url,
- vote_task_warnings=vote_task_warnings,
- recheck_form=recheck_form,
- cache_reset_form=cache_reset_form,
- csrf_input=str(form.csrf_input()),
- resolve_form=resolve_form,
- has_files=has_files,
- blocker_errors=blocker_errors,
- can_vote=can_vote,
- can_resolve=can_resolve,
- checks_summary_html=checks_summary_html,
- move_file_html=move_file_html,
- scripts=str(scripts),
- )
def render_checks_summary(
@@ -284,113 +78,3 @@ def render_checks_summary(
def _checker_display_name(checker: str) -> str:
return checker.removeprefix("atr.tasks.checks.").replace("_", " ").replace(".", " ").title()
-
-
-def _render_move_section(max_files_to_show: int = 10) -> htm.Element:
- """Render the move files section with JavaScript interaction."""
- section = htm.Block()
-
- section.h2["Move items to a different directory"]
- section.p[
- "You may ",
- htm.strong["optionally"],
- " move files between your directories here if you want change their location for the final release. "
- "Note that files with associated metadata (e.g. ",
- htm.code[".asc"],
- " or ",
- htm.code[".sha512"],
- " files) are treated as a single unit and will be moved together if any one of them is selected for movement.",
- ]
-
- section.append(htm.div("#move-error-alert.alert.alert-danger.d-none", role="alert", **{"aria-live": "assertive"}))
-
- left_card = htm.Block(htm.div, classes=".card.mb-4")
- left_card.div(".card-header.bg-light")[htm.h3(".mb-0")["Select items to move"]]
- left_card.div(".card-body")[
- htpy.input(
- "#file-filter.form-control.mb-2",
- type="text",
- placeholder="Search for an item to move...",
- ),
- htm.table(".table.table-sm.table-striped.border.mt-3")[htm.tbody("#file-list-table-body")],
- htm.div("#file-list-more-info.text-muted.small.mt-1"),
- htpy.button(
- "#select-files-toggle-button.btn.btn-outline-secondary.w-100.mt-2",
- type="button",
- )["Select these files"],
- ]
-
- right_card = htm.Block(htm.div, classes=".card.mb-4")
- right_card.div(".card-header.bg-light")[
- htm.h3(".mb-0")[
- htm.span("#selected-file-name-title")["Select a destination for the file"],
- htm.span(".text-muted.small")[" (enter a new directory name to create it)"],
- ]
- ]
- right_card.div(".card-body")[
- htpy.input(
- "#dir-filter-input.form-control.mb-2",
- type="text",
- placeholder="Search for a directory to move to...",
- ),
- htm.table(".table.table-sm.table-striped.border.mt-3")[htm.tbody("#dir-list-table-body")],
- htm.div("#dir-list-more-info.text-muted.small.mt-1"),
- ]
-
- section.form(".atr-canary")[
- htm.div(".row")[
- htm.div(".col-lg-6")[left_card.collect()],
- htm.div(".col-lg-6")[right_card.collect()],
- ],
- htm.div(".mb-3")[
- htpy.label(".form-label", for_="maxFilesInput")["Items to show per list:"],
- htpy.input(
- "#max-files-input.form-control.form-control-sm.w-25",
- type="number",
- value=str(max_files_to_show),
- min="1",
- ),
- ],
- htm.div("#current-move-selection-info.text-muted")["Please select a file and a destination."],
- htm.div[htpy.button("#confirm-move-button.btn.btn-success.mt-2", type="button")["Move to selected directory"]],
- ]
-
- return section.collect()
-
-
-async def _sources_and_targets(latest_revision_dir: pathlib.Path) -> tuple[list[pathlib.Path], set[pathlib.Path]]:
- source_items_rel: list[pathlib.Path] = []
- target_dirs: set[pathlib.Path] = {pathlib.Path(".")}
-
- async for item_rel_path in util.paths_recursive_all(latest_revision_dir):
- current_parent = item_rel_path.parent
- source_items_rel.append(item_rel_path)
-
- while True:
- target_dirs.add(current_parent)
- if current_parent == pathlib.Path("."):
- break
- current_parent = current_parent.parent
-
- item_abs_path = latest_revision_dir / item_rel_path
- if await aiofiles.os.path.isfile(item_abs_path):
- pass
- elif await aiofiles.os.path.isdir(item_abs_path):
- target_dirs.add(item_rel_path)
-
- return source_items_rel, target_dirs
-
-
-def _warnings_from_vote_result(vote_task: sql.Task | None) -> list[str]:
- # TODO: Replace this with a schema.Strict model
- # But we'd still need to do some of this parsing and validation
- # We should probably rethink how to send data through tasks
-
- if (not vote_task) or (not vote_task.result):
- return ["No vote task result found."]
-
- vote_task_result = vote_task.result
- if not isinstance(vote_task_result, results.VoteInitiate):
- return ["Vote task result is not a results.VoteInitiate instance."]
-
- return vote_task_result.mail_send_warnings
diff --git a/atr/static/js/ts/create-a-jwt.js b/atr/static/js/ts/create-a-jwt.js
index caf57e24..8f9b6bfb 100644
--- a/atr/static/js/ts/create-a-jwt.js
+++ b/atr/static/js/ts/create-a-jwt.js
@@ -43,6 +43,7 @@ document.addEventListener("DOMContentLoaded", () => {
timeoutObj = setTimeout(() => {
output.textContent = "";
outputContainer.classList.add("d-none");
+ clearInterval(intervalObj);
}, 60000);
intervalObj = setInterval(() => {
time = time - 1;
diff --git a/atr/static/js/ts/create-a-jwt.js.map b/atr/static/js/ts/create-a-jwt.js.map
index 2ebe6f4b..72e5e216 100644
--- a/atr/static/js/ts/create-a-jwt.js.map
+++ b/atr/static/js/ts/create-a-jwt.js.map
@@ -1 +1 @@
-{"version":3,"file":"create-a-jwt.js","sourceRoot":"","sources":["../../ts/create-a-jwt.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;EAiBE;AAEF,QAAQ,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,GAAS,EAAE;IACvD,MAAM,IAAI,GAAG,QAAQ,CAAC,cAAc,CAAC,gBAAgB,CAA2B,CAAC;IACjF,MAAM,eAAe,GAAG,QAAQ,CAAC,cAAc,CAAC,eAAe,CAAC,CAAA;IAChE,MAAM,MAAM,GAAG,QAAQ,CAAC,cAAc,CAAC,YAAY,CAAC,CAAC;IACrD,MAAM,SAAS,GAAG,QAAQ,CAAC,cAAc,CAAC,gBAAgB,CAAC,CAAC;IAC5D,IAAI,UAAkB,EAAE,WAAmB,CAAC;IAE5C,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,I [...]
\ No newline at end of file
+{"version":3,"file":"create-a-jwt.js","sourceRoot":"","sources":["../../ts/create-a-jwt.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;EAiBE;AAEF,QAAQ,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,GAAS,EAAE;IACvD,MAAM,IAAI,GAAG,QAAQ,CAAC,cAAc,CAAC,gBAAgB,CAA2B,CAAC;IACjF,MAAM,eAAe,GAAG,QAAQ,CAAC,cAAc,CAAC,eAAe,CAAC,CAAA;IAChE,MAAM,MAAM,GAAG,QAAQ,CAAC,cAAc,CAAC,YAAY,CAAC,CAAC;IACrD,MAAM,SAAS,GAAG,QAAQ,CAAC,cAAc,CAAC,gBAAgB,CAAC,CAAC;IAC5D,IAAI,UAAkB,EAAE,WAAmB,CAAC;IAE5C,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,I [...]
\ No newline at end of file
diff --git a/atr/static/js/ts/finish-selected-move.js.map b/atr/static/js/ts/finish-selected-move.js.map
deleted file mode 100644
index efa6a0a6..00000000
--- a/atr/static/js/ts/finish-selected-move.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"finish-selected-move.js","sourceRoot":"","sources":["../../ts/finish-selected-move.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;EAiBE;AAEF,YAAY,CAAC;AAEb,IAAK,QAGJ;AAHD,WAAK,QAAQ;IACT,yBAAa,CAAA;IACb,uBAAW,CAAA;AACf,CAAC,EAHI,QAAQ,KAAR,QAAQ,QAGZ;AAED,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC;IACrB,iBAAiB,EAAE,qBAAqB;IACxC,wBAAwB,EAAE,6BAA6B;IACvD,OAAO,EAAE,UAAU;IACnB,cAAc,EAAE,kBAAkB;IAClC,eAAe,EAAE,oBAAoB;IACrC,gBAAgB,EAAE,qBAAqB;IACvC,UAAU,EAAE,kBAAkB;IAC9B,QAAQ,EAAE, [...]
\ No newline at end of file
diff --git a/atr/static/js/ts/finish-selected-move.js b/atr/static/js/ts/move-files.js
similarity index 99%
rename from atr/static/js/ts/finish-selected-move.js
rename to atr/static/js/ts/move-files.js
index 56aebc7f..13d5c5d9 100644
--- a/atr/static/js/ts/finish-selected-move.js
+++ b/atr/static/js/ts/move-files.js
@@ -486,4 +486,4 @@ document.addEventListener("DOMContentLoaded", () => {
});
renderAllLists();
});
-//# sourceMappingURL=finish-selected-move.js.map
\ No newline at end of file
+//# sourceMappingURL=move-files.js.map
\ No newline at end of file
diff --git a/atr/static/js/ts/move-files.js.map b/atr/static/js/ts/move-files.js.map
new file mode 100644
index 00000000..1d8810d5
--- /dev/null
+++ b/atr/static/js/ts/move-files.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"move-files.js","sourceRoot":"","sources":["../../ts/move-files.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;EAiBE;AAEF,YAAY,CAAC;AAEb,IAAK,QAGJ;AAHD,WAAK,QAAQ;IACT,yBAAa,CAAA;IACb,uBAAW,CAAA;AACf,CAAC,EAHI,QAAQ,KAAR,QAAQ,QAGZ;AAED,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC;IACrB,iBAAiB,EAAE,qBAAqB;IACxC,wBAAwB,EAAE,6BAA6B;IACvD,OAAO,EAAE,UAAU;IACnB,cAAc,EAAE,kBAAkB;IAClC,eAAe,EAAE,oBAAoB;IACrC,gBAAgB,EAAE,qBAAqB;IACvC,UAAU,EAAE,kBAAkB;IAC9B,QAAQ,EAAE,WAAW;IACrB,UAAU,EAAE [...]
\ No newline at end of file
diff --git a/atr/static/ts/finish-selected-move.ts b/atr/static/ts/move-files.ts
similarity index 100%
rename from atr/static/ts/finish-selected-move.ts
rename to atr/static/ts/move-files.ts
diff --git a/tests/e2e/announce/test_get.py b/tests/e2e/announce/test_get.py
index 72872f86..56cf8000 100644
--- a/tests/e2e/announce/test_get.py
+++ b/tests/e2e/announce/test_get.py
@@ -19,19 +19,6 @@ import e2e.announce.helpers as helpers # type: ignore[reportMissingImports]
from playwright.sync_api import Page, expect
-def test_finish_move_form_populates_from_json(page_finish: Page) -> None:
- """The finish move form should populate rows from script JSON data."""
- file_option = page_finish.locator("#file-list-table-body input[type='checkbox'][data-item-path]").first
- dir_option = page_finish.locator("#dir-list-table-body input[type='radio'][name='target-directory-radio']").first
- expect(file_option).to_be_visible()
- expect(dir_option).to_be_visible()
-
- toggle_button = page_finish.locator("#select-files-toggle-button")
- expect(toggle_button).to_have_text("Select these files")
- toggle_button.click()
- expect(toggle_button).to_have_text("Unselect all")
-
-
def test_path_adds_leading_slash(page_announce: Page) -> None:
"""Paths without a leading '/' should have one added."""
help_text = helpers.fill_path_suffix(page_announce, "apple/banana")
diff --git a/tests/e2e/compose/conftest.py b/tests/e2e/compose/conftest.py
index 3ab325f8..4962ca42 100644
--- a/tests/e2e/compose/conftest.py
+++ b/tests/e2e/compose/conftest.py
@@ -75,6 +75,6 @@ def page_compose(compose_context: BrowserContext) -> Generator[Page]:
"""Navigate to the compose page with a fresh page for each test."""
page = compose_context.new_page()
helpers.visit(page, COMPOSE_URL)
- page.get_by_role("cell", name=FILE_NAME, exact=True).wait_for(timeout=60000)
+ page.locator("#files-table-container").get_by_role("cell", name=FILE_NAME, exact=True).wait_for(timeout=60000)
yield page
page.close()
diff --git a/tests/e2e/compose/test_get.py b/tests/e2e/compose/test_get.py
index 2c0fcda9..f25ce7e2 100644
--- a/tests/e2e/compose/test_get.py
+++ b/tests/e2e/compose/test_get.py
@@ -111,3 +111,16 @@ def test_start_vote_button_has_title(page_compose: Page) -> None:
"""The start vote button should have a descriptive title."""
vote_button = page_compose.locator("#start-vote-button")
expect(vote_button).to_have_attribute("title", "Start a vote on this draft")
+
+
+def test_move_form_populates_from_json(page_compose: Page) -> None:
+ """The move form should populate rows from script JSON data."""
+ file_option = page_compose.locator("#file-list-table-body input[type='checkbox'][data-item-path]").first
+ dir_option = page_compose.locator("#dir-list-table-body input[type='radio'][name='target-directory-radio']").first
+ expect(file_option).to_be_visible()
+ expect(dir_option).to_be_visible()
+
+ toggle_button = page_compose.locator("#select-files-toggle-button")
+ expect(toggle_button).to_have_text("Select these files")
+ toggle_button.click()
+ expect(toggle_button).to_have_text("Unselect all")
diff --git a/tests/e2e/sbom/test_post.py b/tests/e2e/sbom/test_post.py
index dac26578..ebceedfe 100644
--- a/tests/e2e/sbom/test_post.py
+++ b/tests/e2e/sbom/test_post.py
@@ -22,7 +22,9 @@ from playwright.sync_api import Page, expect
def test_sbom_generate(page_release_with_file: Page) -> None:
# Make sure that the test file exists
- file_cell = page_release_with_file.get_by_role("cell", name=sbom_helpers.FILE_NAME, exact=True)
+ file_cell = page_release_with_file.locator("#files-table-container").get_by_role(
+ "cell", name=sbom_helpers.FILE_NAME, exact=True
+ )
expect(file_cell).to_be_visible()
# Generate an SBOM for the file
@@ -38,5 +40,7 @@ def test_sbom_generate(page_release_with_file: Page) -> None:
page_release_with_file.wait_for_selector("#ongoing-tasks-banner", state="hidden")
page_release_with_file.reload()
- sbom_cell = page_release_with_file.get_by_role("cell", name=f"{sbom_helpers.FILE_NAME}.cdx.json")
+ sbom_cell = page_release_with_file.locator("#files-table-container").get_by_role(
+ "cell", name=f"{sbom_helpers.FILE_NAME}.cdx.json"
+ )
expect(sbom_cell).to_be_visible()
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org