This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-atr-experiments.git


The following commit(s) were added to refs/heads/main by this push:
     new 77258a9  Ensure that podlings are added to the database and displayed
77258a9 is described below

commit 77258a90d996278dcf2e2cf26aa2c544a30288da
Author: Sean B. Palmer <s...@miscoranda.com>
AuthorDate: Thu Feb 20 17:27:52 2025 +0200

    Ensure that podlings are added to the database and displayed
---
 atr/blueprints/secret/secret.py                    |  86 ++++++++++---
 atr/db/models.py                                   |   9 ++
 atr/routes.py                                      | 141 ++++++++++++---------
 atr/templates/candidate-attach.html                |   9 +-
 atr/templates/candidate-create.html                |   9 +-
 atr/templates/candidate-review.html                |   2 +-
 atr/templates/candidate-signature-verify.html      |   2 +-
 atr/templates/pmc-directory.html                   |  10 +-
 atr/templates/secret/update-pmcs.html              |  19 ++-
 ...al_schema.py => d3ff8c13bb8b_initial_schema.py} |   6 +-
 10 files changed, 197 insertions(+), 96 deletions(-)

diff --git a/atr/blueprints/secret/secret.py b/atr/blueprints/secret/secret.py
index c17f599..ae7b791 100644
--- a/atr/blueprints/secret/secret.py
+++ b/atr/blueprints/secret/secret.py
@@ -26,7 +26,7 @@ from werkzeug.wrappers.response import Response
 
 from asfquart.base import ASFQuartException
 from asfquart.session import read as session_read
