Skip to content

Commit 24e7259

Browse files
committed
Merge branch 'task_scheduler' into main
2 parents ddeab67 + 99aea41 commit 24e7259

File tree

11 files changed

+432
-1
lines changed

11 files changed

+432
-1
lines changed

.idea/runConfigurations/createtask.xml

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/runConfigurations/runserver.xml

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/runConfigurations/test.xml

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Features
3030
- Authorize against **Active Directory** using `ldap3 <https://ldap3.readthedocs.io/en/latest/>`_ package
3131
- Manage **LDAP Connections** for easy integrations
3232
- Debug using `django-debug-toolbar <https://django-debug-toolbar.readthedocs.io/en/latest/>`_
33+
- **NEW** Create Task Schedulers for Django management commands
3334

3435
Quick Start
3536
-----------

docs/source/howto/create_tasks.rst

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
2+
Create Task Scheduler Jobs
3+
==========================
4+
5+
It is usually necessary to **execute tasks** from you project on a schedule.
6+
This module has a shortcut to create scheduled jobs for **Django management commands** in the **Windows Task Scheduler**.
7+
8+
.. warning::
9+
This feature requires the installation of the ``pywin32`` module.
10+
Install it with ``pip install pywin32``
11+
12+
Create a task
13+
-------------
14+
15+
Creating a new task is done with the ``createtask`` command.
16+
17+
For example, lets say you want to run the following command every hour::
18+
19+
$ py manage.py say_hello --new-users
20+
21+
You can create a schedule with this command::
22+
23+
$ py manage.py createtask "say_hello --new-users" -i hours=1
24+
25+
Now the following command will be executed every hour by the Windows Task Scheduler.
26+
27+
Using predefined tasks
28+
----------------------
29+
30+
Included with this module are some **predefined tasks** for some Django and Third-Party app management commands.
31+
Those commands can be created using the ``--predefined`` or ``-p`` argument
32+
33+
:clearsessions:
34+
Clear expired sessions from database, once a week::
35+
36+
$ py manage.py createtask clearsessions -p
37+
38+
.. seealso::
39+
See more at https://docs.djangoproject.com/en/3.1/ref/django-admin/#django-admin-clearsessions
40+
41+
:clean_duplicate_history:
42+
Clean duplicate history records from all models with history every 3 hours (from django-simple-history)::
43+
44+
$ py manage.py createtask clean_duplicate_history -p
45+
46+
:clean_old_history:
47+
Clean history records older then 30 days from all models with history every day (from django-simple-history)::
48+
49+
$ py manage.py createtask clean_old_history -p
50+
51+
52+
.. seealso::
53+
See more at https://django-simple-history.readthedocs.io/en/latest/utils.html#utils
54+
55+
:process_tasks:
56+
Worker for background tasks processing (from django-background-tasks)::
57+
58+
$ py manage.py createtask process_tasks -p
59+
60+
61+
You can also create **multiple workers** by specifying different names with ``--name`` argument.
62+
63+
.. seealso::
64+
See more at https://django-background-tasks.readthedocs.io/en/latest/#running-tasks

docs/source/index.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Features
1515
- Authorize against Active Directory using `ldap3 <https://ldap3.readthedocs.io/en/latest/>`_ package
1616
- Manage LDAP connections for easy integrations
1717
- Debug using `django-debug-toolbar <https://django-debug-toolbar.readthedocs.io/en/latest/>`_
18+
- **NEW** Create Task Schedulers for Django management commands
1819

1920
.. toctree::
2021
:maxdepth: 1
@@ -30,6 +31,7 @@ Features
3031
:caption: How-to Guides
3132

3233
howto/serve_static
34+
howto/create_tasks
3335
howto/custom_user_fields
3436
howto/using_ldap_manager
3537
howto/manage_secrets

docs/source/reference/change_log.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Change Log
88
Release date: coming soon
99

1010
- **ADDED**: LDAPUserManager for manually creating users from LDAP.
11+
- **ADDED**: ``createtask`` management command for creating Task Scheduler jobs.
1112
- **IMPROVED**: LDAP Settings for Group Membership check propagate to one another.
1213
- **MODIFIED**: Increased the default ``WAUTH_RESYNC_DELTA`` to every 1 day.
1314

docs/source/reference/management_commands.rst

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,27 @@ Arguments
2121

