• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

scoringengine / scoringengine / 23385248069

21 Mar 2026 05:50PM UTC coverage: 73.202% (-2.5%) from 75.69%
23385248069

push

github

RustyBower
Fix test to match DB fallback behavior for missing output files

The endpoint now returns check.output from DB (200) instead of 404
when the on-disk file doesn't exist.

3726 of 5090 relevant lines covered (73.2%)

0.73 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

35.65
/scoring_engine/web/views/api/sla.py
1
"""
2
SLA API endpoints for managing SLA penalties and dynamic scoring settings.
3
"""
4

5
from flask import flash, jsonify, redirect, request, url_for
1✔
6
from flask_login import current_user, login_required
1✔
7

8
from scoring_engine.cache import cache
1✔
9
from scoring_engine.cache_helper import update_overview_data, update_scoreboard_data, update_sla_data
1✔
10
from scoring_engine.db import db
1✔
11
from scoring_engine.models.setting import Setting
1✔
12
from scoring_engine.models.team import Team
1✔
13
from scoring_engine.sla import (
1✔
14
    calculate_round_multiplier,
15
    get_dynamic_scoring_info,
16
    get_sla_config,
17
    get_team_sla_summary,
18
)
19

20
from . import make_cache_key, mod
1✔
21

22
# ============================================================================
23
# SLA Summary Endpoints (Public for authenticated users)
24
# ============================================================================
25

26

27
@mod.route("/api/sla/summary")
1✔
28
@login_required
1✔
29
@cache.cached(make_cache_key=make_cache_key)
1✔
30
def sla_summary():
1✔
31
    """Get SLA summary for all blue teams."""
32
    config = get_sla_config()
1✔
33
    blue_teams = Team.get_all_blue_teams()
1✔
34

35
    teams_summary = []
1✔
36
    for team in blue_teams:
1✔
37
        summary = get_team_sla_summary(team, config)
1✔
38
        teams_summary.append(summary)
1✔
39

40
    return jsonify(
1✔
41
        {
42
            "sla_enabled": config.sla_enabled,
43
            "penalty_threshold": config.penalty_threshold,
44
            "penalty_mode": config.penalty_mode,
45
            "teams": teams_summary,
46
        }
47
    )
48

49

50
@mod.route("/api/sla/team/<int:team_id>")
1✔
51
@login_required
1✔
52
@cache.cached(make_cache_key=make_cache_key)
1✔
53
def sla_team_detail(team_id):
1✔
54
    """Get detailed SLA information for a specific team."""
55
    team = db.session.get(Team, team_id)
1✔
56
    if not team:
1✔
57
        return jsonify({"error": "Team not found"}), 404
×
58

59
    # Only allow white team or the team itself to view details
60
    if not current_user.is_white_team and current_user.team_id != team_id:
1✔
61
        return jsonify({"error": "Unauthorized"}), 403
×
62

63
    config = get_sla_config()
1✔
64
    summary = get_team_sla_summary(team, config)
1✔
65

66
    return jsonify(summary)
1✔
67

68

69
@mod.route("/api/sla/config")
1✔
70
@login_required
1✔
71
@cache.cached(make_cache_key=make_cache_key)
1✔
72
def sla_config():
1✔
73
    """Get current SLA configuration (admin only)."""
74
    if not current_user.is_white_team:
×
75
        return jsonify({"error": "Unauthorized"}), 403
×
76

77
    config = get_sla_config()
×
78
    return jsonify(
×
79
        {
80
            "sla_enabled": config.sla_enabled,
81
            "penalty_threshold": config.penalty_threshold,
82
            "penalty_percent": config.penalty_percent,
83
            "penalty_max_percent": config.penalty_max_percent,
84
            "penalty_mode": config.penalty_mode,
85
            "allow_negative": config.allow_negative,
86
        }
87
    )
88

89

