You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by sb...@apache.org on 2025/06/11 19:01:24 UTC

(tooling-trusted-release) branch main updated: Make the code more aware of dormant and retired projects

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-trusted-release.git


The following commit(s) were added to refs/heads/main by this push:
     new 21a5f9a  Make the code more aware of dormant and retired projects
21a5f9a is described below

commit 21a5f9a03cdae03ebc7f7c87c0ccf1d96ce0234c
Author: Sean B. Palmer <se...@miscoranda.com>
AuthorDate: Wed Jun 11 19:57:35 2025 +0100

    Make the code more aware of dormant and retired projects
    
    - Expands Project.is_retired to encompass dormancy
    - Documents that Project.is_retired status is not permanent
    - Sets Project.is_retired based on wider criteria
    - Reflects dormant and retired projects through the interface
    - Creates a list of committees which cannot have releases
    - Disallows release creation for committees which cannot have releases
---
 atr/blueprints/admin/admin.py          |  22 ++-
 atr/construct.py                       |   4 +-
 atr/db/__init__.py                     |   5 +-
 atr/db/models.py                       |   1 +
 atr/routes/committees.py               |   6 +-
 atr/routes/preview.py                  |   2 +-
 atr/routes/projects.py                 |   8 +-
 atr/routes/release.py                  |   4 +-
 atr/routes/start.py                    |   4 +-
 atr/ssh.py                             |   4 +-
 atr/templates/committee-directory.html |   2 +-
 atr/templates/project-view.html        | 270 +++++++++++++++++----------------
 atr/user.py                            |   4 +-
 atr/util.py                            |  10 ++
 14 files changed, 191 insertions(+), 155 deletions(-)

diff --git a/atr/blueprints/admin/admin.py b/atr/blueprints/admin/admin.py
index ebb79f8..608e3dd 100644
--- a/atr/blueprints/admin/admin.py
+++ b/atr/blueprints/admin/admin.py
@@ -677,6 +677,18 @@ async def _process_undiscovered(data: db.Session) -> tuple[int, int]:
     return added_count, updated_count
 
 
