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 1682499 Simplify the model
1682499 is described below
commit 1682499c698fc660d50091d03f620ee4b8b7dc42
Author: Sean B. Palmer <[email protected]>
AuthorDate: Fri Mar 14 19:58:58 2025 +0200
Simplify the model
---
atr/blueprints/admin/admin.py | 71 +++++----
.../{update-pmcs.html => update-committees.html} | 0
atr/blueprints/api/api.py | 40 +++---
atr/db/__init__.py | 159 +++++++--------------
atr/db/models.py | 104 ++++++--------
atr/db/service.py | 28 ++--
atr/routes/candidate.py | 74 +++++-----
atr/routes/download.py | 18 +--
atr/routes/keys.py | 42 +++---
atr/routes/package.py | 20 +--
atr/routes/projects.py | 34 +++--
atr/routes/release.py | 46 +++---
atr/static/css/atr.css | 7 +-
atr/static/css/root.css | 58 --------
atr/tasks/mailtest.py | 14 +-
atr/tasks/signature.py | 17 ++-
atr/tasks/vote.py | 20 +--
atr/templates/candidate-create.html | 28 ++--
atr/templates/candidate-review.html | 6 +-
atr/templates/keys-add.html | 12 +-
atr/templates/keys-review.html | 4 +-
atr/templates/package-add.html | 4 +-
atr/templates/project-directory.html | 24 ++--
atr/templates/project-view.html | 7 +-
atr/templates/release-bulk.html | 4 +-
atr/templates/release-vote.html | 6 +-
docs/plan.html | 2 +-
docs/plan.md | 2 +-
28 files changed, 378 insertions(+), 473 deletions(-)
diff --git a/atr/blueprints/admin/admin.py b/atr/blueprints/admin/admin.py
index 6607322..5e01f73 100644
--- a/atr/blueprints/admin/admin.py
+++ b/atr/blueprints/admin/admin.py
@@ -121,11 +121,10 @@ async def admin_data(model: str = "Committee") -> str:
"""Browse all records in the database."""
async with db.session() as data:
# Map of model names to their classes
- # TODO: Add distribution channel, pmc key link, and any others
+ # TODO: Add distribution channel, key link, and any others
model_methods: dict[str, Callable[[], db.Query[Any]]] = {
"Committee": data.committee,
"Package": data.package,
- "Product": data.product,
"Project": data.project,
"PublicSigningKey": data.public_signing_key,
"Release": data.release,
@@ -165,7 +164,7 @@ async def admin_projects_update() -> str |
response.Response | tuple[Mapping[str
"""Update projects from remote data."""
if quart.request.method == "POST":
try:
- updated_count = await _update_pmcs()
+ updated_count = await _update_committees()
return {
"message": f"Successfully updated {updated_count} projects
(PMCs and PPMCs) with membership data",
"category": "success",
@@ -182,10 +181,10 @@ async def admin_projects_update() -> str |
response.Response | tuple[Mapping[str
}, 200
# For GET requests, show the update form
- return await quart.render_template("update-pmcs.html")
+ return await quart.render_template("update-committees.html")
-async def _update_pmcs() -> int:
+async def _update_committees() -> int:
ldap_projects = await apache.get_ldap_projects_data()
podlings_data = await apache.get_current_podlings_data()
groups_data = await apache.get_groups_data()
@@ -198,70 +197,70 @@ async def _update_pmcs() -> int:
for project in ldap_projects.projects:
name = project.name
# Skip non-PMC committees
- if not project.pmc:
+ if project.pmc is None:
continue
# Get or create PMC
- pmc = await data.committee(name=name).get()
- if not pmc:
- pmc = models.PMC(name=name)
- data.add(pmc)
- pmc_core_project = models.Project(name=name, pmc=pmc)
- data.add(pmc_core_project)
+ committee = await data.committee(name=name).get()
+ if not committee:
+ committee = models.Committee(name=name)
+ data.add(committee)
+ committee_core_project = models.Project(name=name,
committee=committee)
+ data.add(committee_core_project)
# Update PMC data from groups.json
pmc_members = groups_data.get(f"{name}-pmc")
committers = groups_data.get(name)
- pmc.pmc_members = pmc_members if pmc_members is not None else
[]
- pmc.committers = committers if committers is not None else []
+ committee.committee_members = pmc_members if pmc_members is
not None else []
+ committee.committers = committers if committers is not None
else []
# Ensure this is set for PMCs
- pmc.is_podling = False
+ committee.is_podling = False
# For release managers, use PMC members for now
# TODO: Consider a more sophisticated way to determine release
managers
# from my POV, the list of release managers should be
the list of people
# that have actually cut a release for that project
- pmc.release_managers = pmc.pmc_members
+ committee.release_managers = committee.committee_members
updated_count += 1
# Then add PPMCs (podlings)
for podling_name, podling_data in podlings_data:
# Get or create PPMC
- ppmc = await data.committee(name=podling_name).get()
- if not ppmc:
- ppmc = models.PMC(name=podling_name, is_podling=True)
- data.add(ppmc)
- ppmc_core_project = models.Project(name=podling_name,
is_podling=True, pmc=ppmc)
- data.add(ppmc_core_project)
+ podling = await data.committee(name=podling_name).get()
+ if not podling:
+ podling = models.Committee(name=podling_name,
is_podling=True)
+ data.add(podling)
+ podling_core_project = models.Project(name=podling_name,
committee=podling)
+ data.add(podling_core_project)
# Update PPMC data from groups.json
- ppmc.is_podling = True
+ podling.is_podling = True
pmc_members = groups_data.get(f"{podling_name}-pmc")
committers = groups_data.get(podling_name)
- ppmc.pmc_members = pmc_members if pmc_members is not None else
[]
- ppmc.committers = committers if committers is not None else []
+ podling.committee_members = pmc_members if pmc_members is not
None else []
+ podling.committers = committers if committers is not None else
[]
# Use PPMC members as release managers
- ppmc.release_managers = ppmc.pmc_members
+ podling.release_managers = podling.committee_members
updated_count += 1
# Add special entry for Tooling PMC
# Not clear why, but it's not in the Whimsy data
- tooling_pmc = await data.committee(name="tooling").get()
- if not tooling_pmc:
- tooling_pmc = models.PMC(name="tooling")
- data.add(tooling_pmc)
- tooling_project = models.Project(name="tooling",
pmc=tooling_pmc)
+ tooling_committee = await data.committee(name="tooling").get()
+ if not tooling_committee:
+ tooling_committee = models.Committee(name="tooling")
+ data.add(tooling_committee)
+ tooling_project = models.Project(name="tooling",
committee=tooling_committee)
data.add(tooling_project)
updated_count += 1
# Update Tooling PMC data
- # Could put this in the "if not tooling_pmc" block, perhaps
- tooling_pmc.pmc_members = ["wave", "tn", "sbp"]
- tooling_pmc.committers = ["wave", "tn", "sbp"]
- tooling_pmc.release_managers = ["wave"]
- tooling_pmc.is_podling = False
+ # Could put this in the "if not tooling_committee" block, perhaps
+ tooling_committee.committee_members = ["wave", "tn", "sbp"]
+ tooling_committee.committers = ["wave", "tn", "sbp"]
+ tooling_committee.release_managers = ["wave"]
+ tooling_committee.is_podling = False
return updated_count
diff --git a/atr/blueprints/admin/templates/update-pmcs.html
b/atr/blueprints/admin/templates/update-committees.html
similarity index 100%
rename from atr/blueprints/admin/templates/update-pmcs.html
rename to atr/blueprints/admin/templates/update-committees.html
diff --git a/atr/blueprints/api/api.py b/atr/blueprints/api/api.py
index 2038569..e93f887 100644
--- a/atr/blueprints/api/api.py
+++ b/atr/blueprints/api/api.py
@@ -15,16 +15,17 @@
# specific language governing permissions and limitations
# under the License.
+import dataclasses
from collections.abc import Mapping
-from dataclasses import dataclass
-from quart import Response, jsonify
-from quart_schema import validate_querystring, validate_response
-from werkzeug.exceptions import NotFound
+import quart
+import quart_schema
+import werkzeug.exceptions as exceptions
import atr.blueprints.api as api
-from atr.db.models import PMC
-from atr.db.service import get_pmc_by_name, get_pmcs, get_tasks
+import atr.db as db
+import atr.db.models as models
+import atr.db.service as service
# FIXME: we need to return the dumped model instead of the actual pydantic
class
# as otherwise pyright will complain about the return type
@@ -33,32 +34,31 @@ from atr.db.service import get_pmc_by_name, get_pmcs,
get_tasks
@api.BLUEPRINT.route("/projects/<name>")
-@validate_response(PMC, 200)
+@quart_schema.validate_response(models.Committee, 200)
async def project_by_name(name: str) -> tuple[Mapping, int]:
- pmc = await get_pmc_by_name(name)
- if pmc:
- return pmc.model_dump(), 200
- else:
- raise NotFound()
+ async with db.session() as data:
+ committee = await
data.committee(name=name).demand(exceptions.NotFound())
+ return committee.model_dump(), 200
@api.BLUEPRINT.route("/projects")
-@validate_response(list[PMC], 200)
+@quart_schema.validate_response(list[models.Committee], 200)
async def projects() -> tuple[list[Mapping], int]:
"""List all projects in the database."""
- pmcs = await get_pmcs()
- return [pmc.model_dump() for pmc in pmcs], 200
+ async with db.session() as data:
+ committees = await data.committee().all()
+ return [committee.model_dump() for committee in committees], 200
-@dataclass
[email protected]
class Pagination:
offset: int = 0
limit: int = 20
@api.BLUEPRINT.route("/tasks")
-@validate_querystring(Pagination)
-async def api_tasks(query_args: Pagination) -> Response:
- paged_tasks, count = await get_tasks(limit=query_args.limit,
offset=query_args.offset)
+@quart_schema.validate_querystring(Pagination)
+async def api_tasks(query_args: Pagination) -> quart.Response:
+ paged_tasks, count = await service.get_tasks(limit=query_args.limit,
offset=query_args.offset)
result = {"data": [x.model_dump(exclude={"result"}) for x in paged_tasks],
"count": count}
- return jsonify(result)
+ return quart.jsonify(result)
diff --git a/atr/db/__init__.py b/atr/db/__init__.py
index 70a09d5..22d133b 100644
--- a/atr/db/__init__.py
+++ b/atr/db/__init__.py
@@ -85,44 +85,44 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
name: Any = _DEFAULT,
full_name: Any = _DEFAULT,
is_podling: Any = _DEFAULT,
- parent_pmc_id: Any = _DEFAULT,
- pmc_members: Any = _DEFAULT,
+ parent_committee_id: Any = _DEFAULT,
+ committee_members: Any = _DEFAULT,
committers: Any = _DEFAULT,
release_managers: Any = _DEFAULT,
vote_policy_id: Any = _DEFAULT,
name_in: list[str] | _DefaultArgument = _DEFAULT,
_public_signing_keys: bool = False,
_vote_policy: bool = False,
- ) -> Query[models.PMC]:
- query = sqlmodel.select(models.PMC)
+ ) -> Query[models.Committee]:
+ query = sqlmodel.select(models.Committee)
if id is not _DEFAULT:
- query = query.where(models.PMC.id == id)
+ query = query.where(models.Committee.id == id)
if name is not _DEFAULT:
- query = query.where(models.PMC.name == name)
+ query = query.where(models.Committee.name == name)
if full_name is not _DEFAULT:
- query = query.where(models.PMC.full_name == full_name)
+ query = query.where(models.Committee.full_name == full_name)
if is_podling is not _DEFAULT:
- query = query.where(models.PMC.is_podling == is_podling)
- if parent_pmc_id is not _DEFAULT:
- query = query.where(models.PMC.parent_pmc_id == parent_pmc_id)
- if pmc_members is not _DEFAULT:
- query = query.where(models.PMC.pmc_members == pmc_members)
+ query = query.where(models.Committee.is_podling == is_podling)
+ if parent_committee_id is not _DEFAULT:
+ query = query.where(models.Committee.parent_committee_id ==
parent_committee_id)
+ if committee_members is not _DEFAULT:
+ query = query.where(models.Committee.committee_members ==
committee_members)
if committers is not _DEFAULT:
- query = query.where(models.PMC.committers == committers)
+ query = query.where(models.Committee.committers == committers)
if release_managers is not _DEFAULT:
- query = query.where(models.PMC.release_managers ==
release_managers)
+ query = query.where(models.Committee.release_managers ==
release_managers)
if vote_policy_id is not _DEFAULT:
- query = query.where(models.PMC.vote_policy_id == vote_policy_id)
+ query = query.where(models.Committee.vote_policy_id ==
vote_policy_id)
if not isinstance(name_in, _DefaultArgument):
- models_pmc_name = validate_instrumented_attribute(models.PMC.name)
- query = query.where(models_pmc_name.in_(name_in))
+ models_committee_name =
validate_instrumented_attribute(models.Committee.name)
+ query = query.where(models_committee_name.in_(name_in))
if _public_signing_keys:
- query =
query.options(select_in_load(models.PMC.public_signing_keys))
+ query =
query.options(select_in_load(models.Committee.public_signing_keys))
if _vote_policy:
- query = query.options(select_in_load(models.PMC.vote_policy))
+ query = query.options(select_in_load(models.Committee.vote_policy))
return Query(self, query)
@@ -138,8 +138,8 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
release_key: Any = _DEFAULT,
_release: bool = False,
_tasks: bool = False,
- _release_product: bool = False,
- _release_pmc: bool = False,
+ _release_project: bool = False,
+ _release_committee: bool = False,
) -> Query[models.Package]:
query = sqlmodel.select(models.Package)
@@ -163,73 +163,27 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
query = query.options(select_in_load(models.Package.release))
if _tasks:
query = query.options(select_in_load(models.Package.tasks))
- if _release_product:
- query = query.options(select_in_load(models.Package.release,
models.Release.product))
- if _release_pmc:
+ if _release_project:
+ query = query.options(select_in_load(models.Package.release,
models.Release.project))
+ if _release_committee:
query = query.options(
- select_in_load_nested(
- models.Package.release, models.Release.product,
models.Product.project, models.Project.pmc
- )
+ select_in_load_nested(models.Package.release,
models.Release.project, models.Project.committee)
)
return Query(self, query)
- def product(
- self,
- id: Any = _DEFAULT,
- project_id: Any = _DEFAULT,
- product_name: Any = _DEFAULT,
- latest_version: Any = _DEFAULT,
- vote_policy_id: Any = _DEFAULT,
- project_pmc_id: Any = _DEFAULT,
- _project: bool = False,
- _distribution_channels: bool = False,
- _vote_policy: bool = False,
- _releases: bool = False,
- _project_pmc: bool = False,
- ) -> Query[models.Product]:
- query = sqlmodel.select(models.Product)
-
- if id is not _DEFAULT:
- query = query.where(models.Product.id == id)
- if project_id is not _DEFAULT:
- query = query.where(models.Product.project_id == project_id)
- if product_name is not _DEFAULT:
- query = query.where(models.Product.product_name == product_name)
- if latest_version is not _DEFAULT:
- query = query.where(models.Product.latest_version ==
latest_version)
- if vote_policy_id is not _DEFAULT:
- query = query.where(models.Product.vote_policy_id ==
vote_policy_id)
-
- if project_pmc_id is not _DEFAULT:
- query = query.join(
- models.Project,
validate_instrumented_attribute(models.Product.project_id) == models.Project.id
- ).where(models.Project.pmc_id == project_pmc_id)
-
- if _project:
- query = query.options(select_in_load(models.Product.project))
- if _distribution_channels:
- query =
query.options(select_in_load(models.Product.distribution_channels))
- if _vote_policy:
- query = query.options(select_in_load(models.Product.vote_policy))
- if _releases:
- query = query.options(select_in_load(models.Product.releases))
- if _project_pmc:
- query =
query.options(select_in_load_nested(models.Product.project, models.Project.pmc))
-
- return Query(self, query)
-
def project(
self,
id: Any = _DEFAULT,
name: Any = _DEFAULT,
full_name: Any = _DEFAULT,
is_podling: Any = _DEFAULT,
- pmc_id: Any = _DEFAULT,
+ committee_id: Any = _DEFAULT,
vote_policy_id: Any = _DEFAULT,
- _pmc: bool = False,
- _products: bool = False,
+ _committee: bool = False,
+ _releases: bool = False,
+ _distribution_channels: bool = False,
_vote_policy: bool = False,
- _pmc_public_signing_keys: bool = False,
+ _committee_public_signing_keys: bool = False,
) -> Query[models.Project]:
query = sqlmodel.select(models.Project)
@@ -241,19 +195,21 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
query = query.where(models.Project.full_name == full_name)
if is_podling is not _DEFAULT:
query = query.where(models.Project.is_podling == is_podling)
- if pmc_id is not _DEFAULT:
- query = query.where(models.Project.pmc_id == pmc_id)
+ if committee_id is not _DEFAULT:
+ query = query.where(models.Project.committee_id == committee_id)
if vote_policy_id is not _DEFAULT:
query = query.where(models.Project.vote_policy_id ==
vote_policy_id)
- if _pmc:
- query = query.options(select_in_load(models.Project.pmc))
- if _products:
- query = query.options(select_in_load(models.Project.products))
+ if _committee:
+ query = query.options(select_in_load(models.Project.committee))
+ if _releases:
+ query = query.options(select_in_load(models.Project.releases))
+ if _distribution_channels:
+ query =
query.options(select_in_load(models.Project.distribution_channels))
if _vote_policy:
query = query.options(select_in_load(models.Project.vote_policy))
- if _pmc_public_signing_keys:
- query = query.options(select_in_load_nested(models.Project.pmc,
models.PMC.public_signing_keys))
+ if _committee_public_signing_keys:
+ query =
query.options(select_in_load_nested(models.Project.committee,
models.Committee.public_signing_keys))
return Query(self, query)
@@ -267,7 +223,7 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
declared_uid: Any = _DEFAULT,
apache_uid: Any = _DEFAULT,
ascii_armored_key: Any = _DEFAULT,
- _pmcs: bool = False,
+ _committees: bool = False,
) -> Query[models.PublicSigningKey]:
query = sqlmodel.select(models.PublicSigningKey)
@@ -288,8 +244,8 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
if ascii_armored_key is not _DEFAULT:
query = query.where(models.PublicSigningKey.ascii_armored_key ==
ascii_armored_key)
- if _pmcs:
- query = query.options(select_in_load(models.PublicSigningKey.pmcs))
+ if _committees:
+ query =
query.options(select_in_load(models.PublicSigningKey.committees))
return Query(self, query)
@@ -299,16 +255,16 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
stage: Any = _DEFAULT,
phase: Any = _DEFAULT,
created: Any = _DEFAULT,
- product_id: Any = _DEFAULT,
+ project_id: Any = _DEFAULT,
package_managers: Any = _DEFAULT,
version: Any = _DEFAULT,
sboms: Any = _DEFAULT,
vote_policy_id: Any = _DEFAULT,
votes: Any = _DEFAULT,
- _product: bool = False,
+ _project: bool = False,
_packages: bool = False,
_vote_policy: bool = False,
- _pmc: bool = False,
+ _committee: bool = False,
_packages_tasks: bool = False,
) -> Query[models.Release]:
query = sqlmodel.select(models.Release)
@@ -321,8 +277,8 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
query = query.where(models.Release.phase == phase)
if created is not _DEFAULT:
query = query.where(models.Release.created == created)
- if product_id is not _DEFAULT:
- query = query.where(models.Release.product_id == product_id)
+ if project_id is not _DEFAULT:
+ query = query.where(models.Release.project_id == project_id)
if package_managers is not _DEFAULT:
query = query.where(models.Release.package_managers ==
package_managers)
if version is not _DEFAULT:
@@ -334,16 +290,14 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
if votes is not _DEFAULT:
query = query.where(models.Release.votes == votes)
- if _product:
- query = query.options(select_in_load(models.Release.product))
+ if _project:
+ query = query.options(select_in_load(models.Release.project))
if _packages:
query = query.options(select_in_load(models.Release.packages))
if _vote_policy:
query = query.options(select_in_load(models.Release.vote_policy))
- if _pmc:
- query = query.options(
- select_in_load_nested(models.Release.product,
models.Product.project, models.Project.pmc)
- )
+ if _committee:
+ query =
query.options(select_in_load_nested(models.Release.project,
models.Project.committee))
if _packages_tasks:
query =
query.options(select_in_load_nested(models.Release.packages,
models.Package.tasks))
@@ -405,9 +359,8 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
min_hours: Any = _DEFAULT,
release_checklist: Any = _DEFAULT,
pause_for_rm: Any = _DEFAULT,
- _pmcs: bool = False,
+ _committees: bool = False,
_projects: bool = False,
- _products: bool = False,
_releases: bool = False,
) -> Query[models.VotePolicy]:
query = sqlmodel.select(models.VotePolicy)
@@ -425,12 +378,10 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
if pause_for_rm is not _DEFAULT:
query = query.where(models.VotePolicy.pause_for_rm == pause_for_rm)
- if _pmcs:
- query = query.options(select_in_load(models.VotePolicy.pmcs))
+ if _committees:
+ query = query.options(select_in_load(models.VotePolicy.committees))
if _projects:
query = query.options(select_in_load(models.VotePolicy.projects))
- if _products:
- query = query.options(select_in_load(models.VotePolicy.products))
if _releases:
query = query.options(select_in_load(models.VotePolicy.releases))
diff --git a/atr/db/models.py b/atr/db/models.py
index 917d9d0..1776156 100644
--- a/atr/db/models.py
+++ b/atr/db/models.py
@@ -29,7 +29,7 @@ import sqlmodel
class UserRole(str, enum.Enum):
- PMC_MEMBER = "pmc_member"
+ COMMITTEE_MEMBER = "committee_member"
RELEASE_MANAGER = "release_manager"
COMMITTER = "committer"
VISITOR = "visitor"
@@ -37,8 +37,8 @@ class UserRole(str, enum.Enum):
SYSADMIN = "sysadmin"
-class PMCKeyLink(sqlmodel.SQLModel, table=True):
- pmc_id: int = sqlmodel.Field(foreign_key="pmc.id", primary_key=True)
+class KeyLink(sqlmodel.SQLModel, table=True):
+ committee_id: int = sqlmodel.Field(foreign_key="committee.id",
primary_key=True)
key_fingerprint: str =
sqlmodel.Field(foreign_key="publicsigningkey.fingerprint", primary_key=True)
@@ -59,8 +59,8 @@ class PublicSigningKey(sqlmodel.SQLModel, table=True):
apache_uid: str
# The ASCII armored key
ascii_armored_key: str
- # The PMCs that use this key
- pmcs: list["PMC"] =
sqlmodel.Relationship(back_populates="public_signing_keys",
link_model=PMCKeyLink)
+ # The committees that use this key
+ committees: list["Committee"] =
sqlmodel.Relationship(back_populates="public_signing_keys", link_model=KeyLink)
class VotePolicy(sqlmodel.SQLModel, table=True):
@@ -71,65 +71,67 @@ class VotePolicy(sqlmodel.SQLModel, table=True):
release_checklist: str = sqlmodel.Field(default="")
pause_for_rm: bool = sqlmodel.Field(default=False)
- # One-to-many: A vote policy can be used by multiple PMCs
- pmcs: list["PMC"] = sqlmodel.Relationship(back_populates="vote_policy")
+ # One-to-many: A vote policy can be used by multiple committees
+ committees: list["Committee"] =
sqlmodel.Relationship(back_populates="vote_policy")
# One-to-many: A vote policy can be used by multiple projects
projects: list["Project"] =
sqlmodel.Relationship(back_populates="vote_policy")
- # One-to-many: A vote policy can be used by multiple product lines
- products: list["Product"] =
sqlmodel.Relationship(back_populates="vote_policy")
# One-to-many: A vote policy can be used by multiple releases
releases: list["Release"] =
sqlmodel.Relationship(back_populates="vote_policy")
-class PMC(sqlmodel.SQLModel, table=True):
+class Committee(sqlmodel.SQLModel, table=True):
id: int = sqlmodel.Field(default=None, primary_key=True)
name: str = sqlmodel.Field(unique=True)
full_name: str | None = sqlmodel.Field(default=None)
# True only if this is an incubator podling with a PPMC
is_podling: bool = sqlmodel.Field(default=False)
- # One-to-many: A PMC can have multiple child PMCs, each child PMC belongs
to one parent PMC
- child_pmcs: list["PMC"] = sqlmodel.Relationship(
+ # One-to-many: A committee can have multiple child committees, each child
committee belongs to one parent committee
+ child_committees: list["Committee"] = sqlmodel.Relationship(
sa_relationship_kwargs=dict(
- backref=sqlalchemy.orm.backref("parent_pmc", remote_side="PMC.id"),
+ backref=sqlalchemy.orm.backref("parent_committee",
remote_side="Committee.id"),
),
)
- parent_pmc_id: int | None = sqlmodel.Field(default=None,
foreign_key="pmc.id")
- # One-to-many: A PMC can have multiple projects, each project belongs to
one PMC
- projects: list["Project"] = sqlmodel.Relationship(back_populates="pmc")
+ parent_committee_id: int | None = sqlmodel.Field(default=None,
foreign_key="committee.id")
+ # One-to-many: A committee can have multiple projects, each project
belongs to one committee
+ projects: list["Project"] =
sqlmodel.Relationship(back_populates="committee")
- pmc_members: list[str] = sqlmodel.Field(default_factory=list,
sa_column=sqlalchemy.Column(sqlalchemy.JSON))
+ committee_members: list[str] = sqlmodel.Field(default_factory=list,
sa_column=sqlalchemy.Column(sqlalchemy.JSON))
committers: list[str] = sqlmodel.Field(default_factory=list,
sa_column=sqlalchemy.Column(sqlalchemy.JSON))
release_managers: list[str] = sqlmodel.Field(default_factory=list,
sa_column=sqlalchemy.Column(sqlalchemy.JSON))
- # Many-to-many: A PMC can have multiple signing keys, and a signing key
can belong to multiple PMCs
- public_signing_keys: list[PublicSigningKey] =
sqlmodel.Relationship(back_populates="pmcs", link_model=PMCKeyLink)
+ # Many-to-many: A committee can have multiple signing keys, and a signing
key can belong to multiple committees
+ public_signing_keys: list[PublicSigningKey] =
sqlmodel.Relationship(back_populates="committees", link_model=KeyLink)
- # Many-to-one: A PMC can have one vote policy, a vote policy can be used
by multiple entities
+ # Many-to-one: A committee can have one vote policy, a vote policy can be
used by multiple entities
vote_policy_id: int | None = sqlmodel.Field(default=None,
foreign_key="votepolicy.id")
- vote_policy: VotePolicy | None =
sqlmodel.Relationship(back_populates="pmcs")
+ vote_policy: VotePolicy | None =
sqlmodel.Relationship(back_populates="committees")
@property
def display_name(self) -> str:
- """Get the display name for the PMC/PPMC."""
+ """Get the display name for the committee."""
name = self.name if self.full_name is None else self.full_name
- return f"{name} (PPMC)" if self.is_podling else name
+ return f"{name} (podling)" if self.is_podling else name
class Project(sqlmodel.SQLModel, table=True):
- id: int | None = sqlmodel.Field(default=None, primary_key=True)
+ id: int = sqlmodel.Field(default=None, primary_key=True)
name: str = sqlmodel.Field(unique=True)
full_name: str | None = sqlmodel.Field(default=None)
- # True if this a podling PPMC
+ # True if this a podling project
+ # TODO: We should have this on Committee too, or instead
is_podling: bool = sqlmodel.Field(default=False)
- # Many-to-one: A product line belongs to one PMC, a PMC can have multiple
product lines
- pmc_id: int | None = sqlmodel.Field(default=None, foreign_key="pmc.id")
- pmc: PMC | None = sqlmodel.Relationship(back_populates="projects")
+ # Many-to-one: A project belongs to one committee, a committee can have
multiple projects
+ committee_id: int | None = sqlmodel.Field(default=None,
foreign_key="committee.id")
+ committee: Committee | None =
sqlmodel.Relationship(back_populates="projects")
+
+ # One-to-many: A project can have multiple releases, each release belongs
to one project
+ releases: list["Release"] = sqlmodel.Relationship(back_populates="project")
- # One-to-many: A PMC can have multiple product lines, each product line
belongs to one PMC
- products: list["Product"] = sqlmodel.Relationship(back_populates="project")
+ # One-to-many: A project can have multiple distribution channels, each
channel belongs to one project
+ distribution_channels: list["DistributionChannel"] =
sqlmodel.Relationship(back_populates="project")
# Many-to-one: A Project can have one vote policy, a vote policy can be
used by multiple entities
vote_policy_id: int | None = sqlmodel.Field(default=None,
foreign_key="votepolicy.id")
@@ -142,28 +144,6 @@ class Project(sqlmodel.SQLModel, table=True):
return f"{name} (podling)" if self.is_podling else name
-class Product(sqlmodel.SQLModel, table=True):
- id: int = sqlmodel.Field(default=None, primary_key=True)
-
- # Many-to-one: A product line belongs to one project, a project can have
multiple product lines
- project_id: int | None = sqlmodel.Field(default=None,
foreign_key="project.id")
- project: Project | None = sqlmodel.Relationship(back_populates="products")
-
- product_name: str
- # TODO: This could be computed dynamically from
Product.releases[-1].version
- latest_version: str
-
- # One-to-many: A product line can have multiple distribution channels,
each channel belongs to one product line
- distribution_channels: list["DistributionChannel"] =
sqlmodel.Relationship(back_populates="product")
-
- # Many-to-one: A product line can have one vote policy, a vote policy can
be used by multiple entities
- vote_policy_id: int | None = sqlmodel.Field(default=None,
foreign_key="votepolicy.id")
- vote_policy: VotePolicy | None =
sqlmodel.Relationship(back_populates="products")
-
- # One-to-many: A product line can have multiple releases, each release
belongs to one product line
- releases: list["Release"] = sqlmodel.Relationship(back_populates="product")
-
-
class DistributionChannel(sqlmodel.SQLModel, table=True):
id: int = sqlmodel.Field(default=None, primary_key=True)
name: str = sqlmodel.Field(index=True, unique=True)
@@ -172,9 +152,9 @@ class DistributionChannel(sqlmodel.SQLModel, table=True):
is_test: bool = sqlmodel.Field(default=False)
automation_endpoint: str
- # Many-to-one: A distribution channel belongs to one product line, a
product line can have multiple channels
- product_id: int = sqlmodel.Field(foreign_key="product.id")
- product: Product =
sqlmodel.Relationship(back_populates="distribution_channels")
+ # Many-to-one: A distribution channel belongs to one project, a project
can have multiple channels
+ project_id: int = sqlmodel.Field(foreign_key="project.id")
+ project: Project =
sqlmodel.Relationship(back_populates="distribution_channels")
class Package(sqlmodel.SQLModel, table=True):
@@ -293,9 +273,9 @@ class Release(sqlmodel.SQLModel, table=True):
phase: ReleasePhase
created: datetime.datetime
- # Many-to-one: A release belongs to one product line, a product line can
have multiple releases
- product_id: int = sqlmodel.Field(foreign_key="product.id")
- product: Product = sqlmodel.Relationship(back_populates="releases")
+ # Many-to-one: A release belongs to one project, a project can have
multiple releases
+ project_id: int = sqlmodel.Field(foreign_key="project.id")
+ project: Project = sqlmodel.Relationship(back_populates="releases")
package_managers: list[str] = sqlmodel.Field(default_factory=list,
sa_column=sqlalchemy.Column(sqlalchemy.JSON))
# TODO: Not all releases have a version
@@ -314,9 +294,9 @@ class Release(sqlmodel.SQLModel, table=True):
votes: list[VoteEntry] = sqlmodel.Field(default_factory=list,
sa_column=sqlalchemy.Column(sqlalchemy.JSON))
@property
- def pmc(self) -> PMC | None:
- """Get the PMC for the release."""
- project = self.product.project
+ def committee(self) -> Committee | None:
+ """Get the committee for the release."""
+ project = self.project
if project is None:
return None
- return project.pmc
+ return project.committee
diff --git a/atr/db/service.py b/atr/db/service.py
index 9a50fba..e97eb06 100644
--- a/atr/db/service.py
+++ b/atr/db/service.py
@@ -26,21 +26,23 @@ import atr.db as db
import atr.db.models as models
-async def get_pmc_by_name(name: str, session:
sqlalchemy.ext.asyncio.AsyncSession | None = None) -> models.PMC | None:
- """Returns a PMC object by name."""
+async def get_committee_by_name(
+ name: str, session: sqlalchemy.ext.asyncio.AsyncSession | None = None
+) -> models.Committee | None:
+ """Returns a Committee object by name."""
async with db.create_async_db_session() if session is None else
contextlib.nullcontext(session) as db_session:
- statement = sqlmodel.select(models.PMC).where(models.PMC.name == name)
- pmc = (await db_session.execute(statement)).scalar_one_or_none()
- return pmc
+ statement =
sqlmodel.select(models.Committee).where(models.Committee.name == name)
+ committee = (await db_session.execute(statement)).scalar_one_or_none()
+ return committee
-async def get_pmcs(session: sqlalchemy.ext.asyncio.AsyncSession | None = None)
-> Sequence[models.PMC]:
- """Returns a list of PMC objects."""
+async def get_committees(session: sqlalchemy.ext.asyncio.AsyncSession | None =
None) -> Sequence[models.Committee]:
+ """Returns a list of Committee objects."""
async with db.create_async_db_session() if session is None else
contextlib.nullcontext(session) as db_session:
- # Get all PMCs and their latest releases
- statement = sqlmodel.select(models.PMC).order_by(models.PMC.name)
- pmcs = (await db_session.execute(statement)).scalars().all()
- return pmcs
+ # Get all Committees
+ statement =
sqlmodel.select(models.Committee).order_by(models.Committee.name)
+ committees = (await db_session.execute(statement)).scalars().all()
+ return committees
async def get_release_by_key(storage_key: str) -> models.Release | None:
@@ -50,7 +52,7 @@ async def get_release_by_key(storage_key: str) ->
models.Release | None:
query = (
sqlmodel.select(models.Release)
.where(models.Release.storage_key == storage_key)
- .options(db.select_in_load_nested(models.Release.product,
models.Product.project, models.Project.pmc))
+ .options(db.select_in_load_nested(models.Release.project,
models.Project.committee))
)
result = await db_session.execute(query)
return result.scalar_one_or_none()
@@ -63,7 +65,7 @@ def get_release_by_key_sync(storage_key: str) ->
models.Release | None:
query = (
sqlmodel.select(models.Release)
.where(models.Release.storage_key == storage_key)
- .options(db.select_in_load_nested(models.Release.product,
models.Product.project, models.Project.pmc))
+ .options(db.select_in_load_nested(models.Release.project,
models.Project.committee))
)
result = session.execute(query)
return result.scalar_one_or_none()
diff --git a/atr/routes/candidate.py b/atr/routes/candidate.py
index cd74caf..d426f47 100644
--- a/atr/routes/candidate.py
+++ b/atr/routes/candidate.py
@@ -35,15 +35,17 @@ if asfquart.APP is ...:
raise RuntimeError("APP is not set")
-def format_artifact_name(project_name: str, product_name: str, version: str,
is_podling: bool = False) -> str:
+def format_artifact_name(project_name: str, version: str, is_podling: bool =
False) -> str:
"""Format an artifact name according to Apache naming conventions.
- For regular projects: apache-${project}-${product}-${version}
- For podlings: apache-${project}-incubating-${product}-${version}
+ For regular projects: apache-${project}-${version}
+ For podlings: apache-${project}-incubating-${version}
"""
+ # TODO: Format this better based on committee and project
+ # Must depend on whether project is a subproject or not
if is_podling:
- return f"apache-{project_name}-incubating-{product_name}-{version}"
- return f"apache-{project_name}-{product_name}-{version}"
+ return f"apache-{project_name}-incubating-{version}"
+ return f"apache-{project_name}-{version}"
# Release functions
@@ -53,57 +55,55 @@ async def release_add_post(session: session.ClientSession,
request: quart.Reques
"""Handle POST request for creating a new release."""
form = await routes.get_form(request)
- project_name = form.get("project_name")
- if not project_name:
- raise base.ASFQuartException("Project name is required", errorcode=400)
-
- product_name = form.get("product_name")
- if not product_name:
- raise base.ASFQuartException("Product name is required", errorcode=400)
+ committee_name = form.get("committee_name")
+ if not committee_name:
+ raise base.ASFQuartException("Committee name is required",
errorcode=400)
version = form.get("version")
if not version:
raise base.ASFQuartException("Version is required", errorcode=400)
- # TODO: Forbid creating a release with an existing project, product, and
version
+ project_name = form.get("project_name")
+ if not project_name:
+ raise base.ASFQuartException("Project name is required", errorcode=400)
+
+ # TODO: Forbid creating a release with an existing project and version
# Create the release record in the database
async with db.session() as data:
async with data.begin():
- pmc = await data.committee(name=project_name).get()
- if not pmc:
- asfquart.APP.logger.error(f"PMC not found for project
{project_name}")
- raise base.ASFQuartException("Project not found",
errorcode=404)
+ committee = await data.committee(name=committee_name).get()
+ if not committee:
+ asfquart.APP.logger.error(f"Committee not found for project
{committee_name}")
+ raise base.ASFQuartException("Committee 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 pmc.name not in (session.committees + session.projects):
+ # We use committee.name, so this must come within the transaction
+ if committee.name not in (session.committees + session.projects):
raise base.ASFQuartException(
- f"You must be a PMC member or committer of
{pmc.display_name} to submit a release candidate",
+ f"You must be a PMC member or committer of
{committee.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 or get existing product line
- product = await data.product(product_name=product_name,
project_pmc_id=pmc.id).get()
- if not product:
- # Create new product line if it doesn't exist
- project = await data.project(name=project_name,
pmc_id=pmc.id).demand(
- base.ASFQuartException(f"Project {project_name} not
found", errorcode=404)
+ project = await data.project(name=project_name).get()
+ if not project:
+ # Create a new project record
+ project = models.Project(
+ name=project_name,
+ committee_id=committee.id,
)
- product = models.Product(project_id=project.id,
product_name=product_name, latest_version=version)
- data.add(product)
- # Flush to make the product.id available
+ data.add(project)
+ # Must flush to get the project ID
await data.flush()
- # Create release record with product line
+ # Create release record with project
release = models.Release(
storage_key=storage_key,
stage=models.ReleaseStage.CANDIDATE,
phase=models.ReleasePhase.RELEASE_CANDIDATE,
- product_id=product.id,
+ project_id=project.id,
version=version,
created=datetime.datetime.now(datetime.UTC),
)
@@ -131,13 +131,13 @@ async def root_candidate_create() -> response.Response |
str:
# Get PMC objects for all projects the user is a member of
async with db.session() as data:
project_list = web_session.committees + web_session.projects
- user_pmcs = await data.committee(name_in=project_list).all()
+ user_committees = await data.committee(name_in=project_list).all()
# For GET requests, show the form
return await quart.render_template(
"candidate-create.html",
asf_id=web_session.uid,
- user_pmcs=user_pmcs,
+ user_committees=user_committees,
)
@@ -158,17 +158,17 @@ async def root_candidate_review() -> str:
# TODO: This duplicates code in root_package_add
releases = await data.release(
stage=models.ReleaseStage.CANDIDATE,
- _pmc=True,
+ _committee=True,
_packages_tasks=True,
).all()
# 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:
+ if r.committee is None:
continue
# For PPMCs the "members" are stored in the committers field
- if web_session.uid in r.pmc.pmc_members or web_session.uid in
r.pmc.committers:
+ if (web_session.uid in r.committee.committee_members) or
(web_session.uid in r.committee.committers):
user_releases.append(r)
# time.sleep(0.37)
diff --git a/atr/routes/download.py b/atr/routes/download.py
index b80665a..8496c7e 100644
--- a/atr/routes/download.py
+++ b/atr/routes/download.py
@@ -47,7 +47,7 @@ async def root_download_artifact(release_key: str,
artifact_sha3: str) -> respon
package = await data.package(
artifact_sha3=artifact_sha3,
release_key=release_key,
- _release_pmc=True,
+ _release_committee=True,
).get()
if not package:
@@ -55,9 +55,9 @@ async def root_download_artifact(release_key: str,
artifact_sha3: str) -> respon
return quart.redirect(quart.url_for("root_candidate_review"))
# Check permissions
- if package.release and package.release.pmc:
- if (web_session.uid not in package.release.pmc.pmc_members) and (
- web_session.uid not in package.release.pmc.committers
+ if package.release and package.release.committee:
+ if (web_session.uid not in
package.release.committee.committee_members) and (
+ web_session.uid not in package.release.committee.committers
):
await quart.flash("You don't have permission to download this
file", "error")
return quart.redirect(quart.url_for("root_candidate_review"))
@@ -86,15 +86,17 @@ async def root_download_signature(release_key: str,
signature_sha3: str) -> quar
async with db.session() as data:
# Find the package that has this signature
- package = await data.package(signature_sha3=signature_sha3,
release_key=release_key, _release_pmc=True).get()
+ package = await data.package(
+ signature_sha3=signature_sha3, release_key=release_key,
_release_committee=True
+ ).get()
if not package:
await quart.flash("Signature not found", "error")
return quart.redirect(quart.url_for("root_candidate_review"))
# Check permissions
- if package.release and package.release.pmc:
- if (web_session.uid not in package.release.pmc.pmc_members) and (
- web_session.uid not in package.release.pmc.committers
+ if package.release and package.release.committee:
+ if (web_session.uid not in
package.release.committee.committee_members) and (
+ web_session.uid not in package.release.committee.committers
):
await quart.flash("You don't have permission to download this
file", "error")
return quart.redirect(quart.url_for("root_candidate_review"))
diff --git a/atr/routes/keys.py b/atr/routes/keys.py
index f737d8c..7831335 100644
--- a/atr/routes/keys.py
+++ b/atr/routes/keys.py
@@ -53,7 +53,7 @@ async def ephemeral_gpg_home() -> AsyncGenerator[str]:
async def key_add_post(
- web_session: session.ClientSession, request: quart.Request, user_pmcs:
Sequence[models.PMC]
+ web_session: session.ClientSession, request: quart.Request,
user_committees: Sequence[models.Committee]
) -> dict | None:
form = await routes.get_form(request)
public_key = form.get("public_key")
@@ -61,21 +61,25 @@ async def key_add_post(
raise routes.FlashError("Public key is required")
# Get selected PMCs from form
- selected_pmcs = form.getlist("selected_pmcs")
- if not selected_pmcs:
+ selected_committees = form.getlist("selected_committees")
+ if not selected_committees:
raise routes.FlashError("You must select at least one PMC")
# Ensure that the selected PMCs are ones of which the user is actually a
member
- invalid_pmcs = [
- pmc for pmc in selected_pmcs if (pmc not in web_session.committees)
and (pmc not in web_session.projects)
+ invalid_committees = [
+ committee
+ for committee in selected_committees
+ if (committee not in web_session.committees) and (committee not in
web_session.projects)
]
- if invalid_pmcs:
- raise routes.FlashError(f"Invalid PMC selection: {',
'.join(invalid_pmcs)}")
+ if invalid_committees:
+ raise routes.FlashError(f"Invalid PMC selection: {',
'.join(invalid_committees)}")
- return await key_user_add(web_session, public_key, selected_pmcs)
+ return await key_user_add(web_session, public_key, selected_committees)
-async def key_user_add(web_session: session.ClientSession, public_key: str,
selected_pmcs: list[str]) -> dict | None:
+async def key_user_add(
+ web_session: session.ClientSession, public_key: str, selected_committees:
list[str]
+) -> dict | None:
if not public_key:
raise routes.FlashError("Public key is required")
@@ -110,14 +114,14 @@ async def key_user_add(web_session:
session.ClientSession, public_key: str, sele
# Store key in database
async with db.session() as data:
- return await key_user_session_add(web_session, public_key, key,
selected_pmcs, data)
+ return await key_user_session_add(web_session, public_key, key,
selected_committees, data)
async def key_user_session_add(
web_session: session.ClientSession,
public_key: str,
key: dict,
- selected_pmcs: list[str],
+ selected_committees: list[str],
data: db.Session,
) -> dict | None:
# TODO: Check if key already exists
@@ -151,10 +155,10 @@ async def key_user_session_add(
data.add(key_record)
# Link key to selected PMCs
- for pmc_name in selected_pmcs:
- pmc = await data.committee(name=pmc_name).get()
- if pmc and pmc.id:
- link = models.PMCKeyLink(pmc_id=pmc.id,
key_fingerprint=key_record.fingerprint)
+ for committee_name in selected_committees:
+ committee = await data.committee(name=committee_name).get()
+ if committee and committee.id:
+ link = models.KeyLink(committee_id=committee.id,
key_fingerprint=key_record.fingerprint)
data.add(link)
else:
# TODO: Log? Add to "error"?
@@ -183,11 +187,11 @@ async def root_keys_add() -> str:
# Get PMC objects for all projects the user is a member of
async with db.session() as data:
project_list = web_session.committees + web_session.projects
- user_pmcs = await data.committee(name_in=project_list).all()
+ user_committees = await data.committee(name_in=project_list).all()
if quart.request.method == "POST":
try:
- key_info = await key_add_post(web_session, quart.request,
user_pmcs)
+ key_info = await key_add_post(web_session, quart.request,
user_committees)
except routes.FlashError as e:
logging.exception("FlashError:")
await quart.flash(str(e), "error")
@@ -198,7 +202,7 @@ async def root_keys_add() -> str:
return await quart.render_template(
"keys-add.html",
asf_id=web_session.uid,
- user_pmcs=user_pmcs,
+ user_committees=user_committees,
key_info=key_info,
algorithms=routes.algorithms,
)
@@ -243,7 +247,7 @@ async def root_keys_review() -> str:
# Get all existing keys for the user
async with db.session() as data:
- user_keys = await data.public_signing_key(apache_uid=web_session.uid,
_pmcs=True).all()
+ user_keys = await data.public_signing_key(apache_uid=web_session.uid,
_committees=True).all()
status_message = quart.request.args.get("status_message")
status_type = quart.request.args.get("status_type")
diff --git a/atr/routes/package.py b/atr/routes/package.py
index 8af4d21..4f65146 100644
--- a/atr/routes/package.py
+++ b/atr/routes/package.py
@@ -197,7 +197,7 @@ async def package_add_validate(
async def package_data_get(data: db.Session, artifact_sha3: str, release_key:
str, session_uid: str) -> models.Package:
"""Validate package deletion request and return the package if valid."""
# Get the package and its associated release
- package = await data.package(artifact_sha3=artifact_sha3,
_release_pmc=True).demand(
+ package = await data.package(artifact_sha3=artifact_sha3,
_release_committee=True).demand(
routes.FlashError("Package not found")
)
@@ -205,8 +205,10 @@ async def package_data_get(data: db.Session,
artifact_sha3: str, release_key: st
raise routes.FlashError("Invalid release key")
# Check permissions
- if package.release and package.release.pmc:
- if session_uid not in package.release.pmc.pmc_members and session_uid
not in package.release.pmc.committers:
+ if package.release and package.release.committee:
+ if (session_uid not in package.release.committee.committee_members)
and (
+ session_uid not in package.release.committee.committers
+ ):
raise routes.FlashError("You don't have permission to access this
package")
return package
@@ -360,16 +362,16 @@ async def root_package_add() -> response.Response | str:
# Get all releases where the user is a PMC member or committer of the
associated PMC
async with db.session() as data:
# Load all the necessary relationships for the pmc property to work
- releases = await data.release(stage=models.ReleaseStage.CANDIDATE,
_pmc=True).all()
+ releases = await data.release(stage=models.ReleaseStage.CANDIDATE,
_committee=True).all()
# 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:
+ if r.committee is None:
continue
# For PPMCs the "members" are stored in the committers field
- if web_session.uid in r.pmc.pmc_members or web_session.uid in
r.pmc.committers:
+ if (web_session.uid in r.committee.committee_members) or
(web_session.uid in r.committee.committers):
user_releases.append(r)
# For GET requests, show the form
@@ -545,8 +547,8 @@ async def task_package_status_get(data: db.Session,
artifact_sha3: str) -> tuple
async def task_verification_create(data: db.Session, package: models.Package)
-> list[models.Task]:
"""Create verification tasks for a package."""
- if not package.release or not package.release.pmc:
- raise routes.FlashError("Could not determine PMC for package")
+ if not package.release or not package.release.committee:
+ raise routes.FlashError("Could not determine committee for package")
if package.signature_sha3 is None:
raise routes.FlashError("Package has no signature")
@@ -569,7 +571,7 @@ async def task_verification_create(data: db.Session,
package: models.Package) ->
status=models.TaskStatus.QUEUED,
task_type="verify_signature",
task_args=[
- package.release.pmc.name,
+ package.release.committee.name,
"releases/" + package.artifact_sha3,
"releases/" + package.signature_sha3,
],
diff --git a/atr/routes/projects.py b/atr/routes/projects.py
index 96176bc..be98a24 100644
--- a/atr/routes/projects.py
+++ b/atr/routes/projects.py
@@ -27,7 +27,6 @@ import asfquart.base as base
import asfquart.session as session
import atr.db as db
import atr.db.models as models
-import atr.db.service as service
import atr.routes as routes
import atr.util as util
@@ -56,10 +55,13 @@ async def add_voting_policy(session: session.ClientSession,
form: CreateVotePoli
async with db.session() as data:
async with data.begin():
- pmc = await
data.committee(name=name).demand(base.ASFQuartException("PMC not found",
errorcode=404))
- if pmc.name not in session.committees:
+ committee = await data.committee(name=name).demand(
+ base.ASFQuartException("Committee not found", errorcode=404)
+ )
+ if committee.name not in session.committees:
raise base.ASFQuartException(
- f"You must be a PMC member of {pmc.display_name} to submit
a voting policy", errorcode=403
+ f"You must be a committee member of
{committee.display_name} to submit a voting policy",
+ errorcode=403,
)
vote_policy = models.VotePolicy(
@@ -72,24 +74,24 @@ async def add_voting_policy(session: session.ClientSession,
form: CreateVotePoli
data.add(vote_policy)
# Redirect to the add package page with the storage token
- return quart.redirect(quart.url_for("root_project_view",
project_name=name))
+ return quart.redirect(quart.url_for("root_project_view", name=name))
@routes.app_route("/projects")
async def root_project_directory() -> str:
"""Main project directory page."""
- async with db.create_async_db_session() as session:
- projects = await service.get_pmcs(session)
+ async with db.session() as data:
+ projects = await data.project(_committee=True).all()
return await quart.render_template("project-directory.html",
projects=projects)
@routes.app_route("/projects/<name>")
async def root_project_view(name: str) -> str:
async with db.session() as data:
- pmc = await data.committee(name=name, _public_signing_keys=True,
_vote_policy=True).demand(
+ project = await data.project(name=name,
_committee_public_signing_keys=True, _vote_policy=True).demand(
http.client.HTTPException(404)
)
- return await quart.render_template("project-view.html", project=pmc,
algorithms=routes.algorithms)
+ return await quart.render_template("project-view.html",
project=project, algorithms=routes.algorithms)
@routes.app_route("/projects/<name>/voting/create", methods=["GET", "POST"])
@@ -99,13 +101,17 @@ async def root_project_voting_policy_add(name: str) ->
response.Response | str:
raise base.ASFQuartException("Not authenticated", errorcode=401)
async with db.session() as data:
- pmc = await
data.committee(name=name).demand(base.ASFQuartException("PMC not found",
errorcode=404))
- if pmc.name not in web_session.committees:
+ project = await data.project(name=name, _committee=True).demand(
+ base.ASFQuartException("Project not found", errorcode=404)
+ )
+ if project.committee is None:
+ raise base.ASFQuartException("Project is not associated with a
committee", errorcode=404)
+ if project.committee.name not in web_session.committees:
raise base.ASFQuartException(
- f"You must be a PMC member of {pmc.display_name} to submit a
voting policy", errorcode=403
+ f"You must be a committee member of {project.display_name} to
submit a voting policy", errorcode=403
)
- form = await CreateVotePolicyForm.create_form(data={"project_name":
pmc.name})
+ form = await CreateVotePolicyForm.create_form(data={"project_name":
project.name})
if await form.validate_on_submit():
return await add_voting_policy(web_session, form)
@@ -114,6 +120,6 @@ async def root_project_voting_policy_add(name: str) ->
response.Response | str:
return await quart.render_template(
"vote-policy-add.html",
asf_id=web_session.uid,
- project=pmc,
+ project=project,
form=form,
)
diff --git a/atr/routes/release.py b/atr/routes/release.py
index 03dd977..bb0f9fa 100644
--- a/atr/routes/release.py
+++ b/atr/routes/release.py
@@ -43,13 +43,15 @@ if asfquart.APP is ...:
async def release_delete_validate(data: db.Session, release_key: str,
session_uid: str) -> models.Release:
"""Validate release deletion request and return the release if valid."""
- # if Release.pmc is None:
- # raise FlashError("Release has no associated PMC")
- release = await data.release(storage_key=release_key,
_pmc=True).demand(routes.FlashError("Release not found"))
+ release = await data.release(storage_key=release_key,
_committee=True).demand(
+ routes.FlashError("Release not found")
+ )
# Check permissions
- if release.pmc:
- if (session_uid not in release.pmc.pmc_members) and (session_uid not
in release.pmc.committers):
+ if release.committee:
+ if (session_uid not in release.committee.committee_members) and (
+ session_uid not in release.committee.committers
+ ):
raise routes.FlashError("You don't have permission to delete this
release")
return release
@@ -126,12 +128,14 @@ async def release_bulk_status(task_id: int) -> str |
response.Response:
# Debug print the task.task_args using the logger
logging.debug(f"Task args: {task.task_args}")
if task.task_args and isinstance(task.task_args, dict) and
("release_key" in task.task_args):
- release = await
data.release(storage_key=task.task_args["release_key"], _pmc=True).get()
+ release = await
data.release(storage_key=task.task_args["release_key"], _committee=True).get()
# Check whether the user has permission to view this task
# Either they're a PMC member or committer for the release's PMC
- if release and release.pmc:
- if (web_session.uid not in release.pmc.pmc_members) and
(web_session.uid not in release.pmc.committers):
+ if release and release.committee:
+ if (web_session.uid not in
release.committee.committee_members) and (
+ web_session.uid not in release.committee.committers
+ ):
await quart.flash("You don't have permission to view this
task.", "error")
return
quart.redirect(quart.url_for("root_candidate_review"))
@@ -170,11 +174,11 @@ async def root_release_vote() -> response.Response | str:
# These fields are just for testing, we'll do something better in the
real UI
gpg_key_id = form.get("gpg_key_id", "")
commit_hash = form.get("commit_hash", "")
- if release.pmc is None:
- raise base.ASFQuartException("Release has no associated PMC",
errorcode=400)
+ if release.committee is None:
+ raise base.ASFQuartException("Release has no associated
committee", errorcode=400)
# Prepare email recipient
- email_to = f"{mailing_list}@{release.pmc.name}.apache.org"
+ email_to = f"{mailing_list}@{release.committee.name}.apache.org"
# Create a task for vote initiation
task = models.Task(
@@ -214,26 +218,26 @@ def generate_vote_email_preview(release: models.Release)
-> str:
version = release.version
# Get PMC details
- if release.pmc is None:
- raise base.ASFQuartException("Release has no associated PMC",
errorcode=400)
- pmc_name = release.pmc.name
- pmc_display = release.pmc.display_name
+ if release.committee is None:
+ raise base.ASFQuartException("Release has no associated committee",
errorcode=400)
+ committee_name = release.committee.name
+ committee_display = release.committee.display_name
- # Get product information
- product_name = release.product.product_name if release.product else
"Unknown"
+ # Get project information
+ project_name = release.project.name if release.project else "Unknown"
# Create email subject
- subject = f"[VOTE] Release Apache {pmc_display} {product_name} {version}"
+ subject = f"[VOTE] Release Apache {committee_display} {project_name}
{version}"
# Create email body
- body = f"""Hello {pmc_name},
+ body = f"""Hello {committee_name},
I'd like to call a vote on releasing the following artifacts as
-Apache {pmc_display} {product_name} {version}.
+Apache {committee_display} {project_name} {version}.
The release candidate can be found at:
-https://apache.example.org/{pmc_name}/{product_name}-{version}/
+https://apache.example.org/{committee_name}/{project_name}-{version}/
The release artifacts are signed with my GPG key, [KEY_ID].
diff --git a/atr/static/css/atr.css b/atr/static/css/atr.css
index 8615229..59a2373 100644
--- a/atr/static/css/atr.css
+++ b/atr/static/css/atr.css
@@ -44,7 +44,8 @@ input, textarea, button, select, option {
label[for] {
font-weight: 450;
- border-bottom: 1px dashed #d1d2d3;
+
+ /* border-bottom: 1px dashed #d1d2d3; */
cursor: pointer;
}
@@ -163,6 +164,10 @@ textarea {
min-height: 200px;
}
+summary {
+ cursor: pointer;
+}
+
form.striking {
background-color: #ffffee;
border: 2px solid #ddddbb;
diff --git a/atr/static/css/root.css b/atr/static/css/root.css
deleted file mode 100644
index ada4c11..0000000
--- a/atr/static/css/root.css
+++ /dev/null
@@ -1,58 +0,0 @@
-body {
- font-family: sans-serif;
- margin: 2em;
- line-height: 1.6;
-}
-
-h1 {
- color: #003366;
-}
-
-.pmc-list {
- list-style: none;
- padding: 0;
- max-width: 1200px;
-}
-
-.pmc-item {
- padding: 1.5em;
- margin: 0.75em 0;
- border: 1px solid #dddddd;
- border-radius: 8px;
- background: #ffffff;
- box-shadow: 0 2px 4px rgb(0 0 0 / 5%);
- transition: transform 0.2s ease, box-shadow 0.2s ease;
-}
-
-.pmc-item:hover {
- transform: translateY(-2px);
- box-shadow: 0 4px 8px rgb(0 0 0 / 10%);
-}
-
-.pmc-name {
- font-weight: bold;
- font-size: 1.2em;
- color: #003366;
- margin-bottom: 0.5em;
-}
-
-.pmc-stats {
- color: #666666;
- display: flex;
- gap: 1.5em;
- flex-wrap: wrap;
-}
-
-.stat-item {
- display: flex;
- gap: 0.5em;
-}
-
-.intro {
- max-width: 800px;
- color: #555555;
-}
-
-.ribbon {
- background: linear-gradient(90deg, #282661 0%, #662f8f 20%, #9e2165 40%,
#cb2138 60%, #ea7826 80%, #f7ae18 100%);
-}
diff --git a/atr/tasks/mailtest.py b/atr/tasks/mailtest.py
index 8e298bd..49ac8d1 100644
--- a/atr/tasks/mailtest.py
+++ b/atr/tasks/mailtest.py
@@ -103,7 +103,7 @@ def send_core(args_list: list[str]) -> tuple[task.Status,
str | None, tuple[Any,
import asyncio
import atr.mail
- from atr.db.service import get_pmc_by_name
+ from atr.db.service import get_committee_by_name
_LOGGER.info("Starting send_core")
try:
@@ -136,10 +136,10 @@ def send_core(args_list: list[str]) -> tuple[task.Status,
str | None, tuple[Any,
# Must be a PMC member of tooling
# Since get_pmc_by_name is async, we need to run it in an event
loop
# TODO: We could make a sync version
- tooling_pmc = asyncio.run(get_pmc_by_name("tooling"))
+ tooling_committee = asyncio.run(get_committee_by_name("tooling"))
- if not tooling_pmc:
- error_msg = "Tooling PMC not found in database"
+ if not tooling_committee:
+ error_msg = "Tooling committee not found in database"
_LOGGER.error(error_msg)
return task.FAILED, error_msg, tuple()
@@ -148,12 +148,12 @@ def send_core(args_list: list[str]) -> tuple[task.Status,
str | None, tuple[Any,
_LOGGER.error(error_msg)
return task.FAILED, error_msg, tuple()
- if local_part not in tooling_pmc.pmc_members:
- error_msg = f"Email recipient {local_part} is not a member of
the tooling PMC"
+ if local_part not in tooling_committee.committee_members:
+ error_msg = f"Email recipient {local_part} is not a member of
the tooling committee"
_LOGGER.error(error_msg)
return task.FAILED, error_msg, tuple()
- _LOGGER.info(f"Recipient {email_recipient} is a tooling PMC
member, allowed")
+ _LOGGER.info(f"Recipient {email_recipient} is a tooling committee
member, allowed")
# Load and set DKIM key
try:
diff --git a/atr/tasks/signature.py b/atr/tasks/signature.py
index 1c44415..15d5c3e 100644
--- a/atr/tasks/signature.py
+++ b/atr/tasks/signature.py
@@ -41,15 +41,20 @@ def check(args: list[str]) -> tuple[task.Status, str |
None, tuple[Any, ...]]:
return status, error, task_results
-def _check_core(pmc_name: str, artifact_path: str, signature_path: str) ->
dict[str, Any]:
- """Verify a signature file using the PMC's public signing keys."""
- # Query only the signing keys associated with this PMC
+def _check_core(committee_name: str, artifact_path: str, signature_path: str)
-> dict[str, Any]:
+ """Verify a signature file using the committee's public signing keys."""
+ # Query only the signing keys associated with this committee
# TODO: Rename create_sync_db_session to create_session_sync
# Using isinstance does not work here, with pyright
- name = db.validate_instrumented_attribute(models.PMC.name)
+ name = db.validate_instrumented_attribute(models.Committee.name)
with db.create_sync_db_session() as session:
# TODO: This is our only remaining use of select
- statement =
sql.select(models.PublicSigningKey).join(models.PMCKeyLink).join(models.PMC).where(name
== pmc_name)
+ statement = (
+ sql.select(models.PublicSigningKey)
+ .join(models.KeyLink)
+ .join(models.Committee)
+ .where(name == committee_name)
+ )
result = session.execute(statement)
public_keys = [key.ascii_armored_key for key in result.scalars().all()]
@@ -94,7 +99,7 @@ def _signature_gpg_file(sig_file: BinaryIO, artifact_path:
str, ascii_armored_ke
"trust_level": verified.trust_level if hasattr(verified,
"trust_level") else "Not available",
"trust_text": verified.trust_text if hasattr(verified, "trust_text")
else "Not available",
"stderr": verified.stderr if hasattr(verified, "stderr") else "Not
available",
- "num_pmc_keys": len(ascii_armored_keys),
+ "num_committee_keys": len(ascii_armored_keys),
}
if not verified:
diff --git a/atr/tasks/vote.py b/atr/tasks/vote.py
index 866d375..c449628 100644
--- a/atr/tasks/vote.py
+++ b/atr/tasks/vote.py
@@ -170,29 +170,29 @@ def initiate_core(args_list: list[str]) ->
tuple[task.Status, str | None, tuple[
_LOGGER.error(error_msg)
return task.FAILED, error_msg, tuple()
- # Get PMC and product details
- if release.pmc is None:
- error_msg = "Release has no associated PMC"
+ # Get PMC and project details
+ if release.committee is None:
+ error_msg = "Release has no associated committee"
_LOGGER.error(error_msg)
return task.FAILED, error_msg, tuple()
- pmc_name = release.pmc.name
- pmc_display = release.pmc.display_name
- product_name = release.product.product_name if release.product else
"Unknown"
+ committee_name = release.committee.name
+ committee_display = release.committee.display_name
+ project_name = release.project.name if release.project else "Unknown"
version = release.version
# Create email subject
- subject = f"[VOTE] Release Apache {pmc_display} {product_name}
{version}"
+ subject = f"[VOTE] Release Apache {committee_display} {project_name}
{version}"
# Create email body with initiator ID
- body = f"""Hello {pmc_name},
+ body = f"""Hello {committee_name},
I'd like to call a vote on releasing the following artifacts as
-Apache {pmc_display} {product_name} {version}.
+Apache {committee_display} {project_name} {version}.
The release candidate can be found at:
-https://apache.example.org/{pmc_name}/{product_name}-{version}/
+https://apache.example.org/{committee_name}/{project_name}-{version}/
The release artifacts are signed with my GPG key, {gpg_key_id}.
diff --git a/atr/templates/candidate-create.html
b/atr/templates/candidate-create.html
index 1861070..106d30c 100644
--- a/atr/templates/candidate-create.html
+++ b/atr/templates/candidate-create.html
@@ -24,19 +24,19 @@
enctype="multipart/form-data"
class="striking py-4 px-5">
<div class="mb-3 pb-3 row border-bottom">
- <label for="project_name" class="col-sm-3 col-form-label
text-sm-end">Project:</label>
+ <label for="committee_name" class="col-sm-3 col-form-label
text-sm-end">Committee:</label>
<div class="col-sm-8">
- <select id="project_name"
- name="project_name"
+ <select id="committee_name"
+ name="committee_name"
class="mb-2 form-select"
required>
- <option value="">Select a project...</option>
- {% for pmc in user_pmcs|sort(attribute='name') %}
- <option value="{{ pmc.name }}">{{ pmc.display_name }}</option>
+ <option value="">Select a committee...</option>
+ {% for committee in user_committees|sort(attribute='name') %}
+ <option value="{{ committee.name }}">{{ committee.display_name
}}</option>
{% endfor %}
</select>
- {% if not user_pmcs %}
- <p class="text-danger">You must be a PMC member or committer to
submit a release candidate.</p>
+ {% if not user_committees %}
+ <p class="text-danger">You must be a (P)PMC member or committer to
submit a release candidate.</p>
{% endif %}
</div>
</div>
@@ -49,14 +49,16 @@
</div>
<div class="mb-3 pb-3 row border-bottom">
- <label for="product_name" class="col-sm-3 col-form-label
text-sm-end">Product name:</label>
+ <label for="project_name" class="col-sm-3 col-form-label
text-sm-end">Project name:</label>
<div class="col-sm-8">
- <!-- TODO: Add a dropdown for the product name, plus "add new product"
-->
+ <!-- TODO: Add a dropdown for the project name, plus "add new project"
-->
<input type="text"
- id="product_name"
- name="product_name"
+ id="project_name"
+ name="project_name"
class="form-control"
required />
+ <!-- TODO: Add a subproject checkbox -->
+ <small class="text-muted">This can be the same as the committee name,
but may be different if e.g. this is a subproject.</small>
</div>
</div>
@@ -64,7 +66,7 @@
<div class="col-sm-9 offset-sm-3">
<button type="submit"
class="btn btn-primary mt-2"
- {% if not user_pmcs %}disabled{% endif %}>Create
release</button>
+ {% if not user_committees %}disabled{% endif %}>Create
release</button>
</div>
</div>
</form>
diff --git a/atr/templates/candidate-review.html
b/atr/templates/candidate-review.html
index fe08b65..9004b61 100644
--- a/atr/templates/candidate-review.html
+++ b/atr/templates/candidate-review.html
@@ -32,12 +32,12 @@
{% for release in releases %}
<div class="card mb-3 bg-light">
<div class="card-body">
- <h3 class="card-title mb-2">{{ release.pmc.display_name }}</h3>
+ <h3 class="card-title mb-2">{{ release.committee.display_name }}</h3>
<div class="d-flex flex-wrap gap-3 pb-3 mb-2 border-bottom
candidate-meta text-secondary fs-6">
<span class="candidate-meta-item">Version: {{ release.version
}}</span>
<span class="candidate-meta-item">Stage: {{ release.stage.value
}}</span>
<span class="candidate-meta-item">Phase: {{ release.phase.value
}}</span>
- <span class="candidate-meta-item">Product: {{
release.product.product_name if release.product else "unknown" }}</span>
+ <span class="candidate-meta-item">Project: {{ release.project.name
if release.project else "unknown" }}</span>
<span class="candidate-meta-item">Created: {{
release.created.strftime("%Y-%m-%d %H:%M UTC") }}</span>
</div>
<div class="d-flex gap-3 align-items-center pt-2">
@@ -64,7 +64,7 @@
<tr>
<th>Name</th>
<td>
- {{ format_artifact_name(release.pmc.name,
release.product.product_name if release.product else "unknown",
release.version, release.pmc.is_podling) }}
+ {{ format_artifact_name(release.project.name if release.project
else "unknown", release.version, release.committee.is_podling) }}
</td>
</tr>
<tr>
diff --git a/atr/templates/keys-add.html b/atr/templates/keys-add.html
index e2f2495..d035e93 100644
--- a/atr/templates/keys-add.html
+++ b/atr/templates/keys-add.html
@@ -68,20 +68,20 @@
</small>
</div>
- {% if user_pmcs %}
+ {% if user_committees %}
<div class="mb-4">
<div class="mb-3">
<label class="form-label">Associate with projects:</label>
</div>
<div class="d-flex flex-wrap gap-3 mb-2">
- {% for pmc in user_pmcs|sort(attribute='name') %}
+ {% for committee in user_committees|sort(attribute='name') %}
<div class="form-check d-flex align-items-center gap-2">
<input type="checkbox"
class="form-check-input"
- id="pmc_{{ pmc.name }}"
- name="selected_pmcs"
- value="{{ pmc.name }}" />
- <label class="form-check-label mb-0" for="pmc_{{ pmc.name }}">{{
pmc.display_name }}</label>
+ id="committee_{{ committee.name }}"
+ name="selected_committees"
+ value="{{ committee.name }}" />
+ <label class="form-check-label mb-0" for="committee_{{
committee.name }}">{{ committee.display_name }}</label>
</div>
{% endfor %}
</div>
diff --git a/atr/templates/keys-review.html b/atr/templates/keys-review.html
index a7a5b7a..9196611 100644
--- a/atr/templates/keys-review.html
+++ b/atr/templates/keys-review.html
@@ -67,8 +67,8 @@
<tr>
<th class="p-2 text-dark">Associated projects</th>
<td class="text-break">
- {% if key.pmcs %}
- {{ key.pmcs|map(attribute='name') |join(', ') }}
+ {% if key.committees %}
+ {{ key.committees|map(attribute='name') |join(', ') }}
{% else %}
No projects associated
{% endif %}
diff --git a/atr/templates/package-add.html b/atr/templates/package-add.html
index 1a76ce3..8745dec 100644
--- a/atr/templates/package-add.html
+++ b/atr/templates/package-add.html
@@ -29,7 +29,7 @@
{% for release in releases %}
<option value="{{ release.storage_key }}"
{% if release.storage_key == selected_release
%}selected{% endif %}>
- {{ release.pmc.display_name }} - {{
release.product.product_name if release.product else "unknown" }} - {{
release.version }}
+ {{ release.committee.display_name }} - {{
release.project.name if release.project else "unknown" }} - {{ release.version
}}
</option>
{% endfor %}
</select>
@@ -155,7 +155,7 @@
{% for release in releases %}
<option value="{{ release.storage_key }}"
{% if release.storage_key == selected_release
%}selected{% endif %}>
- {{ release.pmc.display_name }} - {{
release.product.product_name if release.product else "unknown" }} - {{
release.version }}
+ {{ release.committee.display_name }} - {{
release.project.name if release.project else "unknown" }} - {{ release.version
}}
</option>
{% endfor %}
</select>
diff --git a/atr/templates/project-directory.html
b/atr/templates/project-directory.html
index 903940c..cd134dc 100644
--- a/atr/templates/project-directory.html
+++ b/atr/templates/project-directory.html
@@ -14,24 +14,24 @@
<div class="mb-3">
<input type="text"
- id="pmc-filter"
+ id="project-filter"
class="form-control d-inline-block w-auto" />
<button type="button" class="btn btn-primary"
id="filter-button">Filter</button>
</div>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
- {% for pmc in projects %}
+ {% for project in projects %}
<div class="col">
<div class="card h-100 shadow-sm cursor-pointer hover-lift
project-card"
- data-project-url="{{ url_for('root_project_view', name=pmc.name)
}}">
+ data-project-url="{{ url_for('root_project_view',
name=project.name) }}">
<div class="card-body">
- <h2 class="card-title fs-4 mb-3">{{ pmc.display_name }}</h2>
+ <h2 class="card-title fs-4 mb-3">{{ project.display_name }}</h2>
<div class="row g-3">
<div class="col-4">
<div class="card h-100 bg-light border-0">
<div class="card-body p-2 d-flex flex-column
justify-content-between text-center">
- <small class="text-secondary">PMC Members</small>
- <span class="fs-4 fw-medium mt-2">{{
pmc.pmc_members|length }}</span>
+ <small class="text-secondary">Project committee
members</small>
+ <span class="fs-4 fw-medium mt-2">{{
project.committee.committee_members|length }}</span>
</div>
</div>
</div>
@@ -39,7 +39,7 @@
<div class="card h-100 bg-light border-0">
<div class="card-body p-2 d-flex flex-column
justify-content-between text-center">
<small class="text-secondary">Committers</small>
- <span class="fs-4 fw-medium mt-2">{{ pmc.committers|length
}}</span>
+ <span class="fs-4 fw-medium mt-2">{{
project.committers|length }}</span>
</div>
</div>
</div>
@@ -47,7 +47,7 @@
<div class="card h-100 bg-light border-0">
<div class="card-body p-2 d-flex flex-column
justify-content-between text-center">
<small class="text-secondary">Release Managers</small>
- <span class="fs-4 fw-medium mt-2">{{
pmc.release_managers|length }}</span>
+ <span class="fs-4 fw-medium mt-2">{{
project.release_managers|length }}</span>
</div>
</div>
</div>
@@ -63,22 +63,22 @@
{{ super() }}
<script>
function filter() {
- const pmcFilter = document.getElementById("pmc-filter").value;
+ const projectFilter =
document.getElementById("project-filter").value;
const cards = document.querySelectorAll(".project-card");
for (let card of cards) {
const nameElement = card.querySelector(".card-title");
const name = nameElement.innerHTML;
- if (!pmcFilter) {
+ if (!projectFilter) {
card.parentElement.hidden = false;
} else {
- card.parentElement.hidden = !name.match(new
RegExp(pmcFilter, 'i'));
+ card.parentElement.hidden = !name.match(new
RegExp(projectFilter, 'i'));
}
}
}
// Add event listeners
document.getElementById("filter-button").addEventListener("click",
filter);
- document.getElementById("pmc-filter").addEventListener("keydown",
function(event) {
+ document.getElementById("project-filter").addEventListener("keydown",
function(event) {
if (event.key === "Enter") {
filter();
event.preventDefault();
diff --git a/atr/templates/project-view.html b/atr/templates/project-view.html
index 9945c52..d773aea 100644
--- a/atr/templates/project-view.html
+++ b/atr/templates/project-view.html
@@ -17,7 +17,7 @@
</div>
<div class="card-body">
<div class="d-flex flex-wrap gap-3 small mb-1">
- <span>PMC: {{ project.pmc_members|length }}</span>
+ <span>Committee members: {{ project.committee.committee_members|length
}}</span>
<span class="d-flex align-items-center">
<span>Committers: {{ project.committers|length }}</span>
</span>
@@ -90,7 +90,6 @@
<div class="card-header bg-light d-flex justify-content-between
align-items-center">
<h3 class="mb-0">Voting Policy</h3>
<div>
- <!-- TODO: This is a PMC, not a project -->
<a class="btn btn-primary"
href="{{ url_for('root_project_voting_policy_add',
name=project.name) }}"><i class="fa-solid fa-plus"></i></a>
</div>
@@ -114,12 +113,14 @@
</div>
</div>
+ <!-- TODO: Subprojects?
<div class="card mb-4">
<div class="card-header bg-light">
- <h3 class="mb-0">Product Lines</h3>
+ <h3 class="mb-0">Projects</h3>
</div>
<div class="card-body"></div>
</div>
+ -->
{% endblock content %}
diff --git a/atr/templates/release-bulk.html b/atr/templates/release-bulk.html
index c77d858..850be68 100644
--- a/atr/templates/release-bulk.html
+++ b/atr/templates/release-bulk.html
@@ -23,8 +23,8 @@
class="text-decoration-none">Release candidates</a>
</li>
{% if release %}
- <li class="breadcrumb-item">{{ release.pmc.display_name }}</li>
- <li class="breadcrumb-item">{{ release.product.product_name if
release.product else "Unknown product" }}</li>
+ <li class="breadcrumb-item">{{ release.committee.display_name }}</li>
+ <li class="breadcrumb-item">{{ release.project.name if
release.project else "Unknown project" }}</li>
<li class="breadcrumb-item">{{ release.version }}</li>
{% endif %}
<li class="breadcrumb-item active">Bulk download status</li>
diff --git a/atr/templates/release-vote.html b/atr/templates/release-vote.html
index 0ea117e..1acb71c 100644
--- a/atr/templates/release-vote.html
+++ b/atr/templates/release-vote.html
@@ -14,7 +14,7 @@
<div class="px-3 pb-4 mb-4 bg-light border rounded">
<h2 class="mt-4 mb-3 fs-5 border-0">
- {{ release.pmc.display_name }} - {{ release.product.product_name if
release.product else "Unknown" }} {{ release.version }}
+ {{ release.committee.display_name }} - {{ release.project.name if
release.project else "Unknown" }} {{ release.version }}
</h2>
<p class="mb-0">
Initiating a vote for this release candidate will prepare an email to
be sent to the appropriate mailing list.
@@ -44,7 +44,7 @@
name="mailing_list"
value="dev"
checked />
- <label class="form-check-label" for="list_dev">dev@{{
release.pmc.name }}.apache.org</label>
+ <label class="form-check-label" for="list_dev">dev@{{
release.committee.name }}.apache.org</label>
</div>
<div class="form-check">
<input type="radio"
@@ -52,7 +52,7 @@
id="list_private"
name="mailing_list"
value="private" />
- <label class="form-check-label" for="list_private">private@{{
release.pmc.name }}.apache.org</label>
+ <label class="form-check-label" for="list_private">private@{{
release.committee.name }}.apache.org</label>
</div>
</div>
</div>
diff --git a/docs/plan.html b/docs/plan.html
index c6c447f..4c15092 100644
--- a/docs/plan.html
+++ b/docs/plan.html
@@ -24,7 +24,7 @@
<li>[DONE] Add file size and upload timestamp</li>
<li>[DONE] Improve the layout of file listings</li>
<li>[DONE] Show KB, MB, or GB units for file sizes</li>
-<li>[DONE] Add a standard artifact naming pattern based on the project and
product</li>
+<li>[DONE] Add a standard artifact naming pattern based on the committee and
project</li>
<li>[DONE] Potentially add the option to upload package artifacts without
signatures</li>
<li>[DONE] Show validation status indicators</li>
<li>[DONE] Add developer RC download buttons with clear verification
instructions</li>
diff --git a/docs/plan.md b/docs/plan.md
index 88ca3f4..91c8d46 100644
--- a/docs/plan.md
+++ b/docs/plan.md
@@ -21,7 +21,7 @@ This is a rough plan of immediate tasks. The priority of
these tasks may change,
- [DONE] Add file size and upload timestamp
- [DONE] Improve the layout of file listings
- [DONE] Show KB, MB, or GB units for file sizes
- - [DONE] Add a standard artifact naming pattern based on the project and
product
+ - [DONE] Add a standard artifact naming pattern based on the committee and
project
- [DONE] Potentially add the option to upload package artifacts without
signatures
- [DONE] Show validation status indicators
- [DONE] Add developer RC download buttons with clear verification
instructions
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]