diff --git a/conf/controller/scoring.sql b/conf/controller/scoring.sql index 7c1bd0c..08503a4 100644 --- a/conf/controller/scoring.sql +++ b/conf/controller/scoring.sql @@ -28,22 +28,22 @@ WITH FROM flagdefense GROUP BY team_id, service_id ), - sla_ok AS ( - SELECT count(*) as sla_ok, - team_id, - service_id - FROM scoring_statuscheck - WHERE status = 0 - GROUP BY team_id, service_id - ), - sla_recover AS ( - SELECT 0.5 * count(*) as sla_recover, - team_id, - service_id - FROM scoring_statuscheck - WHERE status = 4 - GROUP BY team_id, service_id - ), +-- sla_ok AS ( +-- SELECT count(*) as sla_ok, +-- team_id, +-- service_id +-- FROM scoring_statuscheck +-- WHERE status = 0 +-- GROUP BY team_id, service_id +-- ), +-- sla_recover AS ( +-- SELECT 0.5 * count(*) as sla_recover, +-- team_id, +-- service_id +-- FROM scoring_statuscheck +-- WHERE status = 4 +-- GROUP BY team_id, service_id +-- ), teams as ( SELECT user_id as team_id FROM registration_team @@ -51,26 +51,98 @@ WITH WHERE is_active = true AND nop_team = false ), - sla AS ( +-- sla AS ( +-- SELECT (SELECT sqrt(count(*)) FROM teams) * (coalesce(sla_ok, 0) + coalesce(sla_recover, 0)) as sla, +-- team_id, +-- service_id +-- FROM sla_ok +-- NATURAL FULL OUTER JOIN sla_recover +-- ), + fill AS ( + SELECT team_id, scoring_servicegroup.id AS service_group_id + FROM teams, scoring_servicegroup + ), + servicegroup AS ( + SELECT scoring_service.service_group_id AS service_group_id, + count(scoring_service.id) AS services_count + FROM scoring_service + GROUP BY scoring_service.service_group_id + ), + attack_by_servicegroup AS ( + SELECT team_id, + scoring_service.service_group_id AS service_group_id, + sum(attack) AS attack, + sum(bonus) AS bonus + FROM attack + INNER JOIN scoring_service ON scoring_service.id = attack.service_id + GROUP BY team_id, scoring_service.service_group_id + ), + defense_by_servicegroup AS ( + SELECT team_id, + scoring_service.service_group_id AS service_group_id, + sum(defense) AS defense + FROM defense + INNER JOIN scoring_service ON scoring_service.id = defense.service_id + GROUP BY team_id, scoring_service.service_group_id + ), + sla_ok_by_servicegroup_tick AS ( + SELECT team_id, + scoring_service.service_group_id AS service_group_id, + 0.5 * servicegroup.services_count * (COUNT(*) = servicegroup.services_count)::int AS sla_ok + FROM scoring_statuscheck + INNER JOIN scoring_service ON scoring_service.id = scoring_statuscheck.service_id + INNER JOIN servicegroup ON servicegroup.service_group_id = scoring_service.service_group_id + WHERE scoring_statuscheck.status = 0 + GROUP BY team_id, scoring_service.service_group_id, servicegroup.services_count, scoring_statuscheck.tick + ), + sla_ok_by_servicegroup AS ( + SELECT team_id, + service_group_id, + sum(sla_ok) AS sla_ok + FROM sla_ok_by_servicegroup_tick + GROUP BY team_id, service_group_id + ), + sla_recover_by_servicegroup_tick AS ( + SELECT team_id, + scoring_service.service_group_id AS service_group_id, + 0.5 * servicegroup.services_count * (COUNT(*) = servicegroup.services_count)::int AS sla_recover + FROM scoring_statuscheck + INNER JOIN scoring_service ON scoring_service.id = scoring_statuscheck.service_id + INNER JOIN servicegroup ON servicegroup.service_group_id = scoring_service.service_group_id + WHERE scoring_statuscheck.status = 4 OR scoring_statuscheck.status = 0 + GROUP BY team_id, scoring_service.service_group_id, servicegroup.services_count, scoring_statuscheck.tick + ), + sla_recover_by_servicegroup AS ( + SELECT team_id, + service_group_id, + sum(sla_recover) AS sla_recover + FROM sla_recover_by_servicegroup_tick + GROUP BY team_id, service_group_id + ), + sla_by_servicegroup AS ( SELECT (SELECT sqrt(count(*)) FROM teams) * (coalesce(sla_ok, 0) + coalesce(sla_recover, 0)) as sla, team_id, - service_id - FROM sla_ok - NATURAL FULL OUTER JOIN sla_recover - ), - fill AS ( - SELECT team_id, scoring_service.id AS service_id - FROM teams, scoring_service + service_group_id + FROM sla_ok_by_servicegroup + NATURAL FULL OUTER JOIN sla_recover_by_servicegroup +-- ), +-- sla_by_servicegroup AS ( -- alternative sla without interplay between servicegroups +-- SELECT team_id, +-- scoring_service.service_group_id AS service_group_id, +-- sum(sla) AS sla +-- FROM sla +-- INNER JOIN scoring_service ON scoring_service.id = sla.service_id +-- GROUP BY team_id, scoring_service.service_group_id ) SELECT team_id, - service_id, + service_group_id, (coalesce(attack, 0)+coalesce(bonus, 0))::double precision as attack, coalesce(bonus, 0) as bonus, coalesce(defense, 0)::double precision as defense, coalesce(sla, 0) as sla, coalesce(attack, 0) + coalesce(defense, 0) + coalesce(bonus, 0) + coalesce(sla, 0) as total -FROM attack -NATURAL FULL OUTER JOIN defense -NATURAL FULL OUTER JOIN sla +FROM attack_by_servicegroup +NATURAL FULL OUTER JOIN defense_by_servicegroup +NATURAL FULL OUTER JOIN sla_by_servicegroup NATURAL INNER JOIN fill -ORDER BY team_id, service_id; +ORDER BY team_id, service_group_id; diff --git a/examples/checker/example_checker.py b/examples/checker/example_checker.py index 393fdf3..dce623d 100755 --- a/examples/checker/example_checker.py +++ b/examples/checker/example_checker.py @@ -19,13 +19,13 @@ def place_flag(self, tick): logging.info('Received response to SET command: %s', repr(resp)) except UnicodeDecodeError: logging.warning('Received non-UTF-8 data: %s', repr(resp)) - return checkerlib.CheckResult.FAULTY + return checkerlib.CheckResult.FAULTY, 'Received non-UTF-8 data' if resp != 'OK': logging.warning('Received wrong response to SET command') - return checkerlib.CheckResult.FAULTY + return checkerlib.CheckResult.FAULTY, 'Received wrong response to SET command' conn.close() - return checkerlib.CheckResult.OK + return checkerlib.CheckResult.OK, '' def check_service(self): conn = connect(self.ip) @@ -37,10 +37,10 @@ def check_service(self): logging.info('Received response to dummy command') except UnicodeDecodeError: logging.warning('Received non-UTF-8 data') - return checkerlib.CheckResult.FAULTY + return checkerlib.CheckResult.FAULTY, 'Received non-UTF-8 data' conn.close() - return checkerlib.CheckResult.OK + return checkerlib.CheckResult.OK, '' def check_flag(self, tick): flag = checkerlib.get_flag(tick) @@ -54,13 +54,13 @@ def check_flag(self, tick): logging.info('Received response to GET command: %s', repr(resp)) except UnicodeDecodeError: logging.warning('Received non-UTF-8 data: %s', repr(resp)) - return checkerlib.CheckResult.FAULTY + return checkerlib.CheckResult.FAULTY, 'Received non-UTF-8 data' if resp != flag: logging.warning('Received wrong response to GET command') - return checkerlib.CheckResult.FLAG_NOT_FOUND + return checkerlib.CheckResult.FLAG_NOT_FOUND, 'Received wrong response to GET command' conn.close() - return checkerlib.CheckResult.OK + return checkerlib.CheckResult.OK, '' def connect(ip): diff --git a/src/ctf_gameserver/checker/database.py b/src/ctf_gameserver/checker/database.py index 8cb7926..26b4a14 100644 --- a/src/ctf_gameserver/checker/database.py +++ b/src/ctf_gameserver/checker/database.py @@ -73,7 +73,11 @@ def get_check_duration(db_conn, service_id, std_dev_count, prohibit_changes=Fals ' WHERE service_id = %s AND tick < current_tick', (std_dev_count, service_id)) result = cursor.fetchone() - return result[0] + if result and len(result) == 1 and result[0] != None: + return float(result[0]) + else: + logging.warning('could not calculate the average script runtime in get_check_duration') + return 20 def get_task_count(db_conn, service_id, prohibit_changes=False): @@ -152,7 +156,7 @@ def _net_no_to_team_id(cursor, team_net_no, fake_team_id): return data[0] -def commit_result(db_conn, service_id, team_net_no, tick, result, prohibit_changes=False, fake_team_id=None): +def commit_result(db_conn, service_id, team_net_no, tick, result, message='', prohibit_changes=False, fake_team_id=None): """ Saves the result from a Checker run to game database. """ @@ -164,8 +168,8 @@ def commit_result(db_conn, service_id, team_net_no, tick, result, prohibit_chang return cursor.execute('INSERT INTO scoring_statuscheck' - ' (service_id, team_id, tick, status, timestamp)' - ' VALUES (%s, %s, %s, %s, NOW())', (service_id, team_id, tick, result)) + ' (service_id, team_id, tick, status, timestamp, message)' + ' VALUES (%s, %s, %s, %s, NOW(), %s)', (service_id, team_id, tick, result, message)) # (In case of `prohibit_changes`,) PostgreSQL checks the database grants even if nothing is matched # by `WHERE` cursor.execute('UPDATE scoring_flag' diff --git a/src/ctf_gameserver/checker/master.py b/src/ctf_gameserver/checker/master.py index c45af84..93ef25a 100644 --- a/src/ctf_gameserver/checker/master.py +++ b/src/ctf_gameserver/checker/master.py @@ -311,24 +311,32 @@ def handle_store_request(self, task_info, params): def handle_result_request(self, task_info, param): try: - result = int(param) - except ValueError: + result = int(param['value']) + except ValueError | KeyError: logging.error('Invalid result from Checker Script for team %d (net number %d) in tick %d: %s', task_info['_team_id'], task_info['team'], task_info['tick'], param) return try: - check_result = CheckResult(result) + check_result = CheckResult(param['value']) except ValueError: logging.error('Invalid result from Checker Script for team %d (net number %d) in tick %d: %d', task_info['_team_id'], task_info['team'], task_info['tick'], result) return - logging.info('Result from Checker Script for team %d (net number %d) in tick %d: %s', - task_info['_team_id'], task_info['team'], task_info['tick'], check_result) + try: + message = str(param['message']) + except ValueError | KeyError: + logging.error('Invalid result from Checker Script for team %d (net number %d) in tick %d: %s', + task_info['_team_id'], task_info['team'], task_info['tick'], param) + return + + + logging.info('Result from Checker Script for team %d (net number %d) in tick %d: %s, msg: %s', + task_info['_team_id'], task_info['team'], task_info['tick'], check_result, message) metrics.inc(self.metrics_queue, 'completed_tasks', labels={'result': check_result.name}) database.commit_result(self.db_conn, self.service['id'], task_info['team'], task_info['tick'], - result) + result, message) def launch_tasks(self): def change_tick(new_tick): diff --git a/src/ctf_gameserver/checker/supervisor.py b/src/ctf_gameserver/checker/supervisor.py index 6d9c852..1478db5 100644 --- a/src/ctf_gameserver/checker/supervisor.py +++ b/src/ctf_gameserver/checker/supervisor.py @@ -361,13 +361,12 @@ def handle_script_message(message, ctrlin_fd, runner_id, queue_to_master, pipe_f if action == ACTION_RESULT: try: - result = CheckResult(int(param)) - except ValueError: + result = CheckResult(int(param['value'])) + script_logger.info('[RUNNER] Checker Script result: %s, msg: %s', result.name, param['message'], + extra={'result': result.value}) + except ValueError | KeyError: # Ignore malformed message from the Checker Script, will be logged by the Master pass - else: - script_logger.info('[RUNNER] Checker Script result: %s', result.name, - extra={'result': result.value}) queue_to_master.put((runner_id, action, param)) response = pipe_from_master.recv() diff --git a/src/ctf_gameserver/checkerlib/__init__.py b/src/ctf_gameserver/checkerlib/__init__.py index 0aacdda..5a7f82c 100644 --- a/src/ctf_gameserver/checkerlib/__init__.py +++ b/src/ctf_gameserver/checkerlib/__init__.py @@ -1 +1 @@ -from .lib import BaseChecker, CheckResult, get_flag, set_flagid, load_state, run_check, store_state +from .lib import BaseChecker, CheckResult, get_flag, set_flagid, get_flagid, load_state, run_check, store_state diff --git a/src/ctf_gameserver/checkerlib/lib.py b/src/ctf_gameserver/checkerlib/lib.py index b66b7f8..d82860f 100644 --- a/src/ctf_gameserver/checkerlib/lib.py +++ b/src/ctf_gameserver/checkerlib/lib.py @@ -12,7 +12,7 @@ import ssl import sys import threading -from typing import Any, Type +from typing import Any, Type, Tuple import ctf_gameserver.lib.flag from ctf_gameserver.lib.checkresult import CheckResult @@ -104,13 +104,13 @@ def __init__(self, ip: str, team: int) -> None: self.ip = ip self.team = team - def place_flag(self, tick: int) -> CheckResult: + def place_flag(self, tick: int) -> Tuple[CheckResult, str]: raise NotImplementedError('place_flag() must be implemented by the subclass') - def check_service(self) -> CheckResult: + def check_service(self) -> Tuple[CheckResult, str]: raise NotImplementedError('check_service() must be implemented by the subclass') - def check_flag(self, tick: int) -> CheckResult: + def check_flag(self, tick: int) -> Tuple[CheckResult, str]: raise NotImplementedError('check_flag() must be implemented by the subclass') @@ -151,7 +151,17 @@ def set_flagid(data: str) -> None: # Wait for acknowledgement _recv_ctrl_message() else: - print('Storing Flag ID: {}'.format(data)) + logging.info('Storing Flag ID: {}'.format(data)) + + store_state(f'__flagid_{tick}', data) + + +def get_flagid(tick: int) -> str: + """ + Allows to retrieve Flag ID for the current team and tick. + """ + + return load_state(f'__flagid_{tick}') def store_state(key: str, data: Any) -> None: @@ -214,6 +224,7 @@ def run_check(checker_cls: Type[BaseChecker]) -> None: """ Launch execution of the specified Checker implementation. Must be called by all Checker Scripts. """ + global tick if len(sys.argv) != 4: raise Exception('Invalid arguments, usage: {} '.format(sys.argv[0])) @@ -230,58 +241,66 @@ def run_check(checker_cls: Type[BaseChecker]) -> None: get_flag._team = team # pylint: disable=protected-access checker = checker_cls(ip, team) - result = _run_check_steps(checker, tick) + result, phase, message = _run_check_steps(checker, tick) + + msg = f'{phase}{message}' if not _launched_without_runner(): - _send_ctrl_message({'action': 'RESULT', 'param': result.value}) + _send_ctrl_message({'action': 'RESULT', 'param': { + 'value': result.value, + 'message': msg, + }}) # Wait for acknowledgement _recv_ctrl_message() else: - print('Check result: {}'.format(result)) + print('Check result: {}, message: "{}"'.format(result, msg)) def _run_check_steps(checker, tick): tick_lookback = 5 + phase = 'placing flag: ' try: logging.info('Placing flag') - result = checker.place_flag(tick) + result, message = checker.place_flag(tick) logging.info('Flag placement result: %s', result) if result != CheckResult.OK: - return result + return result, phase, message + phase = 'checking service: ' logging.info('Checking service') - result = checker.check_service() + result, message = checker.check_service() logging.info('Service check result: %s', result) if result != CheckResult.OK: - return result + return result, phase, message + phase = 'checking flag: ' current_tick = tick oldest_tick = max(tick-tick_lookback, 0) recovering = False while current_tick >= oldest_tick: logging.info('Checking flag of tick %d', current_tick) - result = checker.check_flag(current_tick) + result, message = checker.check_flag(current_tick) logging.info('Flag check result of tick %d: %s', current_tick, result) if result != CheckResult.OK: if current_tick != tick and result == CheckResult.FLAG_NOT_FOUND: recovering = True else: - return result + return result, phase, message current_tick -= 1 if recovering: - return CheckResult.RECOVERING + return CheckResult.RECOVERING, phase, message else: - return CheckResult.OK + return CheckResult.OK, '', message except Exception as e: # pylint: disable=broad-except if _is_conn_error(e): logging.warning('Connection error during check', exc_info=e) - return CheckResult.DOWN + return CheckResult.DOWN, phase, 'connection timed out' else: - # Just let the Checker Script die, logging will be handled by the Runner - raise e + logging.exception('Exception in checker') + return CheckResult.DOWN, phase, 'checker error' def _launched_without_runner(): diff --git a/src/ctf_gameserver/web/scoring/admin.py b/src/ctf_gameserver/web/scoring/admin.py index 1dc0375..f8d98e0 100644 --- a/src/ctf_gameserver/web/scoring/admin.py +++ b/src/ctf_gameserver/web/scoring/admin.py @@ -6,6 +6,12 @@ from . import models, forms +@admin.register(models.ServiceGroup, site=admin_site) +class ServiceGroupAdmin(admin.ModelAdmin): + + prepopulated_fields = {'slug': ('name',)} + + @admin.register(models.Service, site=admin_site) class ServiceAdmin(admin.ModelAdmin): @@ -67,8 +73,8 @@ def tick(self, capture): @admin.register(models.StatusCheck, site=admin_site) class StatusCheckAdmin(admin.ModelAdmin): - list_display = ('id', 'service', 'team', 'tick', 'status') - list_filter = ('service', 'tick', 'status') + list_display = ('id', 'service', 'team', 'tick', 'status', 'message') + list_filter = ('service', 'tick', 'status', 'message') search_fields = ('service__name', 'team__user__username') ordering = ('tick', 'timestamp') diff --git a/src/ctf_gameserver/web/scoring/calculations.py b/src/ctf_gameserver/web/scoring/calculations.py index cc1307a..45bb0aa 100644 --- a/src/ctf_gameserver/web/scoring/calculations.py +++ b/src/ctf_gameserver/web/scoring/calculations.py @@ -6,6 +6,18 @@ from . import models +STATUS_PRIORITY = [ + 0, # up + 4, # recovering + 3, # flag not found + 2, # faulty + 1, # down + -1, # not checked +] + +STATUS_PRIORITY_DICT = {n:i for i,n in enumerate(STATUS_PRIORITY)} + + def scores(select_related_fields=None, only_fields=None): """ Returns the scores as currently stored in the database as an OrderedDict in this format: @@ -24,11 +36,11 @@ def scores(select_related_fields=None, only_fields=None): if select_related_fields is None: select_related_fields = [] - select_related_fields = list(set(select_related_fields + ['service', 'team'])) + select_related_fields = list(set(select_related_fields + ['service_group', 'team'])) if only_fields is None: only_fields = [] only_fields = list(set(only_fields + - ['attack', 'defense', 'sla', 'total', 'service__id', 'team__user__id'])) + ['attack', 'defense', 'sla', 'total', 'service_group__id', 'team__user__id'])) # No good way to invalidate the cache, so use a generic key with a short timeout cache_key = 'scores' @@ -40,11 +52,11 @@ def scores(select_related_fields=None, only_fields=None): team_scores = defaultdict(lambda: {'offense': [{}, 0], 'defense': [{}, 0], 'sla': [{}, 0], 'total': 0}) for score in models.ScoreBoard.objects.select_related(*select_related_fields).only(*only_fields).all(): - team_scores[score.team]['offense'][0][score.service] = score.attack + team_scores[score.team]['offense'][0][score.service_group] = score.attack team_scores[score.team]['offense'][1] += score.attack - team_scores[score.team]['defense'][0][score.service] = score.defense + team_scores[score.team]['defense'][0][score.service_group] = score.defense team_scores[score.team]['defense'][1] += score.defense - team_scores[score.team]['sla'][0][score.service] = score.sla + team_scores[score.team]['sla'][0][score.service_group] = score.sla team_scores[score.team]['sla'][1] += score.sla team_scores[score.team]['total'] += score.total @@ -88,13 +100,39 @@ def team_statuses(from_tick, to_tick, select_related_team_fields=None, only_team for team in team_qset.order_by('user__username').all(): statuses[team] = defaultdict(lambda: {}) teams[team.pk] = team + for tick in range(from_tick, to_tick+1): + statuses[team][tick] = defaultdict(lambda: {}) + + service_to_group = {} + services_by_group = {} + + for service in models.Service.objects.all(): + service_to_group[service.id] = service.service_group.id + if service.service_group.id in services_by_group: + services_by_group[service.service_group.id].append(service.id) + else: + services_by_group[service.service_group.id] = [service.id] status_checks = models.StatusCheck.objects.filter(tick__gte=from_tick, tick__lte=to_tick) for check in status_checks: - statuses[teams[check.team_id]][check.tick][check.service_id] = check.status + statuses[teams[check.team_id]][check.tick][service_to_group[check.service_id]][check.service_id] = (check.status, check.message) + + for team in statuses.values(): + for team_tick in team.values(): + for group, services in services_by_group.items(): + joint_status = 0 + for service in services: + if service not in team_tick[group]: + team_tick[group][service] = (-1, '') + status, _ = team_tick[group][service] + if STATUS_PRIORITY_DICT[status] > STATUS_PRIORITY_DICT[joint_status]: + joint_status = status + team_tick[group][-1] = joint_status # Convert defaultdicts to dicts because serialization in `cache.set()` can't handle them otherwise for key, val in statuses.items(): + for key2, val2 in val.items(): + statuses[key][key2] = dict(val2) statuses[key] = dict(val) cache.set(cache_key, statuses, 10) diff --git a/src/ctf_gameserver/web/scoring/models.py b/src/ctf_gameserver/web/scoring/models.py index 8e433cb..65d8aa1 100644 --- a/src/ctf_gameserver/web/scoring/models.py +++ b/src/ctf_gameserver/web/scoring/models.py @@ -6,6 +6,17 @@ from ctf_gameserver.web.registration.models import Team +class ServiceGroup(models.Model): + """ + Database representation of a service from the competition. + """ + + name = models.CharField(max_length=30, unique=True) + slug = models.SlugField(max_length=30, unique=True) + + def __str__(self): + return self.name + class Service(models.Model): """ Database representation of a service from the competition. @@ -13,6 +24,7 @@ class Service(models.Model): name = models.CharField(max_length=30, unique=True) slug = models.SlugField(max_length=30, unique=True, help_text=_('Simplified name for use in paths')) + service_group = models.ForeignKey(ServiceGroup, on_delete=models.CASCADE) def __str__(self): # pylint: disable=invalid-str-returned return self.name @@ -87,6 +99,7 @@ class StatusCheck(models.Model): # REVISIT: Add check constraint for the values as soon as we have Django >= 2.2 status = models.PositiveSmallIntegerField(choices=[(i, t) for t, i in STATUSES.items()]) timestamp = models.DateTimeField(auto_now_add=True) + message = models.CharField(max_length=100) class Meta: unique_together = ('service', 'team', 'tick') @@ -106,7 +119,7 @@ class ScoreBoard(models.Model): read-only from within the website. """ team = models.OneToOneField(Team, editable=False, primary_key=True, on_delete=models.PROTECT) - service = models.OneToOneField(Service, editable=False, on_delete=models.PROTECT) + service_group = models.OneToOneField(ServiceGroup, editable=False, on_delete=models.PROTECT) attack = models.FloatField(editable=False) bonus = models.FloatField(editable=False) defense = models.FloatField(editable=False) @@ -150,6 +163,7 @@ class GameControl(models.Model): services_public = models.DateTimeField(null=True) start = models.DateTimeField(null=True) end = models.DateTimeField(null=True) + freeze = models.DateTimeField(null=True) # Tick duration in seconds tick_duration = models.PositiveSmallIntegerField(default=180) # Number of ticks a flag is valid for including the one it was generated in @@ -206,6 +220,15 @@ def competition_started(self): return self.start <= timezone.now() + def competition_frozen(self): + """ + Indicates whether the competition scoreboard is frozen. + """ + if self.start is None or self.end is None or self.freeze is None: + return False + + return self.freeze <= timezone.now() + def competition_over(self): """ Indicates whether the competition is already over. diff --git a/src/ctf_gameserver/web/scoring/templates/scoreboard.html b/src/ctf_gameserver/web/scoring/templates/scoreboard.html index b6cd7ca..31c72ee 100644 --- a/src/ctf_gameserver/web/scoring/templates/scoreboard.html +++ b/src/ctf_gameserver/web/scoring/templates/scoreboard.html @@ -78,6 +78,12 @@

{% block title %}{% trans 'Scoreboard' %}{% endblock %}

SLA
+ + + + + {% trans 'not checked' %} diff --git a/src/ctf_gameserver/web/scoring/views.py b/src/ctf_gameserver/web/scoring/views.py index 1dc1817..129eef5 100644 --- a/src/ctf_gameserver/web/scoring/views.py +++ b/src/ctf_gameserver/web/scoring/views.py @@ -17,7 +17,7 @@ def scoreboard(request): return render(request, 'scoreboard.html', { - 'services': models.Service.objects.all() + 'services': models.ServiceGroup.objects.all() }) @@ -33,10 +33,10 @@ def scoreboard_json(_): else: to_tick = game_control.current_tick - 1 - scores = calculations.scores(['team', 'team__user', 'service'], - ['team__image', 'team__user__username', 'service__name']) + scores = calculations.scores(['team', 'team__user', 'service_group'], + ['team__image', 'team__user__username', 'service_group__name']) statuses = calculations.team_statuses(to_tick, to_tick, only_team_fields=['user_id']) - services = models.Service.objects.all() + services = models.ServiceGroup.objects.all() response = { 'tick': to_tick, @@ -68,15 +68,24 @@ def scoreboard_json(_): offense = 0 defense = 0 sla = 0 + flagstores = [] try: - status = statuses[team][to_tick][service.pk] - except KeyError: + service_statuses = statuses[team][to_tick][service.pk] + status = service_statuses[-1] + for key in sorted(service_statuses.keys()): + if key == -1: + continue + flagstores.append(service_statuses[key]) + except KeyError as e: + import traceback + traceback.print_exc() status = '' team_entry['services'].append({ 'status': status, 'offense': offense, 'defense': defense, - 'sla': sla + 'sla': sla, + 'flagstores': flagstores }) response['teams'].append(team_entry) @@ -130,7 +139,7 @@ def service_status_json(_): statuses = calculations.team_statuses(from_tick, to_tick, ['user'], ['image', 'nop_team', 'user__username']) - services = models.Service.objects.all().order_by('name') + services = models.ServiceGroup.objects.all().order_by('name') response = { 'ticks': list(range(from_tick, to_tick+1)), @@ -154,7 +163,7 @@ def service_status_json(_): tick_services = [] for service in services: try: - tick_services.append(tick_statuses[tick][service.pk]) + tick_services.append(tick_statuses[tick][service.pk][-1]) except KeyError: tick_services.append('') team_entry['ticks'].append(tick_services) @@ -178,7 +187,7 @@ def teams_json(_): game_control = models.GameControl.get_instance() # Only publish Flag IDs after the respective Tick is over flagid_max_tick = game_control.current_tick - 1 - flagid_min_tick = flagid_max_tick - game_control.valid_ticks + flagid_min_tick = flagid_max_tick - game_control.valid_ticks + 2 flag_ids = defaultdict(lambda: defaultdict(lambda: [])) for flag in models.Flag.objects.exclude(flagid=None) \ diff --git a/src/ctf_gameserver/web/static/scoreboard.js b/src/ctf_gameserver/web/static/scoreboard.js index fbca811..835121c 100644 --- a/src/ctf_gameserver/web/static/scoreboard.js +++ b/src/ctf_gameserver/web/static/scoreboard.js @@ -44,16 +44,39 @@ function buildTable(data) { const service_node = service_template.cloneNode(true) const spans = service_node.querySelectorAll('span') + const as = service_node.querySelectorAll('a') spans[2].textContent = service['offense'].toFixed(2) spans[5].textContent = service['defense'].toFixed(2) spans[8].textContent = service['sla'].toFixed(2) - service_node.querySelector('a').href += `#team-${team.id}-row` + as[1].href += `#team-${team.id}-row` if (service['status'] !== '') { + console.log(service) + console.log(service['flagstores']) + // TODO move up + const flagstore_classes = new Map(); + flagstore_classes.set(-1, 'glyphicon glyphicon-question-sign text-muted'); // NOT_CHECKED + flagstore_classes.set(0, 'glyphicon glyphicon-ok-sign text-success'); // OK + flagstore_classes.set(1, 'glyphicon glyphicon-minus-sign text-danger'); // DOWN + flagstore_classes.set(2, 'glyphicon glyphicon-exclamation-sign text-danger'); // FAULTY + flagstore_classes.set(3, 'glyphicon glyphicon-question-sign text-warning'); // FLAG_NOT_FOUND + flagstore_classes.set(4, 'glyphicon glyphicon-plus-sign text-info'); // RECOVERING + + var flagstore_id = 1; + for (const flagstore of service['flagstores']) { + const fs_box = as[0].cloneNode(true) + fs_box.firstElementChild.setAttribute('class', flagstore_classes.get(flagstore[0])) + fs_box.setAttribute('title', `Flagstore ${flagstore_id}`) + fs_box.setAttribute('data-content', flagstore[1] === '' ? 'up' : flagstore[1]) + spans[9].appendChild(fs_box) + flagstore_id++ + } + spans[9].removeChild(as[0]) + const statusClass = statusClasses[service['status']] - spans[9].setAttribute('class', `text-${statusClass}`) - spans[9].textContent = statusDescriptions[service['status']] + spans[11].setAttribute('class', `text-${statusClass}`) + spans[11].textContent = statusDescriptions[service['status']] service_node.setAttribute('class', statusClass) } @@ -74,4 +97,9 @@ function buildTable(data) { template.parentNode.appendChild(entry) entry.hidden = false } + + setTimeout(function () { + //$('[data-toggle="tooltip"]').tooltip() + $('[data-toggle="popover"]').popover() + }, 1000) } diff --git a/src/ctf_gameserver/web/static/style.css b/src/ctf_gameserver/web/static/style.css index e37e152..a337dbb 100644 --- a/src/ctf_gameserver/web/static/style.css +++ b/src/ctf_gameserver/web/static/style.css @@ -76,6 +76,15 @@ img#load-spinner, button#refresh { display: block; } +img#load-spinner { + width: 1.7em; +} + +img#load-spinner, +button#refresh { + margin-right: 20px; +} + #history-table td a:hover { text-decoration: none; } @@ -84,3 +93,15 @@ img#load-spinner, button#refresh { margin-top: 20px; padding-left: 20px; } + +.flagstore-group { + margin-left: -2px; +} + +.flagstore { + margin: 2px; +} + +.flagstore:hover { + text-decoration: none; +}