django-admin-background-task 0.1.0__py3-none-any.whl → 0.3.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 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(bgtask)}
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", "result"]
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,3 @@
1
+ from . import thread_pool
2
+
3
+ default_backend = thread_pool
@@ -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,113 @@
1
+ from datetime import timedelta
2
+ from functools import wraps
3
+
4
+ from django.contrib import admin
5
+ from django.contrib.admin.utils import label_for_field
6
+ from django.contrib.messages import INFO
7
+ from django.db.models import Q
8
+ from django.utils import timezone
9
+
10
+ from .models import BackgroundTask
11
+
12
+
13
+ class BGTaskModelAdmin(admin.ModelAdmin):
14
+ # This is not overridden to avoid messing with the implicit logic for finding change list
15
+ # templates that ModelAdmin uses. So you either need to specify this yourself on your
16
+ # subclass or you need to extend from this in your custom template.
17
+ #
18
+ # change_list_template = "bgtask/admin/change_list.html"
19
+
20
+ # ----------------------------------------------------------------------------------------------
21
+ # Class API
22
+ # ----------------------------------------------------------------------------------------------
23
+ @classmethod
24
+ def starts_task(cls, name, **task_kwargs):
25
+
26
+ def starts_task_decorator(func):
27
+
28
+ @wraps(func)
29
+ def starts_task_wrapper(self, request, *args, **kwargs):
30
+ bgtask = self.start_bgtask(name, **task_kwargs)
31
+ result = func(self, request, *args, bgtask=bgtask, **kwargs)
32
+ self.message_user(request, f"Dispatched task {name}", INFO)
33
+
34
+ func.bgtask_name = name
35
+
36
+ return starts_task_wrapper
37
+
38
+ return starts_task_decorator
39
+
40
+ # ----------------------------------------------------------------------------------------------
41
+ # API for subclasses
42
+ # ----------------------------------------------------------------------------------------------
43
+ def start_bgtask(self, name, **kwargs):
44
+ bgtask = BackgroundTask.objects.create(
45
+ name=name,
46
+ namespace=self._bgtask_namespace,
47
+ **kwargs,
48
+ )
49
+ bgtask.start()
50
+ return bgtask
51
+
52
+ # ----------------------------------------------------------------------------------------------
53
+ # Superclass overrides
54
+ # ----------------------------------------------------------------------------------------------
55
+ def changelist_view(self, request, extra_context=None):
56
+ extra_context = extra_context or {}
57
+ extra_context["admin_bg_tasks"] = self._admin_bg_tasks(request)
58
+ return super().changelist_view(request, extra_context=extra_context)
59
+
60
+ # ----------------------------------------------------------------------------------------------
61
+ # Internal functions
62
+ # ----------------------------------------------------------------------------------------------
63
+ @property
64
+ def _bgtask_namespace(self):
65
+ return type(self).__module__ + "." + type(self).__name__
66
+
67
+ @staticmethod
68
+ def _extract_bgtask_name_from_admin_action(action):
69
+ # recurse through the potentially wrapped action until we find one that declares
70
+ # the bgtask_name
71
+ next_action = action
72
+ while True:
73
+ if hasattr(next_action, "bgtask_name"):
74
+ return next_action.bgtask_name
75
+
76
+ if not hasattr(next_action, "__wrapped__"):
77
+ return None
78
+
79
+ next_action = next_action.__wrapped__
80
+
81
+ def _admin_bg_tasks(self, request):
82
+ task_name_to_desc = {}
83
+ for action, action_name, action_description in self.get_actions(request).values():
84
+ bgtask_name = self._extract_bgtask_name_from_admin_action(action)
85
+ if bgtask_name is not None:
86
+ task_name_to_desc[bgtask_name] = action_description
87
+
88
+ for name in getattr(self, "bgtask_names", []):
89
+ task_name_to_desc[name] = name
90
+
91
+ if not task_name_to_desc:
92
+ return BackgroundTask.objects.none()
93
+
94
+ bgts = list(
95
+ BackgroundTask.objects.filter(
96
+ name__in=task_name_to_desc, namespace=self._bgtask_namespace
97
+ )
98
+ .filter(
99
+ (
100
+ Q(state=BackgroundTask.STATES.running)
101
+ & Q(started_at__gt=timezone.now() - timedelta(days=1))
102
+ )
103
+ | (
104
+ ~Q(state=BackgroundTask.STATES.not_started)
105
+ & Q(completed_at__gt=timezone.now() - timedelta(hours=2))
106
+ )
107
+ )
108
+ .order_by("-started_at")
109
+ )
110
+ for bgt in bgts:
111
+ bgt.admin_description = task_name_to_desc[bgt.name]
112
+
113
+ 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
- return meth(self, *args, **kwargs)
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 CreatedUpdatedMixin(models.Model):
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(max_length=1000)
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.completion = 1
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 failure with no steps to complete configured")
129
- self.state = self.STATES.failed
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._maybe_finish()
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._maybe_finish()
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 _maybe_finish(self):
231
- if self.steps_to_complete is None:
232
- return
233
-
234
- if self.steps_completed >= self.steps_to_complete:
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()
@@ -14,6 +14,33 @@ const TIME_TO_REDUCED_REFRESH_PERIOD_S = 300;
14
14
  const REDUCED_REFRESH_PERIOD_S = 60;