-from atr.apache import get_apache_project_data
+from atr.apache import ApacheProjects, get_apache_project_data
 from atr.db import get_session
 from atr.db.models import (
     PMC,
@@ -45,6 +45,11 @@ from atr.db.service import get_pmcs
 from . import blueprint
 
 _WHIMSY_COMMITTEE_URL = "https://whimsy.apache.org/public/committee-info.json";
+_PROJECT_PODLINGS_URL = 
"https://projects.apache.org/json/foundation/podlings.json";
+_PROJECT_GROUPS_URL = "https://projects.apache.org/json/foundation/groups.json";
+
+
+class FlashError(RuntimeError): ...
 
 
 @blueprint.route("/data")
@@ -96,14 +101,12 @@ async def secret_data(model: str = "PMC") -> str:
 
 @blueprint.route("/pmcs/update", methods=["GET", "POST"])
 async def secret_pmcs_update() -> str | Response:
-    """Update PMCs from remote, authoritative committee-info.json."""
-
+    """Update PMCs and podlings from remote data."""
     if request.method == "POST":
-        # Fetch committee-info.json from Whimsy
         try:
-            apache_projects = await get_apache_project_data()
-        except (httpx.RequestError, json.JSONDecodeError) as e:
-            await flash(f"Failed to fetch committee data: {e!s}", "error")
+            apache_projects, podlings_data, groups_data = await 
secret_pmcs_update_data()
+        except FlashError as e:
+            await flash(f"{e!s}", "error")
             return redirect(url_for("secret_blueprint.secret_pmcs_update"))
 
         updated_count = 0
@@ -111,6 +114,7 @@ async def secret_pmcs_update() -> str | Response:
         try:
             async with get_session() as db_session:
                 async with db_session.begin():
+                    # First update PMCs
                     for project in apache_projects.projects:
                         name = project.name
                         # Skip non-PMC committees
@@ -124,14 +128,32 @@ async def secret_pmcs_update() -> str | Response:
                             pmc = PMC(project_name=name)
                             db_session.add(pmc)
 
-                        # Update PMC data
-                        pmc.pmc_members = project.owners
-                        pmc.committers = project.members
+                        # Update PMC data from groups.json
+                        pmc.pmc_members = groups_data.get(f"{name}-pmc", [])
+                        pmc.committers = groups_data.get(name, [])
+                        pmc.is_podling = False  # Ensure this is set for PMCs
+
+                        # For release managers, use PMC members for now
+                        # TODO: Consider a more sophisticated way to determine 
release managers
+                        pmc.release_managers = pmc.pmc_members
 
-                        # Mark chairs as release managers
-                        # TODO: Who else is a release manager? How do we know?
-                        #       lets assume for now that all owners are also 
release managers
-                        pmc.release_managers = project.owners
+                        updated_count += 1
+
+                    # Then add PPMCs (podlings)
+                    for podling_name, podling_data in podlings_data.items():
+                        # Get or create PPMC
+                        statement = select(PMC).where(PMC.project_name == 
podling_name)
+                        ppmc = (await 
db_session.execute(statement)).scalar_one_or_none()
+                        if not ppmc:
+                            ppmc = PMC(project_name=podling_name)
+                            db_session.add(ppmc)
+
+                        # Update PPMC data from groups.json
+                        ppmc.is_podling = True
+                        ppmc.pmc_members = 
groups_data.get(f"{podling_name}-pmc", [])
+                        ppmc.committers = groups_data.get(podling_name, [])
+                        # Use PPMC members as release managers
+                        ppmc.release_managers = ppmc.pmc_members
 
                         updated_count += 1
 
@@ -149,10 +171,13 @@ async def secret_pmcs_update() -> str | Response:
                     tooling_pmc.pmc_members = ["wave", "tn", "sbp"]
                     tooling_pmc.committers = ["wave", "tn", "sbp"]
                     tooling_pmc.release_managers = ["wave"]
+                    tooling_pmc.is_podling = False
 
-            await flash(f"Successfully updated {updated_count} PMCs from 
Whimsy", "success")
+            await flash(
+                f"Successfully updated {updated_count} projects (PMCs and 
PPMCs) with membership data", "success"
+            )
         except Exception as e:
-            await flash(f"Failed to update PMCs: {e!s}", "error")
+            await flash(f"Failed to update projects: {e!s}", "error")
 
         return redirect(url_for("secret_blueprint.secret_pmcs_update"))
 
@@ -160,6 +185,35 @@ async def secret_pmcs_update() -> str | Response:
     return await render_template("secret/update-pmcs.html")
 
 
+async def secret_pmcs_update_data() -> tuple[ApacheProjects, dict, dict]:
+    """Fetch and update PMCs and podlings from remote data."""
+    # Fetch committee-info.json from whimsy.apache.org
+    try:
+        apache_projects = await get_apache_project_data()
+    except (httpx.RequestError, json.JSONDecodeError) as e:
+        raise FlashError(f"Failed to fetch committee data: {e!s}")
+
+    # Fetch podlings data from projects.apache.org
+    try:
+        async with httpx.AsyncClient() as client:
+            response = await client.get(_PROJECT_PODLINGS_URL)
+            response.raise_for_status()
+            podlings_data = response.json()
+    except (httpx.RequestError, json.JSONDecodeError) as e:
+        raise FlashError(f"Failed to fetch podling data: {e!s}")
+
+    # Fetch groups data from projects.apache.org
+    try:
+        async with httpx.AsyncClient() as client:
+            response = await client.get(_PROJECT_GROUPS_URL)
+            response.raise_for_status()
+            groups_data = response.json()
+    except (httpx.RequestError, json.JSONDecodeError) as e:
+        raise FlashError(f"Failed to fetch groups data: {e!s}")
+
+    return apache_projects, podlings_data, groups_data
+
+
 @blueprint.route("/debug/database")
 async def secret_debug_database() -> str:
     """Debug information about the database."""
diff --git a/atr/db/models.py b/atr/db/models.py
index 226b73f..ce37d13 100644
--- a/atr/db/models.py
+++ b/atr/db/models.py
@@ -80,6 +80,8 @@ class VotePolicy(SQLModel, table=True):
 class PMC(SQLModel, table=True):
     id: int | None = Field(default=None, primary_key=True)
     project_name: str = Field(unique=True)
+    # True if this is an incubator podling with a PPMC, otherwise False
+    is_podling: bool = Field(default=False)
 
     # One-to-many: A PMC can have multiple product lines, each product line 
belongs to one PMC
     product_lines: list["ProductLine"] = Relationship(back_populates="pmc")
@@ -98,6 +100,13 @@ class PMC(SQLModel, table=True):
     # One-to-many: A PMC can have multiple releases
     releases: list["Release"] = Relationship(back_populates="pmc")
 
+    @property
+    def display_name(self) -> str:
+        """Get the display name for the PMC/PPMC."""
+        if self.is_podling:
+            return f"{self.project_name} (podling)"
+        return self.project_name
+
 
 class ProductLine(SQLModel, table=True):
     id: int | None = Field(default=None, primary_key=True)
diff --git a/atr/routes.py b/atr/routes.py
index f6d8ba6..1536891 100644
--- a/atr/routes.py
+++ b/atr/routes.py
@@ -87,6 +87,9 @@ algorithms = {
 }
 
 
+class FlashError(RuntimeError): ...
+
+
 @asynccontextmanager
 async def ephemeral_gpg_home():
     """Create a temporary directory for an isolated GPG home, and clean it up 
on exit."""
@@ -99,11 +102,13 @@ async def ephemeral_gpg_home():
 
 async def release_attach_post(session: ClientSession, request: Request) -> 
Response:
     """Handle POST request for attaching package artifacts to a release."""
-    res = await release_attach_post_validate(request)
-    # Not particularly elegant
-    if isinstance(res, Response):
-        return res
-    release_key, artifact_file, checksum_file, signature_file, artifact_type = 
res
+    try:
+        release_key, artifact_file, checksum_file, signature_file, 
artifact_type = await release_attach_post_validate(
+            request
+        )
+    except FlashError as e:
+        await flash(f"{e!s}", "error")
+        return redirect(url_for("root_candidate_attach"))
     # This must come here to appease the type checker
     if artifact_file.filename is None:
         await flash("Release artifact filename is required", "error")
@@ -114,11 +119,12 @@ async def release_attach_post(session: ClientSession, 
request: Request) -> Respo
         async with db_session.begin():
             # Process and save the files
             try:
-                ok, artifact_sha3, artifact_size, artifact_sha512, 
signature_sha3 = await release_attach_post_session(
-                    db_session, release_key, artifact_file, checksum_file, 
signature_file
-                )
-                if not ok:
-                    # The flash error is already set by the helper
+                try:
+                    artifact_sha3, artifact_size, artifact_sha512, 
signature_sha3 = await release_attach_post_session(
+                        db_session, release_key, artifact_file, checksum_file, 
signature_file
+                    )
+                except FlashError as e:
+                    await flash(f"{e!s}", "error")
                     return redirect(url_for("root_candidate_attach"))
 
                 # Create the package record
@@ -144,18 +150,14 @@ async def release_attach_post(session: ClientSession, 
request: Request) -> Respo
 
 async def release_attach_post_validate(
     request: Request,
-) -> Response | tuple[str, FileStorage, FileStorage | None, FileStorage | 
None, str]:
-    async def flash_error_and_redirect(message: str) -> Response:
-        await flash(message, "error")
-        return redirect(url_for("root_candidate_attach"))
-
+) -> tuple[str, FileStorage, FileStorage | None, FileStorage | None, str]:
     form = await request.form
 
     # TODO: Check that the submitter is a committer of the project
 
     release_key = form.get("release_key")
     if (not release_key) or (not isinstance(release_key, str)):
