Skip to content

Commit ab79dd1

Browse files
committed
Fixes #2011
1 parent 57bad1f commit ab79dd1

File tree

2 files changed

+37
-17
lines changed

2 files changed

+37
-17
lines changed

sonar/portfolios.py

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from __future__ import annotations
2727

2828
import re
29-
from typing import Optional
29+
from typing import Optional, Union, Any
3030
import json
3131
from http import HTTPStatus
3232
from threading import Lock
@@ -37,6 +37,9 @@
3737
import sonar.util.constants as c
3838

3939
from sonar import aggregations, exceptions, applications, app_branches
40+
from sonar.projects import Project
41+
from sonar.branches import Branch
42+
4043
import sonar.permissions.permissions as perms
4144
import sonar.permissions.portfolio_permissions as pperms
4245
import sonar.sqobject as sq
@@ -100,17 +103,17 @@ class Portfolio(aggregations.Aggregation):
100103
def __init__(self, endpoint: pf.Platform, key: str, name: Optional[str] = None) -> None:
101104
"""Constructor, don't use - use class methods instead"""
102105
super().__init__(endpoint=endpoint, key=key)
103-
self.name = name if name is not None else key
104-
self._selection_mode = {_SELECTION_MODE_NONE: True} #: Portfolio project selection mode
105-
self._tags = [] #: Portfolio tags when selection mode is TAGS
106+
self.name: str = name if name is not None else key
107+
self._selection_mode: dict[str, Any] = {_SELECTION_MODE_NONE: True} #: Portfolio project selection mode
108+
self._tags: list[str] = [] #: Portfolio tags when selection mode is TAGS
106109
self._description: Optional[str] = None #: Portfolio description
107110
self._visibility: Optional[str] = None #: Portfolio visibility
108-
self._applications = {} #: applications
109-
self._permissions: Optional[object] = None #: Permissions
110-
111+
self._applications: dict[str, Any] = {} #: applications
112+
self._permissions: Optional[dict[str, list[str]]] = None #: Permissions
113+
self._projects: Optional[dict[str, set[str]]] = None #: Projects and branches in portfolio
111114
self.parent_portfolio: Optional[Portfolio] = None #: Ref to parent portfolio object, if any
112115
self.root_portfolio: Optional[Portfolio] = None #: Ref to root portfolio, if any
113-
self._sub_portfolios = {} #: Subportfolios
116+
self._sub_portfolios: dict[str, Portfolio] = {} #: Subportfolios
114117
Portfolio.CACHE.put(self)
115118
log.debug("Created portfolio object name '%s'", name)
116119

@@ -168,15 +171,15 @@ def __str__(self) -> str:
168171
"""Returns string representation of object"""
169172
return (
170173
f"subportfolio '{self.key}'"
171-
if self.sq_json and self.sq_json.get("qualifier", _PORTFOLIO_QUALIFIER) == _SUBPORTFOLIO_QUALIFIER
174+
if self.sq_json.get("qualifier", _PORTFOLIO_QUALIFIER) == _SUBPORTFOLIO_QUALIFIER
172175
else f"portfolio '{self.key}'"
173176
)
174177

175178
def reload(self, data: types.ApiPayload) -> None:
176179
"""Reloads a portfolio with returned API data"""
177180
super().reload(data)
178181
if "originalKey" not in data and data["qualifier"] == _PORTFOLIO_QUALIFIER:
179-
self.parent_portfolio: Optional[object] = None
182+
self.parent_portfolio = None
180183
self.root_portfolio = self
181184
self.load_selection_mode()
182185
self.reload_sub_portfolios()
@@ -226,12 +229,25 @@ def url(self) -> str:
226229
return f"{self.base_url(local=False)}/portfolio?id={self.key}"
227230

228231
def projects(self) -> Optional[dict[str, str]]:
229-
"""Returns list of projects and their branches if selection mode is manual, None otherwise"""
232+
"""Returns list of projects and their branches if selection mode is manual, or the list of projects for other modes"""
230233
if not self._selection_mode or _SELECTION_MODE_MANUAL not in self._selection_mode:
231-
log.debug("%s: Not manual mode, no projects", str(self))
232-
return None
234+
if self._projects is None:
235+
data = json.loads(self.get("api/views/projects_status", params={"portfolio": self.key}).text)
236+
self._projects = {p["refKey"]: {c.DEFAULT_BRANCH} for p in data["projects"]}
237+
return self._projects
233238
return self._selection_mode[_SELECTION_MODE_MANUAL]
234239

240+
def components(self) -> list[Union[Project, Branch]]:
241+
"""Returns the list of components objects (projects/branches) in the portfolio"""
242+
log.debug("Collecting portfolio components from %s", util.json_dump(self.sq_json))
243+
new_comps = []
244+
for p_key, p_branches in self.projects().items():
245+
proj = Project.get_object(self.endpoint, p_key)
246+
for br in p_branches:
247+
# If br is DEFAULT_BRANCH, next() will find nothing and we'll append the project itself
248+
new_comps.append(next((b for b in proj.branches().values() if b.name == br), proj))
249+
return new_comps
250+
235251
def applications(self) -> Optional[dict[str, str]]:
236252
log.debug("Collecting portfolios applications from %s", util.json_dump(self.sq_json))
237253
if "subViews" not in self.sq_json:
@@ -522,7 +538,7 @@ def add_application_branch(self, app_key: str, branch: str = c.DEFAULT_BRANCH) -
522538
self._applications[app_key].append(branch)
523539
return True
524540

525-
def add_subportfolio(self, key: str, name: Optional[str] = None, by_ref: bool = False) -> Portfolio:
541+
def add_subportfolio(self, key: str, name: str = None, by_ref: bool = False) -> Portfolio:
526542
"""Adds a subportfolio to a portfolio, defined by key, name and by reference option"""
527543

528544
log.info("Adding sub-portfolios to %s", str(self))

sonar/util/component_helper.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import re
2222
from typing import Optional
2323

24+
from sonar.util import constants as c
2425
from sonar import platform, components, projects, applications, portfolios
2526

2627

@@ -32,11 +33,14 @@ def get_components(
3233
if component_type in ("apps", "applications"):
3334
components = [p for p in applications.get_list(endpoint).values() if re.match(rf"^{key_regexp}$", p.key)]
3435
elif component_type == "portfolios":
35-
components = [p for p in portfolios.get_list(endpoint).values() if re.match(rf"^{key_regexp}$", p.key)]
36+
portfolio_list = [p for p in portfolios.get_list(endpoint).values() if re.match(rf"^{key_regexp}$", p.key)]
3637
if kwargs.get("topLevelOnly", False):
37-
components = [p for p in components if p.is_toplevel()]
38+
portfolio_list = [p for p in portfolio_list if p.is_toplevel()]
39+
components = []
40+
for comp in portfolio_list:
41+
components += comp.components()
3842
else:
3943
components = [p for p in projects.get_list(endpoint).values() if re.match(rf"^{key_regexp}$", p.key)]
4044
if component_type != "portfolios" and branch_regexp:
41-
components = [b for c in components for b in c.branches().values() if re.match(rf"^{branch_regexp}$", b.name)]
45+
components = [b for comp in components for b in comp.branches().values() if re.match(rf"^{branch_regexp}$", b.name)]
4246
return components

0 commit comments

Comments
 (0)