Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 70 additions & 20 deletions backend/analytics/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,18 @@
from django.apps import apps
from django.conf import settings
from django.contrib import admin
from django.contrib.humanize.templatetags.humanize import intcomma
from django.core.serializers.json import DjangoJSONEncoder
from django.db import connection
from django.db.models import Count, Q
from django_celery_beat.models import PeriodicTask
from gamedata.models import GameFIOPlayerData, GamePlanet
from unfold.admin import ModelAdmin

from analytics.models import AppStatistic

# Replace /admin index
original_index = admin.site.index


def dashboard_index(request, extra_context=None):
def dashboard_index(request, context):

def kpi_data():
return (
Expand Down Expand Up @@ -50,7 +49,7 @@ def kpi_data():
)
chart_data.reverse()

def automation_status_data():
def get_automation_status_data():

return {
'planet': GamePlanet.objects.aggregate(
Expand Down Expand Up @@ -164,7 +163,7 @@ def get_system_stats():
data['models'] = sorted(data['models'], key=lambda x: x['count'], reverse=True)
return data

def task_data():
def get_task_data():
tasks = PeriodicTask.objects.all().values('name', 'enabled', 'last_run_at', 'total_run_count')

for task in tasks:
Expand All @@ -177,25 +176,76 @@ def task_data():

return tasks

# combine all datapoints
extra_context = extra_context or {}
extra_context['kpi_data'] = kpi_data()
extra_context['model_counts'] = get_system_stats()
extra_context['automation_status_data'] = automation_status_data()
extra_context['task_data'] = task_data()
extra_context['redis_stats'] = get_redis_stats()
extra_context['postgres_stats'] = get_postgres_perf_stats()
extra_context['chart_data'] = json.dumps(chart_data, cls=DjangoJSONEncoder)

return original_index(request, extra_context=extra_context)
pg_stats = get_postgres_perf_stats()
redis_stats = get_redis_stats()
system_stats = get_system_stats()
model_rows = [[r['name'], r['app'], intcomma(r['count']), r['size']] for r in system_stats['models']]
task_data = get_task_data()
automation_status_data = get_automation_status_data()

# combine all datapoints
context.update(
{
'kpi_data': kpi_data(),
'model_counts': system_stats,
'automation_status_data': automation_status_data,
'task_data': task_data,
'redis_stats': redis_stats,
'postgres_stats': pg_stats,
'chart_data': json.dumps(chart_data, cls=DjangoJSONEncoder),
'database_table': {
'rows': [
['Active Connections', pg_stats['active_connections']],
['Database Size', pg_stats['db_size']],
['Cache Hit-Rate', pg_stats['cache_hit_rate']],
['Total Commits', intcomma(pg_stats['total_commits'])],
['Dead Tuples', pg_stats['dead_tuples']],
['Needs Vacuum', pg_stats['needs_vacuum']],
],
},
'redis_table': {
'rows': [
['Active Connections', redis_stats['active_connections']],
['Stream Connections', redis_stats['active_stream_users']],
['Usage', redis_stats['usage']],
['Hit Rate', redis_stats['hit_rate']],
['Blocked Clients', redis_stats['blocked_clients']],
['Fragmentation', redis_stats['fragmentation']],
['Status', redis_stats['status']],
],
},
'models_table': {'headers': ['Model', 'App', 'Record Count', 'Table Size'], 'rows': model_rows},
'task_table': {
'headers': ['Task Name', 'Status', 'Last Run', 'Total Runs'],
'rows': [
[t['name'], t['status_label'], t['last_run_at'], intcomma(t['total_run_count'])] for t in task_data
],
},
'automation_table': {
'headers': ['Model', 'Total', 'Retrying', 'Failed'],
'rows': [
[
'Planets',
intcomma(automation_status_data['planet']['total_count']),
intcomma(automation_status_data['planet']['retrying_count']),
intcomma(automation_status_data['planet']['failed_count']),
],
[
'FIO Userdata',
intcomma(automation_status_data['fio_userdata']['total_count']),
intcomma(automation_status_data['fio_userdata']['retrying_count']),
intcomma(automation_status_data['fio_userdata']['failed_count']),
],
],
},
}
)

# assign new admin site
admin.site.index = dashboard_index # ty:ignore[invalid-assignment]
return context


@admin.register(AppStatistic)
class AppStatisticAdmin(admin.ModelAdmin):
class AppStatisticAdmin(ModelAdmin):
list_display = [
'date',
'user_count',
Expand Down
49 changes: 40 additions & 9 deletions backend/core/admin.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,42 @@
from django.contrib import admin
from django.contrib.auth.models import Group
from django_celery_beat.models import ClockedSchedule, SolarSchedule

# Hide admin elements
try:
admin.site.unregister(Group)
admin.site.unregister(SolarSchedule)
admin.site.unregister(ClockedSchedule)
except Exception:
from django_celery_beat.admin import (
CrontabScheduleAdmin as BaseCrontabScheduleAdmin,
PeriodicTaskAdmin as BasePeriodicTaskAdmin,
PeriodicTaskForm,
TaskSelectWidget,
)
from django_celery_beat.models import ClockedSchedule, CrontabSchedule, IntervalSchedule, PeriodicTask, SolarSchedule
from unfold.admin import ModelAdmin
from unfold.widgets import UnfoldAdminSelectWidget, UnfoldAdminTextInputWidget

admin.site.unregister(PeriodicTask)
admin.site.unregister(IntervalSchedule)
admin.site.unregister(CrontabSchedule)
admin.site.unregister(SolarSchedule)
admin.site.unregister(ClockedSchedule)


class UnfoldTaskSelectWidget(UnfoldAdminSelectWidget, TaskSelectWidget):
pass


class UnfoldPeriodicTaskForm(PeriodicTaskForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['task'].widget = UnfoldAdminTextInputWidget()
self.fields['regtask'].widget = UnfoldTaskSelectWidget()


@admin.register(PeriodicTask)
class PeriodicTaskAdmin(BasePeriodicTaskAdmin, ModelAdmin):
form = UnfoldPeriodicTaskForm


@admin.register(IntervalSchedule)
class IntervalScheduleAdmin(ModelAdmin):
pass


@admin.register(CrontabSchedule)
class CrontabScheduleAdmin(BaseCrontabScheduleAdmin, ModelAdmin):
pass
10 changes: 4 additions & 6 deletions backend/core/config/django/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
# Application definition

INSTALLED_APPS = [
# admin interface
'unfold',
'unfold.contrib.filters',
# django
'django.contrib.admin',
'django.contrib.auth',
Expand All @@ -37,8 +40,6 @@
'django.contrib.humanize',
# 3rd party
'corsheaders',
'django_prometheus',
'dj_redis_panel',
'django_celery_beat',
'django_structlog',
'rest_framework',
Expand All @@ -50,14 +51,12 @@
#
'core',
'user',
'legacy_migration',
'gamedata',
'planning',
'analytics',
]

MIDDLEWARE = [
'django_prometheus.middleware.PrometheusBeforeMiddleware',
'django_structlog.middlewares.RequestMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
Expand All @@ -68,7 +67,6 @@
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django_prometheus.middleware.PrometheusAfterMiddleware',
]

ROOT_URLCONF = 'core.urls'
Expand Down Expand Up @@ -97,9 +95,9 @@
from core.config.settings.cache import * # noqa: E402, F403
from core.config.settings.celery import * # noqa: E402, F403
from core.config.settings.database import * # noqa: E402, F403
from core.config.settings.dj_redis import * # noqa: E402, F403
from core.config.settings.drf import * # noqa: E402, F403
from core.config.settings.logging import * # noqa: E402, F403
from core.config.settings.unfold import * # noqa: E402, F403

AUTHENTICATION_BACKENDS = [
'user.backends.LegacyAwareBackend',
Expand Down
2 changes: 1 addition & 1 deletion backend/core/config/django/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'testing.db', # noqa: F405
}
}
} # ty:ignore[invalid-assignment]

CACHES = {
'default': {
Expand Down
2 changes: 1 addition & 1 deletion backend/core/config/settings/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@
'PASSWORD': settings.database.legacy_password,
'HOST': settings.database.legacy_host,
'PORT': settings.database.legacy_port,
}
} # ty:ignore[invalid-assignment]
11 changes: 0 additions & 11 deletions backend/core/config/settings/dj_redis.py

This file was deleted.

16 changes: 16 additions & 0 deletions backend/core/config/settings/unfold.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Unfold
UNFOLD = {
'SITE_TITLE': 'PRUNPlanner Admin',
'SITE_HEADER': 'PRUNplanner',
'BORDER_RADIUS': '3px',
'THEME': 'dark',
'SHOW_HISTORY': False,
'DASHBOARD_CALLBACK': 'analytics.admin.dashboard_index',
'COLORS': {
'base': {'50': 'rgb(21, 21, 21)', '800': 'rgb(21, 21, 21)', '900': '#030707'},
'primary': {
'500': 'rgb(192, 226, 25)',
'600': 'rgb(192, 226, 25)',
},
},
}
2 changes: 0 additions & 2 deletions backend/core/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@
from django.urls import include, path

urlpatterns = [
path('admin/redis/', include('dj_redis_panel.urls')),
path('admin/', admin.site.urls),
path('prometheus/', include('django_prometheus.urls')),
# API
path('', include('api.urls')),
]
Expand Down
37 changes: 12 additions & 25 deletions backend/gamedata/admin/game_building_admin.py
Original file line number Diff line number Diff line change
@@ -1,54 +1,41 @@
from django.contrib import admin, messages
from django.http import HttpRequest, HttpResponseRedirect
from django.http import HttpRequest
from django.shortcuts import redirect
from django.urls import path
from structlog import get_logger
from unfold.admin import ModelAdmin, TabularInline
from unfold.decorators import action

from gamedata.fio.importers import import_all_buildings
from gamedata.models import GameBuilding, GameBuildingCost

logger = get_logger(__name__)


class BuildingCostInline(admin.TabularInline):
class BuildingCostInline(TabularInline):
model = GameBuildingCost
can_delete = False
extra = 0
fk_name = 'building'
tab = True


@admin.register(GameBuilding)
class GameBuildingAdmin(admin.ModelAdmin):
change_list_template = 'admin/gamedata/building_change_list.html'

class GameBuildingAdmin(ModelAdmin):
list_display = ['building_ticker', 'building_name', 'expertise', 'building_type']
search_fields = ['building_ticker', 'building_name', 'expertise']

inlines = [
BuildingCostInline,
]

def get_urls(self) -> list:
urls = super().get_urls()
custom_urls = [
path(
'fio_building_import/',
self.admin_site.admin_view(self.fio_building_import),
name='fio_building_import',
)
]
return custom_urls + urls

def fio_building_import(self, request: HttpRequest) -> HttpResponseRedirect:
actions_list = ['action_fio_import_building']

@action(description='Import from FIO', url_path='changelist-fio-import-building')
def action_fio_import_building(self, request: HttpRequest):
try:
buildings, costs = import_all_buildings()

self.message_user(
request,
f'Buildings synced! Created: {buildings} with {costs} costs.',
)
except Exception as exc:
self.message_user(request, f'Buildings synced! Created: {buildings} with {costs} costs.', messages.SUCCESS)
except Exception:
self.message_user(request, 'Failed to sync buildings from FIO', messages.ERROR)
logger.error('Failed to sync buildings from FIO', exc_info=exc)

return redirect('../')
Loading
Loading