bnovo plugin

scheduller
This commit is contained in:
2024-12-10 20:07:23 +09:00
parent 8dce756a27
commit 806c611cc7
38 changed files with 1301 additions and 277 deletions

0
scheduler/__init__.py Normal file
View File

59
scheduler/admin.py Normal file
View File

@@ -0,0 +1,59 @@
from django.contrib import admin
from django import forms
from django.utils.functional import cached_property
from .models import ScheduledTask
from django.templatetags.static import static
from scheduler.utils import get_project_functions
class CustomAdmin(admin.ModelAdmin):
class Media:
css = {"all": (static("scheduler/admin.css"),)}
js = (static("scheduler/admin.js"),)
class ScheduledTaskForm(forms.ModelForm):
DAYS_OF_WEEK_CHOICES = [
(0, "Воскресенье"),
(1, "Понедельник"),
(2, "Вторник"),
(3, "Среда"),
(4, "Четверг"),
(5, "Пятница"),
(6, "Суббота"),
]
weekdays = forms.MultipleChoiceField(
choices=DAYS_OF_WEEK_CHOICES,
widget=forms.CheckboxSelectMultiple,
label="Дни недели",
required=False, # Опционально
)
class Meta:
model = ScheduledTask
fields = [
"task_name",
"function_path",
"minutes",
"hours",
"months",
"weekdays", # Используем только поле с галочками
"active",
]
def clean_weekdays(self):
"""
Преобразуем список выбранных дней в строку для хранения в базе.
"""
weekdays = self.cleaned_data.get("weekdays", [])
return ",".join(map(str, weekdays))
@admin.register(ScheduledTask)
class ScheduledTaskAdmin(admin.ModelAdmin):
form = ScheduledTaskForm
list_display = ("task_name", "function_path", "active", "formatted_last_run")
list_filter = ("active",)
search_fields = ("task_name", "function_path")
def formatted_last_run(self, obj):
return obj.last_run.strftime("%Y-%m-%d %H:%M:%S") if obj.last_run else "Никогда"
formatted_last_run.short_description = "Последний запуск"

6
scheduler/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class SchedulerConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'scheduler'

View File

@@ -0,0 +1,30 @@
# Generated by Django 5.1.4 on 2024-12-10 08:38
import scheduler.models
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='ScheduledTask',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('task_name', models.CharField(max_length=255, verbose_name='Название задачи')),
('function_path', models.CharField(choices=scheduler.models.get_available_functions, max_length=500, verbose_name='Путь к функции (модуль.функция)')),
('minutes', models.CharField(default='*', max_length=255, verbose_name='Минуты')),
('hours', models.CharField(default='*', max_length=255, verbose_name='Часы')),
('days', models.CharField(default='*', max_length=255, verbose_name='Дни')),
('months', models.CharField(default='*', max_length=255, verbose_name='Месяцы')),
('weekdays', models.JSONField(default=list, verbose_name='Дни недели')),
('active', models.BooleanField(default=True, verbose_name='Активно')),
('last_run', models.DateTimeField(blank=True, null=True, verbose_name='Последний запуск')),
],
),
]

View File

47
scheduler/models.py Normal file
View File

@@ -0,0 +1,47 @@
from django.db import models
from django.utils.timezone import now
class ScheduledTask(models.Model):
task_name = models.CharField(max_length=255)
function_path = models.CharField(max_length=255)
minutes = models.CharField(max_length=255)
hours = models.CharField(max_length=255)
months = models.CharField(max_length=255)
weekdays = models.CharField(max_length=100, blank=True, default="")
active = models.BooleanField(default=True)
last_run = models.DateTimeField(null=True, blank=True)
def __str__(self):
return self.name
def clean_weekdays(self):
"""Приводим список в строку при сохранении."""
if isinstance(self.weekdays, list):
self.weekdays = ",".join(map(str, self.weekdays))
class Meta:
verbose_name = "Запланированная задача"
verbose_name_plural = "Запланированные задачи"
def get_available_functions():
from scheduler.utils import get_project_functions
return [(path, name) for path, name in get_project_functions()]
class ScheduledTask(models.Model):
task_name = models.CharField(max_length=255, verbose_name="Название задачи")
function_path = models.CharField(
max_length=500,
choices=get_available_functions,
verbose_name="Путь к функции (модуль.функция)",
)
minutes = models.CharField(max_length=255, verbose_name="Минуты", default="*")
hours = models.CharField(max_length=255, verbose_name="Часы", default="*")
days = models.CharField(max_length=255, verbose_name="Дни", default="*")
months = models.CharField(max_length=255, verbose_name="Месяцы", default="*")
weekdays = models.JSONField(default=list, verbose_name="Дни недели")
active = models.BooleanField(default=True, verbose_name="Активно")
last_run = models.DateTimeField(blank=True, null=True, verbose_name="Последний запуск")
def __str__(self):
return self.task_name

View File

