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,269 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import requests
|
|
3
|
+
from typing import Any, TypedDict
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
import xml.etree.ElementTree as ET
|
|
6
|
+
import jenkins
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
from flask import current_app, g
|
|
10
|
+
|
|
11
|
+
from argus.backend.models.web import ArgusGroup, ArgusRelease, ArgusTest, UserOauthToken
|
|
12
|
+
|
|
13
|
+
LOGGER = logging.getLogger(__name__)
|
|
14
|
+
GITHUB_REPO_RE = r"(?P<http>^https?:\/\/(www\.)?github\.com\/(?P<user>[\w\d\-]+)\/(?P<repo>[\w\d\-]+)(\.git)?$)|(?P<ssh>git@github\.com:(?P<ssh_user>[\w\d\-]+)\/(?P<ssh_repo>[\w\d\-]+)(\.git)?)"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Parameter(TypedDict):
|
|
18
|
+
_class: str
|
|
19
|
+
name: str
|
|
20
|
+
description: str
|
|
21
|
+
value: Any
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class JenkinsServiceError(Exception):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class JenkinsService:
|
|
29
|
+
RESERVED_PARAMETER_NAME = "requested_by_user"
|
|
30
|
+
|
|
31
|
+
SETTINGS_CONFIG_MAP = {
|
|
32
|
+
"scylla-cluster-tests": {
|
|
33
|
+
"gitRepo": "*//scm/userRemoteConfigs/hudson.plugins.git.UserRemoteConfig/url",
|
|
34
|
+
"gitBranch": "*//scm/branches/hudson.plugins.git.BranchSpec/name",
|
|
35
|
+
"pipelineFile": "*//scriptPath",
|
|
36
|
+
},
|
|
37
|
+
"driver-matrix-tests": {
|
|
38
|
+
"gitRepo": "*//scm/userRemoteConfigs/hudson.plugins.git.UserRemoteConfig/url",
|
|
39
|
+
"gitBranch": "*//scm/branches/hudson.plugins.git.BranchSpec/name",
|
|
40
|
+
"pipelineFile": "*//scriptPath",
|
|
41
|
+
},
|
|
42
|
+
"sirenada": {
|
|
43
|
+
"gitRepo": "*//scm/userRemoteConfigs/hudson.plugins.git.UserRemoteConfig/url",
|
|
44
|
+
"gitBranch": "*//scm/branches/hudson.plugins.git.BranchSpec/name",
|
|
45
|
+
"pipelineFile": "*//scriptPath",
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
def __init__(self) -> None:
|
|
50
|
+
self._jenkins = jenkins.Jenkins(url=current_app.config["JENKINS_URL"],
|
|
51
|
+
username=current_app.config["JENKINS_USER"],
|
|
52
|
+
password=current_app.config["JENKINS_API_TOKEN"])
|
|
53
|
+
|
|
54
|
+
def retrieve_job_parameters(self, build_id: str, build_number: int | None) -> list[Parameter]:
|
|
55
|
+
job_info = self._jenkins.get_job_info(name=build_id)
|
|
56
|
+
if not build_number:
|
|
57
|
+
next_build_number = job_info.get("nextBuildNumber")
|
|
58
|
+
if not next_build_number:
|
|
59
|
+
raise JenkinsServiceError("#noBuildsAvailable")
|
|
60
|
+
try:
|
|
61
|
+
build_info = self._jenkins.get_build_info(name=build_id, number=next_build_number - 1)
|
|
62
|
+
except jenkins.JenkinsException:
|
|
63
|
+
raise JenkinsServiceError("#noBuildsAvailable")
|
|
64
|
+
else:
|
|
65
|
+
build_info = self._jenkins.get_build_info(name=build_id, number=build_number)
|
|
66
|
+
raw_config = self._jenkins.get_job_config(name=build_id)
|
|
67
|
+
config = ET.fromstring(raw_config)
|
|
68
|
+
parameter_defs = config.find("*//parameterDefinitions")
|
|
69
|
+
if parameter_defs:
|
|
70
|
+
descriptions = {
|
|
71
|
+
define.findtext("name"): f"{define.findtext('description', '')}" + f" (default: <span class=\"fw-bold\">{define.findtext('defaultValue')}</span>)" if define.findtext('defaultValue') else ""
|
|
72
|
+
for define in parameter_defs.iterfind("hudson.model.StringParameterDefinition")
|
|
73
|
+
}
|
|
74
|
+
else:
|
|
75
|
+
descriptions = {}
|
|
76
|
+
params = next((a for a in build_info["actions"] if a.get(
|
|
77
|
+
"_class", "#NONE") == "hudson.model.ParametersAction"), None)
|
|
78
|
+
if params:
|
|
79
|
+
params = [param for param in params["parameters"] if param["name"] != self.RESERVED_PARAMETER_NAME]
|
|
80
|
+
else:
|
|
81
|
+
default_params = next((prop for prop in job_info["property"] if prop.get(
|
|
82
|
+
"_class", "") == "hudson.model.ParametersDefinitionProperty"), {}).get("parameterDefinitions", {})
|
|
83
|
+
params = [{"name": param["name"], "value": param.get("defaultParameterValue", {}).get(
|
|
84
|
+
"value", "")} for param in default_params if param["name"] != self.RESERVED_PARAMETER_NAME]
|
|
85
|
+
for idx, param in enumerate(params):
|
|
86
|
+
params[idx]["description"] = descriptions.get(param["name"], "")
|
|
87
|
+
|
|
88
|
+
return params
|
|
89
|
+
|
|
90
|
+
def latest_build(self, build_id: str) -> int:
|
|
91
|
+
try:
|
|
92
|
+
job_info = self._jenkins.get_job_info(name=build_id)
|
|
93
|
+
last_build = job_info.get("lastBuild")
|
|
94
|
+
if not last_build:
|
|
95
|
+
return -1
|
|
96
|
+
return last_build["number"]
|
|
97
|
+
except jenkins.JenkinsException:
|
|
98
|
+
raise JenkinsServiceError("Job doesn't exist", build_id)
|
|
99
|
+
|
|
100
|
+
def get_releases_for_clone(self, test_id: str):
|
|
101
|
+
test_id = UUID(test_id)
|
|
102
|
+
# TODO: Filtering based on origin location / user preferences
|
|
103
|
+
_: ArgusTest = ArgusTest.get(id=test_id)
|
|
104
|
+
|
|
105
|
+
releases = list(ArgusRelease.all())
|
|
106
|
+
|
|
107
|
+
return sorted(releases, key=lambda r: r.pretty_name if r.pretty_name else r.name)
|
|
108
|
+
|
|
109
|
+
def get_groups_for_release(self, release_id: str):
|
|
110
|
+
groups = list(ArgusGroup.filter(release_id=release_id).all())
|
|
111
|
+
|
|
112
|
+
return sorted(groups, key=lambda g: g.pretty_name if g.pretty_name else g.name)
|
|
113
|
+
|
|
114
|
+
def _verify_sct_settings(self, new_settings: dict[str, str]) -> tuple[bool, str]:
|
|
115
|
+
if not (match := re.match(GITHUB_REPO_RE, new_settings["gitRepo"])):
|
|
116
|
+
return (False, "Repository doesn't conform to GitHub schema")
|
|
117
|
+
|
|
118
|
+
git_info = match.groupdict()
|
|
119
|
+
if git_info.get("ssh"):
|
|
120
|
+
repo = git_info["ssh_repo"]
|
|
121
|
+
user = git_info["ssh_user"]
|
|
122
|
+
else:
|
|
123
|
+
repo = git_info["repo"]
|
|
124
|
+
user = git_info["user"]
|
|
125
|
+
|
|
126
|
+
user_tokens = UserOauthToken.filter(user_id=g.user.id).all()
|
|
127
|
+
token = None
|
|
128
|
+
for tok in user_tokens:
|
|
129
|
+
if tok.kind == "github":
|
|
130
|
+
token = tok.token
|
|
131
|
+
break
|
|
132
|
+
if not token:
|
|
133
|
+
raise JenkinsServiceError("Github token not found")
|
|
134
|
+
|
|
135
|
+
response = requests.get(
|
|
136
|
+
url=f"https://api.github.com/repos/{user}/{repo}/contents/{new_settings['pipelineFile']}?ref={new_settings['gitBranch']}",
|
|
137
|
+
headers={
|
|
138
|
+
"Accept": "application/vnd.github+json",
|
|
139
|
+
"Authorization": f"Bearer {token}",
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if response.status_code == 404:
|
|
144
|
+
return (False, f"Pipeline file not found in the <a href=\"https://github.com/{user}/{repo}/tree/{new_settings['gitBranch']}\"> target repository</a>, please check the repository before continuing")
|
|
145
|
+
|
|
146
|
+
if response.status_code == 403:
|
|
147
|
+
return (True, "No access to this repository using your token. The pipeline file cannot be verified.")
|
|
148
|
+
|
|
149
|
+
if response.status_code == 200:
|
|
150
|
+
return (True, "")
|
|
151
|
+
|
|
152
|
+
return (False, "Generic Error")
|
|
153
|
+
|
|
154
|
+
def verify_job_settings(self, build_id: str, new_settings: dict[str, str]) -> tuple[bool, str]:
|
|
155
|
+
PLUGIN_MAP = {
|
|
156
|
+
"scylla-cluster-tests": self._verify_sct_settings,
|
|
157
|
+
# for now they match
|
|
158
|
+
"sirenada": self._verify_sct_settings,
|
|
159
|
+
"driver-matrix-tests": self._verify_sct_settings,
|
|
160
|
+
}
|
|
161
|
+
test: ArgusTest = ArgusTest.get(build_system_id=build_id)
|
|
162
|
+
plugin_name = test.plugin_name
|
|
163
|
+
|
|
164
|
+
validated, message = PLUGIN_MAP.get(plugin_name, lambda _: (True, ""))(new_settings)
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
"validated": validated,
|
|
168
|
+
"message": message,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
def get_advanced_settings(self, build_id: str):
|
|
172
|
+
test: ArgusTest = ArgusTest.get(build_system_id=build_id)
|
|
173
|
+
plugin_name = test.plugin_name
|
|
174
|
+
|
|
175
|
+
if not (plugin_settings := self.SETTINGS_CONFIG_MAP.get(plugin_name)):
|
|
176
|
+
return {}
|
|
177
|
+
|
|
178
|
+
settings = {}
|
|
179
|
+
raw_config = self._jenkins.get_job_config(name=build_id)
|
|
180
|
+
config = ET.fromstring(raw_config)
|
|
181
|
+
|
|
182
|
+
for setting, xpath in plugin_settings.items():
|
|
183
|
+
value = config.find(xpath)
|
|
184
|
+
settings[setting] = value.text
|
|
185
|
+
|
|
186
|
+
return settings
|
|
187
|
+
|
|
188
|
+
def adjust_job_settings(self, build_id: str, plugin_name: str, settings: dict[str, str]):
|
|
189
|
+
xpath_map = self.SETTINGS_CONFIG_MAP.get(plugin_name)
|
|
190
|
+
if not xpath_map:
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
config = self._jenkins.get_job_config(name=build_id)
|
|
194
|
+
xml = ET.fromstring(config)
|
|
195
|
+
for setting, value in settings.items():
|
|
196
|
+
element = xml.find(xpath_map[setting])
|
|
197
|
+
element.text = value
|
|
198
|
+
|
|
199
|
+
adjusted_config = ET.tostring(xml, encoding="unicode")
|
|
200
|
+
self._jenkins.reconfig_job(name=build_id, config_xml=adjusted_config)
|
|
201
|
+
|
|
202
|
+
def clone_job(self, current_test_id: str, new_name: str, target: str, group: str, advanced_settings: bool | dict[str, str]):
|
|
203
|
+
cloned_test: ArgusTest = ArgusTest.get(id=current_test_id)
|
|
204
|
+
target_release: ArgusRelease = ArgusRelease.get(id=target)
|
|
205
|
+
target_group: ArgusGroup = ArgusGroup.get(id=group)
|
|
206
|
+
|
|
207
|
+
if target_group.id == cloned_test.id and new_name == cloned_test.name:
|
|
208
|
+
raise JenkinsServiceError("Unable to clone: source and destination are the same")
|
|
209
|
+
|
|
210
|
+
if not target_group.build_system_id:
|
|
211
|
+
raise JenkinsServiceError("Unable to clone: target group is missing jenkins folder path")
|
|
212
|
+
|
|
213
|
+
jenkins_new_build_id = f"{target_group.build_system_id}/{new_name}"
|
|
214
|
+
|
|
215
|
+
new_test = ArgusTest()
|
|
216
|
+
new_test.name = new_name
|
|
217
|
+
new_test.build_system_id = jenkins_new_build_id
|
|
218
|
+
new_test.group_id = target_group.id
|
|
219
|
+
new_test.release_id = target_release.id
|
|
220
|
+
new_test.plugin_name = cloned_test.plugin_name
|
|
221
|
+
|
|
222
|
+
old_config = self._jenkins.get_job_config(name=cloned_test.build_system_id)
|
|
223
|
+
LOGGER.info(old_config)
|
|
224
|
+
xml = ET.fromstring(old_config)
|
|
225
|
+
display_name = xml.find("displayName")
|
|
226
|
+
if display_name:
|
|
227
|
+
display_name.text = new_name
|
|
228
|
+
new_config = ET.tostring(xml, encoding="unicode")
|
|
229
|
+
self._jenkins.create_job(name=jenkins_new_build_id, config_xml=new_config)
|
|
230
|
+
new_job_info = self._jenkins.get_job_info(name=jenkins_new_build_id)
|
|
231
|
+
new_test.build_system_url = new_job_info["url"]
|
|
232
|
+
new_test.save()
|
|
233
|
+
|
|
234
|
+
if advanced_settings:
|
|
235
|
+
self.adjust_job_settings(build_id=jenkins_new_build_id,
|
|
236
|
+
plugin_name=new_test.plugin_name, settings=advanced_settings)
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
"new_job": new_job_info,
|
|
240
|
+
"new_entity": new_test,
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
def clone_build_job(self, build_id: str, params: dict[str, str]):
|
|
244
|
+
queue_item = self.build_job(build_id=build_id, params=params)
|
|
245
|
+
return {
|
|
246
|
+
"queueItem": queue_item,
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
def build_job(self, build_id: str, params: dict, user_override: str = None):
|
|
250
|
+
queue_number = self._jenkins.build_job(build_id, {
|
|
251
|
+
**params,
|
|
252
|
+
# use the user's email as the default value for the requested by user parameter,
|
|
253
|
+
# so it would align with how SCT default works, on runs not trigger by argus
|
|
254
|
+
self.RESERVED_PARAMETER_NAME: g.user.email.split('@')[0] if not user_override else user_override
|
|
255
|
+
})
|
|
256
|
+
return queue_number
|
|
257
|
+
|
|
258
|
+
def get_queue_info(self, queue_item: int):
|
|
259
|
+
build_info = self._jenkins.get_queue_item(queue_item)
|
|
260
|
+
LOGGER.info("%s", build_info)
|
|
261
|
+
executable = build_info.get("executable")
|
|
262
|
+
if executable:
|
|
263
|
+
return executable
|
|
264
|
+
else:
|
|
265
|
+
return {
|
|
266
|
+
"why": build_info["why"],
|
|
267
|
+
"inQueueSince": build_info["inQueueSince"],
|
|
268
|
+
"taskUrl": build_info["task"]["url"],
|
|
269
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
from flask import current_app, render_template
|
|
5
|
+
from argus.backend.models.web import ArgusNotification, ArgusNotificationSourceTypes, ArgusNotificationTypes, ArgusNotificationState, User
|
|
6
|
+
from argus.backend.util.send_email import Email
|
|
7
|
+
|
|
8
|
+
LOGGER = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class NotificationManagerException(Exception):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class NotificationManagerService:
|
|
16
|
+
|
|
17
|
+
def __init__(self, notification_senders: list["NotificationSenderBase"] | None = None) -> None:
|
|
18
|
+
self.notification_services: list["NotificationSenderBase"] = [
|
|
19
|
+
ArgusDBNotificationSaver(),
|
|
20
|
+
EmailNotificationServiceSender()
|
|
21
|
+
]
|
|
22
|
+
if notification_senders:
|
|
23
|
+
self.notification_services.extend(notification_senders)
|
|
24
|
+
|
|
25
|
+
def send_notification(self, receiver: UUID, sender: UUID,
|
|
26
|
+
notification_type: ArgusNotificationTypes,
|
|
27
|
+
source_type: ArgusNotificationSourceTypes,
|
|
28
|
+
source_id: UUID, title: str | None = None,
|
|
29
|
+
source_message: str | None = None, content_params: dict | None = None) -> Any:
|
|
30
|
+
|
|
31
|
+
for service in self.notification_services:
|
|
32
|
+
service.send_notification(receiver, sender, notification_type, source_type,
|
|
33
|
+
source_id, title, source_message, content_params)
|
|
34
|
+
|
|
35
|
+
def get_notificaton(self, receiver: UUID, notification_id: UUID) -> ArgusNotification:
|
|
36
|
+
return ArgusNotification.get(receiver=receiver, id=notification_id)
|
|
37
|
+
|
|
38
|
+
def get_unread_count(self, receiver: UUID) -> int:
|
|
39
|
+
query = ArgusNotification.filter(
|
|
40
|
+
receiver=receiver, state__eq=ArgusNotificationState.UNREAD.value).allow_filtering().all()
|
|
41
|
+
return len(query)
|
|
42
|
+
|
|
43
|
+
def read_notification(self, receiver: UUID, notification_id: UUID) -> ArgusNotification:
|
|
44
|
+
notification = ArgusNotification.get(receiver=receiver, id=notification_id)
|
|
45
|
+
notification.state = ArgusNotificationState.READ
|
|
46
|
+
return bool(notification.save().state)
|
|
47
|
+
|
|
48
|
+
def get_notifications(self, receiver: UUID, limit: int = 20, after: UUID | None = None) -> list[ArgusNotification]:
|
|
49
|
+
if after:
|
|
50
|
+
return ArgusNotification.filter(receiver=receiver, id__lte=after).all().limit(limit)
|
|
51
|
+
return ArgusNotification.filter(receiver=receiver).all().limit(limit)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class NotificationSenderBase:
|
|
55
|
+
NOTIFICATION_TITLES = {
|
|
56
|
+
ArgusNotificationTypes.Mention: "You were mentioned in a comment",
|
|
57
|
+
ArgusNotificationTypes.StatusChange: "A run you are assigned to changed status",
|
|
58
|
+
ArgusNotificationTypes.AssigneeChange: "You were assigned to a run",
|
|
59
|
+
ArgusNotificationTypes.ScheduleChange: "You were assigned to a schedule",
|
|
60
|
+
ArgusNotificationTypes.ViewActionItemAssignee: "You were assigned to a view action item",
|
|
61
|
+
ArgusNotificationTypes.ViewHighlightMention: "You were mentioned in a view highlight",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
CONTENT_TEMPLATES = {}
|
|
65
|
+
|
|
66
|
+
def _check_user(self, user_id: UUID) -> bool:
|
|
67
|
+
try:
|
|
68
|
+
user = User.get(id=user_id)
|
|
69
|
+
if user.id == user_id:
|
|
70
|
+
return True
|
|
71
|
+
except User.DoesNotExist:
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
def _get_title_for_notification_type(self, notification_type: ArgusNotificationTypes) -> str:
|
|
75
|
+
if title := self.NOTIFICATION_TITLES.get(notification_type):
|
|
76
|
+
return title
|
|
77
|
+
raise NotificationManagerException(
|
|
78
|
+
f"Title for notification type {notification_type} not found.", notification_type)
|
|
79
|
+
|
|
80
|
+
def _render_content(self, content_type: ArgusNotificationTypes, params: dict):
|
|
81
|
+
if content_renderer := self.CONTENT_TEMPLATES.get(content_type):
|
|
82
|
+
return content_renderer(params)
|
|
83
|
+
raise NotificationManagerException(
|
|
84
|
+
f"Content renderer for notification type {content_type} not found.", content_type)
|
|
85
|
+
|
|
86
|
+
def send_notification(self, receiver: UUID, sender: UUID,
|
|
87
|
+
notification_type: ArgusNotificationTypes,
|
|
88
|
+
source_type: ArgusNotificationSourceTypes,
|
|
89
|
+
source_id: UUID,
|
|
90
|
+
title: str | None = None,
|
|
91
|
+
content: str | None = None,
|
|
92
|
+
content_params: dict | None = None) -> Any:
|
|
93
|
+
raise NotImplementedError()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ArgusDBNotificationSaver(NotificationSenderBase):
|
|
97
|
+
CONTENT_TEMPLATES = {
|
|
98
|
+
ArgusNotificationTypes.Mention: lambda p: render_template("notifications/mention.html.j2", **p if p else {}),
|
|
99
|
+
ArgusNotificationTypes.AssigneeChange: lambda p: render_template("notifications/assigned.html.j2", **p if p else {}),
|
|
100
|
+
ArgusNotificationTypes.ViewActionItemAssignee: lambda p: render_template(
|
|
101
|
+
"notifications/view_action_item_assigned.html.j2", **p if p else {}),
|
|
102
|
+
ArgusNotificationTypes.ViewHighlightMention: lambda p: render_template(
|
|
103
|
+
"notifications/view_highlight_mention.html.j2", **p if p else {}),
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
def send_notification(self, receiver: UUID, sender: UUID, notification_type: ArgusNotificationTypes, source_type: ArgusNotificationSourceTypes,
|
|
107
|
+
source_id: UUID, title: str | None = None, content: str | None = None, content_params: dict | None = None) -> ArgusNotification:
|
|
108
|
+
new_notification = ArgusNotification()
|
|
109
|
+
for user in [sender, receiver]:
|
|
110
|
+
if not self._check_user(user_id=user):
|
|
111
|
+
raise NotificationManagerException(f"UserId {user} not found in the database", user)
|
|
112
|
+
|
|
113
|
+
new_notification.sender = sender
|
|
114
|
+
new_notification.receiver = receiver
|
|
115
|
+
new_notification.type = notification_type.value
|
|
116
|
+
new_notification.source_type = source_type.value
|
|
117
|
+
new_notification.source_id = source_id
|
|
118
|
+
new_notification.title = title if title else self._get_title_for_notification_type(notification_type)
|
|
119
|
+
new_notification.content = content if not content_params else self._render_content(
|
|
120
|
+
notification_type, content_params)
|
|
121
|
+
return new_notification.save()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class EmailNotificationServiceSender(NotificationSenderBase):
|
|
125
|
+
CONTENT_TEMPLATES = {
|
|
126
|
+
ArgusNotificationTypes.Mention: lambda p: render_template(
|
|
127
|
+
"notifications/email_mention.html.j2", **p if p else {}),
|
|
128
|
+
ArgusNotificationTypes.AssigneeChange: lambda p: render_template("notifications/assigned_email.html.j2", **p if p else {}),
|
|
129
|
+
ArgusNotificationTypes.ViewActionItemAssignee: lambda p: render_template("notifications/view_action_item_assigned_email.html.j2", **p if p else {}),
|
|
130
|
+
ArgusNotificationTypes.ViewHighlightMention: lambda p: render_template(
|
|
131
|
+
"notifications/view_highlight_mention_email.html.j2", **p if p else {}),
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
def __init__(self):
|
|
135
|
+
self.email = Email(init_connection=False)
|
|
136
|
+
|
|
137
|
+
def get_user(self, user_id: UUID) -> User:
|
|
138
|
+
return User.get(id=user_id)
|
|
139
|
+
|
|
140
|
+
def send_notification(self, receiver: UUID, sender: UUID,
|
|
141
|
+
notification_type: ArgusNotificationTypes, source_type: ArgusNotificationSourceTypes,
|
|
142
|
+
source_id: UUID, title: str | None = None, content: str | None = None, content_params: dict | None = None):
|
|
143
|
+
try:
|
|
144
|
+
content_params = content_params or {}
|
|
145
|
+
receiver_user = self.get_user(receiver)
|
|
146
|
+
sender_user = self.get_user(sender)
|
|
147
|
+
subject = title if title else self._get_title_for_notification_type(notification_type)
|
|
148
|
+
content_params.update({
|
|
149
|
+
"sender": sender_user.full_name,
|
|
150
|
+
"message": content,
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
email_content = self._render_content(notification_type, content_params)
|
|
154
|
+
|
|
155
|
+
self.email.send(subject=f"Argus Notification - {subject}",
|
|
156
|
+
content=email_content,
|
|
157
|
+
recipients=[receiver_user.email])
|
|
158
|
+
except Exception as details:
|
|
159
|
+
current_app.logger.error("Failed to send email: %s", details)
|