django-admin-background-task 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bgtask/admin.py +6 -4
- bgtask/backends/__init__.py +3 -0
- bgtask/backends/thread_pool.py +12 -0
- bgtask/decorators.py +46 -0
- bgtask/migrations/0002_backgroundtask_namespace_alter_backgroundtask_name.py +31 -0
- bgtask/model_admin.py +72 -0
- bgtask/models.py +70 -21
- bgtask/static/bgtask/js/bgtask.js +35 -31
- bgtask/templates/bgtask/admin/change_list.html +31 -0
- bgtask/templates/bgtask/bg_changelist_status_column.html +4 -0
- bgtask/views.py +1 -8
- {django_admin_background_task-0.1.0.dist-info → django_admin_background_task-0.2.0.dist-info}/METADATA +2 -2
- {django_admin_background_task-0.1.0.dist-info → django_admin_background_task-0.2.0.dist-info}/RECORD +15 -10
- bgtask/.DS_Store +0 -0
- {django_admin_background_task-0.1.0.dist-info → django_admin_background_task-0.2.0.dist-info}/LICENSE +0 -0
- {django_admin_background_task-0.1.0.dist-info → django_admin_background_task-0.2.0.dist-info}/WHEEL +0 -0
bgtask/admin.py
CHANGED
@@ -2,7 +2,6 @@ from django.contrib import admin
|
|
2
2
|
from django.template.loader import render_to_string
|
3
3
|
|
4
4
|
from .models import BackgroundTask
|
5
|
-
from .views import task_dict
|
6
5
|
|
7
6
|
|
8
7
|
def background_task_status(obj):
|
@@ -15,7 +14,7 @@ def background_task_status(obj):
|
|
15
14
|
bgtask = bgtasks.first()
|
16
15
|
|
17
16
|
output = render_to_string(
|
18
|
-
"bgtask/bg_changelist_status_column.html", {"bgtask": bgtask and task_dict
|
17
|
+
"bgtask/bg_changelist_status_column.html", {"bgtask": bgtask and bgtask.task_dict}
|
19
18
|
)
|
20
19
|
return output
|
21
20
|
|
@@ -25,6 +24,9 @@ background_task_status.__name__ = "Task Status"
|
|
25
24
|
|
26
25
|
@admin.register(BackgroundTask)
|
27
26
|
class BackgroundTaskAdmin(admin.ModelAdmin):
|
28
|
-
list_filter = ["state", "
|
29
|
-
list_display = ("created", background_task_status, "result", "completed_at")
|
27
|
+
list_filter = ["state", "namespace", "name"]
|
28
|
+
list_display = ("created", "namespace_name", background_task_status, "result", "completed_at")
|
30
29
|
ordering = ["-created"]
|
30
|
+
|
31
|
+
def namespace_name(self, bgtask):
|
32
|
+
return ".".join(f for f in [bgtask.namespace, bgtask.name] if f)
|
@@ -0,0 +1,12 @@
|
|
1
|
+
from concurrent.futures import ThreadPoolExecutor
|
2
|
+
|
3
|
+
|
4
|
+
SHARED_THREAD_POOL = None
|
5
|
+
|
6
|
+
|
7
|
+
def dispatch(func, bg_task, *args, **kwargs):
|
8
|
+
global SHARED_THREAD_POOL
|
9
|
+
if SHARED_THREAD_POOL is None:
|
10
|
+
SHARED_THREAD_POOL = ThreadPoolExecutor()
|
11
|
+
|
12
|
+
SHARED_THREAD_POOL.submit(func, bg_task, *args, **kwargs)
|
bgtask/decorators.py
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
import logging
|
2
|
+
from functools import wraps
|
3
|
+
|
4
|
+
from django.contrib.messages import INFO
|
5
|
+
|
6
|
+
|
7
|
+
from .models import BackgroundTask
|
8
|
+
|
9
|
+
|
10
|
+
log = logging.getLogger(__name__)
|
11
|
+
|
12
|
+
|
13
|
+
def bgtask_admin_action(func=None):
|
14
|
+
if func is not None:
|
15
|
+
return bgtask_admin_action()(func)
|
16
|
+
|
17
|
+
def bgtask_admin_action_factory(func):
|
18
|
+
|
19
|
+
task_name = f"AdminTask-{func.__name__}"
|
20
|
+
|
21
|
+
@wraps(func)
|
22
|
+
def bgtask_admin_action_wrapper(self, request, queryset):
|
23
|
+
log.info("Running func %s", func.__name__)
|
24
|
+
bg_task = self.start_bgtask(task_name)
|
25
|
+
|
26
|
+
self.message_user(request, "Started background task", level=INFO)
|
27
|
+
|
28
|
+
from .backends import default_backend
|
29
|
+
|
30
|
+
default_backend.dispatch(_run_bg_task_func, func, bg_task, request, queryset)
|
31
|
+
|
32
|
+
|
33
|
+
bgtask_admin_action_wrapper.bgtask_name = task_name
|
34
|
+
|
35
|
+
return bgtask_admin_action_wrapper
|
36
|
+
|
37
|
+
return bgtask_admin_action_factory
|
38
|
+
|
39
|
+
|
40
|
+
def _run_bg_task_func(func, bg_task, request, queryset):
|
41
|
+
try:
|
42
|
+
func(bg_task, request, queryset)
|
43
|
+
except Exception as exc:
|
44
|
+
bg_task.fail(exc)
|
45
|
+
else:
|
46
|
+
bg_task.succeed()
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# Generated by Django 4.2.11 on 2024-04-16 14:59
|
2
|
+
|
3
|
+
from django.db import migrations, models
|
4
|
+
|
5
|
+
|
6
|
+
class Migration(migrations.Migration):
|
7
|
+
|
8
|
+
dependencies = [
|
9
|
+
("bgtask", "0001_initial"),
|
10
|
+
]
|
11
|
+
|
12
|
+
operations = [
|
13
|
+
migrations.AddField(
|
14
|
+
model_name="backgroundtask",
|
15
|
+
name="namespace",
|
16
|
+
field=models.CharField(
|
17
|
+
blank=True,
|
18
|
+
default="",
|
19
|
+
help_text="Optional namespace that can be used to avoid having to make names unique across an entire codebase, allowing them to be shorter and human readable",
|
20
|
+
max_length=1000,
|
21
|
+
),
|
22
|
+
),
|
23
|
+
migrations.AlterField(
|
24
|
+
model_name="backgroundtask",
|
25
|
+
name="name",
|
26
|
+
field=models.CharField(
|
27
|
+
help_text="Name (or type) of this task, is not unique per task instance but generally per task functionality",
|
28
|
+
max_length=1000,
|
29
|
+
),
|
30
|
+
),
|
31
|
+
]
|
bgtask/model_admin.py
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
from datetime import timedelta
|
2
|
+
|
3
|
+
from django.contrib import admin
|
4
|
+
from django.contrib.admin.utils import label_for_field
|
5
|
+
from django.db.models import Q
|
6
|
+
from django.utils import timezone
|
7
|
+
|
8
|
+
from .models import BackgroundTask
|
9
|
+
|
10
|
+
|
11
|
+
class BGTaskModelAdmin(admin.ModelAdmin):
|
12
|
+
# This is not overridden to avoid messing with the implicit logic for finding change list
|
13
|
+
# templates that ModelAdmin uses. So you either need to specify this yourself on your
|
14
|
+
# subclass or you need to extend from this in your custom template.
|
15
|
+
# change_list_template = "bgtask/admin/change_list.html"
|
16
|
+
|
17
|
+
# ----------------------------------------------------------------------------------------------
|
18
|
+
# API for subclasses
|
19
|
+
# ----------------------------------------------------------------------------------------------
|
20
|
+
def start_bgtask(self, name, **kwargs):
|
21
|
+
bgtask = BackgroundTask.objects.create(
|
22
|
+
name=name,
|
23
|
+
namespace=self._bgtask_namespace,
|
24
|
+
**kwargs,
|
25
|
+
)
|
26
|
+
bgtask.start()
|
27
|
+
return bgtask
|
28
|
+
|
29
|
+
# ----------------------------------------------------------------------------------------------
|
30
|
+
# Superclass overrides
|
31
|
+
# ----------------------------------------------------------------------------------------------
|
32
|
+
def changelist_view(self, request, extra_context=None):
|
33
|
+
extra_context = extra_context or {}
|
34
|
+
extra_context["admin_bg_tasks"] = self._admin_bg_tasks(request)
|
35
|
+
return super().changelist_view(request, extra_context=extra_context)
|
36
|
+
|
37
|
+
# ----------------------------------------------------------------------------------------------
|
38
|
+
# Internal functions
|
39
|
+
# ----------------------------------------------------------------------------------------------
|
40
|
+
@property
|
41
|
+
def _bgtask_namespace(self):
|
42
|
+
return type(self).__module__ + "." + type(self).__name__
|
43
|
+
|
44
|
+
def _admin_bg_tasks(self, request):
|
45
|
+
task_name_to_desc = {}
|
46
|
+
for action, action_name, action_description in self.get_actions(request).values():
|
47
|
+
if hasattr(action, "bgtask_name"):
|
48
|
+
task_name_to_desc[action.bgtask_name] = action_description
|
49
|
+
|
50
|
+
for name in getattr(self, "bgtask_names", []):
|
51
|
+
task_name_to_desc[name] = name
|
52
|
+
|
53
|
+
if not task_name_to_desc:
|
54
|
+
return BackgroundTask.objects.none()
|
55
|
+
|
56
|
+
bgts = list(
|
57
|
+
BackgroundTask.objects.filter(
|
58
|
+
name__in=task_name_to_desc, namespace=self._bgtask_namespace
|
59
|
+
)
|
60
|
+
.filter(
|
61
|
+
Q(state=BackgroundTask.STATES.running)
|
62
|
+
| (
|
63
|
+
~Q(state=BackgroundTask.STATES.not_started)
|
64
|
+
& Q(completed_at__gt=timezone.now() - timedelta(minutes=30))
|
65
|
+
)
|
66
|
+
)
|
67
|
+
.order_by("-started_at")
|
68
|
+
)
|
69
|
+
for bgt in bgts:
|
70
|
+
bgt.admin_description = task_name_to_desc[bgt.name]
|
71
|
+
|
72
|
+
return bgts
|
bgtask/models.py
CHANGED
@@ -4,10 +4,12 @@ import os
|
|
4
4
|
import time
|
5
5
|
import traceback
|
6
6
|
import uuid
|
7
|
+
from contextlib import contextmanager
|
7
8
|
|
8
9
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
9
10
|
from django.contrib.contenttypes.models import ContentType
|
10
11
|
from django.db import models, transaction
|
12
|
+
from django.forms.models import model_to_dict
|
11
13
|
from django.utils import timezone
|
12
14
|
|
13
15
|
from model_utils import Choices
|
@@ -19,9 +21,19 @@ log = logging.getLogger(__name__)
|
|
19
21
|
def locked(meth):
|
20
22
|
@functools.wraps(meth)
|
21
23
|
def _locked_meth(self, *args, **kwargs):
|
24
|
+
if getattr(self, "_locked", False):
|
25
|
+
return meth(self, *args, **kwargs)
|
26
|
+
|
22
27
|
with transaction.atomic():
|
23
28
|
BackgroundTask.objects.filter(id=self.id).select_for_update().only("id").get()
|
24
|
-
|
29
|
+
self.refresh_from_db()
|
30
|
+
|
31
|
+
# Mark as locked in case we are called recursively
|
32
|
+
self._locked = True
|
33
|
+
try:
|
34
|
+
return meth(self, *args, **kwargs)
|
35
|
+
finally:
|
36
|
+
self._locked = False
|
25
37
|
|
26
38
|
return _locked_meth
|
27
39
|
|
@@ -41,14 +53,24 @@ def only_if_state(state):
|
|
41
53
|
return only_if_state_decorator
|
42
54
|
|
43
55
|
|
44
|
-
class
|
45
|
-
class Meta:
|
46
|
-
abstract = True
|
47
|
-
|
48
|
-
|
49
|
-
class BackgroundTask(CreatedUpdatedMixin):
|
56
|
+
class BackgroundTask(models.Model):
|
50
57
|
id = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4)
|
51
|
-
name = models.CharField(
|
58
|
+
name = models.CharField(
|
59
|
+
max_length=1000,
|
60
|
+
help_text=(
|
61
|
+
"Name (or type) of this task, is not unique "
|
62
|
+
"per task instance but generally per task functionality"
|
63
|
+
),
|
64
|
+
)
|
65
|
+
namespace = models.CharField(
|
66
|
+
max_length=1000,
|
67
|
+
default="",
|
68
|
+
blank=True,
|
69
|
+
help_text=(
|
70
|
+
"Optional namespace that can be used to avoid having to make names unique across an "
|
71
|
+
"entire codebase, allowing them to be shorter and human readable"
|
72
|
+
),
|
73
|
+
)
|
52
74
|
|
53
75
|
STATES = Choices("not_started", "running", "success", "partial_success", "failed")
|
54
76
|
state = models.CharField(max_length=16, default=STATES.not_started, choices=STATES)
|
@@ -79,6 +101,11 @@ class BackgroundTask(CreatedUpdatedMixin):
|
|
79
101
|
class Meta:
|
80
102
|
ordering = ["created", "id"]
|
81
103
|
|
104
|
+
@property
|
105
|
+
def task_dict(self):
|
106
|
+
task_dict = model_to_dict(self)
|
107
|
+
return {"id": str(self.id), "updated": self.updated.isoformat(), **task_dict}
|
108
|
+
|
82
109
|
@property
|
83
110
|
def num_failed_steps(self):
|
84
111
|
return sum(error.get("num_failed_steps", 0) for error in self.errors)
|
@@ -87,6 +114,24 @@ class BackgroundTask(CreatedUpdatedMixin):
|
|
87
114
|
def incomplete(self):
|
88
115
|
return self.state in [self.STATES.not_started, self.STATES.running]
|
89
116
|
|
117
|
+
@contextmanager
|
118
|
+
def runs_single_step(self):
|
119
|
+
try:
|
120
|
+
yield
|
121
|
+
except Exception as exc:
|
122
|
+
self.steps_failed(1, error=exc)
|
123
|
+
else:
|
124
|
+
self.add_successful_steps(1)
|
125
|
+
|
126
|
+
@contextmanager
|
127
|
+
def finishes(self):
|
128
|
+
try:
|
129
|
+
yield
|
130
|
+
except Exception as exc:
|
131
|
+
self.fail(exc)
|
132
|
+
else:
|
133
|
+
self.succeed()
|
134
|
+
|
90
135
|
@locked
|
91
136
|
@only_if_state(STATES.not_started)
|
92
137
|
def start(self):
|
@@ -109,10 +154,10 @@ class BackgroundTask(CreatedUpdatedMixin):
|
|
109
154
|
|
110
155
|
@locked
|
111
156
|
@only_if_state(STATES.running)
|
112
|
-
def succeed(self, result):
|
157
|
+
def succeed(self, result=None):
|
113
158
|
log.info("%s succeeded.", self)
|
114
159
|
self.state = self.STATES.success
|
115
|
-
self.
|
160
|
+
self.steps_completed = self.steps_to_complete
|
116
161
|
self.completed_at = timezone.now()
|
117
162
|
self.result = self.serialize_result(result)
|
118
163
|
self.save()
|
@@ -125,8 +170,8 @@ class BackgroundTask(CreatedUpdatedMixin):
|
|
125
170
|
log.info("Finishing as success with no errors")
|
126
171
|
self.state = self.STATES.success
|
127
172
|
elif self.steps_to_complete is None:
|
128
|
-
log.info("Finishing as
|
129
|
-
self.state = self.STATES.
|
173
|
+
log.info("Finishing as success with no steps to complete configured")
|
174
|
+
self.state = self.STATES.success
|
130
175
|
elif self.num_failed_steps == self.steps_to_complete:
|
131
176
|
log.info("Finishing as failure with all steps failed")
|
132
177
|
self.state = self.STATES.failed
|
@@ -134,11 +179,13 @@ class BackgroundTask(CreatedUpdatedMixin):
|
|
134
179
|
log.info("Finishing as partial success with some steps failed")
|
135
180
|
self.state = self.STATES.partial_success
|
136
181
|
|
182
|
+
self.completed_at = timezone.now()
|
183
|
+
self.save()
|
184
|
+
|
137
185
|
@locked
|
138
186
|
def add_successful_steps(self, num_steps):
|
139
187
|
self.steps_completed += num_steps
|
140
|
-
self.
|
141
|
-
self.save()
|
188
|
+
self._finish_or_save()
|
142
189
|
|
143
190
|
@locked
|
144
191
|
def steps_failed(self, num_steps, steps_identifier=None, error=None):
|
@@ -153,8 +200,7 @@ class BackgroundTask(CreatedUpdatedMixin):
|
|
153
200
|
error_dict.update(self._error_dict_for_error(error))
|
154
201
|
|
155
202
|
self.errors.append(error_dict)
|
156
|
-
self.
|
157
|
-
self.save()
|
203
|
+
self._finish_or_save()
|
158
204
|
|
159
205
|
def dispatch(self):
|
160
206
|
# double fork to avoid zombies
|
@@ -227,9 +273,12 @@ class BackgroundTask(CreatedUpdatedMixin):
|
|
227
273
|
)
|
228
274
|
return error_dict
|
229
275
|
|
230
|
-
def
|
231
|
-
if
|
232
|
-
|
233
|
-
|
234
|
-
|
276
|
+
def _finish_or_save(self):
|
277
|
+
if (
|
278
|
+
self.steps_to_complete is not None
|
279
|
+
and self.steps_completed is not None
|
280
|
+
and self.steps_completed >= self.steps_to_complete
|
281
|
+
):
|
235
282
|
self.finish()
|
283
|
+
else:
|
284
|
+
self.save()
|
@@ -21,7 +21,7 @@ class ProgressState {
|
|
21
21
|
constructor(element, {value, max} = {value: null, max: null}) {
|
22
22
|
this.element = element;
|
23
23
|
|
24
|
-
this.
|
24
|
+
this.animationFrameRequestId = null;
|
25
25
|
|
26
26
|
this.max = max;
|
27
27
|
// set it immediately initially
|
@@ -56,56 +56,60 @@ class ProgressState {
|
|
56
56
|
this._cancelTimer();
|
57
57
|
const currentValue = this.progEle.value || 0;
|
58
58
|
if (value === null || value === undefined) {
|
59
|
+
// console.info("null value");
|
59
60
|
this.progEle.removeAttribute("value");
|
60
|
-
this._clearState();
|
61
61
|
return
|
62
62
|
}
|
63
63
|
if (value >= this.max) {
|
64
|
+
// console.info("greater than max", value, this.max);
|
64
65
|
this.progEle.value = this.max;
|
65
|
-
this._clearState();
|
66
66
|
return;
|
67
67
|
}
|
68
68
|
if (currentValue === value) {
|
69
|
-
|
69
|
+
// console.info("already at value", currentValue, value);
|
70
70
|
return;
|
71
71
|
}
|
72
72
|
|
73
73
|
// completion due in the refresh period plus a bit so that we don't get there too soon
|
74
74
|
// and cause a visible stop: we would rather the next update arrived before we reached the
|
75
75
|
// target value
|
76
|
-
const
|
77
|
-
const
|
76
|
+
const now = new Date();
|
77
|
+
const completionDue = new Date(now.getTime() + REFRESH_PERIOD_MS + PROGRESS_REFRESH_MS);
|
78
|
+
const initialMSToCompletion = completionDue - now;
|
79
|
+
const completionDueHighRes = performance.now() + initialMSToCompletion;
|
78
80
|
const previousValue = currentValue;
|
79
81
|
|
80
|
-
|
81
|
-
const now = new Date();
|
82
|
-
if (completionDue <= now) {
|
83
|
-
this.progEle.value = value;
|
84
|
-
this._cancelTimer();
|
85
|
-
this._clearState();
|
86
|
-
return;
|
87
|
-
}
|
88
|
-
|
89
|
-
const msRemaining = completionDue - now;
|
90
|
-
|
91
|
-
const newValue = previousValue + (
|
92
|
-
((initialMSToCompletion - msRemaining) / initialMSToCompletion)
|
93
|
-
* (value - previousValue)
|
94
|
-
);
|
95
|
-
this.progEle.value = newValue;
|
96
|
-
};
|
97
|
-
|
98
|
-
this.timerId = setInterval(updateProgress, PROGRESS_REFRESH_MS);
|
99
|
-
}
|
82
|
+
let startTimestamp = null;
|
100
83
|
|
101
|
-
|
102
|
-
if (
|
103
|
-
|
84
|
+
const updateProgressStep = (now) => {
|
85
|
+
if (startTimestamp === null) {
|
86
|
+
startTimestamp = now;
|
87
|
+
this.animationFrameRequestId = window.requestAnimationFrame(updateProgressStep);
|
88
|
+
return;
|
89
|
+
}
|
90
|
+
const msRemaining = completionDueHighRes - now;
|
91
|
+
const newValue = previousValue + (
|
92
|
+
((initialMSToCompletion - msRemaining) / initialMSToCompletion)
|
93
|
+
* (value - previousValue)
|
94
|
+
);
|
95
|
+
|
96
|
+
if (completionDue <= now || Math.abs((value - newValue) / this.progEle.max) < 0.001) {
|
97
|
+
this.progEle.value = value;
|
98
|
+
return;
|
104
99
|
}
|
105
|
-
|
100
|
+
|
101
|
+
this.progEle.value = newValue;
|
102
|
+
this.animationFrameRequestId = window.requestAnimationFrame(updateProgressStep);
|
103
|
+
}
|
104
|
+
|
105
|
+
this.animationFrameRequestId = window.requestAnimationFrame(updateProgressStep);
|
106
106
|
}
|
107
107
|
|
108
|
-
|
108
|
+
_cancelTimer() {
|
109
|
+
if (this.animationFrameRequestId !== null) {
|
110
|
+
window.cancelAnimationFrame(this.animationFrameRequestId)
|
111
|
+
}
|
112
|
+
this.animationFrameRequestId = null;
|
109
113
|
}
|
110
114
|
}
|
111
115
|
|
@@ -0,0 +1,31 @@
|
|
1
|
+
{% extends "admin/change_list.html" %}
|
2
|
+
{% load static %}
|
3
|
+
|
4
|
+
{% block extrastyle %}
|
5
|
+
{{ block.super }}
|
6
|
+
<link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}">
|
7
|
+
{% endblock %}
|
8
|
+
|
9
|
+
{% block object-tools %}
|
10
|
+
{{ block.super }}
|
11
|
+
<div>
|
12
|
+
<h3>Action background tasks</h3>
|
13
|
+
{% if not admin_bg_tasks|length %}
|
14
|
+
<p class="help" style="font-style: italic;">No recent background tasks</p>
|
15
|
+
{% else %}
|
16
|
+
<table>
|
17
|
+
<thead>
|
18
|
+
<tr><th>Task name</th><th>Started</th><th>Finished</th><th>Status</th></tr>
|
19
|
+
</thead>
|
20
|
+
<tbody>
|
21
|
+
{% for bgt in admin_bg_tasks %}
|
22
|
+
{% with bgtask=bgt.task_dict %}
|
23
|
+
<tr class="bgtask-row"><td><a href="{% url 'admin:bgtask_backgroundtask_change' bgtask.id %}">{{ bgt.admin_description }}</a></td><td>{{ bgt.started_at|timesince }} ago</td><td>{% if bgt.completed_at %}{{ bgt.completed_at|timesince }} ago{% else %}–{% endif %}</td><td>
|
24
|
+
{% include "bgtask/bg_changelist_status_column.html" %}</td></tr>
|
25
|
+
{% endwith %}
|
26
|
+
{% endfor %}
|
27
|
+
</tbody>
|
28
|
+
</table>
|
29
|
+
{% endif %}
|
30
|
+
</div>
|
31
|
+
{% endblock %}
|
@@ -6,7 +6,11 @@
|
|
6
6
|
{{ bgtask|json_script:initialTasksJsonId }}
|
7
7
|
{% endwith %}
|
8
8
|
<script src="{% static 'bgtask/js/bgtask-once.js' %}"></script>
|
9
|
+
{% if bgtask.acted_on_object_id %}
|
9
10
|
<a href="{% url 'bgtask:tasks' %}?object_id={{bgtask.acted_on_object_id}}">
|
11
|
+
{% else %}
|
12
|
+
<a href="{% url 'admin:bgtask_backgroundtask_change' bgtask.id %}">
|
13
|
+
{% endif %}
|
10
14
|
<div id="bgtask-column-{{ bgtask.id }}">
|
11
15
|
{% include 'bgtask/progress.html' %}
|
12
16
|
</div>
|
bgtask/views.py
CHANGED
@@ -10,15 +10,8 @@ from .models import BackgroundTask
|
|
10
10
|
Q_NONE = Q(pk__in=[])
|
11
11
|
|
12
12
|
|
13
|
-
def task_dict(task):
|
14
|
-
task_dict = model_to_dict(task)
|
15
|
-
if "stack_context" in task_dict:
|
16
|
-
del task_dict["stack_context"]
|
17
|
-
return {"id": str(task.id), "updated": task.updated.isoformat(), **task_dict}
|
18
|
-
|
19
|
-
|
20
13
|
def _tasks_dict(tasks):
|
21
|
-
td = {str(task.id): task_dict
|
14
|
+
td = {str(task.id): task.task_dict for task in tasks}
|
22
15
|
return td
|
23
16
|
|
24
17
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: django-admin-background-task
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.2.0
|
4
4
|
Summary: A set of tools for django apps to persist and monitor the status of background tasks
|
5
5
|
Home-page: https://github.com/daphtdazz/django-bgtask
|
6
6
|
License: Apache-2.0
|
@@ -41,7 +41,7 @@ INSTALLED_APPS = [
|
|
41
41
|
]
|
42
42
|
```
|
43
43
|
|
44
|
-
And mount the
|
44
|
+
And mount the admin monitoring URLs:
|
45
45
|
|
46
46
|
```
|
47
47
|
urlpatterns = [
|
{django_admin_background_task-0.1.0.dist-info → django_admin_background_task-0.2.0.dist-info}/RECORD
RENAMED
@@ -1,14 +1,19 @@
|
|
1
|
-
bgtask/.DS_Store,sha256=TdvU4O6xL31hGFKIbymkCVOa_vP5cJS4x-K9dIzwxGI,6148
|
2
1
|
bgtask/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
-
bgtask/admin.py,sha256=
|
2
|
+
bgtask/admin.py,sha256=cOhbR__7WJ3rmjlbZjGKh_gJDpEz_c6Ll2zwHo7mpiM,965
|
4
3
|
bgtask/apps.py,sha256=zZHsNaW9nnJgK7jnpty0IjM0TTpsk33xhpsGTa-x3iE,144
|
4
|
+
bgtask/backends/__init__.py,sha256=zZlL7gHXL12heAhHD_7D7lEBihCE-LHYb07yIbk055s,57
|
5
|
+
bgtask/backends/thread_pool.py,sha256=y5EtO9ltA5lc7GUtvqHPJuoo1brJPOurp1IQ7JdCUEI,304
|
6
|
+
bgtask/decorators.py,sha256=UsiCBhqaWm1Yn4n4hmFbauGQvNezoeDm7Iqpjbvxbjk,1119
|
5
7
|
bgtask/migrations/0001_initial.py,sha256=YDTEy_hiXHru-1ZdIH-n3po82r5vyzbkyYIrF9_yiI8,2013
|
8
|
+
bgtask/migrations/0002_backgroundtask_namespace_alter_backgroundtask_name.py,sha256=XIG_gIvdA73tz0NRbNkGU_ieKde3ZtQltPeLdslD5Xo,968
|
6
9
|
bgtask/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
7
|
-
bgtask/
|
10
|
+
bgtask/model_admin.py,sha256=tx90pz3MGMkN2DsZ8jEwEXY875QVXySGg_9rGcID50k,2925
|
11
|
+
bgtask/models.py,sha256=VulETkcBXJkyUgmExSx9fuOvpp1HnY2sLQLD5BUvbHk,9184
|
8
12
|
bgtask/stack_contexts.py,sha256=2Hun6O5Mm949uAioC2E3i54Rg-_H8P7LsHdhJYc0w6E,231
|
9
13
|
bgtask/static/bgtask/js/bgtask-once.js,sha256=DBerpxPLP21IH--HpBxVFk3kN52FtBiuyJsDCjg7mCA,746
|
10
|
-
bgtask/static/bgtask/js/bgtask.js,sha256=
|
11
|
-
bgtask/templates/bgtask/
|
14
|
+
bgtask/static/bgtask/js/bgtask.js,sha256=VQ853jxRxs53E5E4XKLLeRByVcFuyqHEsniBIb8wq4Q,13151
|
15
|
+
bgtask/templates/bgtask/admin/change_list.html,sha256=_Vjm1SLGj_Z2lX1H5y-jfLNWFiRfmvKLgh2YG0QfCpc,1062
|
16
|
+
bgtask/templates/bgtask/bg_changelist_status_column.html,sha256=4ATqCKTx8yVINvSUfRKckjrs8JteX45R4fnAAHm9c9g,978
|
12
17
|
bgtask/templates/bgtask/bgtask_templates.html,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
13
18
|
bgtask/templates/bgtask/bgtask_view.html,sha256=AeubqERFF14PHWjInBD39wGImnRx4taS42iOMOPDUlM,1845
|
14
19
|
bgtask/templates/bgtask/progress.html,sha256=VVYbrunGasOshRlz8Nm8hFPxqcVtrpaW1e2QxzAMcMM,166
|
@@ -16,8 +21,8 @@ bgtask/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
|
|
16
21
|
bgtask/templatetags/bgtask.py,sha256=WUEWlQX5AarbXJBV6EGp-khT1zegVwGNigSKRF5mCTE,413
|
17
22
|
bgtask/tests/test_bgtask.py,sha256=9AsBppPphaIdphuB-pHuQjgLhYNp8ySWtmcWF2uQl3Q,1090
|
18
23
|
bgtask/urls.py,sha256=DXXjgj18LTSCq2-xZf23-pkgO4RmZglv67mgrp-fDyk,161
|
19
|
-
bgtask/views.py,sha256=
|
20
|
-
django_admin_background_task-0.
|
21
|
-
django_admin_background_task-0.
|
22
|
-
django_admin_background_task-0.
|
23
|
-
django_admin_background_task-0.
|
24
|
+
bgtask/views.py,sha256=tPblidBq8mIv9cbyAfzJfNlM-I-Z11XVoobdA3DZBVM,1765
|
25
|
+
django_admin_background_task-0.2.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
26
|
+
django_admin_background_task-0.2.0.dist-info/METADATA,sha256=zM0kMweJLTim6uyYHhQIsFKR-RpYW-wwcNlwcZAyk6M,1416
|
27
|
+
django_admin_background_task-0.2.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
28
|
+
django_admin_background_task-0.2.0.dist-info/RECORD,,
|
bgtask/.DS_Store
DELETED
Binary file
|
File without changes
|
{django_admin_background_task-0.1.0.dist-info → django_admin_background_task-0.2.0.dist-info}/WHEEL
RENAMED
File without changes
|