@@ -0,0 +1,16 @@
.checkbox-row {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.checkbox-row label {
display: inline-block;
width: auto;
margin-right: 10px;
}
.form-row.last_run span {
font-weight: bold;
margin-left: 10px;
}

66
scheduler/tasks.py Normal file
View File

@@ -0,0 +1,66 @@
from apscheduler.schedulers.base import BaseScheduler
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from importlib import import_module
from scheduler.models import ScheduledTask
import importlib
from apscheduler.triggers.cron import CronTrigger
def format_weekdays(weekdays):
"""Преобразует список дней недели в строку."""
if isinstance(weekdays, list):
return ",".join(map(str, weekdays))
return str(weekdays)
def run_task(task):
"""
Выполняет задачу, указанную в модели ScheduledTask.
"""
module_name, func_name = task.module_path.rsplit(".", 1)
module = import_module(module_name)
func = getattr(module, func_name)
func()
def setup_scheduler():
"""Настройка планировщика задач из БД."""
print("Настройка планировщика задач...")
scheduler = AsyncIOScheduler()
tasks = ScheduledTask.objects.filter(active=True)
for task in tasks:
scheduler.add_job(
run_task,
"cron",
id=task.name,
minute=task.cron_minute,
hour=task.cron_hour,
day=task.cron_day,
month=task.cron_month,
day_of_week=task.cron_weekday,
args=[task],
)
scheduler.start()
print("Планировщик запущен.")
return scheduler
def load_tasks_to_scheduler(scheduler: BaseScheduler):
tasks = ScheduledTask.objects.filter(active=True)
for task in tasks:
try:
module_name, func_name = task.function_path.rsplit('.', 1)
module = import_module(module_name)
func = getattr(module, func_name)
scheduler.add_job(
func,
trigger="cron",
minute=task.minutes,
hour=task.hours,
day=task.days or "*",
month=task.months or "*",
day_of_week=task.weekdays or "*",
id=str(task.id),
replace_existing=True,
)
except Exception as e:
print(f"Ошибка при добавлении задачи '{task.task_name}': {e}")

View File

@@ -0,0 +1,42 @@
{% extends "admin/change_form.html" %}
{% load static %}
{% block extrahead %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static 'scheduler/admin.css' %}">
{% endblock %}
{% block content %}
<div class="form-container">
<form method="post" class="change-form" enctype="multipart/form-data" novalidate>
{% csrf_token %}
{{ adminform.non_field_errors }}
<div>
{% for fieldset in adminform %}
<fieldset class="{{ fieldset.classes }}">
<legend>{{ fieldset.name }}</legend>
{% for line in fieldset %}
<div class="form-row {{ line.field.field.name }}">
{% if line.field.name == 'last_run' %}
<div>
<label>{{ line.label }}</label>
<span>{{ line.field.contents }}</span>
</div>
{% else %}
{{ line.errors }}
{{ line.field }}
{% if line.field.is_checkbox %}
<div class="checkbox-row">{{ line.field }}</div>
{% endif %}
{% endif %}
</div>
{% endfor %}
</fieldset>
{% endfor %}
</div>
<div class="submit-row">
{{ submit_buttons }}
</div>
</form>
</div>
{% endblock %}

6
scheduler/test_module.py Normal file
View File

@@ -0,0 +1,6 @@
def test_function():
"""тестовая функция для проверки планировщика
"""
print("Hello, World!")
return "Hello, World!"

3
scheduler/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

71
scheduler/utils.py Normal file
View File

@@ -0,0 +1,71 @@
from apscheduler.schedulers.asyncio import AsyncIOScheduler
import os
import inspect
import importlib
from typing import List, Tuple
from pathspec import PathSpec
def reload_tasks_periodically(scheduler: AsyncIOScheduler):
"""Перезагрузка задач из базы данных каждые 5 минут."""
from scheduler.tasks import load_tasks_to_scheduler
scheduler.add_job(lambda: load_tasks_to_scheduler(scheduler), "interval", minutes=5)
def load_gitignore_patterns(project_root: str) -> PathSpec:
"""
Загружает паттерны из файла .gitignore.
"""
gitignore_path = os.path.join(project_root, ".gitignore")
if os.path.exists(gitignore_path):
with open(gitignore_path, "r", encoding="utf-8") as f:
patterns = f.readlines()
return PathSpec.from_lines("gitwildmatch", patterns)
return PathSpec.from_lines("gitwildmatch", []) # Пустой PathSpec
def get_project_functions() -> List[Tuple[str, str]]:
"""
Сканирует проект и возвращает список всех функций в формате (путь, имя функции),
исключая файлы и папки, указанные в .gitignore.
"""
functions = []
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Загружаем паттерны из .gitignore
gitignore_spec = load_gitignore_patterns(project_root)
for root, dirs, files in os.walk(project_root):
# Исключаем директории, указанные в .gitignore
dirs[:] = [d for d in dirs if not gitignore_spec.match_file(os.path.relpath(os.path.join(root, d), project_root))]
for file in files:
file_path = os.path.relpath(os.path.join(root, file), project_root)
if (
file.endswith(".py") and
not file.startswith("__") and
not gitignore_spec.match_file(file_path)
):
module_path = os.path.relpath(os.path.join(root, file), project_root)
module_name = module_path.replace(os.sep, ".").replace(".py", "")
try:
module = importlib.import_module(module_name)
for name, func in inspect.getmembers(module, inspect.isfunction):
functions.append((f"{module_name}.{name}", name))
except Exception as e:
print(f"Ошибка загрузки модуля {module_name}: {e}")
return functions
import importlib
def execute_function(function_path):
"""
Выполняет функцию по указанному пути.
"""
module_name, func_name = function_path.rsplit(".", 1)
module = importlib.import_module(module_name)
func = getattr(module, func_name)
return func()

3
scheduler/views.py Normal file
View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.