django-admin-background-task 0.2.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/model_admin.py CHANGED
@@ -1,7 +1,9 @@
1
1
  from datetime import timedelta
2
+ from functools import wraps
2
3
 
3
4
  from django.contrib import admin
4
5
  from django.contrib.admin.utils import label_for_field
6
+ from django.contrib.messages import INFO
5
7
  from django.db.models import Q
6
8
  from django.utils import timezone
7
9
 
@@ -12,8 +14,29 @@ class BGTaskModelAdmin(admin.ModelAdmin):
12
14
  # This is not overridden to avoid messing with the implicit logic for finding change list
13
15
  # templates that ModelAdmin uses. So you either need to specify this yourself on your
14
16
  # subclass or you need to extend from this in your custom template.
17
+ #
15
18
  # change_list_template = "bgtask/admin/change_list.html"
16
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
+
17
40
  # ----------------------------------------------------------------------------------------------
18
41
  # API for subclasses
19
42
  # ----------------------------------------------------------------------------------------------
@@ -41,11 +64,26 @@ class BGTaskModelAdmin(admin.ModelAdmin):
41
64
  def _bgtask_namespace(self):
42
65
  return type(self).__module__ + "." + type(self).__name__
43
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
+
44
81
  def _admin_bg_tasks(self, request):
45
82
  task_name_to_desc = {}
46
83
  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
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
49
87
 
50
88
  for name in getattr(self, "bgtask_names", []):
51
89
  task_name_to_desc[name] = name
@@ -58,10 +96,13 @@ class BGTaskModelAdmin(admin.ModelAdmin):
58
96
  name__in=task_name_to_desc, namespace=self._bgtask_namespace
59
97
  )
60
98
  .filter(
61
- Q(state=BackgroundTask.STATES.running)
99
+ (
100
+ Q(state=BackgroundTask.STATES.running)
101
+ & Q(started_at__gt=timezone.now() - timedelta(days=1))
102
+ )
62
103
  | (
63
104
  ~Q(state=BackgroundTask.STATES.not_started)
64
- & Q(completed_at__gt=timezone.now() - timedelta(minutes=30))
105
+ & Q(completed_at__gt=timezone.now() - timedelta(hours=2))
65
106
  )
66
107
  )
67
108
  .order_by("-started_at")
@@ -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
  // -------------------------------------------------------------------------------------------------
@@ -140,7 +167,6 @@ class TaskProgressDiv {
140
167
  }
141
168
 
142
169
  updateFromTask(task) {
143
- // console.log(`TaskProgressDiv.updateFromTask`, task);
144
170
  this.div.title = "";
145
171
 
146
172
  switch (task.state) {
@@ -152,7 +178,14 @@ class TaskProgressDiv {
152
178
  case "failed":
153
179
  this._hideProgress();
154
180
  this._showState();
155
- 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);
156
189
  break;
157
190
  case "success":
158
191
  this._hideProgress();
@@ -205,6 +238,26 @@ class TaskProgressDiv {
205
238
  }
206
239
  }
207
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
+
208
261
  // -------------------------------------------------------------------------------------------------
209
262
  // Manage a task detail div
210
263
  // -------------------------------------------------------------------------------------------------
@@ -299,6 +352,14 @@ class BGTaskPoller {
299
352
 
300
353
  static normalizeTask(task) {
301
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
+ }
302
363
  }
303
364
 
304
365
  static sharedInstance(baseURL) {
@@ -357,7 +418,9 @@ class BGTaskPoller {
357
418
  const req = new XMLHttpRequest();
358
419
  const self = this;
359
420
  req.addEventListener("load", function () { self._receivePoll(this); });
360
- 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}`);
361
424
  req.open("GET", url);
362
425
  req.setRequestHeader('Accept', 'application/json');
363
426
  req.send();
@@ -8,7 +8,7 @@
8
8
 
9
9
  {% block object-tools %}
10
10
  {{ block.super }}
11
- <div>
11
+ <div style="margin-bottom: 10px;">
12
12
  <h3>Action background tasks</h3>
13
13
  {% if not admin_bg_tasks|length %}
14
14
  <p class="help" style="font-style: italic;">No recent background tasks</p>
@@ -20,7 +20,7 @@
20
20
  <tbody>
21
21
  {% for bgt in admin_bg_tasks %}
22
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>
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
24
  {% include "bgtask/bg_changelist_status_column.html" %}</td></tr>
25
25
  {% endwith %}
26
26
  {% endfor %}
@@ -6,11 +6,7 @@
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 %}
10
- <a href="{% url 'bgtask:tasks' %}?object_id={{bgtask.acted_on_object_id}}">
11
- {% else %}
12
9
  <a href="{% url 'admin:bgtask_backgroundtask_change' bgtask.id %}">
13
- {% endif %}
14
10
  <div id="bgtask-column-{{ bgtask.id }}">
15
11
  {% include 'bgtask/progress.html' %}
16
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 %}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-admin-background-task
3
- Version: 0.2.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
@@ -7,13 +7,14 @@ bgtask/decorators.py,sha256=UsiCBhqaWm1Yn4n4hmFbauGQvNezoeDm7Iqpjbvxbjk,1119
7
7
  bgtask/migrations/0001_initial.py,sha256=YDTEy_hiXHru-1ZdIH-n3po82r5vyzbkyYIrF9_yiI8,2013
8
8
  bgtask/migrations/0002_backgroundtask_namespace_alter_backgroundtask_name.py,sha256=XIG_gIvdA73tz0NRbNkGU_ieKde3ZtQltPeLdslD5Xo,968
9
9
  bgtask/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- bgtask/model_admin.py,sha256=tx90pz3MGMkN2DsZ8jEwEXY875QVXySGg_9rGcID50k,2925
10
+ bgtask/model_admin.py,sha256=mwRoljw1CwMA-c3zbssmrRsV9l8xsgYJOBrkgVNilRs,4402
11
11
  bgtask/models.py,sha256=VulETkcBXJkyUgmExSx9fuOvpp1HnY2sLQLD5BUvbHk,9184
12
12
  bgtask/stack_contexts.py,sha256=2Hun6O5Mm949uAioC2E3i54Rg-_H8P7LsHdhJYc0w6E,231
13
13
  bgtask/static/bgtask/js/bgtask-once.js,sha256=DBerpxPLP21IH--HpBxVFk3kN52FtBiuyJsDCjg7mCA,746
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
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
17
18
  bgtask/templates/bgtask/bgtask_templates.html,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
18
19
  bgtask/templates/bgtask/bgtask_view.html,sha256=AeubqERFF14PHWjInBD39wGImnRx4taS42iOMOPDUlM,1845
19
20
  bgtask/templates/bgtask/progress.html,sha256=VVYbrunGasOshRlz8Nm8hFPxqcVtrpaW1e2QxzAMcMM,166
@@ -22,7 +23,7 @@ bgtask/templatetags/bgtask.py,sha256=WUEWlQX5AarbXJBV6EGp-khT1zegVwGNigSKRF5mCTE
22
23
  bgtask/tests/test_bgtask.py,sha256=9AsBppPphaIdphuB-pHuQjgLhYNp8ySWtmcWF2uQl3Q,1090
23
24
  bgtask/urls.py,sha256=DXXjgj18LTSCq2-xZf23-pkgO4RmZglv67mgrp-fDyk,161
24
25
  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,,
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,,