bnovo plugin
scheduller
This commit is contained in:
0
scheduler/__init__.py
Normal file
0
scheduler/__init__.py
Normal file
59
scheduler/admin.py
Normal file
59
scheduler/admin.py
Normal 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
6
scheduler/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SchedulerConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'scheduler'
|
||||
30
scheduler/migrations/0001_initial.py
Normal file
30
scheduler/migrations/0001_initial.py
Normal 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='Последний запуск')),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
scheduler/migrations/__init__.py
Normal file
0
scheduler/migrations/__init__.py
Normal file
47
scheduler/models.py
Normal file
47
scheduler/models.py
Normal 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
|
||||
16
scheduler/static/scheduler/admin.css
Normal file
16
scheduler/static/scheduler/admin.css
Normal 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
66
scheduler/tasks.py
Normal 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}")
|
||||
@@ -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
6
scheduler/test_module.py
Normal file
@@ -0,0 +1,6 @@
|
||||
def test_function():
|
||||
"""тестовая функция для проверки планировщика
|
||||
|
||||
"""
|
||||
print("Hello, World!")
|
||||
return "Hello, World!"
|
||||
3
scheduler/tests.py
Normal file
3
scheduler/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
71
scheduler/utils.py
Normal file
71
scheduler/utils.py
Normal 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
3
scheduler/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
Reference in New Issue
Block a user