Skip to content
Closed
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ac0ca35
feat: turn reviewer of artefact into list of reviewers (support multi…
almeidaraul Feb 24, 2026
1db712c
wip: test fixing
almeidaraul Feb 24, 2026
bed7d7b
chore: update change_assignee keeping backwards compatibility
almeidaraul Feb 25, 2026
8714e79
fix: failing tests; replace assignee with reviewer everywhere
almeidaraul Feb 25, 2026
250acfa
tests: add multiple reviewers to artefacts
almeidaraul Feb 25, 2026
4ee1371
feat: use reviewer list in frontend (for now using only the first rev…
almeidaraul Feb 25, 2026
0446d2b
chore: update API schema
almeidaraul Feb 25, 2026
65c7f93
feat: multiple reviewers for artefacts in frontend
almeidaraul Feb 25, 2026
b8a96c4
tests: single-env artefact gets assigned a single reviewer
almeidaraul Feb 25, 2026
14f10a5
tests: artefact with many envs is assigned more than one reviewer (fa…
almeidaraul Feb 26, 2026
eee11e9
feat: number of assigned reviewers is a function of the number of env…
almeidaraul Feb 27, 2026
3461893
feat: add reviewer list to artefact build environment review
almeidaraul Feb 27, 2026
87d5026
tests: get and patch env reviews includes reviewers
almeidaraul Feb 27, 2026
56fe04c
feat: environment review API supports reviewers
almeidaraul Feb 27, 2026
81a9fc6
feat: show environment reviewers in frontend
almeidaraul Feb 27, 2026
61d3038
tests: environments get reviewers as subset of artefact
almeidaraul Feb 27, 2026
49a7501
feat: environments get reviewers as subset of artefact
almeidaraul Feb 27, 2026
89e8b71
enh: change expected behaviour for env reviewer assignment in small a…
almeidaraul Mar 2, 2026
264bd38
Merge branch 'main' into TO-51/assign-reviewers-to-environments
almeidaraul Mar 17, 2026
46ba1bb
fix: linting, duplicate definition
almeidaraul Mar 17, 2026
e59e36b
fix: count of environments
almeidaraul Mar 17, 2026
2920f37
revert last 3 commits
almeidaraul Mar 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Copyright (C) 2023 Canonical Ltd.
#
# This file is part of Test Observer Backend.
#
# Test Observer Backend is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License version 3, as
# published by the Free Software Foundation.
#
# Test Observer Backend is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

"""Change assignee to reviewers

Revision ID: 3514f071a2e5
Revises: f5f3abf809b3
Create Date: 2026-02-24 17:44:00.000000+00:00

"""

from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = "3514f071a2e5"
down_revision = "f5f3abf809b3"
branch_labels = None
depends_on = None


def upgrade() -> None:
# Create the association table for many-to-many relationship
op.create_table(
"artefact_reviewers_association",
sa.Column("artefact_id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(
["artefact_id"],
["artefact.id"],
name="artefact_reviewers_association_artefact_id_fkey",
ondelete="CASCADE",
),
sa.ForeignKeyConstraint(
["user_id"],
["app_user.id"],
name="artefact_reviewers_association_user_id_fkey",
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint(
"artefact_id", "user_id", name="artefact_reviewers_association_pkey"
),
)

# Migrate existing assignee data to the new association table
op.execute("""
INSERT INTO artefact_reviewers_association (artefact_id, user_id)
SELECT id, assignee_id
FROM artefact
WHERE assignee_id IS NOT NULL
""")

# Drop the old foreign key constraint and column
op.drop_constraint("artefact_assignee_id_fkey", "artefact", type_="foreignkey")
op.drop_index("artefact_assignee_id_ix", table_name="artefact")
op.drop_column("artefact", "assignee_id")


def downgrade() -> None:
# Re-add the assignee_id column
op.add_column(
"artefact",
sa.Column("assignee_id", sa.Integer(), nullable=True),
)
op.create_index("artefact_assignee_id_ix", "artefact", ["assignee_id"])
op.create_foreign_key(
"artefact_assignee_id_fkey",
"artefact",
"app_user",
["assignee_id"],
["id"],
)

# Migrate data back (take the first reviewer as the assignee)
op.execute("""
UPDATE artefact
SET assignee_id = (
SELECT user_id
FROM artefact_reviewers_association
WHERE artefact_reviewers_association.artefact_id = artefact.id
LIMIT 1
)
""")

# Drop the association table
op.drop_table("artefact_reviewers_association")
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Add reviewers to ArtefactBuildEnvironmentReview

Revision ID: 2f7a130290ea
Revises: 3514f071a2e5
Create Date: 2026-02-27 16:21:41.550648+00:00

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '2f7a130290ea'
down_revision = '3514f071a2e5'
branch_labels = None
depends_on = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('environment_review_reviewers_association',
sa.Column('environment_review_id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['environment_review_id'], ['artefact_build_environment_review.id'], name=op.f('environment_review_reviewers_association_environment_review_id_fkey'), ondelete='CASCADE'),
sa.ForeignKeyConstraint(['user_id'], ['app_user.id'], name=op.f('environment_review_reviewers_association_user_id_fkey'), ondelete='CASCADE'),
sa.PrimaryKeyConstraint('environment_review_id', 'user_id', name=op.f('environment_review_reviewers_association_pkey'))
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('environment_review_reviewers_association')
# ### end Alembic commands ###
115 changes: 59 additions & 56 deletions backend/schemata/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -4105,27 +4105,33 @@
],
"title": "Comment"
},
"assignee_id": {
"reviewer_ids": {
"anyOf": [
{
"type": "integer"
"items": {
"type": "integer"
},
"type": "array"
},
{
"type": "null"
}
],
"title": "Assignee Id"
"title": "Reviewer Ids"
},
"assignee_email": {
"reviewer_emails": {
"anyOf": [
{
"type": "string"
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"title": "Assignee Email"
"title": "Reviewer Emails"
}
},
"type": "object",
Expand Down Expand Up @@ -4208,15 +4214,12 @@
"type": "boolean",
"title": "Archived"
},
"assignee": {
"anyOf": [
{
"$ref": "#/components/schemas/AssigneeResponse"
},
{
"type": "null"
}
]
"reviewers": {
"items": {
"$ref": "#/components/schemas/ReviewerResponse"
},
"type": "array",
"title": "Reviewers"
},
"due_date": {
"anyOf": [
Expand Down Expand Up @@ -4269,7 +4272,7 @@
"status",
"comment",
"archived",
"assignee",
"reviewers",
"due_date",
"created_at",
"bug_link",
Expand Down Expand Up @@ -4321,46 +4324,6 @@
],
"title": "ArtefactVersionResponse"
},
"AssigneeResponse": {
"properties": {
"id": {
"type": "integer",
"title": "Id"
},
"launchpad_handle": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Launchpad Handle"
},
"email": {
"type": "string",
"title": "Email"
},
"launchpad_email": {
"type": "string",
"title": "Launchpad Email",
"deprecated": true
},
"name": {
"type": "string",
"title": "Name"
}
},
"type": "object",
"required": [
"id",
"email",
"launchpad_email",
"name"
],
"title": "AssigneeResponse"
},
"C3TestResult": {
"properties": {
"name": {
Expand Down Expand Up @@ -5284,6 +5247,46 @@
"type": "object",
"title": "RerunRequest"
},
"ReviewerResponse": {
"properties": {
"id": {
"type": "integer",
"title": "Id"
},
"launchpad_handle": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Launchpad Handle"
},
"email": {
"type": "string",
"title": "Email"
},
"launchpad_email": {
"type": "string",
"title": "Launchpad Email",
"deprecated": true
},
"name": {
"type": "string",
"title": "Name"
}
},
"type": "object",
"required": [
"id",
"email",
"launchpad_email",
"name"
],
"title": "ReviewerResponse"
},
"SnapStage": {
"type": "string",
"enum": [
Expand Down
64 changes: 36 additions & 28 deletions backend/test_observer/controllers/artefacts/artefacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,43 +181,51 @@ def patch_artefact(
if request.comment is not None:
artefact.comment = request.comment

assignee_id_set = (
hasattr(request, "assignee_id") and "assignee_id" in request.model_fields_set
reviewer_ids_set = (
hasattr(request, "reviewer_ids") and "reviewer_ids" in request.model_fields_set
)
assignee_email_set = (
hasattr(request, "assignee_email")
and "assignee_email" in request.model_fields_set
reviewer_emails_set = (
hasattr(request, "reviewer_emails")
and "reviewer_emails" in request.model_fields_set
)

if assignee_id_set and assignee_email_set:
if reviewer_ids_set and reviewer_emails_set:
raise HTTPException(
status_code=422,
detail="Cannot specify both assignee_id and assignee_email",
detail="Cannot specify both reviewer_ids and reviewer_emails",
)

if assignee_id_set:
if request.assignee_id is None:
artefact.assignee = None
# Handle reviewer_ids
if reviewer_ids_set:
if request.reviewer_ids is None:
artefact.reviewers = []
else:
user = db.get(User, request.assignee_id)
if user is None:
raise HTTPException(
status_code=422,
detail=f"User with id {request.assignee_id} not found",
)
artefact.assignee = user

if assignee_email_set:
if request.assignee_email is None:
artefact.assignee = None
reviewers = []
for user_id in request.reviewer_ids:
user = db.get(User, user_id)
if user is None:
raise HTTPException(
status_code=422,
detail=f"User with id {user_id} not found",
)
reviewers.append(user)
artefact.reviewers = reviewers

# Handle reviewer_emails
if reviewer_emails_set:
if request.reviewer_emails is None:
artefact.reviewers = []
else:
user = db.scalar(select(User).where(User.email == request.assignee_email))
if user is None:
raise HTTPException(
status_code=422,
detail=f"User with email '{request.assignee_email}' not found",
)
artefact.assignee = user
reviewers = []
for email in request.reviewer_emails:
user = db.scalar(select(User).where(User.email == email))
if user is None:
raise HTTPException(
status_code=422,
detail=f"User with email '{email}' not found",
)
reviewers.append(user)
artefact.reviewers = reviewers

db.commit()
return artefact
Expand Down
Loading
Loading