Skip to content
Open
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
244 changes: 157 additions & 87 deletions open_schools_platform/marketplace_management/admin.py
Original file line number Diff line number Diff line change
@@ -1,141 +1,211 @@
from typing import Any, List, Tuple
from django.contrib import admin
from django.utils.html import format_html
from django.http import HttpRequest
from django.utils import timezone
from typing import Any, List, Tuple
from django.utils.html import format_html

from open_schools_platform.common.admin import admin_wrapper, BaseAdmin
from open_schools_platform.marketplace_management.models import Installation, DeveloperProfile, Category, App, \
AppRelease, Review
from open_schools_platform.marketplace_management.models import (
DeveloperProfile,
Category,
App,
AppRelease,
Review,
Installation,
InstallationStatusLog,
)
from open_schools_platform.marketplace_management.services.installation_status import (
InstallationStatusService,
)


@admin_wrapper(DeveloperProfile)
class DeveloperProfileModelAdmin(BaseAdmin):
list_display = ("id", "user", "email", "github")
field_to_highlight = "id"
list_display: Tuple[str, ...] = ("id", "user", "email", "github")
field_to_highlight: str = "id"


@admin_wrapper(Category)
class CategoryModelAdmin(BaseAdmin):
list_display = ("id", "name")
list_display: Tuple[str, ...] = ("id", "name")


@admin_wrapper(App)
class AppModelAdmin(BaseAdmin):
list_display = ("id", "name", "status", "developer_profile")
list_display: Tuple[str, ...] = ("id", "name", "status", "developer_profile")


@admin_wrapper(AppRelease)
class AppReleaseModelAdmin(BaseAdmin):
list_display = ("id", "app", "version")
field_to_highlight = "id"
list_display: Tuple[str, ...] = ("id", "app", "version")
field_to_highlight: str = "id"


@admin_wrapper(Review)
class ReviewModelAdmin(BaseAdmin):
list_display = ("id", "user", "app", "rating")
field_to_highlight = "app"
list_display: Tuple[str, ...] = ("id", "user", "app", "rating")
field_to_highlight: str = "app"


class InstallationFilter(admin.SimpleListFilter):
"""Custom filter for the status"""
title = 'Статус'
parameter_name = 'status'
class InstallationStatusLogInline(admin.TabularInline):
model = InstallationStatusLog
extra: int = 0
can_delete: bool = False
show_change_link: bool = False

readonly_fields: Tuple[str, ...] = (
"old_status",
"new_status",
"changed_by",
"changed_at",
)

fields: Tuple[str, ...] = readonly_fields
ordering = ("-changed_at",)

def lookups(self, request: Any, model_admin: Any) -> List[Tuple[str, str]]:
def has_add_permission(self, request: HttpRequest, obj=None) -> bool:
return False

def has_change_permission(self, request: HttpRequest, obj=None) -> bool:
return False


class InstallationStatusFilter(admin.SimpleListFilter):
title: str = "Статус"
parameter_name: str = "status"

def lookups(
self,
request: HttpRequest,
model_admin: admin.ModelAdmin,
) -> List[Tuple[Any, str]]:
return [
('active', 'Активные'),
('inactive', 'Неактивные'),
(Installation.STATUS_ACTIVE, "Активные"),
(Installation.STATUS_DISABLED, "Отключённые"),
(Installation.STATUS_UNINSTALLED, "Удалённые"),
]

def queryset(self, request: Any, queryset: Any) -> Any:
if self.value() == 'active':
return queryset.filter(active=True)
if self.value() == 'inactive':
return queryset.filter(active=False)
def queryset(self, request: HttpRequest, queryset):
if self.value():
return queryset.filter(status=self.value())
return queryset


@admin_wrapper(Installation)
class InstallationModelAdmin(BaseAdmin):
list_display: Tuple[str, ...] = (
'get_organization_name',
'get_app_name',
'get_installed_at',
'get_status_display',
'active',
'id'
"get_organization_name",
"get_app_name",
"get_installed_at",
"get_status_display",
"status",
"active",
"id",
)

# Default sorting (new installations first)
ordering = ('-installed_at',)
ordering: Tuple[str, ...] = ("-installed_at",)

