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.
- argus/_version.py +21 -0
- argus/backend/.gitkeep +0 -0
- argus/backend/__init__.py +0 -0
- argus/backend/cli.py +57 -0
- argus/backend/controller/__init__.py +0 -0
- argus/backend/controller/admin.py +20 -0
- argus/backend/controller/admin_api.py +355 -0
- argus/backend/controller/api.py +589 -0
- argus/backend/controller/auth.py +67 -0
- argus/backend/controller/client_api.py +109 -0
- argus/backend/controller/main.py +316 -0
- argus/backend/controller/notification_api.py +72 -0
- argus/backend/controller/notifications.py +13 -0
- argus/backend/controller/planner_api.py +194 -0
- argus/backend/controller/team.py +129 -0
- argus/backend/controller/team_ui.py +19 -0
- argus/backend/controller/testrun_api.py +513 -0
- argus/backend/controller/view_api.py +188 -0
- argus/backend/controller/views_widgets/__init__.py +0 -0
- argus/backend/controller/views_widgets/graphed_stats.py +54 -0
- argus/backend/controller/views_widgets/graphs.py +68 -0
- argus/backend/controller/views_widgets/highlights.py +135 -0
- argus/backend/controller/views_widgets/nemesis_stats.py +26 -0
- argus/backend/controller/views_widgets/summary.py +43 -0
- argus/backend/db.py +98 -0
- argus/backend/error_handlers.py +41 -0
- argus/backend/events/event_processors.py +34 -0
- argus/backend/models/__init__.py +0 -0
- argus/backend/models/argus_ai.py +24 -0
- argus/backend/models/github_issue.py +60 -0
- argus/backend/models/plan.py +24 -0
- argus/backend/models/result.py +187 -0
- argus/backend/models/runtime_store.py +58 -0
- argus/backend/models/view_widgets.py +25 -0
- argus/backend/models/web.py +403 -0
- argus/backend/plugins/__init__.py +0 -0
- argus/backend/plugins/core.py +248 -0
- argus/backend/plugins/driver_matrix_tests/controller.py +66 -0
- argus/backend/plugins/driver_matrix_tests/model.py +429 -0
- argus/backend/plugins/driver_matrix_tests/plugin.py +21 -0
- argus/backend/plugins/driver_matrix_tests/raw_types.py +62 -0
- argus/backend/plugins/driver_matrix_tests/service.py +61 -0
- argus/backend/plugins/driver_matrix_tests/udt.py +42 -0
- argus/backend/plugins/generic/model.py +86 -0
- argus/backend/plugins/generic/plugin.py +15 -0
- argus/backend/plugins/generic/types.py +14 -0
- argus/backend/plugins/loader.py +39 -0
- argus/backend/plugins/sct/controller.py +224 -0
- argus/backend/plugins/sct/plugin.py +37 -0
- argus/backend/plugins/sct/resource_setup.py +177 -0
- argus/backend/plugins/sct/service.py +682 -0
- argus/backend/plugins/sct/testrun.py +288 -0
- argus/backend/plugins/sct/udt.py +100 -0
- argus/backend/plugins/sirenada/model.py +118 -0
- argus/backend/plugins/sirenada/plugin.py +16 -0
- argus/backend/service/admin.py +26 -0
- argus/backend/service/argus_service.py +696 -0
- argus/backend/service/build_system_monitor.py +185 -0
- argus/backend/service/client_service.py +127 -0
- argus/backend/service/event_service.py +18 -0
- argus/backend/service/github_service.py +233 -0
- argus/backend/service/jenkins_service.py +269 -0
- argus/backend/service/notification_manager.py +159 -0
- argus/backend/service/planner_service.py +608 -0
- argus/backend/service/release_manager.py +229 -0
- argus/backend/service/results_service.py +690 -0
- argus/backend/service/stats.py +610 -0
- argus/backend/service/team_manager_service.py +82 -0
- argus/backend/service/test_lookup.py +172 -0
- argus/backend/service/testrun.py +489 -0
- argus/backend/service/user.py +308 -0
- argus/backend/service/views.py +219 -0
- argus/backend/service/views_widgets/__init__.py +0 -0
- argus/backend/service/views_widgets/graphed_stats.py +180 -0
- argus/backend/service/views_widgets/highlights.py +374 -0
- argus/backend/service/views_widgets/nemesis_stats.py +34 -0
- argus/backend/template_filters.py +27 -0
- argus/backend/tests/__init__.py +0 -0
- argus/backend/tests/client_service/__init__.py +0 -0
- argus/backend/tests/client_service/test_submit_results.py +79 -0
- argus/backend/tests/conftest.py +180 -0
- argus/backend/tests/results_service/__init__.py +0 -0
- argus/backend/tests/results_service/test_best_results.py +178 -0
- argus/backend/tests/results_service/test_cell.py +65 -0
- argus/backend/tests/results_service/test_chartjs_additional_functions.py +259 -0
- argus/backend/tests/results_service/test_create_chartjs.py +220 -0
- argus/backend/tests/results_service/test_result_metadata.py +100 -0
- argus/backend/tests/results_service/test_results_service.py +203 -0
- argus/backend/tests/results_service/test_validation_rules.py +213 -0
- argus/backend/tests/view_widgets/__init__.py +0 -0
- argus/backend/tests/view_widgets/test_highlights_api.py +532 -0
- argus/backend/util/common.py +65 -0
- argus/backend/util/config.py +38 -0
- argus/backend/util/encoders.py +56 -0
- argus/backend/util/logsetup.py +80 -0
- argus/backend/util/module_loaders.py +30 -0
- argus/backend/util/send_email.py +91 -0
- argus/client/base.py +1 -3
- argus/client/driver_matrix_tests/cli.py +17 -8
- argus/client/generic/cli.py +4 -2
- argus/client/generic/client.py +1 -0
- argus/client/generic_result.py +48 -9
- argus/client/sct/client.py +1 -3
- argus/client/sirenada/client.py +4 -1
- argus/client/tests/__init__.py +0 -0
- argus/client/tests/conftest.py +19 -0
- argus/client/tests/test_package.py +45 -0
- argus/client/tests/test_results.py +224 -0
- argus/common/sct_types.py +3 -0
- argus/common/sirenada_types.py +1 -1
- {argus_alm-0.14.2.dist-info → argus_alm-0.15.2.dist-info}/METADATA +43 -19
- argus_alm-0.15.2.dist-info/RECORD +122 -0
- {argus_alm-0.14.2.dist-info → argus_alm-0.15.2.dist-info}/WHEEL +2 -1
- argus_alm-0.15.2.dist-info/entry_points.txt +3 -0
- argus_alm-0.15.2.dist-info/top_level.txt +1 -0
- argus_alm-0.14.2.dist-info/RECORD +0 -20
- argus_alm-0.14.2.dist-info/entry_points.txt +0 -4
- {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
|