2222
.. warning::
2323
In order for the ``web.config`` files to work correctly, you will need to **unlock** some IIS Configuration Section.
24-
See the **Install and Setup IIS** section at :doc:`../installation/installation` docs.
24+
See the **Install and Setup IIS** section at :doc:`../installation/installation` docs.
25+
26+
createtask
27+
----------
28+
29+
Add a management command to Windows Task Scheduler.
30+
31+
Arguments
32+
* **command** Management command, wrapped with "command".
33+
* **--predefined**, **-p** Create from a predefined task.
34+
* **--name**, **-n** Task name.
35+
* **--desc**, **-d** Task description.
36+
* **--identity**, **-u** Task principal identity (default: "NT Authority\\LocalSystem").
37+
* **--folder**, **-f** Task folder location (default: Project's name).
38+
* **--interval**, **-i** Task execution interval as timedelta kwargs, e.g. "days=1,hours=12.5".
39+
* **--random**, **-r** Randomize execution time as timedelta kwargs, e.g. "days=1,hours=12.5".
40+
* **--timeout**, **-t** Execution time limit as timedelta kwargs, e.g. "days=1,hours=12.5" (default: 1 hour).
41+
* **--priority** Task priority https://docs.microsoft.com/en-us/windows/win32/taskschd/tasksettings-priority
42+
43+
Predefined tasks
44+
* **clearsessions** Clear sessions from database every week.
45+
* **clean_duplicate_history** Clean duplicate history records from all models with history every 3 hours (from django-simple-history).
46+
* **clean_old_history** Clean history records older then 30 days from all models with history every day (from django-simple-history).
47+
* **process_tasks** Worker for background tasks processing (from django-background-tasks).
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import os
2+
from argparse import ArgumentTypeError
3+
4+
from django.conf import settings
5+
from django.core.management import BaseCommand, CommandParser, CommandError
6+
from django.utils import timezone
7+
from pythoncom import com_error
8+
9+
from windows_auth.scheduler import create_task_definition, LOCAL_SERVICE, add_schedule_trigger, register_task
10+
from windows_auth.predefined_tasks import clear_sessions_task, clean_duplicate_history_task, clean_old_history_task, process_tasks_task
11+
12+
13+
def parse_datetime(string):
14+
result = {}
15+
for arg in string.split(","):
16+
if arg:
17+
try:
18+
# split key & value
19+
key, value = arg.split("=", 2)
20+
except ValueError:
21+
raise ArgumentTypeError("Argument must be in \"key=value\" format.")
22+
23+
try:
24+
# parse to float
25+
result[key] = float(value)
26+
except ValueError as e:
27+
raise ArgumentTypeError(e)
28+
try:
29+
# create time delta
30+
return timezone.timedelta(**result)
31+
except TypeError as e:
32+
raise ArgumentTypeError(str(e).replace("__new__()", "timedelta()"))
33+
34+
35+
PREDEFINED_TASKS = {
36+
"clearsessions": clear_sessions_task,
37+
"clean_duplicate_history": clean_duplicate_history_task,
38+
"clean_old_history": clean_old_history_task,
39+
"process_tasks": process_tasks_task,
40+
}
41+
42+
43+
class Command(BaseCommand):
44+
help = "Add a management command to Windows Task Scheduler."
45+
46+
def add_arguments(self, parser: CommandParser):
47+
parser.add_argument("command", help="Management command, wrapped with \"command\"")
48+
parser.add_argument("-p", "--predefined", action="store_true",
49+
help=f"Create from a predefined task {tuple(PREDEFINED_TASKS.keys())}")
50+
parser.add_argument("-n", "--name", type=str, help="Task name")
51+
parser.add_argument("-d", "--desc", type=str, default="", help="Task description")
52+
parser.add_argument("-u", "--identity", type=str, default=LOCAL_SERVICE, help="Task principle identity"),
53+
parser.add_argument("-f", "--folder", type=str, default=os.path.basename(settings.BASE_DIR),
54+
help="Task folder location")
55+
parser.add_argument("-i", "--interval", type=parse_datetime,
56+
help="Task interval as timedelta kwargs, e.g. \"days=1,hours=12.5\".")
57+
parser.add_argument("-r", "--random", type=parse_datetime,
58+
help="Randomize execution time as timedelta kwargs, e.g. \"days=1,hours=12.5\".")
59+
parser.add_argument("-t", "--timeout", type=parse_datetime, default="hours=1",
60+
help="Execution Time Limit as timedelta kwargs, e.g. \"days=1,hours=12.5\".")
61+
parser.add_argument("--priority", type=int, default=3,
62+
help="Task Priority "
63+
"https://docs.microsoft.com/en-us/windows/win32/taskschd/tasksettings-priority")
64+
65+
def handle(self, command="", predefined=False, name=None, desc="", identity=LOCAL_SERVICE, folder=None,
66+
interval=None, random=None, timeout=None, priority=None, **options):
67+
try:
68+
if predefined:
69+
# predefined tasks
70+
if command in PREDEFINED_TASKS:
71+
create_task = PREDEFINED_TASKS[command]
72+
create_task(command=command, name=name, desc=desc, identity=identity, folder=folder,
73+
interval=interval, random=random, timeout=timeout, priority=priority)
74+
else:
75+
raise CommandError(f"Predefined task for \"{command}\" does not exist.")
76+
else:
77+
# create task definition
78+
task_def = create_task_definition(command, description=desc, priority=priority, timeout=timeout)
79+
# add trigger
80+
if interval:
81+
add_schedule_trigger(task_def, interval, random=random)
82+
# register task
83+
register_task(task_def, name or command.split(" ", 1)[0], folder=folder, username=identity)
84+
except com_error as e:
85+
raise CommandError("Failed to register task. Did you run as administrator?\n" + str(e))

windows_auth/predefined_tasks.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from django.utils import timezone
2+
3+
from windows_auth.scheduler import create_task_definition, add_schedule_trigger, register_task
4+
5+
6+
def _get_options(options: dict, *args) -> dict:
7+
"""
8+
Filter specific keys from a dictionary
9+
:param options: Source dictionary
10+
:param args: Keys to keep
11+
:return: Filtered dictionary
12+
"""
13+
return {
14+
key: value
15+
for key, value in options.items()
16+
if key in args
17+
}
18+
19+
20+
def clear_sessions_task(**options):
21+
"""
22+
Clear sessions from database every week
23+
"""
24+
task_def = create_task_definition("clearsessions",
25+
description=options.get("desc") or "Clear expired sessions from database",
26+
**_get_options(options, "priority", "timeout"))
27+
add_schedule_trigger(task_def, timezone.timedelta(weeks=1), random=timezone.timedelta(1))
28+
register_task(task_def, options.get("name") or "Clear Sessions",
29+
**_get_options(options, "folder", "username", "password"))
30+
31+
32+
def clean_duplicate_history_task(**options):
33+
"""
34+
Clean duplicate history records from all models with history every 3 hours (from django-simple-history).
35+
"""
36+
interval = options.get("interval") or timezone.timedelta(hours=3)
37+
task_def = create_task_definition(f"clean_duplicate_history -m {interval.seconds / 60} --auto",
38+
description=options.get(
39+
"desc") or "Clean duplicate history records from database",
40+
**_get_options(options, "priority", "timeout"))
41+
add_schedule_trigger(task_def, interval)
42+
register_task(task_def, options.get("name") or "Clean Duplicate History",
43+
**_get_options(options, "folder", "username", "password"))
44+
45+
46+
def clean_old_history_task(**options):
47+
"""
48+
Clean history records older then 30 days from all models with history every day (from django-simple-history).
49+
"""
50+
task_def = create_task_definition("clean_old_history --auto",
51+
description=options.get("desc") or "Clean old history records from database",
52+
**_get_options(options, "priority", "timeout"))
53+
add_schedule_trigger(task_def, timezone.timedelta(days=1))
54+
register_task(task_def, options.get("name") or "Clean Old History",
55+
**_get_options(options, "folder", "username", "password"))
56+
57+
58+
def process_tasks_task(**options):
59+
"""
60+
Worker for background tasks processing (from django-background-tasks)
61+
"""
62+
interval = options.get("interval") or timezone.timedelta(hours=1)
63+
task_def = create_task_definition(f"process_tasks --log-std --duration {interval.seconds}",
64+
description=options.get("desc") or "Background tasks worker",
65+
**_get_options(options, "priority", "timeout"))
66+
add_schedule_trigger(task_def, interval)
67+
task = register_task(task_def, options.get("name") or "Process background tasks",
68+
**_get_options(options, "folder", "username", "password"))
69+
task.Run(None) # start immediately

0 commit comments

Comments
 (0)