15
15
  const PROGRESS_REFRESH_MS = 100;
16
16
 
17
+ function millisecondsToTimeAgoString(ms) {
18
+ const seconds = Math.floor(ms / 1000);
19
+ const minutes = Math.floor(seconds / 60);
20
+ const hours = Math.floor(minutes / 60);
21
+ const days = Math.floor(hours / 24);
22
+
23
+ if (days > 0) {
24
+ if (days === 1) {
25
+ return `${days} day, ${hours % 24} hours ago`;
26
+ }
27
+ return `${days} days ago`;
28
+ }
29
+ if (hours > 0) {
30
+ if (hours === 1) {
31
+ return `${hours} hour, ${minutes % 60} minutes ago`;
32
+ }
33
+ return `${hours} hours ago`;
34
+ }
35
+ if (minutes > 0) {
36
+ if (minutes === 1) {
37
+ return `${minutes} minute, ${seconds % 60} seconds ago`;
38
+ }
39
+ return `${minutes} minutes ago`;
40
+ }
41
+ return `${seconds} seconds ago`;
42
+ }
43
+
17
44
  // -------------------------------------------------------------------------------------------------
18
45
  // Generic live-looking progress bar manager.
19
46
  // -------------------------------------------------------------------------------------------------
@@ -21,7 +48,7 @@ class ProgressState {
21
48
  constructor(element, {value, max} = {value: null, max: null}) {
22
49
  this.element = element;
23
50
 
24
- this.timerId = null;
51
+ this.animationFrameRequestId = null;
25
52
 
26
53
  this.max = max;
27
54
  // set it immediately initially
@@ -56,56 +83,60 @@ class ProgressState {
56
83
  this._cancelTimer();
57
84
  const currentValue = this.progEle.value || 0;
58
85
  if (value === null || value === undefined) {
86
+ // console.info("null value");
59
87
  this.progEle.removeAttribute("value");
60
- this._clearState();
61
88
  return
62
89
  }
63
90
  if (value >= this.max) {
91
+ // console.info("greater than max", value, this.max);
64
92
  this.progEle.value = this.max;
65
- this._clearState();
66
93
  return;
67
94
  }
68
95
  if (currentValue === value) {
69
- this._clearState();
96
+ // console.info("already at value", currentValue, value);
70
97
  return;
71
98
  }
72
99
 
73
100
  // completion due in the refresh period plus a bit so that we don't get there too soon
74
101
  // and cause a visible stop: we would rather the next update arrived before we reached the
75
102
  // target value
76
- const completionDue = new Date(new Date().getTime() + REFRESH_PERIOD_MS + 500);
77
- const initialMSToCompletion = completionDue - new Date();
103
+ const now = new Date();
104
+ const completionDue = new Date(now.getTime() + REFRESH_PERIOD_MS + PROGRESS_REFRESH_MS);
105
+ const initialMSToCompletion = completionDue - now;
106
+ const completionDueHighRes = performance.now() + initialMSToCompletion;
78
107
  const previousValue = currentValue;
79
108
 
80
- const updateProgress = () => {
81
- const now = new Date();
82
- if (completionDue <= now) {
83
- this.progEle.value = value;
84
- this._cancelTimer();
85
- this._clearState();
86
- return;
87
- }
109
+ let startTimestamp = null;
88
110
 
89
- const msRemaining = completionDue - now;
111
+ const updateProgressStep = (now) => {
112
+ if (startTimestamp === null) {
113
+ startTimestamp = now;
114
+ this.animationFrameRequestId = window.requestAnimationFrame(updateProgressStep);
115
+ return;
116
+ }
117
+ const msRemaining = completionDueHighRes - now;
118
+ const newValue = previousValue + (
119
+ ((initialMSToCompletion - msRemaining) / initialMSToCompletion)
120
+ * (value - previousValue)
121
+ );
122
+
123
+ if (completionDue <= now || Math.abs((value - newValue) / this.progEle.max) < 0.001) {
124
+ this.progEle.value = value;
125
+ return;
126
+ }
90
127
 
91
- const newValue = previousValue + (
92
- ((initialMSToCompletion - msRemaining) / initialMSToCompletion)
93
- * (value - previousValue)
94
- );
95
- this.progEle.value = newValue;
96
- };
128
+ this.progEle.value = newValue;
129
+ this.animationFrameRequestId = window.requestAnimationFrame(updateProgressStep);
130
+ }
97
131
 
98
- this.timerId = setInterval(updateProgress, PROGRESS_REFRESH_MS);
132
+ this.animationFrameRequestId = window.requestAnimationFrame(updateProgressStep);
99
133
  }
100
134
 
101
135
  _cancelTimer() {
102
- if (this.timerId !== null) {
103
- clearInterval(this.timerId);
136
+ if (this.animationFrameRequestId !== null) {
137
+ window.cancelAnimationFrame(this.animationFrameRequestId)
104
138
  }
105
- this.timerId = null;
106
- }
107
-
108
- _clearState() {
139
+ this.animationFrameRequestId = null;
109
140
  }
110
141
  }
111
142
 
@@ -136,7 +167,6 @@ class TaskProgressDiv {
136
167
  }
137
168
 
138
169
  updateFromTask(task) {
139
- // console.log(`TaskProgressDiv.updateFromTask`, task);
140
170
  this.div.title = "";
141
171
 
142
172
  switch (task.state) {
@@ -148,7 +178,14 @@ class TaskProgressDiv {
148
178
  case "failed":
149
179
  this._hideProgress();
150
180
  this._showState();
151
- this._addTitle("Task failed");
181
+ let title = "Task failed";
182
+ for (const error of task.errors) {
183
+ if (error.traceback) {
184
+ title = `${title}\n${error.traceback}\n\n${error.error_message}`;
185
+ break;
186
+ }
187
+ }
188
+ this._addTitle(title);
152
189
  break;
153
190
  case "success":
154
191
  this._hideProgress();
@@ -201,6 +238,26 @@ class TaskProgressDiv {
201
238
  }
202
239
  }
203
240
 
241
+ // -------------------------------------------------------------------------------------------------
242
+ // Manage an element which just shows one field from the task
243
+ // -------------------------------------------------------------------------------------------------
244
+ class TaskFieldElement {
245
+ constructor (element, fieldName, task) {
246
+ this.taskId = task.id;
247
+ this.element = element;
248
+ this.fieldName = fieldName;
249
+ this.updateFromTask(task);
250
+ }
251
+
252
+ attachToPoller(poller) {
253
+ poller.monitorTask(this.taskId, task => this.updateFromTask(task));
254
+ }
255
+
256
+ updateFromTask(task) {
257
+ this.element.innerHTML = task[this.fieldName];
258
+ }
259
+ }
260
+
204
261
  // -------------------------------------------------------------------------------------------------
205
262
  // Manage a task detail div
206
263
  // -------------------------------------------------------------------------------------------------
@@ -295,6 +352,14 @@ class BGTaskPoller {
295
352
 
296
353
  static normalizeTask(task) {
297
354
  task.updated = new Date(task.updated);
355
+
356
+ if (task.completed_at !== null) {
357
+ task.completed_at = new Date(task.completed_at);
358
+ const completedTimeAgoMS = Date.now() - task.completed_at.getTime();
359
+ task.completed_at_time_ago = millisecondsToTimeAgoString(completedTimeAgoMS);
360
+ } else {
361
+ task.completed_at_time_ago = "–";
362
+ }
298
363
  }
299
364
 
300
365
  static sharedInstance(baseURL) {
@@ -353,7 +418,9 @@ class BGTaskPoller {
353
418
  const req = new XMLHttpRequest();
354
419
  const self = this;
355
420
  req.addEventListener("load", function () { self._receivePoll(this); });
356
- const url = `${this.baseURL}?tasks=${Object.keys(this.taskCallbacks).join(",")}`;
421
+ const taskIds = Object.keys(this.taskCallbacks).join(",");
422
+ const url = `${this.baseURL}?tasks=${taskIds}`;
423
+ console.log(`Poll for tasks ${taskIds}`);
357
424
  req.open("GET", url);
358
425
  req.setRequestHeader('Accept', 'application/json');
359
426
  req.send();
@@ -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 style="margin-bottom: 10px;">
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>{% include "bgtask/bg_completed_column.html" %}</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,7 @@
6
6
  {{ bgtask|json_script:initialTasksJsonId }}
7
7
  {% endwith %}
8
8
  <script src="{% static 'bgtask/js/bgtask-once.js' %}"></script>
9
- <a href="{% url 'bgtask:tasks' %}?object_id={{bgtask.acted_on_object_id}}">
9
+ <a href="{% url 'admin:bgtask_backgroundtask_change' bgtask.id %}">
10
10
  <div id="bgtask-column-{{ bgtask.id }}">
11
11
  {% include 'bgtask/progress.html' %}
12
12
  </div>
@@ -0,0 +1,24 @@
1
+ {% load static %}
2
+ {% if not bgtask %}
3
+
4
+ {% else %}
5
+ {% with initialTasksJsonId="initialTasksJson-completedcolumn-"|add:bgtask.id %}
6
+ {{ bgtask|json_script:initialTasksJsonId }}
7
+ {% endwith %}
8
+ <script src="{% static 'bgtask/js/bgtask-once.js' %}"></script>
9
+ <span></span>
10
+ <script>
11
+ (() => {
12
+ const completedSpan = document.currentScript.previousElementSibling;
13
+
14
+ const originalTask = JSON.parse(
15
+ document.getElementById("initialTasksJson-completedcolumn-{{ bgtask.id }}").textContent
16
+ );
17
+ BGTaskPoller.normalizeTask(originalTask);
18
+
19
+ const statusCol = new TaskFieldElement(completedSpan, "completed_at_time_ago", originalTask);
20
+ const poller = BGTaskPoller.sharedInstance("{% url 'bgtask:tasks' %}");
21
+ statusCol.attachToPoller(poller);
22
+ })();
23
+ </script>
24
+ {% endif %}
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(task) for task in tasks}
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.1.0
3
+ Version: 0.3.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 amdin monitoring URLs:
44
+ And mount the admin monitoring URLs:
45
45
 
46
46
  ```
47
47
  urlpatterns = [
@@ -0,0 +1,29 @@
1
+ bgtask/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ bgtask/admin.py,sha256=cOhbR__7WJ3rmjlbZjGKh_gJDpEz_c6Ll2zwHo7mpiM,965
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
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
9
+ bgtask/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ bgtask/model_admin.py,sha256=mwRoljw1CwMA-c3zbssmrRsV9l8xsgYJOBrkgVNilRs,4402
11
+ bgtask/models.py,sha256=VulETkcBXJkyUgmExSx9fuOvpp1HnY2sLQLD5BUvbHk,9184
12
+ bgtask/stack_contexts.py,sha256=2Hun6O5Mm949uAioC2E3i54Rg-_H8P7LsHdhJYc0w6E,231
13
+ bgtask/static/bgtask/js/bgtask-once.js,sha256=DBerpxPLP21IH--HpBxVFk3kN52FtBiuyJsDCjg7mCA,746
14
+ bgtask/static/bgtask/js/bgtask.js,sha256=aYG02BY2F0aBlyRT_19turcC6RNLmyhJZUiuABUszoo,15026
15
+ bgtask/templates/bgtask/admin/change_list.html,sha256=nR7LQ9yPv1LgD97g9oU4d8P6wxq9HrEJ4nneA14msNM,1053
16
+ bgtask/templates/bgtask/bg_changelist_status_column.html,sha256=SA7EAL50oBrcRPGstXROXkFJ541vYA8RlHidM3srTPU,844
17
+ bgtask/templates/bgtask/bg_completed_column.html,sha256=Q4l3EQaOXHYVOzv0jpx3ww3Wa2Bb33RyZp3RSWI1zog,788
18
+ bgtask/templates/bgtask/bgtask_templates.html,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
19
+ bgtask/templates/bgtask/bgtask_view.html,sha256=AeubqERFF14PHWjInBD39wGImnRx4taS42iOMOPDUlM,1845
20
+ bgtask/templates/bgtask/progress.html,sha256=VVYbrunGasOshRlz8Nm8hFPxqcVtrpaW1e2QxzAMcMM,166
21
+ bgtask/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
+ bgtask/templatetags/bgtask.py,sha256=WUEWlQX5AarbXJBV6EGp-khT1zegVwGNigSKRF5mCTE,413
23
+ bgtask/tests/test_bgtask.py,sha256=9AsBppPphaIdphuB-pHuQjgLhYNp8ySWtmcWF2uQl3Q,1090
24
+ bgtask/urls.py,sha256=DXXjgj18LTSCq2-xZf23-pkgO4RmZglv67mgrp-fDyk,161
25
+ bgtask/views.py,sha256=tPblidBq8mIv9cbyAfzJfNlM-I-Z11XVoobdA3DZBVM,1765
26
+ django_admin_background_task-0.3.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
27
+ django_admin_background_task-0.3.0.dist-info/METADATA,sha256=mDJm-1PabE_GFKUSxv9chBhiBhmtbBAaiyvlKxr0MQ4,1416
28
+ django_admin_background_task-0.3.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
29
+ django_admin_background_task-0.3.0.dist-info/RECORD,,
bgtask/.DS_Store DELETED
Binary file
@@ -1,23 +0,0 @@
1
- bgtask/.DS_Store,sha256=TdvU4O6xL31hGFKIbymkCVOa_vP5cJS4x-K9dIzwxGI,6148
2
- bgtask/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- bgtask/admin.py,sha256=G400_bFRTFzYexKajrJ3eS6lvy4mFBWQq7hGXEZxEJk,854
4
- bgtask/apps.py,sha256=zZHsNaW9nnJgK7jnpty0IjM0TTpsk33xhpsGTa-x3iE,144
5
- bgtask/migrations/0001_initial.py,sha256=YDTEy_hiXHru-1ZdIH-n3po82r5vyzbkyYIrF9_yiI8,2013
6
- bgtask/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- bgtask/models.py,sha256=04Cz_XgibkRukbteZcaI2qFuYEzwHfe04Ucxt1cLCpE,7683
8
- bgtask/stack_contexts.py,sha256=2Hun6O5Mm949uAioC2E3i54Rg-_H8P7LsHdhJYc0w6E,231
9
- bgtask/static/bgtask/js/bgtask-once.js,sha256=DBerpxPLP21IH--HpBxVFk3kN52FtBiuyJsDCjg7mCA,746
10
- bgtask/static/bgtask/js/bgtask.js,sha256=0S_emqYF1WStRWgOAqPn0gKst4Ytn9EGnWJelkecesU,12636
11
- bgtask/templates/bgtask/bg_changelist_status_column.html,sha256=AKvJvIoPG7LVmRvcsNGzOExe0qT71Yt4OqOpXqxsMKc,852
12
- bgtask/templates/bgtask/bgtask_templates.html,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
13
- bgtask/templates/bgtask/bgtask_view.html,sha256=AeubqERFF14PHWjInBD39wGImnRx4taS42iOMOPDUlM,1845
14
- bgtask/templates/bgtask/progress.html,sha256=VVYbrunGasOshRlz8Nm8hFPxqcVtrpaW1e2QxzAMcMM,166
15
- bgtask/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- bgtask/templatetags/bgtask.py,sha256=WUEWlQX5AarbXJBV6EGp-khT1zegVwGNigSKRF5mCTE,413
17
- bgtask/tests/test_bgtask.py,sha256=9AsBppPphaIdphuB-pHuQjgLhYNp8ySWtmcWF2uQl3Q,1090
18
- bgtask/urls.py,sha256=DXXjgj18LTSCq2-xZf23-pkgO4RmZglv67mgrp-fDyk,161
19
- bgtask/views.py,sha256=4ypwMNwH_N9iKOrP5Ub_rLGFVV5BUMvuZVkCBXoSa7k,1983
20
- django_admin_background_task-0.1.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
21
- django_admin_background_task-0.1.0.dist-info/METADATA,sha256=IIZyk-zPNWp-1f_xiA7Q-7LjZ-_0fQ-LObBMuwCqs90,1416
22
- django_admin_background_task-0.1.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
23
- django_admin_background_task-0.1.0.dist-info/RECORD,,