-        return await flash_error_and_redirect("Release key is required")
+        raise FlashError("Release key is required")
 
     # Get all uploaded files
     files = await request.files
@@ -163,31 +165,50 @@ async def release_attach_post_validate(
     checksum_file = files.get("release_checksum")
     signature_file = files.get("release_signature")
     if not isinstance(artifact_file, FileStorage):
-        return await flash_error_and_redirect("Release artifact file is 
required")
+        raise FlashError("Release artifact file is required")
     if checksum_file is not None and not isinstance(checksum_file, 
FileStorage):
-        return await flash_error_and_redirect("Problem with checksum file")
+        raise FlashError("Problem with checksum file")
     if signature_file is not None and not isinstance(signature_file, 
FileStorage):
-        return await flash_error_and_redirect("Problem with signature file")
+        raise FlashError("Problem with signature file")
 
     # Get and validate artifact type
     artifact_type = form.get("artifact_type")
     if (not artifact_type) or (not isinstance(artifact_type, str)):
-        return await flash_error_and_redirect("Artifact type is required")
+        raise FlashError("Artifact type is required")
     if artifact_type not in ["source", "binary", "reproducible"]:
-        return await flash_error_and_redirect("Invalid artifact type")
+        raise FlashError("Invalid artifact type")
 
     return release_key, artifact_file, checksum_file, signature_file, 
artifact_type
 
 
+async def get_artifact_info(
+    db_session: AsyncSession, uploads_path: Path, artifact_file: FileStorage
+) -> tuple[str, str, int]:
+    # In a separate function to appease the complexity checker
+    artifact_sha3, artifact_size = await save_file_by_hash(uploads_path, 
artifact_file)
+
+    # Check for duplicates by artifact_sha3 before proceeding
+    statement = select(Package).where(Package.artifact_sha3 == artifact_sha3)
+    duplicate = (await db_session.execute(statement)).first()
+    if duplicate:
+        # Remove the saved file since we won't be using it
+        await aiofiles.os.remove(uploads_path / artifact_sha3)
+        raise FlashError("This exact file has already been uploaded to another 
release")
+
+    # Compute SHA-512 of the artifact for the package record
+    return artifact_sha3, compute_sha512(uploads_path / artifact_sha3), 
artifact_size
+
+
 async def release_attach_post_session(
     db_session: AsyncSession,
     release_key: str,
     artifact_file: FileStorage,
     checksum_file: FileStorage | None,
     signature_file: FileStorage | None,
-) -> tuple[bool, str, int, str, str | None]:
+) -> tuple[str, int, str, str | None]:
     """Helper function for release_attach_post."""
-    # First check for duplicates
+
+    # First check for duplicates by filename
     statement = select(Package).where(
         Package.release_key == release_key,
         Package.filename == artifact_file.filename,
@@ -195,18 +216,14 @@ async def release_attach_post_session(
     duplicate = (await db_session.execute(statement)).first()
 
     if duplicate:
-        await flash("This release artifact has already been uploaded", "error")
-        return False, "", 0, "", None
+        raise FlashError("This release artifact has already been uploaded")
 
     # Save files using their hashes as filenames
     uploads_path = Path(get_release_storage_dir())
     try:
-        artifact_sha3, artifact_size = await save_file_by_hash(uploads_path, 
artifact_file)
-        # Compute SHA-512 of the artifact for the package record
-        artifact_sha512 = compute_sha512(uploads_path / artifact_sha3)
+        artifact_sha3, artifact_sha512, artifact_size = await 
get_artifact_info(db_session, uploads_path, artifact_file)
     except Exception as e:
-        await flash(f"Error saving artifact file: {e!s}", "error")
-        return False, "", 0, "", None
+        raise FlashError(f"Error saving artifact file: {e!s}")
     # Note: "error" is not permitted past this point
     # Because we don't want to roll back saving the artifact
 
@@ -233,7 +250,7 @@ async def release_attach_post_session(
         except Exception as e:
             await flash(f"Warning: Could not save signature file: {e!s}", 
"warning")
 
-    return True, artifact_sha3, artifact_size, artifact_sha512, signature_sha3
+    return artifact_sha3, artifact_size, artifact_sha512, signature_sha3
 
 
 async def release_create_post(session: ClientSession, request: Request) -> 
Response:
@@ -244,24 +261,15 @@ async def release_create_post(session: ClientSession, 
request: Request) -> Respo
     if not project_name:
         raise ASFQuartException("Project name is required", errorcode=400)
 
-    version = form.get("version")
-    if not version:
-        raise ASFQuartException("Version is required", errorcode=400)
-
     product_name = form.get("product_name")
     if not product_name:
         raise ASFQuartException("Product name is required", errorcode=400)
 
-    # Verify user is a PMC member or committer of the project
-    if project_name not in session.committees and project_name not in 
session.projects:
-        raise ASFQuartException(
-            f"You must be a PMC member or committer of {project_name} to 
submit a release candidate", errorcode=403
-        )
-
-    # Generate a 128-bit random token for the release storage key
-    # TODO: Perhaps we should call this the release_id instead
-    storage_token = secrets.token_hex(16)
+    version = form.get("version")
+    if not version:
+        raise ASFQuartException("Version is required", errorcode=400)
 
+    # TODO: Forbid creating a release with an existing project, product, and 
version
     # Create the release record in the database
     async with get_session() as db_session:
         async with db_session.begin():
@@ -273,9 +281,21 @@ async def release_create_post(session: ClientSession, 
request: Request) -> Respo
                 APP.logger.debug(f"Available projects: {session.projects}")
                 raise ASFQuartException("PMC not found", errorcode=404)
 
+            # Verify user is a PMC member or committer of the project
+            # We use pmc.display_name, so this must come within the transaction
+            if project_name not in session.committees and project_name not in 
session.projects:
+                raise ASFQuartException(
+                    f"You must be a PMC member or committer of 
{pmc.display_name} to submit a release candidate",
+                    errorcode=403,
+                )
+
+            # Generate a 128-bit random token for the release storage key
+            # TODO: Perhaps we should call this the release_key instead
+            storage_key = secrets.token_hex(16)
+
             # Create release record
             release = Release(
-                storage_key=storage_token,
+                storage_key=storage_key,
                 stage=ReleaseStage.CANDIDATE,
                 phase=ReleasePhase.RELEASE_CANDIDATE,
                 pmc_id=pmc.id,
@@ -284,13 +304,8 @@ async def release_create_post(session: ClientSession, 
request: Request) -> Respo
             )
             db_session.add(release)
 
-            # TODO: Create or link to product line
-            # For now, we'll just create releases without product lines
-            # What sort of role do product lines play in our UX?
-
     # Redirect to the attach artifacts page with the storage token
-    # We should possibly have a results, or list of releases, page instead
-    return redirect(url_for("root_candidate_attach", 
storage_key=storage_token))
+    return redirect(url_for("root_candidate_attach", storage_key=storage_key))
 
 
 @APP.route("/")
@@ -316,16 +331,18 @@ async def root_candidate_attach() -> Response | str:
 
     # Get all releases where the user is a PMC member or committer of the 
associated PMC
     async with get_session() as db_session:
+        # TODO: This duplicates code in root_candidate_review
         release_pmc = selectinload(cast(InstrumentedAttribute[PMC], 
Release.pmc))
         statement = 
select(Release).options(release_pmc).join(PMC).where(Release.stage == 
ReleaseStage.CANDIDATE)
         releases = (await db_session.execute(statement)).scalars().all()
 
-        # Filter to only show releases for PMCs where the user is a member or 
committer
+        # Filter to only show releases for PMCs or PPMCs where the user is a 
member or committer
         # Can we do this in sqlmodel using JSON container operators?
         user_releases = []
         for r in releases:
             if r.pmc is None:
                 continue
+            # For PPMCs the "members" are stored in the committers field
             if session.uid in r.pmc.pmc_members or session.uid in 
r.pmc.committers:
                 user_releases.append(r)
 
@@ -350,12 +367,20 @@ async def root_candidate_create() -> Response | str:
     if request.method == "POST":
         return await release_create_post(session, request)
 
+    # Get PMC objects for all projects the user is a member of
+    async with get_session() as db_session:
+        from sqlalchemy.sql.expression import ColumnElement
+
+        project_list = session.committees + session.projects
+        project_name: ColumnElement[str] = cast(ColumnElement[str], 
PMC.project_name)
+        statement = select(PMC).where(project_name.in_(project_list))
+        user_pmcs = (await db_session.execute(statement)).scalars().all()
+
     # For GET requests, show the form
     return await render_template(
         "candidate-create.html",
         asf_id=session.uid,
-        pmc_memberships=session.committees,
-        committer_projects=session.projects,
+        user_pmcs=user_pmcs,
     )
 
 
@@ -620,9 +645,10 @@ async def root_candidate_review() -> str:
         raise ASFQuartException("Not authenticated", errorcode=401)
 
     async with get_session() as db_session:
-        # Get all releases where the user is a PMC member of the associated PMC
+        # Get all releases where the user is a PMC member or committer
         # TODO: We don't actually record who uploaded the release candidate
         # We should probably add that information!
+        # TODO: This duplicates code in root_candidate_attach
         release_pmc = selectinload(cast(InstrumentedAttribute[PMC], 
Release.pmc))
         release_packages = 
selectinload(cast(InstrumentedAttribute[list[Package]], Release.packages))
         statement = (
@@ -633,12 +659,13 @@ async def root_candidate_review() -> str:
         )
         releases = (await db_session.execute(statement)).scalars().all()
 
-        # Filter to only show releases for PMCs where the user is a member
+        # Filter to only show releases for PMCs or PPMCs where the user is a 
member or committer
         user_releases = []
         for r in releases:
             if r.pmc is None:
                 continue
-            if session.uid in r.pmc.pmc_members:
+            # For PPMCs the "members" are stored in the committers field
+            if session.uid in r.pmc.pmc_members or session.uid in 
r.pmc.committers:
                 user_releases.append(r)
 
         return await render_template("candidate-review.html", 
releases=user_releases)
diff --git a/atr/templates/candidate-attach.html 
b/atr/templates/candidate-attach.html
index 010d0c6..9280205 100644
--- a/atr/templates/candidate-attach.html
+++ b/atr/templates/candidate-attach.html
@@ -133,7 +133,7 @@
               {% for release in releases %}
                 <option value="{{ release.storage_key }}"
                         {% if release.storage_key == selected_release 
%}selected{% endif %}>
-                  {{ release.pmc.project_name }} - {{ release.version }}
+                  {{ release.pmc.display_name }} - {{ release.version }}
                 </option>
               {% endfor %}
             </select>
@@ -168,7 +168,12 @@
           <td>
             <div class="radio-group">
               <div>
-                <input type="radio" id="source" name="artifact_type" 
value="source" required />
+                <input type="radio"
+                       id="source"
+                       name="artifact_type"
+                       value="source"
+                       required
+                       checked />
                 <label for="source">Source archive</label>
               </div>
               <div>
diff --git a/atr/templates/candidate-create.html 
b/atr/templates/candidate-create.html
index b0d476c..5922607 100644
--- a/atr/templates/candidate-create.html
+++ b/atr/templates/candidate-create.html
@@ -83,11 +83,11 @@
           <td>
             <select id="project_name" name="project_name" required>
               <option value="">Select a project...</option>
-              {% for project in (pmc_memberships + 
committer_projects)|unique|sort %}
-                <option value="{{ project }}">{{ project }}</option>
+              {% for pmc in user_pmcs|sort(attribute='project_name') %}
+                <option value="{{ pmc.project_name }}">{{ pmc.display_name 
}}</option>
               {% endfor %}
             </select>
-            {% if not pmc_memberships and not committer_projects %}
+            {% if not user_pmcs %}
               <p class="error-message">You must be a PMC member or committer 
to submit a release candidate.</p>
             {% endif %}
           </td>
@@ -115,8 +115,7 @@
         <tr>
           <td></td>
           <td>
-            <button type="submit"
-                    {% if not pmc_memberships and not committer_projects 
%}disabled{% endif %}>Create release</button>
+            <button type="submit" {% if not user_pmcs %}disabled{% endif 
%}>Create release</button>
           </td>
         </tr>
       </tbody>
diff --git a/atr/templates/candidate-review.html 
b/atr/templates/candidate-review.html
index d118e6b..94dfe5f 100644
--- a/atr/templates/candidate-review.html
+++ b/atr/templates/candidate-review.html
@@ -136,7 +136,7 @@
   {% if releases %}
     {% for release in releases %}
       <div class="candidate-header">
-        <h3>{{ release.pmc.project_name }}</h3>
+        <h3>{{ release.pmc.display_name }}</h3>
         <div class="candidate-meta">
           <span class="candidate-meta-item">Version: {{ release.version 
}}</span>
           <span class="candidate-meta-item">Stage: {{ release.stage.value 
}}</span>
diff --git a/atr/templates/candidate-signature-verify.html 
b/atr/templates/candidate-signature-verify.html
index f9be33f..a3e0295 100644
--- a/atr/templates/candidate-signature-verify.html
+++ b/atr/templates/candidate-signature-verify.html
@@ -154,7 +154,7 @@
   <h1>Verify release signatures</h1>
 
   <div class="candidate-header">
-    <h3>{{ release.pmc.project_name }}</h3>
+    <h3>{{ release.pmc.display_name }}</h3>
     <div class="candidate-meta">
       <span class="candidate-meta-item">Version: {{ release.version }}</span>
       <span class="candidate-meta-item">Stage: {{ release.stage.value }}</span>
diff --git a/atr/templates/pmc-directory.html b/atr/templates/pmc-directory.html
index 65f7f14..881c6e4 100644
--- a/atr/templates/pmc-directory.html
+++ b/atr/templates/pmc-directory.html
@@ -1,11 +1,11 @@
 {% extends "layouts/base.html" %}
 
 {% block title %}
-  Project Management Committees ~ ATR
+  Project directory ~ ATR
 {% endblock title %}
 
 {% block description %}
-  List of all PMCs and their latest releases.
+  List of all ASF projects and their latest releases.
 {% endblock description %}
 
 {% block head_extra %}
@@ -68,8 +68,8 @@
 {% endblock head_extra %}
 
 {% block content %}
-  <h1>Project Management Committees</h1>
-  <p class="intro">Current Apache PMCs and their releases:</p>
+  <h1>Project directory</h1>
+  <p class="intro">Current ASF projects and their releases:</p>
 
   <input type="text"
          id="pmc-filter"
@@ -79,7 +79,7 @@
   <div class="pmc-grid">
     {% for pmc in pmcs %}
       <div class="pmc-card">
-        <div class="pmc-name">{{ pmc.project_name }}</div>
+        <div class="pmc-name">{{ pmc.display_name }}</div>
         <div class="pmc-stats">
           <div class="stat-item">
             <span class="stat-label">PMC Members</span>
diff --git a/atr/templates/secret/update-pmcs.html 
b/atr/templates/secret/update-pmcs.html
index 274147b..40bf154 100644
--- a/atr/templates/secret/update-pmcs.html
+++ b/atr/templates/secret/update-pmcs.html
@@ -1,11 +1,11 @@
 {% extends "layouts/base.html" %}
 
 {% block title %}
-  Update PMCs ~ ATR
+  Update projects ~ ATR
 {% endblock title %}
 
 {% block description %}
-  Update PMCs from remote, authoritative committee-info.json.
+  Update PMCs and podlings from remote data sources.
 {% endblock description %}
 
 {% block stylesheets %}
@@ -68,8 +68,10 @@
 {% endblock stylesheets %}
 
 {% block content %}
-  <h1>Update PMCs</h1>
-  <p class="intro">This page allows you to update the PMC information in the 
database from committee-info.json.</p>
+  <h1>Update Projects</h1>
+  <p class="intro">
+    This page allows you to update PMC and podling information in the database 
from remote data sources.
+  </p>
 
   {% with messages = get_flashed_messages(with_categories=true) %}
     {% if messages %}
@@ -79,11 +81,16 @@
 
   <div class="warning">
     <p>
-      <strong>Note:</strong> This operation will update all PMC information, 
including member lists and release manager assignments.
+      <strong>Note:</strong> This operation will update all project 
information, including:
     </p>
+    <ul>
+      <li>PMC member lists and release manager assignments</li>
+      <li>Podling status and basic information</li>
+      <li>Project metadata and relationships</li>
+    </ul>
   </div>
 
   <form method="post">
-    <button type="submit">Update PMCs</button>
+    <button type="submit">Update Projects</button>
   </form>
 {% endblock content %}
diff --git a/migrations/versions/6776f795ec62_initial_schema.py 
b/migrations/versions/d3ff8c13bb8b_initial_schema.py
similarity index 84%
rename from migrations/versions/6776f795ec62_initial_schema.py
rename to migrations/versions/d3ff8c13bb8b_initial_schema.py
index 3f1e9ef..a19128c 100644
--- a/migrations/versions/6776f795ec62_initial_schema.py
+++ b/migrations/versions/d3ff8c13bb8b_initial_schema.py
@@ -1,15 +1,15 @@
 """initial_schema
 
-Revision ID: 6776f795ec62
+Revision ID: d3ff8c13bb8b
 Revises:
-Create Date: 2025-02-20 16:02:41.618093
+Create Date: 2025-02-20 16:49:57.374714
 
 """
 
 from collections.abc import Sequence
 
 # revision identifiers, used by Alembic.
-revision: str = "6776f795ec62"
+revision: str = "d3ff8c13bb8b"
 down_revision: str | None = None
 branch_labels: str | Sequence[str] | None = None
 depends_on: str | Sequence[str] | None = None


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tooling.apache.org
For additional commands, e-mail: dev-h...@tooling.apache.org

Reply via email to