From c90099ab58be92e7863f307ac449f4607590ce69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20D=C3=B6tsch?= Date: Thu, 18 Dec 2025 10:58:07 +0100 Subject: [PATCH 1/2] polish changes page - filter by attribute - show attribute changes with a toggle - fixed pagination - link on app/user for fast filtering --- serveradmin/common/static/js/serveradmin.js | 2 +- serveradmin/serverdb/query_committer.py | 2 +- .../serverdb/templates/serverdb/changes.html | 106 +++++++++++++++++- serveradmin/serverdb/templatetags/changes.py | 60 +++++++++- serveradmin/serverdb/views.py | 5 + 5 files changed, 166 insertions(+), 9 deletions(-) diff --git a/serveradmin/common/static/js/serveradmin.js b/serveradmin/common/static/js/serveradmin.js index 60dd8d3a5..9cf775103 100644 --- a/serveradmin/common/static/js/serveradmin.js +++ b/serveradmin/common/static/js/serveradmin.js @@ -49,7 +49,7 @@ $(document).ready(function() { form = document.getElementById(formId); } else { - this.closest('form'); + form = this.closest('form'); } if (form) { diff --git a/serveradmin/serverdb/query_committer.py b/serveradmin/serverdb/query_committer.py index 80ece7880..816e03e7c 100644 --- a/serveradmin/serverdb/query_committer.py +++ b/serveradmin/serverdb/query_committer.py @@ -178,7 +178,7 @@ def _validate(attribute_lookup, changed, changed_objects): newer = _validate_commit(changed, changed_objects) if newer: - raise CommitNewerData('Newer data available', newer) + raise CommitNewerData(f'Newer data available for attribute {newer}', newer) def _delete_attributes(attribute_lookup, changed, changed_servers, deleted): diff --git a/serveradmin/serverdb/templates/serverdb/changes.html b/serveradmin/serverdb/templates/serverdb/changes.html index 99fd07f1a..af1fc6a8a 100644 --- a/serveradmin/serverdb/templates/serverdb/changes.html +++ b/serveradmin/serverdb/templates/serverdb/changes.html @@ -4,6 +4,39 @@ {% block title %}Changes{% endblock %} +{% block additional_styles %} + +{% endblock %} + {% block content %}
@@ -57,6 +90,12 @@

+
+ +
+ +
+
@@ -75,7 +114,12 @@

App Owner Created - Changed + + + + + Changed + Deleted @@ -85,8 +129,18 @@

{{ commit.id }} {{ commit.change_on|date:"r" }} {{ commit.change_on|timesince }} - {{ commit.app|default:"Servershell" }} - {{ commit.user }} + + {% if commit.app %} + {{ commit.app }} + {% else %} + Servershell + {% endif %} + + + {% if commit.user %} + {{ commit.user }} + {% endif %} +
    {% for change in commit.change_set.get_queryset %} @@ -100,7 +154,27 @@

      {% for change in commit.change_set.get_queryset %} {% if change.change_type == 'change' %} -
    • History for {{ change.hostname }}
    • +
    • + {% with attr_changes=change|get_attribute_changes %} + {% if attr_changes %} + + {% endif %} + {% endwith %} + History for {{ change.hostname }} + {% with attr_changes=change|get_attribute_changes %} + {% if attr_changes %} +
      +
        + {% for attr_change in attr_changes %} +
      • {{ attr_change }}
      • + {% endfor %} +
      +
      + {% endif %} + {% endwith %} +
    • {% endif %} {% endfor %}
    @@ -127,3 +201,27 @@

{% endblock content %} + +{% block additional_scripts %} + +{% endblock %} diff --git a/serveradmin/serverdb/templatetags/changes.py b/serveradmin/serverdb/templatetags/changes.py index 963b0b5f2..228586ab4 100644 --- a/serveradmin/serverdb/templatetags/changes.py +++ b/serveradmin/serverdb/templatetags/changes.py @@ -1,19 +1,73 @@ """Serveradmin -Copyright (c) 2020 InnoGames GmbH +Copyright (c) 2025 InnoGames GmbH """ +from typing import Any, Union + from django import template from django.core.exceptions import ObjectDoesNotExist +from django.utils.html import escape +from django.utils.safestring import mark_safe -from serveradmin.serverdb.models import Server +from serveradmin.serverdb.models import Change, Server register = template.Library() @register.filter -def hostname(object_id): +def hostname(object_id: int) -> Union[str, int]: try: return Server.objects.get(server_id=object_id).hostname except ObjectDoesNotExist: return object_id + + +def _format_value(value: Any) -> str: + """Format a value for display, handling None/empty as '-'.""" + if value is None or value == '': + return '-' + if isinstance(value, list): + return ', '.join(str(v) for v in value) + if isinstance(value, set): + return ', '.join(str(v) for v in sorted(value)) + return str(value) + + +@register.filter +def get_attribute_changes(change: Change) -> list: + """Extract attribute changes from a Change object's change_json. + + Returns a list of HTML-safe formatted strings. + Only works for change_type='change'. + """ + if change.change_type != 'change': + return [] + + changes_list = [] + + for attr_name, attr_change in change.change_json.items(): + if attr_name == 'object_id' or not isinstance(attr_change, dict): + continue + + prefix = f'{escape(attr_name)}:' + action = attr_change.get('action') + old_val = escape(_format_value(attr_change.get('old'))) + new_val = escape(_format_value(attr_change.get('new'))) + + if action == 'update': + if attr_change.get('new') in (None, ''): + changes_list.append(mark_safe(f'{prefix} {old_val}')) + else: + changes_list.append(mark_safe(f'{prefix} {old_val} → {new_val}')) + elif action == 'new': + changes_list.append(mark_safe(f'{prefix} + {new_val}')) + elif action == 'delete': + changes_list.append(mark_safe(f'{prefix} {old_val}')) + elif action == 'multi': + for val in attr_change.get('remove', []): + changes_list.append(mark_safe(f'{prefix} {escape(str(val))}')) + for val in attr_change.get('add', []): + changes_list.append(mark_safe(f'{prefix} + {escape(str(val))}')) + + return changes_list diff --git a/serveradmin/serverdb/views.py b/serveradmin/serverdb/views.py index 1e46d462e..5c075c3a3 100644 --- a/serveradmin/serverdb/views.py +++ b/serveradmin/serverdb/views.py @@ -66,6 +66,10 @@ def changes(request): commits = commits.filter( Q(app__name=f_user_or_app) | Q(user__username=f_user_or_app)) + f_attribute = request.GET.get('attribute') + if f_attribute: + commits = commits.filter(change__change_json__has_key=f_attribute) + commits = commits.select_related('app', 'user') # This complex statement is just here to be able to prefetch the changes' @@ -108,6 +112,7 @@ def count(self): 'object_id': f_object_id, 'commit_id': f_commit, 'user_or_app': f_user_or_app, + 'attribute': f_attribute, }) From 38108108bc0b59cf0766aad4d527f926d2927e84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20D=C3=B6tsch?= Date: Sun, 21 Dec 2025 20:26:34 +0100 Subject: [PATCH 2/2] polish changes page - review changes - Inspect page: add link to history of current object --- .../serverdb/templates/serverdb/changes.html | 1 + serveradmin/serverdb/templatetags/changes.py | 21 +++++-------------- serveradmin/serverdb/views.py | 6 +++++- .../templates/servershell/inspect.html | 2 ++ 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/serveradmin/serverdb/templates/serverdb/changes.html b/serveradmin/serverdb/templates/serverdb/changes.html index af1fc6a8a..064f26e37 100644 --- a/serveradmin/serverdb/templates/serverdb/changes.html +++ b/serveradmin/serverdb/templates/serverdb/changes.html @@ -94,6 +94,7 @@

+ Requires hostname, object ID, or user/app filter
diff --git a/serveradmin/serverdb/templatetags/changes.py b/serveradmin/serverdb/templatetags/changes.py index 228586ab4..bfacee657 100644 --- a/serveradmin/serverdb/templatetags/changes.py +++ b/serveradmin/serverdb/templatetags/changes.py @@ -23,17 +23,6 @@ def hostname(object_id: int) -> Union[str, int]: return object_id -def _format_value(value: Any) -> str: - """Format a value for display, handling None/empty as '-'.""" - if value is None or value == '': - return '-' - if isinstance(value, list): - return ', '.join(str(v) for v in value) - if isinstance(value, set): - return ', '.join(str(v) for v in sorted(value)) - return str(value) - - @register.filter def get_attribute_changes(change: Change) -> list: """Extract attribute changes from a Change object's change_json. @@ -46,14 +35,14 @@ def get_attribute_changes(change: Change) -> list: changes_list = [] - for attr_name, attr_change in change.change_json.items(): - if attr_name == 'object_id' or not isinstance(attr_change, dict): + for attribute_id, attr_change in change.change_json.items(): + if attribute_id == 'object_id' or not isinstance(attr_change, dict): continue - prefix = f'{escape(attr_name)}:' + prefix = f'{attribute_id}:' action = attr_change.get('action') - old_val = escape(_format_value(attr_change.get('old'))) - new_val = escape(_format_value(attr_change.get('new'))) + old_val = escape(attr_change.get('old')) + new_val = escape(attr_change.get('new')) if action == 'update': if attr_change.get('new') in (None, ''): diff --git a/serveradmin/serverdb/views.py b/serveradmin/serverdb/views.py index 5c075c3a3..d16b6582f 100644 --- a/serveradmin/serverdb/views.py +++ b/serveradmin/serverdb/views.py @@ -68,7 +68,11 @@ def changes(request): f_attribute = request.GET.get('attribute') if f_attribute: - commits = commits.filter(change__change_json__has_key=f_attribute) + # Only allow attribute filter if another filter is set (not fully index) + if f_hostname or f_object_id or f_user_or_app: + commits = commits.filter(change__change_json__has_key=f_attribute) + else: + f_attribute = None commits = commits.select_related('app', 'user') diff --git a/serveradmin/servershell/templates/servershell/inspect.html b/serveradmin/servershell/templates/servershell/inspect.html index cbb37e781..3fb6bd8be 100644 --- a/serveradmin/servershell/templates/servershell/inspect.html +++ b/serveradmin/servershell/templates/servershell/inspect.html @@ -26,6 +26,7 @@

@@ -72,6 +73,7 @@