90
@mod.route("/api/sla/dynamic-scoring")
1✔
91
@login_required
1✔
92
@cache.cached(make_cache_key=make_cache_key)
1✔
93
def dynamic_scoring_info():
1✔
94
    """Get current dynamic scoring configuration."""
95
    config = get_sla_config()
1✔
96
    info = get_dynamic_scoring_info(config)
1✔
97

98
    # Add current round multiplier and phase
99
    from scoring_engine.models.round import Round
1✔
100

101
    current_round = Round.get_last_round_num()
1✔
102
    info["current_round"] = current_round
1✔
103
    info["current_multiplier"] = calculate_round_multiplier(current_round, config)
1✔
104

105
    # Determine current phase based on round number
106
    if current_round <= config.early_rounds:
1✔
107
        info["current_phase"] = "early"
1✔
108
    elif current_round >= config.late_start_round:
×
109
        info["current_phase"] = "late"
×
110
    else:
111
        info["current_phase"] = "normal"
×
112

113
    return jsonify(info)
1✔
114

115

116
# ============================================================================
117
# Admin API Endpoints for SLA Settings
118
# ============================================================================
119

120

121
def _update_setting(name, value, redirect_route="admin.sla"):
1✔
122
    """Helper to update a setting value."""
123
    setting = Setting.get_setting(name)
×
124
    if setting:
×
125
        setting.value = value
×
126
        db.session.add(setting)
×
127
        db.session.commit()
×
128
        Setting.clear_cache(name)
×
129
        # Clear Flask cache for scoreboard/overview when SLA-related settings change
130
        _clear_scoring_cache()
×
131
    return redirect(url_for(redirect_route))
×
132

133

134
def _clear_scoring_cache():
1✔
135
    """Clear Flask cache for scoreboard, overview, and SLA data."""
136
    update_scoreboard_data()
×
137
    update_overview_data()
×
138
    update_sla_data()
×
139

140

141
@mod.route("/api/admin/update_sla_enabled", methods=["POST"])
1✔
142
@login_required
1✔
143
def admin_update_sla_enabled():
1✔
144
    if current_user.is_white_team:
×
145
        setting = Setting.get_setting("sla_enabled")
×
146
        if setting:
×
147
            # Toggle the value
148
            current_val = setting.value
×
149
            if isinstance(current_val, bool):
×
150
                setting.value = not current_val
×
151
            else:
152
                setting.value = str(current_val).lower() not in ("true", "1", "yes")
×
153
            db.session.add(setting)
×
154
            db.session.commit()
×
155
            Setting.clear_cache("sla_enabled")
×
156
            _clear_scoring_cache()
×
157
        return redirect(url_for("admin.sla"))
×
158
    return {"status": "Unauthorized"}, 403
×
159

160

161
@mod.route("/api/admin/update_sla_penalty_threshold", methods=["POST"])
1✔
162
@login_required
1✔
163
def admin_update_sla_penalty_threshold():
1✔
164
    if current_user.is_white_team:
×
165
        if "sla_penalty_threshold" in request.form:
×
166
            value = request.form["sla_penalty_threshold"]
×
167
            if not value.isdigit() or int(value) < 1:
×
168
                flash("Error: Penalty threshold must be a positive integer.", "danger")
×
169
                return redirect(url_for("admin.sla"))
×
170
            return _update_setting("sla_penalty_threshold", value)
×
171
        flash("Error: sla_penalty_threshold not specified.", "danger")
×
172
        return redirect(url_for("admin.sla"))
×
173
    return {"status": "Unauthorized"}, 403
×
174

175

176
@mod.route("/api/admin/update_sla_penalty_percent", methods=["POST"])
1✔
177
@login_required
1✔
178
def admin_update_sla_penalty_percent():
1✔
179
    if current_user.is_white_team:
×
180
        if "sla_penalty_percent" in request.form:
×
181
            value = request.form["sla_penalty_percent"]