+def _project_is_retired(pmc: models.Committee, project_name: str, project_status: apache.ProjectStatus) -> bool:
+    if util.committee_without_releases(pmc.name):
+        return True
+    elif ("_dormant_" in project_name) or project_status.name.endswith("(Dormant)"):
+        # TODO: Dormant is not quite the same thing as retired
+        # But we can act on the assumption that projects can be unretired
+        # The Project.is_retired setting is not permanent
+        # We have to check the names because projects.json does not have a dormant property
+        return True
+    return False
+
+
 def _session_data(
     ldap_data: dict[str, Any],
     new_uid: str,
@@ -862,11 +874,14 @@ async def _update_projects(data: db.Session, projects: apache.ProjectsData) -> t
             continue
 
         project_model = await data.project(name=project_name).get()
+        # Check whether the project is retired, whether temporarily or otherwise
+        is_retired = _project_is_retired(pmc, project_name, project_status)
         if not project_model:
-            project_model = models.Project(name=project_name, committee=pmc)
+            project_model = models.Project(name=project_name, committee=pmc, is_retired=is_retired)
             data.add(project_model)
             added_count += 1
         else:
+            project_model.is_retired = is_retired
             updated_count += 1
 
         project_model.full_name = project_status.name
@@ -874,11 +889,6 @@ async def _update_projects(data: db.Session, projects: apache.ProjectsData) -> t
         project_model.description = project_status.description
         project_model.programming_languages = project_status.programming_language
 
-        # TODO: find a better way to declare a project retired
-        #       right now we assume that a project is retired if its assigned to the attic PMC
-        #       maybe make that information configurable
-        project_model.is_retired = pmc.name == "attic"
-
     return added_count, updated_count
 
 
diff --git a/atr/construct.py b/atr/construct.py
index 413773c..929a047 100644
--- a/atr/construct.py
+++ b/atr/construct.py
@@ -83,7 +83,7 @@ async def announce_release_body(body: str, options: AnnounceReleaseOptions) -> s
 
 async def announce_release_default(project_name: str) -> str:
     async with db.session() as data:
-        project = await data.project(name=project_name, _release_policy=True).demand(
+        project = await data.project(name=project_name, is_retired=False, _release_policy=True).demand(
             RuntimeError(f"Project {project_name} not found")
         )
 
@@ -137,7 +137,7 @@ async def start_vote_body(body: str, options: StartVoteOptions) -> str:
 
 async def start_vote_default(project_name: str) -> str:
     async with db.session() as data:
-        project = await data.project(name=project_name, _release_policy=True).demand(
+        project = await data.project(name=project_name, is_retired=False, _release_policy=True).demand(
             RuntimeError(f"Project {project_name} not found")
         )
 
diff --git a/atr/db/__init__.py b/atr/db/__init__.py
index c1cfd31..a199e94 100644
--- a/atr/db/__init__.py
+++ b/atr/db/__init__.py
@@ -272,6 +272,7 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
         full_name: Opt[str] = NOT_SET,
         committee_name: Opt[str] = NOT_SET,
         release_policy_id: Opt[int] = NOT_SET,
+        is_retired: Opt[bool] = NOT_SET,
         _committee: bool = True,
         _releases: bool = False,
         _distribution_channels: bool = False,
@@ -289,6 +290,8 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
             query = query.where(models.Project.committee_name == committee_name)
         if is_defined(release_policy_id):
             query = query.where(models.Project.release_policy_id == release_policy_id)
+        if is_defined(is_retired):
+            query = query.where(models.Project.is_retired == is_retired)
 
         if _committee:
             query = query.options(select_in_load(models.Project.committee))
@@ -581,7 +584,7 @@ async def create_async_engine(app_config: type[config.AppConfig]) -> sqlalchemy.
 
 async def get_project_release_policy(data: Session, project_name: str) -> models.ReleasePolicy | None:
     """Fetch the ReleasePolicy for a project."""
-    project = await data.project(name=project_name, _release_policy=True).demand(
+    project = await data.project(name=project_name, is_retired=False, _release_policy=True).demand(
         RuntimeError(f"Project {project_name} not found")
     )
     return project.release_policy
diff --git a/atr/db/models.py b/atr/db/models.py
index 6aec0f2..1483951 100644
--- a/atr/db/models.py
+++ b/atr/db/models.py
@@ -170,6 +170,7 @@ class Project(sqlmodel.SQLModel, table=True):
     # We always include "Apache" in the full_name
     full_name: str | None = sqlmodel.Field(default=None)
 
+    # The is_retired flag is not permanent, and is also used for dormant projects
     is_retired: bool = sqlmodel.Field(default=False)
     super_project_name: str | None = sqlmodel.Field(default=None, foreign_key="project.name")
     # NOTE: Neither "Project" | None nor "Project | None" works
diff --git a/atr/routes/committees.py b/atr/routes/committees.py
index 4570282..9066f10 100644
--- a/atr/routes/committees.py
+++ b/atr/routes/committees.py
@@ -32,7 +32,11 @@ async def directory() -> str:
     """Main committee directory page."""
     async with db.session() as data:
         committees = await data.committee(_projects=True).order_by(models.Committee.name).all()
-        return await template.render("committee-directory.html", committees=committees)
+        return await template.render(
+            "committee-directory.html",
+            committees=committees,
+            committee_without_releases=util.committee_without_releases,
+        )
 
 
 @routes.public("/committees/<name>")
diff --git a/atr/routes/preview.py b/atr/routes/preview.py
index d33193c..11865e7 100644
--- a/atr/routes/preview.py
+++ b/atr/routes/preview.py
@@ -111,7 +111,7 @@ async def delete(session: routes.CommitterSession) -> response.Response:
 
     # Check that the user has access to the project
     async with db.session() as data:
-        project = await data.project(name=project_name).get()
+        project = await data.project(name=project_name, is_retired=False).get()
         if not project or not any(
             (
                 (c.name == project.committee_name)
diff --git a/atr/routes/projects.py b/atr/routes/projects.py
index 42fb06e..06ae117 100644
--- a/atr/routes/projects.py
+++ b/atr/routes/projects.py
@@ -159,7 +159,9 @@ async def delete(session: routes.CommitterSession) -> response.Response:
         return await session.redirect(projects, error="Missing project name for deletion.")
 
     async with db.session() as data:
-        project = await data.project(name=project_name, _releases=True, _distribution_channels=True).get()
+        project = await data.project(
+            name=project_name, is_retired=False, _releases=True, _distribution_channels=True
+        ).get()
 
         if not project:
             return await session.redirect(projects, error=f"Project '{project_name}' not found.")
@@ -205,7 +207,7 @@ async def select(session: routes.CommitterSession) -> str:
     if session.uid:
         async with db.session() as data:
             # TODO: Move this filtering logic somewhere else
-            all_projects = await data.project(_committee=True).all()
+            all_projects = await data.project(is_retired=False, _committee=True).all()
             user_projects = [
                 p
                 for p in all_projects
@@ -410,6 +412,8 @@ async def _project_add(form: AddFormProtocol, asf_id: str) -> response.Response:
     super_project = None
     async with db.session() as data:
         # Get the base project to derive from
+        # We're allowing derivation from a retired project here
+        # TODO: Should we disallow this instead?
         committee_projects = await data.project(committee_name=committee_name, _committee=True).all()
         for committee_project in committee_projects:
             if label.startswith(committee_project.name + "-"):
diff --git a/atr/routes/release.py b/atr/routes/release.py
index 655f796..0cbff53 100644
--- a/atr/routes/release.py
+++ b/atr/routes/release.py
@@ -75,7 +75,7 @@ async def bulk_status(session: routes.CommitterSession, task_id: int) -> str | r
 async def finished(project_name: str) -> str:
     """View all finished releases for a project."""
     async with db.session() as data:
-        project = await data.project(name=project_name).demand(
+        project = await data.project(name=project_name, is_retired=False).demand(
             base.ASFQuartException(f"Project {project_name} not found", errorcode=404)
         )
 
@@ -126,7 +126,7 @@ async def select(session: routes.CommitterSession, project_name: str) -> str:
     await session.check_access(project_name)
 
     async with db.session() as data:
-        project = await data.project(name=project_name, _releases=True).demand(
+        project = await data.project(name=project_name, is_retired=False, _releases=True).demand(
             base.ASFQuartException(f"Project {project_name} not found", errorcode=404)
         )
         releases = await project.releases_in_progress
diff --git a/atr/routes/start.py b/atr/routes/start.py
index e1e684f..58397fd 100644
--- a/atr/routes/start.py
+++ b/atr/routes/start.py
@@ -50,7 +50,7 @@ async def create_release_draft(project_name: str, version: str, asf_uid: str) ->
     # Get the project from the project name
     async with db.session() as data:
         async with data.begin():
-            project = await data.project(name=project_name, _committee=True).get()
+            project = await data.project(name=project_name, is_retired=False, _committee=True).get()
             if not project:
                 raise routes.FlashError(f"Project {project_name} not found")
 
@@ -104,7 +104,7 @@ async def selected(session: routes.CommitterSession, project_name: str) -> respo
     await session.check_access(project_name)
 
     async with db.session() as data:
-        project = await data.project(name=project_name).demand(
+        project = await data.project(name=project_name, is_retired=False).demand(
             base.ASFQuartException(f"Project {project_name} not found", errorcode=404)
         )
 
diff --git a/atr/ssh.py b/atr/ssh.py
index 45f5987..a09fcbf 100644
--- a/atr/ssh.py
+++ b/atr/ssh.py
@@ -284,7 +284,7 @@ async def _step_04_command_validate(
     ssh_uid = process.get_extra_info("username")
 
     async with db.session() as data:
-        project = await data.project(name=path_project, _committee=True).get()
+        project = await data.project(name=path_project, is_retired=False, _committee=True).get()
         if project is None:
             # Projects are public, so existence information is public
             return _fail(process, f"Project '{path_project}' does not exist", None)
@@ -540,7 +540,7 @@ async def _step_07c_ensure_release_object_for_write(
                     name=models.release_name(project_name, version_name), _committee=True
                 ).get()
                 if release is None:
-                    project = await data.project(name=project_name, _committee=True).demand(
+                    project = await data.project(name=project_name, is_retired=False, _committee=True).demand(
                         RuntimeError("Project not found after validation")
                     )
                     if version_name_error := util.version_name_error(version_name):
diff --git a/atr/templates/committee-directory.html b/atr/templates/committee-directory.html
index 7401509..ad69221 100644
--- a/atr/templates/committee-directory.html
+++ b/atr/templates/committee-directory.html
@@ -152,7 +152,7 @@
                 {# Add an else clause here if we decide to show an alternative to an empty card #}
               {% endif %}
             </div>
-            {% if current_user and is_part %}
+            {% if current_user and is_part and (not committee_without_releases(committee.name)) %}
               <a href="{{ as_url(routes.projects.add_project, committee_name=committee.name) }}"
                  title="Create a project for {{ committee.display_name }}"
                  class="text-decoration-none d-block mt-4 mb-3">
diff --git a/atr/templates/project-view.html b/atr/templates/project-view.html
index f89862f..d3bc960 100644
--- a/atr/templates/project-view.html
+++ b/atr/templates/project-view.html
@@ -65,159 +65,161 @@
     </div>
   </div>
 
-  <div class="card mb-4">
-    <div class="card-header bg-light d-flex justify-content-between align-items-center">
-      <h3 class="mb-0">Release policy</h3>
-    </div>
-    <div class="card-body">
-      {% if can_edit and policy_form %}
-        <form method="post"
-              action="{{ as_url(routes.projects.view, name=project.name) }}"
-              class="atr-canary py-4 px-5"
-              novalidate>
-          {{ policy_form.hidden_tag() if policy_form.hidden_tag }}
-          {{ policy_form.project_name(value=project.name) }}
-          {{ policy_form.default_start_vote_template_hash() }}
-          {{ policy_form.default_announce_release_template_hash() }}
-          {{ policy_form.default_min_hours_value_at_render() }}
+  {% if not project.is_retired %}
+    <div class="card mb-4">
+      <div class="card-header bg-light d-flex justify-content-between align-items-center">
+        <h3 class="mb-0">Release policy</h3>
+      </div>
+      <div class="card-body">
+        {% if can_edit and policy_form %}
+          <form method="post"
+                action="{{ as_url(routes.projects.view, name=project.name) }}"
+                class="atr-canary py-4 px-5"
+                novalidate>
+            {{ policy_form.hidden_tag() if policy_form.hidden_tag }}
+            {{ policy_form.project_name(value=project.name) }}
+            {{ policy_form.default_start_vote_template_hash() }}
+            {{ policy_form.default_announce_release_template_hash() }}
+            {{ policy_form.default_min_hours_value_at_render() }}
 
-          <div class="mb-3 pb-3 row border-bottom">
-            {{ forms.label(policy_form.mailto_addresses.entries[0], col="md3") }}
-            <div class="col-sm-8">
-              {{ forms.widget(policy_form.mailto_addresses.entries[0]) }}
-              {{ forms.errors(policy_form.mailto_addresses.entries[0]) }}
-              {{ forms.description(policy_form.mailto_addresses.entries[0]) }}
+            <div class="mb-3 pb-3 row border-bottom">
+              {{ forms.label(policy_form.mailto_addresses.entries[0], col="md3") }}
+              <div class="col-sm-8">
+                {{ forms.widget(policy_form.mailto_addresses.entries[0]) }}
+                {{ forms.errors(policy_form.mailto_addresses.entries[0]) }}
+                {{ forms.description(policy_form.mailto_addresses.entries[0]) }}
+              </div>
             </div>
-          </div>
 
-          <div class="mb-3 pb-3 row border-bottom">
-            {{ forms.label(policy_form.manual_vote, col="md3-high") }}
-            <div class="col-sm-8">
-              <div class="form-check">
-                {{ forms.widget(policy_form.manual_vote, classes="form-check-input", boolean_label="Enable") }}
-                {{ forms.errors(policy_form.manual_vote, classes="invalid-feedback d-block") }}
+            <div class="mb-3 pb-3 row border-bottom">
+              {{ forms.label(policy_form.manual_vote, col="md3-high") }}
+              <div class="col-sm-8">
+                <div class="form-check">
+                  {{ forms.widget(policy_form.manual_vote, classes="form-check-input", boolean_label="Enable") }}
+                  {{ forms.errors(policy_form.manual_vote, classes="invalid-feedback d-block") }}
+                </div>
+                {{ forms.description(policy_form.manual_vote) }}
               </div>
-              {{ forms.description(policy_form.manual_vote) }}
             </div>
-          </div>
 
-          <div class="mb-3 pb-3 row border-bottom">
-            {{ forms.label(policy_form.min_hours, col="md3") }}
-            <div class="col-sm-8">
-              {{ forms.widget(policy_form.min_hours) }}
-              {{ forms.errors(policy_form.min_hours) }}
-              {{ forms.description(policy_form.min_hours) }}
+            <div class="mb-3 pb-3 row border-bottom">
+              {{ forms.label(policy_form.min_hours, col="md3") }}
+              <div class="col-sm-8">
+                {{ forms.widget(policy_form.min_hours) }}
+                {{ forms.errors(policy_form.min_hours) }}
+                {{ forms.description(policy_form.min_hours) }}
+              </div>
             </div>
-          </div>
 
-          <div class="mb-3 pb-3 row border-bottom">
-            {{ forms.label(policy_form.release_checklist, col="md3") }}
-            <div class="col-sm-8">
-              {{ forms.widget(policy_form.release_checklist, rows="10", classes="form-control font-monospace") }}
-              {{ forms.errors(policy_form.release_checklist) }}
-              {{ forms.description(policy_form.release_checklist) }}
+            <div class="mb-3 pb-3 row border-bottom">
+              {{ forms.label(policy_form.release_checklist, col="md3") }}
+              <div class="col-sm-8">
+                {{ forms.widget(policy_form.release_checklist, rows="10", classes="form-control font-monospace") }}
+                {{ forms.errors(policy_form.release_checklist) }}
+                {{ forms.description(policy_form.release_checklist) }}
+              </div>
             </div>
-          </div>
 
-          <div class="mb-3 pb-3 row border-bottom">
-            {{ forms.label(policy_form.start_vote_template, col="md3") }}
-            <div class="col-sm-8">
-              {{ forms.widget(policy_form.start_vote_template, rows="10", classes="form-control font-monospace") }}
-              {{ forms.errors(policy_form.start_vote_template) }}
-              {{ forms.description(policy_form.start_vote_template) }}
+            <div class="mb-3 pb-3 row border-bottom">
+              {{ forms.label(policy_form.start_vote_template, col="md3") }}
+              <div class="col-sm-8">
+                {{ forms.widget(policy_form.start_vote_template, rows="10", classes="form-control font-monospace") }}
+                {{ forms.errors(policy_form.start_vote_template) }}
+                {{ forms.description(policy_form.start_vote_template) }}
+              </div>
             </div>
-          </div>
 
-          <div class="mb-3 pb-3 row border-bottom">
-            {{ forms.label(policy_form.announce_release_template, col="md3") }}
-            <div class="col-sm-8">
-              {{ forms.widget(policy_form.announce_release_template, rows="10", classes="form-control font-monospace") }}
-              {{ forms.errors(policy_form.announce_release_template) }}
-              {{ forms.description(policy_form.announce_release_template) }}
+            <div class="mb-3 pb-3 row border-bottom">
+              {{ forms.label(policy_form.announce_release_template, col="md3") }}
+              <div class="col-sm-8">
+                {{ forms.widget(policy_form.announce_release_template, rows="10", classes="form-control font-monospace") }}
+                {{ forms.errors(policy_form.announce_release_template) }}
+                {{ forms.description(policy_form.announce_release_template) }}
+              </div>
             </div>
-          </div>
 
-          <div class="mb-3 pb-3 row border-bottom">
-            {{ forms.label(policy_form.pause_for_rm, col="md3-high") }}
-            <div class="col-sm-8">
-              <div class="form-check">
-                {{ forms.widget(policy_form.pause_for_rm, classes="form-check-input", boolean_label="Enable") }}
-                {{ forms.errors(policy_form.pause_for_rm, classes="invalid-feedback d-block") }}
+            <div class="mb-3 pb-3 row border-bottom">
+              {{ forms.label(policy_form.pause_for_rm, col="md3-high") }}
+              <div class="col-sm-8">
+                <div class="form-check">
+                  {{ forms.widget(policy_form.pause_for_rm, classes="form-check-input", boolean_label="Enable") }}
+                  {{ forms.errors(policy_form.pause_for_rm, classes="invalid-feedback d-block") }}
+                </div>
+                {{ forms.description(policy_form.pause_for_rm) }}
               </div>
-              {{ forms.description(policy_form.pause_for_rm) }}
             </div>
-          </div>
 
-          <div class="row">
-            <div class="col-sm-9 offset-sm-3">{{ policy_form.submit_policy(class_="btn btn-primary mt-2") }}</div>
-          </div>
-        </form>
-      {% elif project.release_policy or project.name %}
-        <div class="card h-100 border">
-          <div class="card-body">
-            <table class="table mb-0">
-              <tbody>
-                <tr>
-                  <th class="border-0 w-25">Email</th>
-                  <td class="text-break border-0">
-                    {% if project.policy_mailto_addresses %}
-                      <a href="mailto:{{ project.policy_mailto_addresses[0] }}">{{ project.policy_mailto_addresses[0] }}</a>
-                    {% else %}
-                      Not set
-                    {% endif %}
-                  </td>
-                </tr>
-                <tr>
-                  <th class="border-0">Manual vote process</th>
-                  <td class="text-break border-0">{{ project.policy_manual_vote }}</td>
-                </tr>
-                <tr>
-                  <th class="border-0">Minimum voting period</th>
-                  <td class="text-break border-0">{{ project.policy_min_hours }}h</td>
-                </tr>
-                <tr>
-                  <th class="border-0">Release checklist</th>
-                  <td class="text-break border-0">
-                    {% if project.policy_release_checklist|length > 0 %}
-                      <textarea readonly class="form-control font-monospace" rows="10">{{ project.policy_release_checklist }}</textarea>
-                    {% else %}
-                      None
-                    {% endif %}
-                  </td>
-                </tr>
-                <tr>
-                  <th class="border-0">Start vote template</th>
-                  <td class="text-break border-0">
-                    {% if project.policy_start_vote_template|length > 0 %}
-                      <textarea readonly class="form-control font-monospace" rows="10">{{ project.policy_start_vote_template }}</textarea>
-                    {% else %}
-                      None (System default will be used)
-                    {% endif %}
-                  </td>
-                </tr>
-                <tr>
-                  <th class="border-0">Announce release template</th>
-                  <td class="text-break border-0">
-                    {% if project.policy_announce_release_template|length > 0 %}
-                      <textarea readonly class="form-control font-monospace" rows="10">{{ project.policy_announce_release_template }}</textarea>
-                    {% else %}
-                      None (System default will be used)
-                    {% endif %}
-                  </td>
-                </tr>
-                <tr>
-                  <th class="border-0">Pause for RM</th>
-                  <td class="text-break border-0">{{ project.policy_pause_for_rm }}</td>
-                </tr>
-              </tbody>
-            </table>
+            <div class="row">
+              <div class="col-sm-9 offset-sm-3">{{ policy_form.submit_policy(class_="btn btn-primary mt-2") }}</div>
+            </div>
+          </form>
+        {% elif project.release_policy or project.name %}
+          <div class="card h-100 border">
+            <div class="card-body">
+              <table class="table mb-0">
+                <tbody>
+                  <tr>
+                    <th class="border-0 w-25">Email</th>
+                    <td class="text-break border-0">
+                      {% if project.policy_mailto_addresses %}
+                        <a href="mailto:{{ project.policy_mailto_addresses[0] }}">{{ project.policy_mailto_addresses[0] }}</a>
+                      {% else %}
+                        Not set
+                      {% endif %}
+                    </td>
+                  </tr>
+                  <tr>
+                    <th class="border-0">Manual vote process</th>
+                    <td class="text-break border-0">{{ project.policy_manual_vote }}</td>
+                  </tr>
+                  <tr>
+                    <th class="border-0">Minimum voting period</th>
+                    <td class="text-break border-0">{{ project.policy_min_hours }}h</td>
+                  </tr>
+                  <tr>
+                    <th class="border-0">Release checklist</th>
+                    <td class="text-break border-0">
+                      {% if project.policy_release_checklist|length > 0 %}
+                        <textarea readonly class="form-control font-monospace" rows="10">{{ project.policy_release_checklist }}</textarea>
+                      {% else %}
+                        None
+                      {% endif %}
+                    </td>
+                  </tr>
+                  <tr>
+                    <th class="border-0">Start vote template</th>
+                    <td class="text-break border-0">
+                      {% if project.policy_start_vote_template|length > 0 %}
+                        <textarea readonly class="form-control font-monospace" rows="10">{{ project.policy_start_vote_template }}</textarea>
+                      {% else %}
+                        None (System default will be used)
+                      {% endif %}
+                    </td>
+                  </tr>
+                  <tr>
+                    <th class="border-0">Announce release template</th>
+                    <td class="text-break border-0">
+                      {% if project.policy_announce_release_template|length > 0 %}
+                        <textarea readonly class="form-control font-monospace" rows="10">{{ project.policy_announce_release_template }}</textarea>
+                      {% else %}
+                        None (System default will be used)
+                      {% endif %}
+                    </td>
+                  </tr>
+                  <tr>
+                    <th class="border-0">Pause for RM</th>
+                    <td class="text-break border-0">{{ project.policy_pause_for_rm }}</td>
+                  </tr>
+                </tbody>
+              </table>
+            </div>
           </div>
-        </div>
-      {% else %}
-        <div>None defined.</div>
-      {% endif %}
+        {% else %}
+          <div>None defined.</div>
+        {% endif %}
+      </div>
     </div>
-  </div>
+  {% endif %}
 
   {% if (is_committee_member or is_admin) %}
     {% if candidate_drafts|length > 0 %}
diff --git a/atr/user.py b/atr/user.py
index 16b37bc..4039b83 100644
--- a/atr/user.py
+++ b/atr/user.py
@@ -62,7 +62,9 @@ async def projects(uid: str, committee_only: bool = False, super_project: bool =
     user_projects: list[models.Project] = []
     async with db.session() as data:
         # Must have releases, because this is used in candidate_drafts
-        projects = await data.project(_committee=True, _releases=True, _super_project=super_project).all()
+        projects = await data.project(
+            is_retired=False, _committee=True, _releases=True, _super_project=super_project
+        ).all()
         for p in projects:
             if p.committee is None:
                 continue
diff --git a/atr/util.py b/atr/util.py
index 25aec42..250c60d 100644
--- a/atr/util.py
+++ b/atr/util.py
@@ -52,6 +52,12 @@ import atr.user as user
 F = TypeVar("F", bound="QuartFormTyped")
 T = TypeVar("T")
 
+_COMMITTEES_WITHOUT_RELEASES: Final[set[str]] = {
+    "attic",
+    "comdev",
+    "logodev",
+    "whimsy",
+}
 _LOGGER: Final = logging.getLogger(__name__)
 
 
@@ -154,6 +160,10 @@ def chmod_directories(path: pathlib.Path, permissions: int = 0o755) -> None:
             os.chmod(dir_path, permissions)
 
 
+def committee_without_releases(committee_name: str) -> bool:
+    return committee_name in _COMMITTEES_WITHOUT_RELEASES
+
+
 def compute_sha3_256(file_data: bytes) -> str:
     """Compute SHA3-256 hash of file data."""
     return hashlib.sha3_256(file_data).hexdigest()


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org