argus-alm 0.14.2__py3-none-any.whl → 0.15.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. argus/_version.py +21 -0
  2. argus/backend/.gitkeep +0 -0
  3. argus/backend/__init__.py +0 -0
  4. argus/backend/cli.py +57 -0
  5. argus/backend/controller/__init__.py +0 -0
  6. argus/backend/controller/admin.py +20 -0
  7. argus/backend/controller/admin_api.py +355 -0
  8. argus/backend/controller/api.py +589 -0
  9. argus/backend/controller/auth.py +67 -0
  10. argus/backend/controller/client_api.py +109 -0
  11. argus/backend/controller/main.py +316 -0
  12. argus/backend/controller/notification_api.py +72 -0
  13. argus/backend/controller/notifications.py +13 -0
  14. argus/backend/controller/planner_api.py +194 -0
  15. argus/backend/controller/team.py +129 -0
  16. argus/backend/controller/team_ui.py +19 -0
  17. argus/backend/controller/testrun_api.py +513 -0
  18. argus/backend/controller/view_api.py +188 -0
  19. argus/backend/controller/views_widgets/__init__.py +0 -0
  20. argus/backend/controller/views_widgets/graphed_stats.py +54 -0
  21. argus/backend/controller/views_widgets/graphs.py +68 -0
  22. argus/backend/controller/views_widgets/highlights.py +135 -0
  23. argus/backend/controller/views_widgets/nemesis_stats.py +26 -0
  24. argus/backend/controller/views_widgets/summary.py +43 -0
  25. argus/backend/db.py +98 -0
  26. argus/backend/error_handlers.py +41 -0
  27. argus/backend/events/event_processors.py +34 -0
  28. argus/backend/models/__init__.py +0 -0
  29. argus/backend/models/argus_ai.py +24 -0
  30. argus/backend/models/github_issue.py +60 -0
  31. argus/backend/models/plan.py +24 -0
  32. argus/backend/models/result.py +187 -0
  33. argus/backend/models/runtime_store.py +58 -0
  34. argus/backend/models/view_widgets.py +25 -0
  35. argus/backend/models/web.py +403 -0
  36. argus/backend/plugins/__init__.py +0 -0
  37. argus/backend/plugins/core.py +248 -0
  38. argus/backend/plugins/driver_matrix_tests/controller.py +66 -0
  39. argus/backend/plugins/driver_matrix_tests/model.py +429 -0
  40. argus/backend/plugins/driver_matrix_tests/plugin.py +21 -0
  41. argus/backend/plugins/driver_matrix_tests/raw_types.py +62 -0
  42. argus/backend/plugins/driver_matrix_tests/service.py +61 -0
  43. argus/backend/plugins/driver_matrix_tests/udt.py +42 -0
  44. argus/backend/plugins/generic/model.py +86 -0
  45. argus/backend/plugins/generic/plugin.py +15 -0
  46. argus/backend/plugins/generic/types.py +14 -0
  47. argus/backend/plugins/loader.py +39 -0
  48. argus/backend/plugins/sct/controller.py +224 -0
  49. argus/backend/plugins/sct/plugin.py +37 -0
  50. argus/backend/plugins/sct/resource_setup.py +177 -0
  51. argus/backend/plugins/sct/service.py +682 -0
  52. argus/backend/plugins/sct/testrun.py +288 -0
  53. argus/backend/plugins/sct/udt.py +100 -0
  54. argus/backend/plugins/sirenada/model.py +118 -0
  55. argus/backend/plugins/sirenada/plugin.py +16 -0
  56. argus/backend/service/admin.py +26 -0
  57. argus/backend/service/argus_service.py +696 -0
  58. argus/backend/service/build_system_monitor.py +185 -0
  59. argus/backend/service/client_service.py +127 -0
  60. argus/backend/service/event_service.py +18 -0
  61. argus/backend/service/github_service.py +233 -0
  62. argus/backend/service/jenkins_service.py +269 -0
  63. argus/backend/service/notification_manager.py +159 -0
  64. argus/backend/service/planner_service.py +608 -0
  65. argus/backend/service/release_manager.py +229 -0
  66. argus/backend/service/results_service.py +690 -0
  67. argus/backend/service/stats.py +610 -0
  68. argus/backend/service/team_manager_service.py +82 -0
  69. argus/backend/service/test_lookup.py +172 -0
  70. argus/backend/service/testrun.py +489 -0
  71. argus/backend/service/user.py +308 -0
  72. argus/backend/service/views.py +219 -0
  73. argus/backend/service/views_widgets/__init__.py +0 -0
  74. argus/backend/service/views_widgets/graphed_stats.py +180 -0
  75. argus/backend/service/views_widgets/highlights.py +374 -0
  76. argus/backend/service/views_widgets/nemesis_stats.py +34 -0
  77. argus/backend/template_filters.py +27 -0
  78. argus/backend/tests/__init__.py +0 -0
  79. argus/backend/tests/client_service/__init__.py +0 -0
  80. argus/backend/tests/client_service/test_submit_results.py +79 -0
  81. argus/backend/tests/conftest.py +180 -0
  82. argus/backend/tests/results_service/__init__.py +0 -0
  83. argus/backend/tests/results_service/test_best_results.py +178 -0
  84. argus/backend/tests/results_service/test_cell.py +65 -0
  85. argus/backend/tests/results_service/test_chartjs_additional_functions.py +259 -0
  86. argus/backend/tests/results_service/test_create_chartjs.py +220 -0
  87. argus/backend/tests/results_service/test_result_metadata.py +100 -0
  88. argus/backend/tests/results_service/test_results_service.py +203 -0
  89. argus/backend/tests/results_service/test_validation_rules.py +213 -0
  90. argus/backend/tests/view_widgets/__init__.py +0 -0
  91. argus/backend/tests/view_widgets/test_highlights_api.py +532 -0
  92. argus/backend/util/common.py +65 -0
  93. argus/backend/util/config.py +38 -0
  94. argus/backend/util/encoders.py +56 -0
  95. argus/backend/util/logsetup.py +80 -0
  96. argus/backend/util/module_loaders.py +30 -0
  97. argus/backend/util/send_email.py +91 -0
  98. argus/client/base.py +1 -3
  99. argus/client/driver_matrix_tests/cli.py +17 -8
  100. argus/client/generic/cli.py +4 -2
  101. argus/client/generic/client.py +1 -0
  102. argus/client/generic_result.py +48 -9
  103. argus/client/sct/client.py +1 -3
  104. argus/client/sirenada/client.py +4 -1
  105. argus/client/tests/__init__.py +0 -0
  106. argus/client/tests/conftest.py +19 -0
  107. argus/client/tests/test_package.py +45 -0
  108. argus/client/tests/test_results.py +224 -0
  109. argus/common/sct_types.py +3 -0
  110. argus/common/sirenada_types.py +1 -1
  111. {argus_alm-0.14.2.dist-info → argus_alm-0.15.2.dist-info}/METADATA +43 -19
  112. argus_alm-0.15.2.dist-info/RECORD +122 -0
  113. {argus_alm-0.14.2.dist-info → argus_alm-0.15.2.dist-info}/WHEEL +2 -1
  114. argus_alm-0.15.2.dist-info/entry_points.txt +3 -0
  115. argus_alm-0.15.2.dist-info/top_level.txt +1 -0
  116. argus_alm-0.14.2.dist-info/RECORD +0 -20
  117. argus_alm-0.14.2.dist-info/entry_points.txt +0 -4
  118. {argus_alm-0.14.2.dist-info → argus_alm-0.15.2.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,308 @@
1
+ from datetime import datetime
2
+ import functools
3
+ import hashlib
4
+ import os
5
+ import base64
6
+ from uuid import UUID
7
+ from time import time
8
+ from hashlib import sha384
9
+
10
+ from flask import current_app, flash, g, redirect, request, session, url_for
11
+ import requests
12
+ from werkzeug.security import generate_password_hash, check_password_hash
13
+
14
+ from argus.backend.db import ScyllaCluster
15
+ from argus.backend.error_handlers import APIException
16
+ from argus.backend.models.web import User, UserOauthToken, UserRoles, WebFileStorage
17
+ from argus.backend.util.common import FlaskView
18
+
19
+
20
+ class UserServiceException(Exception):
21
+ pass
22
+
23
+
24
+ class GithubOrganizationMissingError(Exception):
25
+ pass
26
+
27
+
28
+ class UserService:
29
+ def __init__(self) -> None:
30
+ self.cluster = ScyllaCluster.get()
31
+ self.session = self.cluster.session
32
+
33
+ @staticmethod
34
+ def check_roles(roles: list[UserRoles] | UserRoles, user: User) -> bool:
35
+ if not user:
36
+ return False
37
+ if isinstance(roles, str):
38
+ return roles in user.roles
39
+ elif isinstance(roles, list):
40
+ for role in roles:
41
+ if role in user.roles:
42
+ return True
43
+ return False
44
+
45
+ def github_callback(self, req_code: str) -> dict | None:
46
+ if "gh" not in current_app.config.get("LOGIN_METHODS", []):
47
+ raise UserServiceException("Github Login is disabled")
48
+ oauth_response = requests.post(
49
+ "https://github.com/login/oauth/access_token",
50
+ headers={
51
+ "Accept": "application/json",
52
+ },
53
+ params={
54
+ "code": req_code,
55
+ "client_id": current_app.config.get("GITHUB_CLIENT_ID"),
56
+ "client_secret": current_app.config.get("GITHUB_CLIENT_SECRET"),
57
+ }
58
+ )
59
+
60
+ oauth_data = oauth_response.json()
61
+
62
+ user_info = requests.get(
63
+ "https://api.github.com/user",
64
+ headers={
65
+ "Accept": "application/json",
66
+ "Authorization": f"token {oauth_data.get('access_token')}"
67
+ }
68
+ ).json()
69
+ email_info = requests.get(
70
+ "https://api.github.com/user/emails",
71
+ headers={
72
+ "Accept": "application/json",
73
+ "Authorization": f"token {oauth_data.get('access_token')}"
74
+ }
75
+ ).json()
76
+
77
+ organizations = requests.get(
78
+ "https://api.github.com/user/orgs",
79
+ headers={
80
+ "Accept": "application/json",
81
+ "Authorization": f"token {oauth_data.get('access_token')}"
82
+ }
83
+ ).json()
84
+
85
+ temp_password = None
86
+ required_organizations = current_app.config.get("GITHUB_REQUIRED_ORGANIZATIONS")
87
+ if required_organizations:
88
+ logins = set([org["login"] for org in organizations])
89
+ required_organizations = set(required_organizations)
90
+ if len(logins.intersection(required_organizations)) == 0:
91
+ raise GithubOrganizationMissingError(
92
+ "Not a member of a required organization or missing organization scope")
93
+
94
+ try:
95
+ user = User.get(username=user_info.get("login"))
96
+ except User.DoesNotExist:
97
+ user = User()
98
+ user.username = user_info.get("login")
99
+ # pick only scylladb.com emails
100
+ scylla_email = next(iter([email.get("email")
101
+ for email in email_info if email.get("email").endswith("@scylladb.com")]), None)
102
+ primary_email = next(iter([email.get("email")
103
+ for email in email_info if email.get("primary") and email.get("verified")]), None)
104
+ user.email = scylla_email or primary_email
105
+ user.full_name = user_info.get("name", user_info.get("login"))
106
+ user.registration_date = datetime.utcnow()
107
+ user.roles = ["ROLE_USER"]
108
+ temp_password = base64.encodebytes(
109
+ os.urandom(48)).decode("ascii").strip()
110
+ user.password = generate_password_hash(temp_password)
111
+
112
+ avatar_url: str = user_info.get("avatar_url")
113
+ avatar = requests.get(avatar_url).content
114
+ avatar_name = avatar_url.split("/")[-1]
115
+ filename, filepath = self.save_profile_picture_to_disk(avatar_name, avatar, user.username)
116
+
117
+ web_file = WebFileStorage()
118
+ web_file.filename = filename
119
+ web_file.filepath = filepath
120
+ web_file.save()
121
+ user.picture_id = web_file.id
122
+ user.save()
123
+
124
+ try:
125
+ tokens = list(UserOauthToken.filter(user_id=user.id).all())
126
+ github_token = [
127
+ token for token in tokens if token["kind"] == "github"][0]
128
+ github_token.token = oauth_data.get('access_token')
129
+ github_token.save()
130
+ except (UserOauthToken.DoesNotExist, IndexError):
131
+ github_token = UserOauthToken()
132
+ github_token.kind = "github"
133
+ github_token.user_id = user.id
134
+ github_token.token = oauth_data.get('access_token')
135
+ github_token.save()
136
+
137
+ redirect_target = session.get("redirect_target")
138
+ session.clear()
139
+ session["user_id"] = str(user.id)
140
+ session["redirect_target"] = redirect_target
141
+ if temp_password:
142
+ return {
143
+ "password": temp_password,
144
+ "first_login": True
145
+ }
146
+ return None
147
+
148
+ def get_users(self) -> dict:
149
+ users = User.all()
150
+ return {str(user.id): user.to_json() for user in users}
151
+
152
+ def get_users_privileged(self) -> dict:
153
+ users = User.all()
154
+ users = {str(user.id): dict(user.items()) for user in users}
155
+ for user in users.values():
156
+ user.pop("password")
157
+ user.pop("api_token")
158
+
159
+ return users
160
+
161
+ def generate_token(self, user: User):
162
+ token_digest = f"{user.username}-{int(time())}-{base64.encodebytes(os.urandom(128)).decode(encoding='utf-8')}"
163
+ new_token = base64.encodebytes(sha384(token_digest.encode(encoding="utf-8")
164
+ ).digest()).decode(encoding="utf-8").strip()
165
+ user.api_token = new_token
166
+ user.save()
167
+ return new_token
168
+
169
+ def update_email(self, user: User, new_email: str):
170
+ user.email = new_email
171
+ user.save()
172
+
173
+ return True
174
+
175
+ def toggle_admin(self, user_id: str):
176
+ user: User = User.get(id=user_id)
177
+
178
+ if user.id == g.user.id:
179
+ raise UserServiceException("Cannot toggle admin role from yourself.")
180
+
181
+ is_admin = UserService.check_roles(UserRoles.Admin, user)
182
+
183
+ if is_admin:
184
+ user.roles.remove(UserRoles.Admin)
185
+ else:
186
+ user.set_as_admin()
187
+
188
+ user.save()
189
+ return True
190
+
191
+ def delete_user(self, user_id: str):
192
+ user: User = User.get(id=user_id)
193
+ if user.id == g.user.id:
194
+ raise UserServiceException("Cannot delete user that you are logged in as.")
195
+
196
+ if user.is_admin():
197
+ raise UserServiceException("Cannot delete admin users. Unset admin flag before deleting")
198
+
199
+ user.delete()
200
+
201
+ return True
202
+
203
+ def update_password(self, user: User, old_password: str, new_password: str, force=False):
204
+ if not check_password_hash(user.password, old_password) and not force:
205
+ raise UserServiceException("Incorrect old password")
206
+
207
+ if not new_password:
208
+ raise UserServiceException("Empty new password")
209
+
210
+ if len(new_password) < 5:
211
+ raise UserServiceException("New password is too short")
212
+
213
+ user.password = generate_password_hash(new_password)
214
+ user.save()
215
+
216
+ return True
217
+
218
+ def update_name(self, user: User, new_name: str):
219
+ user.full_name = new_name
220
+ user.save()
221
+
222
+ def save_profile_picture_to_disk(self, original_filename: str, filedata: bytes, suffix: str):
223
+ filename_fragment = hashlib.sha256(os.urandom(64)).hexdigest()[:10]
224
+ filename = f"profile_{suffix}_{filename_fragment}"
225
+ filepath = f"storage/profile_pictures/{filename}"
226
+ with open(filepath, "wb") as file:
227
+ file.write(filedata)
228
+
229
+ return original_filename, filepath
230
+
231
+ def update_profile_picture(self, filename: str, filepath: str):
232
+ web_file = WebFileStorage()
233
+ web_file.filename = filename
234
+ web_file.filepath = filepath
235
+ web_file.save()
236
+
237
+ try:
238
+ if old_picture_id := g.user.picture_id:
239
+ old_file = WebFileStorage.get(id=old_picture_id)
240
+ os.unlink(old_file.filepath)
241
+ old_file.delete()
242
+ except Exception as exc:
243
+ print(exc)
244
+
245
+ g.user.picture_id = web_file.id
246
+ g.user.save()
247
+
248
+
249
+ def login_required(view: FlaskView):
250
+ @functools.wraps(view)
251
+ def wrapped_view(*args, **kwargs):
252
+ if g.user is None and not getattr(view, "api_view", False):
253
+ flash(message='Unauthorized, please login', category='error')
254
+ session["redirect_target"] = request.full_path
255
+ return redirect(url_for('auth.login'))
256
+ elif g.user is None and getattr(view, "api_view", True):
257
+ return {
258
+ "status": "error",
259
+ "message": "Authorization required"
260
+ }, 403
261
+
262
+ return view(*args, **kwargs)
263
+
264
+ return wrapped_view
265
+
266
+
267
+ def api_login_required(view: FlaskView):
268
+ view.api_view = True
269
+ return login_required(view)
270
+
271
+
272
+ def check_roles(needed_roles: list[str] | str = None):
273
+ def inner(view: FlaskView):
274
+ @functools.wraps(view)
275
+ def wrapped_view(*args, **kwargs):
276
+ if not UserService.check_roles(needed_roles, g.user):
277
+ flash(message='Not authorized to access this area', category='error')
278
+ return redirect(url_for('main.home'))
279
+
280
+ return view(*args, **kwargs)
281
+
282
+ return wrapped_view
283
+ return inner
284
+
285
+
286
+ def load_logged_in_user():
287
+ user_id = session.get('user_id')
288
+ auth_header = request.headers.get("Authorization")
289
+
290
+ if user_id:
291
+ try:
292
+ g.user = User.get(id=UUID(user_id))
293
+ return
294
+ except User.DoesNotExist:
295
+ session.clear()
296
+
297
+ if auth_header:
298
+ try:
299
+ auth_schema, *auth_data = auth_header.split()
300
+ if auth_schema == "token":
301
+ token = auth_data[0]
302
+ g.user = User.get(api_token=token)
303
+ return
304
+ except IndexError as exception:
305
+ raise APIException("Malformed authorization header") from exception
306
+ except User.DoesNotExist as exception:
307
+ raise APIException("User not found for supplied token") from exception
308
+ g.user = None
@@ -0,0 +1,219 @@
1
+ import datetime
2
+ import logging
3
+ from functools import partial, reduce
4
+ from typing import TypedDict
5
+ from uuid import UUID
6
+
7
+ from cassandra.cqlengine.models import Model
8
+ from argus.backend.models.plan import ArgusReleasePlan
9
+ from argus.backend.models.web import ArgusGroup, ArgusRelease, ArgusTest, ArgusUserView, User
10
+ from argus.backend.plugins.loader import all_plugin_models
11
+ from argus.backend.service.test_lookup import TestLookup
12
+ from argus.backend.util.common import chunk, current_user
13
+
14
+ LOGGER = logging.getLogger(__name__)
15
+
16
+
17
+ class UserViewException(Exception):
18
+ pass
19
+
20
+
21
+ class ViewUpdateRequest(TypedDict):
22
+ name: str
23
+ description: str
24
+ display_name: str
25
+ tests: list[str]
26
+ widget_settings: str
27
+ plan_id: str | None
28
+
29
+
30
+ class UserViewService:
31
+ def create_view(self, name: str, items: list[str], widget_settings: str, description: str = None, display_name: str = None, plan_id: UUID = None) -> ArgusUserView:
32
+ try:
33
+ name_check = ArgusUserView.get(name=name)
34
+ raise UserViewException(
35
+ f"View with name {name} already exists: {name_check.id}", name, name_check, name_check.id)
36
+ except ArgusUserView.DoesNotExist:
37
+ pass
38
+ view = ArgusUserView()
39
+ view.name = name
40
+ view.display_name = display_name or name
41
+ view.description = description
42
+ view.widget_settings = widget_settings
43
+ view.plan_id = plan_id
44
+ view.tests = []
45
+ entities = self.parse_view_entity_list(items)
46
+ view.tests = entities["tests"]
47
+ view.release_ids = entities["release"]
48
+ view.group_ids = entities["group"]
49
+ view.user_id = current_user().id
50
+
51
+ view.save()
52
+ return view
53
+
54
+ def parse_view_entity_list(self, entity_list: list[str]) -> dict[str, list[str]]:
55
+ entities = {
56
+ "release": [],
57
+ "group": [],
58
+ "tests": []
59
+ }
60
+ for entity in entity_list:
61
+ entity_type, entity_id = entity.split(":")
62
+ match (entity_type):
63
+ case "release":
64
+ entities["tests"].extend(t.id for t in ArgusTest.filter(release_id=entity_id).all())
65
+ entities["release"].append(entity_id)
66
+ case "group":
67
+ entities["tests"].extend(t.id for t in ArgusTest.filter(group_id=entity_id).all())
68
+ entities["group"].append(entity_id)
69
+ case "test":
70
+ entities["tests"].append(entity_id)
71
+ return entities
72
+
73
+ def test_lookup(self, query: str):
74
+ return TestLookup.test_lookup(query)
75
+
76
+ def update_view(self, view_id: str | UUID, update_data: ViewUpdateRequest) -> bool:
77
+ view: ArgusUserView = ArgusUserView.get(id=view_id)
78
+ if view.user_id != current_user().id and not current_user().is_admin():
79
+ raise UserViewException("Unable to modify other users' views")
80
+ for key in ["user_id", "id"]:
81
+ update_data.pop(key, None)
82
+ items = update_data.pop("items")
83
+ for k, value in update_data.items():
84
+ view[k] = value
85
+ view.tests = []
86
+ view.release_ids = []
87
+ view.group_ids = []
88
+ for entity in items:
89
+ entity_type, entity_id = entity.split(":")
90
+ match (entity_type):
91
+ case "release":
92
+ view.tests.extend(t.id for t in ArgusTest.filter(release_id=entity_id).all())
93
+ view.release_ids.append(entity_id)
94
+ case "group":
95
+ view.tests.extend(t.id for t in ArgusTest.filter(group_id=entity_id).all())
96
+ view.group_ids.append(entity_id)
97
+ case "test":
98
+ view.tests.append(entity_id)
99
+ view.last_updated = datetime.datetime.utcnow()
100
+ view.save()
101
+ return True
102
+
103
+ def delete_view(self, view_id: str | UUID) -> bool:
104
+ view = ArgusUserView.get(id=view_id)
105
+ if view.user_id != current_user().id and not current_user().is_admin():
106
+ raise UserViewException("Unable to modify other users' views")
107
+ view.delete()
108
+
109
+ return True
110
+
111
+ def get_view(self, view_id: str | UUID) -> ArgusUserView:
112
+ view: ArgusUserView = ArgusUserView.get(id=view_id)
113
+ if datetime.datetime.utcnow() - (view.last_updated or datetime.datetime.fromtimestamp(0)) > datetime.timedelta(hours=1):
114
+ self.refresh_stale_view(view)
115
+ return view
116
+
117
+ def get_view_by_name(self, view_name: str) -> ArgusUserView:
118
+ view: ArgusUserView = ArgusUserView.get(name=view_name)
119
+ if datetime.datetime.utcnow() - (view.last_updated or datetime.datetime.fromtimestamp(0)) > datetime.timedelta(hours=1):
120
+ self.refresh_stale_view(view)
121
+ return view
122
+
123
+ def get_all_views(self, user: User | None = None) -> list[ArgusUserView]:
124
+ if user:
125
+ return list(ArgusUserView.filter(user_id=user.id).all())
126
+ return list(ArgusUserView.filter().all())
127
+
128
+ def resolve_view_tests(self, view_id: str | UUID) -> list[ArgusTest]:
129
+ view = ArgusUserView.get(id=view_id)
130
+ return self.resolve_tests_by_id(view.tests)
131
+
132
+ def resolve_tests_by_id(self, test_ids: list[str | UUID]) -> list[ArgusTest]:
133
+ tests = []
134
+ for batch in chunk(test_ids):
135
+ tests.extend(ArgusTest.filter(id__in=batch).all())
136
+
137
+ return tests
138
+
139
+ def batch_resolve_entity(self, entity: Model, param_name: str, entity_ids: list[UUID]) -> list[Model]:
140
+ result = []
141
+ for batch in chunk(entity_ids):
142
+ result.extend(entity.filter(**{f"{param_name}__in": batch}).allow_filtering().all())
143
+ return result
144
+
145
+ def refresh_stale_view(self, view: ArgusUserView):
146
+ if view.plan_id:
147
+ try:
148
+ view.tests = [test.id for test in self.resolve_tests_by_id(ArgusReleasePlan.get(id=view.plan_id).tests)]
149
+ view.group_ids = ArgusReleasePlan.get(id=view.plan_id).groups
150
+ except ArgusReleasePlan.DoesNotExist:
151
+ LOGGER.warning("Dangling view %s from non-existent release plan %s", view.id, view.plan_id)
152
+ return view
153
+ else:
154
+ view.tests = [test.id for test in self.resolve_view_tests(view.id)]
155
+ all_tests = set(view.tests)
156
+ all_tests.update(test.id for test in self.batch_resolve_entity(ArgusTest, "group_id", view.group_ids))
157
+ all_tests.update(test.id for test in self.batch_resolve_entity(ArgusTest, "release_id", view.release_ids))
158
+ view.tests = list(all_tests)
159
+ view.last_updated = datetime.datetime.utcnow()
160
+ view.save()
161
+
162
+ return view
163
+
164
+ def resolve_releases_for_tests(self, tests: list[ArgusTest]):
165
+ releases = []
166
+ unique_release_ids = reduce(lambda releases, test: releases.add(test.release_id) or releases, tests, set())
167
+ for batch in chunk(unique_release_ids):
168
+ releases.extend(ArgusRelease.filter(id__in=batch).all())
169
+
170
+ return releases
171
+
172
+ def resolve_groups_for_tests(self, tests: list[ArgusTest]):
173
+ releases = []
174
+ unique_release_ids = reduce(lambda groups, test: groups.add(test.group_id) or groups, tests, set())
175
+ for batch in chunk(unique_release_ids):
176
+ releases.extend(ArgusGroup.filter(id__in=batch).all())
177
+
178
+ return releases
179
+
180
+ def get_versions_for_view(self, view_id: str | UUID) -> list[str]:
181
+ tests = self.resolve_view_tests(view_id)
182
+ unique_versions = {ver for plugin in all_plugin_models()
183
+ for ver in plugin.get_distinct_versions_for_view(tests=tests)}
184
+
185
+ return sorted(list(unique_versions), reverse=True)
186
+
187
+ def resolve_view_for_edit(self, view_id: str | UUID) -> dict:
188
+ view: ArgusUserView = ArgusUserView.get(id=view_id)
189
+ resolved = dict(view)
190
+ view_groups = self.batch_resolve_entity(ArgusGroup, "id", view.group_ids)
191
+ view_releases = self.batch_resolve_entity(ArgusRelease, "id", view.release_ids)
192
+ view_tests = self.resolve_view_tests(view.id)
193
+ all_groups = {group.id: partial(TestLookup.index_mapper, type="group")(group)
194
+ for group in self.resolve_releases_for_tests(view_tests)}
195
+ all_releases = {release.id: partial(TestLookup.index_mapper, type="release")(release)
196
+ for release in self.resolve_releases_for_tests(view_tests)}
197
+ entities_by_id = {
198
+ entity.id: partial(TestLookup.index_mapper, type="release" if isinstance(
199
+ entity, ArgusRelease) else "group")(entity)
200
+ for container in [view_releases, view_groups]
201
+ for entity in container
202
+ }
203
+
204
+ items = []
205
+ for test in view_tests:
206
+ if not (entities_by_id.get(test.group_id) or entities_by_id.get(test.release_id)):
207
+ item = dict(test)
208
+ item["type"] = "test"
209
+ items.append(item)
210
+
211
+ items = [*entities_by_id.values(), *items]
212
+ for entity in items:
213
+ entity["group"] = all_groups.get(entity.get("group_id"), {}).get(
214
+ "pretty_name") or all_groups.get(entity.get("group_id"), {}).get("name")
215
+ entity["release"] = all_releases.get(entity.get("release_id"), {}).get(
216
+ "pretty_name") or all_releases.get(entity.get("release_id"), {}).get("name")
217
+
218
+ resolved["items"] = items
219
+ return resolved
File without changes