×
182
            if not value.isdigit() or int(value) < 1:
×
183
                flash("Error: Penalty percent must be a positive integer.", "danger")
×
184
                return redirect(url_for("admin.sla"))
×
185
            return _update_setting("sla_penalty_percent", value)
×
186
        flash("Error: sla_penalty_percent not specified.", "danger")
×
187
        return redirect(url_for("admin.sla"))
×
188
    return {"status": "Unauthorized"}, 403
×
189

190

191
@mod.route("/api/admin/update_sla_penalty_max_percent", methods=["POST"])
1✔
192
@login_required
1✔
193
def admin_update_sla_penalty_max_percent():
1✔
194
    if current_user.is_white_team:
×
195
        if "sla_penalty_max_percent" in request.form:
×
196
            value = request.form["sla_penalty_max_percent"]
×
197
            if not value.isdigit() or int(value) < 1:
×
198
                flash("Error: Max penalty percent must be a positive integer.", "danger")
×
199
                return redirect(url_for("admin.sla"))
×
200
            return _update_setting("sla_penalty_max_percent", value)
×
201
        flash("Error: sla_penalty_max_percent not specified.", "danger")
×
202
        return redirect(url_for("admin.sla"))
×
203
    return {"status": "Unauthorized"}, 403
×
204

205

206
@mod.route("/api/admin/update_sla_penalty_mode", methods=["POST"])
1✔
207
@login_required
1✔
208
def admin_update_sla_penalty_mode():
1✔
209
    if current_user.is_white_team:
×
210
        if "sla_penalty_mode" in request.form:
×
211
            value = request.form["sla_penalty_mode"]
×
212
            valid_modes = ["additive", "flat", "exponential", "next_check_reduction"]
×
213
            if value not in valid_modes:
×
214
                flash(
×
215
                    f"Error: Penalty mode must be one of: {', '.join(valid_modes)}",
216
                    "danger",
217
                )
218
                return redirect(url_for("admin.sla"))
×
219
            return _update_setting("sla_penalty_mode", value)
×
220
        flash("Error: sla_penalty_mode not specified.", "danger")
×
221
        return redirect(url_for("admin.sla"))
×
222
    return {"status": "Unauthorized"}, 403
×
223

224

225
@mod.route("/api/admin/update_sla_allow_negative", methods=["POST"])
1✔
226
@login_required
1✔
227
def admin_update_sla_allow_negative():
1✔
228
    if current_user.is_white_team:
×
229
        setting = Setting.get_setting("sla_allow_negative")
×
230
        if setting:
×
231
            current_val = setting.value
×
232
            if isinstance(current_val, bool):
×
233
                setting.value = not current_val
×
234
            else:
235
                setting.value = str(current_val).lower() not in ("true", "1", "yes")
×
236
            db.session.add(setting)
×
237
            db.session.commit()
×
238
            Setting.clear_cache("sla_allow_negative")
×
239
            _clear_scoring_cache()
×
240
        return redirect(url_for("admin.sla"))
×
241
    return {"status": "Unauthorized"}, 403
×
242

243

244
# ============================================================================
245
# Admin API Endpoints for Dynamic Scoring Settings
246
# ============================================================================
247

248

249
@mod.route("/api/admin/update_dynamic_scoring_enabled", methods=["POST"])
1✔
250
@login_required
1✔
251
def admin_update_dynamic_scoring_enabled():
1✔
252
    if current_user.is_white_team:
×
253
        setting = Setting.get_setting("dynamic_scoring_enabled")
×
254
        if setting:
×
255
            current_val = setting.value
×
256
            if isinstance(current_val, bool):
×
257
                setting.value = not current_val
×
258
            else:
259
                setting.value = str(current_val).lower() not in ("true", "1", "yes")
×
260
            db.session.add(setting)
×
261
            db.session.commit()
×
262
            Setting.clear_cache("dynamic_scoring_enabled")