# Filters in the right panel
list_filter = (
'organization',
'app',
InstallationFilter,
list_filter: Tuple[Any, ...] = (
"organization",
"app",
InstallationStatusFilter,
)

search_fields = (
'organization__name',
'app__name'
search_fields: Tuple[str, ...] = (
"organization__name",
"app__name",
)

list_per_page = 20
list_per_page: int = 20

# Fields for quick editing
list_editable = ('active',)
readonly_fields: Tuple[str, ...] = (
"active",
"installed_at",
"disabled_at",
"re_activated_at",
"uninstalled_at",
)

def get_organization_name(self, obj: Installation) -> str:
return obj.organization.name if obj.organization else 'Нет школы'
field_to_highlight: str = "app"

inlines = (InstallationStatusLogInline,)

fields: Tuple[str, ...] = (
"organization",
"app",
"user",
"status",
"active",
"installed_at",
"disabled_at",
"re_activated_at",
"uninstalled_at",
"config_data",
)

get_organization_name.short_description = 'Школа' # type: ignore[attr-defined]
get_organization_name.admin_order_field = 'organization__name' # type: ignore[attr-defined]
@admin.display(description="Школа", ordering="organization__name")
def get_organization_name(self, obj: Installation) -> str:
return obj.organization.name if obj.organization else "Нет школы"

@admin.display(description="Приложение", ordering="app__name")
def get_app_name(self, obj: Installation) -> str:
return obj.app.name if obj.app else 'Нет приложения'

get_app_name.short_description = 'Приложение' # type: ignore[attr-defined]
get_app_name.admin_order_field = 'app__name' # type: ignore[attr-defined]
return obj.app.name if obj.app else "Нет приложения"

@admin.display(description="Дата установки", ordering="installed_at")
def get_installed_at(self, obj: Installation) -> str:
if obj.installed_at:
local_time = timezone.localtime(obj.installed_at)
return local_time.strftime('%d.%m.%Y %H:%M')
return 'Нет даты'

get_installed_at.short_description = 'Дата установки' # type: ignore[attr-defined]
get_installed_at.admin_order_field = 'installed_at' # type: ignore[attr-defined]
if not obj.installed_at:
return "Нет даты"
local_time = timezone.localtime(obj.installed_at)
return local_time.strftime("%d.%m.%Y %H:%M")

@admin.display(description="Статус")
def get_status_display(self, obj: Installation) -> str:
if obj.active:
return format_html(
'<span style="color: green; font-weight: bold;">✓ Активен</span>'
color_map: dict[str, str] = {
Installation.STATUS_ACTIVE: "green",
Installation.STATUS_DISABLED: "orange",
Installation.STATUS_UNINSTALLED: "red",
}
label_map: dict[str, str] = {
Installation.STATUS_ACTIVE: "Активен",
Installation.STATUS_DISABLED: "Отключён",
Installation.STATUS_UNINSTALLED: "Удалён",
}

return format_html(
'<span style="color: {}; font-weight: bold;">{}</span>',
color_map.get(obj.status, "gray"),
label_map.get(obj.status, obj.status),
)

def get_queryset(self, request: HttpRequest):
return (
super()
.get_queryset(request)
.select_related("organization", "app")
)

def save_model(
self,
request: HttpRequest,
obj: Installation,
form,
change: bool,
) -> None:
if not change:
super().save_model(request, obj, form, change)
return

old_status: str = Installation.objects.get(pk=obj.pk).status
new_status: str = form.cleaned_data.get("status")

if old_status != new_status:
# rollback in-memory value
obj.status = old_status

InstallationStatusService.change_status(
installation=obj,
new_status=new_status,
user=request.user,
)
else:
return format_html(
'<span style="color: red; font-weight: bold;">✗ Неактивен</span>'
)

get_status_display.short_description = 'Статус' # type: ignore[attr-defined]

def get_queryset(self, request: Any) -> Any:
qs = super().get_queryset(request)
return qs.select_related('organization', 'app')

# Adding custom actions
actions = ('activate_installations', 'deactivate_installations',)

def activate_installations(self, request: Any, queryset: Any) -> None:
updated = queryset.update(active=True)
self.message_user(request, f'{updated} установок активировано')

activate_installations.short_description = "Активировать выбранные установки" # type: ignore[attr-defined]

def deactivate_installations(self, request: Any, queryset: Any) -> None:
updated = queryset.update(active=False)
self.message_user(request, f'{updated} установок деактивировано')

deactivate_installations.short_description = "Деактивировать выбранные установки" # type: ignore[attr-defined]

field_to_highlight = "app"
super().save_model(request, obj, form, change)
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Generated by Django 3.2.12 on 2026-01-12 14:30

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import rules.contrib.models
import uuid


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('marketplace_management', '0001_initial'),
]

operations = [
migrations.AddField(
model_name='installation',
name='disabled_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='installation',
name='re_activated_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='installation',
name='status',
field=models.CharField(choices=[('active', 'Active'), ('disabled', 'Disabled'), ('uninstalled', 'Uninstalled')], db_index=True, default='active', max_length=20),
),
migrations.AddField(
model_name='installation',
name='uninstalled_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.CreateModel(
name='InstallationStatusLog',
fields=[
('deleted', models.DateTimeField(db_index=True, editable=False, null=True)),
('deleted_by_cascade', models.BooleanField(default=False, editable=False)),
('created_at', models.DateTimeField(db_index=True, default=django.utils.timezone.now)),
('updated_at', models.DateTimeField(auto_now=True)),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('old_status', models.CharField(max_length=20)),
('new_status', models.CharField(max_length=20)),
('changed_at', models.DateTimeField(auto_now_add=True)),
('changed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
('installation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='status_logs', to='marketplace_management.installation')),
],
options={
'abstract': False,
},
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
]
Loading
Loading