×
263
            _clear_scoring_cache()
×
264
        return redirect(url_for("admin.sla"))
×
265
    return {"status": "Unauthorized"}, 403
×
266

267

268
@mod.route("/api/admin/update_dynamic_scoring_early_rounds", methods=["POST"])
1✔
269
@login_required
1✔
270
def admin_update_dynamic_scoring_early_rounds():
1✔
271
    if current_user.is_white_team:
×
272
        if "dynamic_scoring_early_rounds" in request.form:
×
273
            value = request.form["dynamic_scoring_early_rounds"]
×
274
            if not value.isdigit() or int(value) < 1:
×
275
                flash("Error: Early rounds must be a positive integer.", "danger")
×
276
                return redirect(url_for("admin.sla"))
×
277
            return _update_setting("dynamic_scoring_early_rounds", value)
×
278
        flash("Error: dynamic_scoring_early_rounds not specified.", "danger")
×
279
        return redirect(url_for("admin.sla"))
×
280
    return {"status": "Unauthorized"}, 403
×
281

282

283
@mod.route("/api/admin/update_dynamic_scoring_early_multiplier", methods=["POST"])
1✔
284
@login_required
1✔
285
def admin_update_dynamic_scoring_early_multiplier():
1✔
286
    if current_user.is_white_team:
×
287
        if "dynamic_scoring_early_multiplier" in request.form:
×
288
            value = request.form["dynamic_scoring_early_multiplier"]
×
289
            try:
×
290
                float_val = float(value)
×
291
                if float_val <= 0:
×
292
                    raise ValueError("Must be positive")
×
293
            except ValueError:
×
294
                flash("Error: Early multiplier must be a positive number.", "danger")
×
295
                return redirect(url_for("admin.sla"))
×
296
            return _update_setting("dynamic_scoring_early_multiplier", value)
×
297
        flash("Error: dynamic_scoring_early_multiplier not specified.", "danger")
×
298
        return redirect(url_for("admin.sla"))
×
299
    return {"status": "Unauthorized"}, 403
×
300

301

302
@mod.route("/api/admin/update_dynamic_scoring_late_start_round", methods=["POST"])
1✔
303
@login_required
1✔
304
def admin_update_dynamic_scoring_late_start_round():
1✔
305
    if current_user.is_white_team:
×
306
        if "dynamic_scoring_late_start_round" in request.form:
×
307
            value = request.form["dynamic_scoring_late_start_round"]
×
308
            if not value.isdigit() or int(value) < 1:
×
309
                flash("Error: Late start round must be a positive integer.", "danger")
×
310
                return redirect(url_for("admin.sla"))
×
311
            return _update_setting("dynamic_scoring_late_start_round", value)
×
312
        flash("Error: dynamic_scoring_late_start_round not specified.", "danger")
×
313
        return redirect(url_for("admin.sla"))
×
314
    return {"status": "Unauthorized"}, 403
×
315

316

317
@mod.route("/api/admin/update_dynamic_scoring_late_multiplier", methods=["POST"])
1✔
318
@login_required
1✔
319
def admin_update_dynamic_scoring_late_multiplier():
1✔
320
    if current_user.is_white_team:
×
321
        if "dynamic_scoring_late_multiplier" in request.form:
×
322
            value = request.form["dynamic_scoring_late_multiplier"]
×
323
            try:
×
324
                float_val = float(value)
×
325
                if float_val <= 0:
×
326
                    raise ValueError("Must be positive")
×
327
            except ValueError:
×
328
                flash("Error: Late multiplier must be a positive number.", "danger")
×
329
                return redirect(url_for("admin.sla"))
×
330
            return _update_setting("dynamic_scoring_late_multiplier", value)
×
331
        flash("Error: dynamic_scoring_late_multiplier not specified.", "danger")
×
332
        return redirect(url_for("admin.sla"))
×
333
    return {"status": "Unauthorized"}, 403
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc