django-spire 0.23.3__py3-none-any.whl → 0.23.5__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.
- django_spire/auth/group/tests/__init__.py +0 -0
- django_spire/auth/permissions/tests/__init__.py +0 -0
- django_spire/consts.py +1 -1
- django_spire/contrib/generic_views/portal_views.py +1 -1
- django_spire/contrib/progress/__init__.py +11 -0
- django_spire/contrib/progress/enums.py +10 -0
- django_spire/contrib/progress/mixins.py +8 -7
- django_spire/contrib/progress/runner.py +140 -0
- django_spire/contrib/progress/states.py +64 -0
- django_spire/contrib/progress/task.py +40 -0
- django_spire/contrib/progress/templates/django_spire/contrib/progress/modal/content.html +55 -21
- django_spire/contrib/progress/tracker.py +223 -17
- django_spire/contrib/progress/views.py +31 -17
- django_spire/contrib/queryset/mixins.py +9 -6
- django_spire/core/templates/django_spire/infinite_scroll/base.html +1 -1
- django_spire/core/templates/django_spire/infinite_scroll/scroll.html +3 -3
- django_spire/core/templates/django_spire/table/element/loading_skeleton.html +4 -2
- django_spire/core/templates/django_spire/table/element/refreshing_skeleton.html +4 -2
- {django_spire-0.23.3.dist-info → django_spire-0.23.5.dist-info}/METADATA +1 -1
- {django_spire-0.23.3.dist-info → django_spire-0.23.5.dist-info}/RECORD +23 -17
- {django_spire-0.23.3.dist-info → django_spire-0.23.5.dist-info}/WHEEL +0 -0
- {django_spire-0.23.3.dist-info → django_spire-0.23.5.dist-info}/licenses/LICENSE.md +0 -0
- {django_spire-0.23.3.dist-info → django_spire-0.23.5.dist-info}/top_level.txt +0 -0
|
File without changes
|
|
File without changes
|
django_spire/consts.py
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
|
+
from django_spire.contrib.progress.enums import ProgressStatus
|
|
1
2
|
from django_spire.contrib.progress.mixins import ProgressTrackingMixin
|
|
3
|
+
from django_spire.contrib.progress.runner import TaskProgressUpdater
|
|
4
|
+
from django_spire.contrib.progress.states import TaskState, TrackerState
|
|
5
|
+
from django_spire.contrib.progress.task import ProgressMessages, Task, TaskResult
|
|
2
6
|
from django_spire.contrib.progress.tracker import ProgressTracker
|
|
3
7
|
from django_spire.contrib.progress.views import sse_stream_view
|
|
4
8
|
|
|
5
9
|
|
|
6
10
|
__all__ = [
|
|
11
|
+
'ProgressMessages',
|
|
12
|
+
'ProgressStatus',
|
|
7
13
|
'ProgressTracker',
|
|
8
14
|
'ProgressTrackingMixin',
|
|
15
|
+
'Task',
|
|
16
|
+
'TaskProgressUpdater',
|
|
17
|
+
'TaskResult',
|
|
18
|
+
'TaskState',
|
|
19
|
+
'TrackerState',
|
|
9
20
|
'sse_stream_view',
|
|
10
21
|
]
|
|
@@ -7,6 +7,8 @@ from django_spire.contrib.progress.tracker import ProgressTracker
|
|
|
7
7
|
if TYPE_CHECKING:
|
|
8
8
|
from typing import Any
|
|
9
9
|
|
|
10
|
+
from django_spire.contrib.progress.enums import ProgressStatus
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
class ProgressTrackingMixin:
|
|
12
14
|
_tracker: ProgressTracker | None = None
|
|
@@ -17,19 +19,18 @@ class ProgressTrackingMixin:
|
|
|
17
19
|
@property
|
|
18
20
|
def tracker(self) -> ProgressTracker:
|
|
19
21
|
if self._tracker is None:
|
|
20
|
-
|
|
21
|
-
self._tracker = ProgressTracker(key)
|
|
22
|
+
self._tracker = ProgressTracker(self.get_tracker_key())
|
|
22
23
|
|
|
23
24
|
return self._tracker
|
|
24
25
|
|
|
26
|
+
def progress_error(self, message: str) -> None:
|
|
27
|
+
self.tracker.error(message)
|
|
28
|
+
|
|
25
29
|
def update_progress(
|
|
26
30
|
self,
|
|
27
|
-
|
|
31
|
+
status: ProgressStatus,
|
|
28
32
|
message: str,
|
|
29
33
|
progress: int,
|
|
30
34
|
**kwargs: Any
|
|
31
35
|
) -> None:
|
|
32
|
-
self.tracker.update(
|
|
33
|
-
|
|
34
|
-
def progress_error(self, message: str) -> None:
|
|
35
|
-
self.tracker.error(message)
|
|
36
|
+
self.tracker.update(status, message, progress, **kwargs)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import random
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from django_spire.contrib.progress.enums import ProgressStatus
|
|
11
|
+
from django_spire.contrib.progress.task import ProgressMessages, Task, TaskResult
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from typing import Any, Callable
|
|
15
|
+
|
|
16
|
+
from django_spire.contrib.progress.tracker import ProgressTracker
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TaskProgressUpdater:
|
|
20
|
+
def __init__(self, tracker: ProgressTracker, task: Task) -> None:
|
|
21
|
+
self._messages = ProgressMessages()
|
|
22
|
+
self._task = task
|
|
23
|
+
self._tracker = tracker
|
|
24
|
+
|
|
25
|
+
def complete(self, message: str | None = None) -> None:
|
|
26
|
+
self._tracker._update_task(
|
|
27
|
+
self._task.name,
|
|
28
|
+
ProgressStatus.COMPLETE,
|
|
29
|
+
self._format(message or self._messages.complete),
|
|
30
|
+
100
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def error(self, message: str) -> None:
|
|
34
|
+
self._tracker._update_task(
|
|
35
|
+
self._task.name,
|
|
36
|
+
ProgressStatus.ERROR,
|
|
37
|
+
self._format(message),
|
|
38
|
+
0
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def start(self) -> None:
|
|
42
|
+
self._tracker._update_task(
|
|
43
|
+
self._task.name,
|
|
44
|
+
ProgressStatus.PROCESSING,
|
|
45
|
+
self._format(self._messages.starting),
|
|
46
|
+
2
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def update(self, message: str, progress: int) -> None:
|
|
50
|
+
self._tracker._update_task(
|
|
51
|
+
self._task.name,
|
|
52
|
+
ProgressStatus.PROCESSING,
|
|
53
|
+
self._format(message),
|
|
54
|
+
progress
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def _format(self, message: str) -> str:
|
|
58
|
+
return f'{self._task.label}: {message}'
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ProgressSimulator:
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
updater: TaskProgressUpdater,
|
|
65
|
+
max_progress: int = 90,
|
|
66
|
+
update_interval: float = 0.15
|
|
67
|
+
) -> None:
|
|
68
|
+
self._max_progress = max_progress
|
|
69
|
+
self._messages = ProgressMessages()
|
|
70
|
+
self._update_interval = update_interval
|
|
71
|
+
self._updater = updater
|
|
72
|
+
|
|
73
|
+
def run(self, stop_event: threading.Event) -> None:
|
|
74
|
+
start_time = time.time()
|
|
75
|
+
duration = random.uniform(8.0, 15.0)
|
|
76
|
+
|
|
77
|
+
while not stop_event.is_set():
|
|
78
|
+
elapsed = time.time() - start_time
|
|
79
|
+
t = min(elapsed / duration, 1.0)
|
|
80
|
+
|
|
81
|
+
progress = self._ease_out_expo(t) * self._max_progress
|
|
82
|
+
progress = min(int(progress), self._max_progress)
|
|
83
|
+
|
|
84
|
+
jitter = random.uniform(-1.5, 1.5) if progress < self._max_progress - 5 else 0
|
|
85
|
+
progress = max(2, min(int(progress + jitter), self._max_progress))
|
|
86
|
+
|
|
87
|
+
message = self._get_message_for_progress(progress)
|
|
88
|
+
self._updater.update(message, progress)
|
|
89
|
+
|
|
90
|
+
if progress >= self._max_progress:
|
|
91
|
+
break
|
|
92
|
+
|
|
93
|
+
time.sleep(self._update_interval + random.uniform(0, 0.1))
|
|
94
|
+
|
|
95
|
+
def _ease_out_expo(self, t: float) -> float:
|
|
96
|
+
if t >= 1.0:
|
|
97
|
+
return 1.0
|
|
98
|
+
|
|
99
|
+
return 1.0 - math.pow(2, -10 * t)
|
|
100
|
+
|
|
101
|
+
def _get_message_for_progress(self, progress: int) -> str:
|
|
102
|
+
steps = self._messages.steps
|
|
103
|
+
index = min(int(progress / (self._max_progress / len(steps))), len(steps) - 1)
|
|
104
|
+
|
|
105
|
+
return steps[index]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class TaskRunner:
|
|
109
|
+
def __init__(self, tracker: ProgressTracker, task: Task) -> None:
|
|
110
|
+
self._stop_event = threading.Event()
|
|
111
|
+
self._task = task
|
|
112
|
+
self._tracker = tracker
|
|
113
|
+
self._updater = TaskProgressUpdater(tracker, task)
|
|
114
|
+
|
|
115
|
+
def run_parallel(self, results: dict[str, TaskResult]) -> None:
|
|
116
|
+
self._execute(results, lambda: self._task.func())
|
|
117
|
+
|
|
118
|
+
def run_sequential(self, results: dict[str, TaskResult]) -> None:
|
|
119
|
+
unwrapped = {name: result.value for name, result in results.items()}
|
|
120
|
+
self._execute(results, lambda: self._task.func(unwrapped))
|
|
121
|
+
|
|
122
|
+
def stop(self) -> None:
|
|
123
|
+
self._stop_event.set()
|
|
124
|
+
|
|
125
|
+
def _execute(self, results: dict[str, TaskResult], executor: Callable[[], Any]) -> None:
|
|
126
|
+
simulator = ProgressSimulator(self._updater)
|
|
127
|
+
progress_thread = threading.Thread(target=simulator.run, args=(self._stop_event,))
|
|
128
|
+
progress_thread.start()
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
self._updater.start()
|
|
132
|
+
value = executor()
|
|
133
|
+
results[self._task.name] = TaskResult(value=value)
|
|
134
|
+
self._updater.complete()
|
|
135
|
+
except BaseException as e:
|
|
136
|
+
results[self._task.name] = TaskResult(error=e)
|
|
137
|
+
self._updater.error(str(e))
|
|
138
|
+
finally:
|
|
139
|
+
self._stop_event.set()
|
|
140
|
+
progress_thread.join(timeout=1)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from django_spire.contrib.progress.enums import ProgressStatus
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class TaskState:
|
|
14
|
+
message: str = 'Waiting...'
|
|
15
|
+
progress: int = 0
|
|
16
|
+
status: ProgressStatus = ProgressStatus.PENDING
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def from_dict(cls, data: dict[str, Any]) -> TaskState:
|
|
20
|
+
return cls(
|
|
21
|
+
message=data.get('message', 'Waiting...'),
|
|
22
|
+
progress=data.get('progress', 0),
|
|
23
|
+
status=ProgressStatus(data.get('step', ProgressStatus.PENDING)),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def to_dict(self) -> dict[str, Any]:
|
|
27
|
+
return {
|
|
28
|
+
'message': self.message,
|
|
29
|
+
'progress': self.progress,
|
|
30
|
+
'step': self.status.value,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class TrackerState:
|
|
36
|
+
message: str = 'Initializing...'
|
|
37
|
+
progress: int = 0
|
|
38
|
+
status: ProgressStatus = ProgressStatus.PENDING
|
|
39
|
+
task_order: list[str] = field(default_factory=list)
|
|
40
|
+
tasks: dict[str, TaskState] = field(default_factory=dict)
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def from_dict(cls, data: dict[str, Any]) -> TrackerState:
|
|
44
|
+
tasks = {
|
|
45
|
+
name: TaskState.from_dict(task_data)
|
|
46
|
+
for name, task_data in data.get('tasks', {}).items()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return cls(
|
|
50
|
+
message=data.get('message', 'Initializing...'),
|
|
51
|
+
progress=data.get('progress', 0),
|
|
52
|
+
status=ProgressStatus(data.get('step', ProgressStatus.PENDING)),
|
|
53
|
+
task_order=data.get('task_order', []),
|
|
54
|
+
tasks=tasks,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def to_dict(self) -> dict[str, Any]:
|
|
58
|
+
return {
|
|
59
|
+
'message': self.message,
|
|
60
|
+
'progress': self.progress,
|
|
61
|
+
'step': self.status.value,
|
|
62
|
+
'task_order': self.task_order,
|
|
63
|
+
'tasks': {name: task.to_dict() for name, task in self.tasks.items()},
|
|
64
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ProgressMessages:
|
|
12
|
+
complete: str = 'Complete'
|
|
13
|
+
error: str = 'Error'
|
|
14
|
+
starting: str = 'Starting...'
|
|
15
|
+
steps: list[str] = field(default_factory=lambda: [
|
|
16
|
+
'Initializing...',
|
|
17
|
+
'Processing...',
|
|
18
|
+
'Analyzing...',
|
|
19
|
+
'Working...',
|
|
20
|
+
'Almost there...',
|
|
21
|
+
'Finalizing...',
|
|
22
|
+
])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Task:
|
|
27
|
+
func: Callable
|
|
28
|
+
label: str
|
|
29
|
+
name: str
|
|
30
|
+
parallel: bool = False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class TaskResult:
|
|
35
|
+
error: BaseException | None = None
|
|
36
|
+
value: Any = None
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def failed(self) -> bool:
|
|
40
|
+
return self.error is not None
|
|
@@ -14,21 +14,28 @@
|
|
|
14
14
|
data-redirect-url="{% block progress_redirect_url %}{% endblock %}"
|
|
15
15
|
data-redirect-delay="{% block progress_redirect_delay %}1000{% endblock %}"
|
|
16
16
|
x-data="{
|
|
17
|
-
|
|
17
|
+
tasks: {},
|
|
18
|
+
task_order: [],
|
|
19
|
+
overall_progress: 0,
|
|
18
20
|
message: 'Initializing...',
|
|
19
|
-
progress: 0,
|
|
20
21
|
has_error: false,
|
|
21
22
|
stream: null,
|
|
23
|
+
|
|
22
24
|
init() {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
let stream_url = this.$el.dataset.streamUrl;
|
|
26
|
+
let redirect_url = this.$el.dataset.redirectUrl;
|
|
27
|
+
let redirect_delay = parseInt(this.$el.dataset.redirectDelay);
|
|
26
28
|
|
|
27
29
|
this.stream = new ProgressStream(stream_url, {
|
|
28
30
|
on_update: (data) => {
|
|
29
|
-
this.step = data.step;
|
|
30
31
|
this.message = data.message;
|
|
31
|
-
this.
|
|
32
|
+
this.overall_progress = data.progress;
|
|
33
|
+
if (data.task_order) {
|
|
34
|
+
this.task_order = data.task_order;
|
|
35
|
+
}
|
|
36
|
+
if (data.tasks) {
|
|
37
|
+
this.tasks = data.tasks;
|
|
38
|
+
}
|
|
32
39
|
},
|
|
33
40
|
on_complete: (data) => {
|
|
34
41
|
setTimeout(() => {
|
|
@@ -43,30 +50,57 @@
|
|
|
43
50
|
redirect_delay: redirect_delay
|
|
44
51
|
});
|
|
45
52
|
this.stream.start();
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
format_task_name(name) {
|
|
56
|
+
return name.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
get_progress_class(step) {
|
|
60
|
+
if (step === 'complete') return 'bg-success';
|
|
61
|
+
if (step === 'error') return 'bg-danger';
|
|
62
|
+
if (step === 'processing') return 'bg-app-accent';
|
|
63
|
+
return 'bg-secondary';
|
|
46
64
|
}
|
|
47
65
|
}"
|
|
48
66
|
>
|
|
49
67
|
<div class="row g-3">
|
|
50
68
|
<div class="col-12" x-show="!has_error">
|
|
51
|
-
<
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
class="progress
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
69
|
+
<template x-for="name in task_order" :key="name">
|
|
70
|
+
<div class="mb-3" x-show="tasks[name]">
|
|
71
|
+
<div class="d-flex justify-content-between align-items-center mb-1">
|
|
72
|
+
<span class="fs-7" x-text="format_task_name(name)"></span>
|
|
73
|
+
<span class="fs-7" x-text="tasks[name]?.progress + '%'"></span>
|
|
74
|
+
</div>
|
|
75
|
+
<div class="progress" style="height: 6px;">
|
|
76
|
+
<div
|
|
77
|
+
class="progress-bar"
|
|
78
|
+
:class="get_progress_class(tasks[name]?.step)"
|
|
79
|
+
role="progressbar"
|
|
80
|
+
:style="'width: ' + (tasks[name]?.progress || 0) + '%'"
|
|
81
|
+
></div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</template>
|
|
85
|
+
|
|
86
|
+
<div class="mt-4 pt-3 border-top">
|
|
87
|
+
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
88
|
+
<span class="fs-6 fw-medium">Overall Progress</span>
|
|
89
|
+
<span class="fs-6" x-text="overall_progress + '%'"></span>
|
|
90
|
+
</div>
|
|
91
|
+
<div class="progress" style="height: 8px;">
|
|
92
|
+
<div
|
|
93
|
+
class="progress-bar bg-app-accent"
|
|
94
|
+
role="progressbar"
|
|
95
|
+
:style="'width: ' + overall_progress + '%'"
|
|
96
|
+
></div>
|
|
97
|
+
</div>
|
|
64
98
|
</div>
|
|
65
99
|
</div>
|
|
66
100
|
|
|
67
101
|
<div class="col-12" x-show="!has_error">
|
|
68
102
|
<p class="text-app-secondary mb-0 fs-7">
|
|
69
|
-
{% block progress_wait_message %}
|
|
103
|
+
{% block progress_wait_message %}Processing your request. This may take a moment.{% endblock %}
|
|
70
104
|
</p>
|
|
71
105
|
</div>
|
|
72
106
|
|
|
@@ -1,39 +1,245 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import logging
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
|
|
3
7
|
from typing import TYPE_CHECKING
|
|
4
8
|
|
|
5
9
|
from django.core.cache import cache
|
|
6
10
|
|
|
11
|
+
from django_spire.contrib.progress.enums import ProgressStatus
|
|
12
|
+
from django_spire.contrib.progress.runner import TaskRunner
|
|
13
|
+
from django_spire.contrib.progress.states import TaskState, TrackerState
|
|
14
|
+
from django_spire.contrib.progress.task import Task, TaskResult
|
|
15
|
+
|
|
7
16
|
if TYPE_CHECKING:
|
|
8
|
-
from typing import Any
|
|
17
|
+
from typing import Any, Callable
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
log = logging.getLogger(__name__)
|
|
9
21
|
|
|
10
22
|
|
|
11
23
|
class ProgressTracker:
|
|
12
24
|
def __init__(self, key: str, timeout: int = 300) -> None:
|
|
13
|
-
self.
|
|
14
|
-
self.
|
|
25
|
+
self._key = f'progress_tracker_{key}'
|
|
26
|
+
self._lock = threading.Lock()
|
|
27
|
+
self._tasks: list[Task] = []
|
|
28
|
+
self._timeout = timeout
|
|
29
|
+
|
|
30
|
+
def clear(self) -> None:
|
|
31
|
+
cache.delete(self._key)
|
|
32
|
+
|
|
33
|
+
def complete(self, message: str = 'Complete!') -> None:
|
|
34
|
+
with self._lock:
|
|
35
|
+
state = self._get_state()
|
|
36
|
+
state.message = message
|
|
37
|
+
state.progress = 100
|
|
38
|
+
state.status = ProgressStatus.COMPLETE
|
|
39
|
+
self._save_state(state)
|
|
40
|
+
|
|
41
|
+
def error(self, message: str) -> None:
|
|
42
|
+
with self._lock:
|
|
43
|
+
state = self._get_state()
|
|
44
|
+
state.message = message
|
|
45
|
+
state.progress = 0
|
|
46
|
+
state.status = ProgressStatus.ERROR
|
|
47
|
+
self._save_state(state)
|
|
48
|
+
|
|
49
|
+
def execute(self) -> dict[str, Any] | None:
|
|
50
|
+
self._create_state()
|
|
51
|
+
|
|
52
|
+
error = False
|
|
53
|
+
results: dict[str, TaskResult] = {}
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
error = self._execute_parallel_tasks(results)
|
|
57
|
+
|
|
58
|
+
if not error:
|
|
59
|
+
error = self._execute_sequential_tasks(results)
|
|
60
|
+
|
|
61
|
+
if not error:
|
|
62
|
+
self.complete()
|
|
63
|
+
time.sleep(1)
|
|
64
|
+
except BaseException:
|
|
65
|
+
log.exception('Progress tracker failed')
|
|
66
|
+
error = True
|
|
67
|
+
|
|
68
|
+
if error:
|
|
69
|
+
self.error('An unexpected error occurred')
|
|
70
|
+
time.sleep(2)
|
|
71
|
+
|
|
72
|
+
self.clear()
|
|
73
|
+
|
|
74
|
+
if error:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
return {name: result.value for name, result in results.items()}
|
|
78
|
+
|
|
79
|
+
def get(self) -> dict[str, Any] | None:
|
|
80
|
+
return cache.get(self._key)
|
|
81
|
+
|
|
82
|
+
def parallel(
|
|
83
|
+
self,
|
|
84
|
+
name: str,
|
|
85
|
+
func: Callable[[], Any],
|
|
86
|
+
label: str | None = None
|
|
87
|
+
) -> ProgressTracker:
|
|
88
|
+
task = Task(
|
|
89
|
+
func=func,
|
|
90
|
+
label=label or self._generate_label(name),
|
|
91
|
+
name=name,
|
|
92
|
+
parallel=True,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
self._tasks.append(task)
|
|
96
|
+
|
|
97
|
+
return self
|
|
98
|
+
|
|
99
|
+
def sequential(
|
|
100
|
+
self,
|
|
101
|
+
name: str,
|
|
102
|
+
func: Callable[[dict[str, Any]], Any],
|
|
103
|
+
label: str | None = None
|
|
104
|
+
) -> ProgressTracker:
|
|
105
|
+
task = Task(
|
|
106
|
+
func=func,
|
|
107
|
+
label=label or self._generate_label(name),
|
|
108
|
+
name=name,
|
|
109
|
+
parallel=False,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
self._tasks.append(task)
|
|
113
|
+
|
|
114
|
+
return self
|
|
115
|
+
|
|
116
|
+
def start(self) -> None:
|
|
117
|
+
state = TrackerState()
|
|
118
|
+
cache.set(self._key, state.to_dict(), timeout=self._timeout)
|
|
15
119
|
|
|
16
120
|
def update(
|
|
17
121
|
self,
|
|
18
|
-
|
|
122
|
+
status: ProgressStatus,
|
|
19
123
|
message: str,
|
|
20
124
|
progress: int,
|
|
21
125
|
**kwargs: Any
|
|
22
126
|
) -> None:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
127
|
+
with self._lock:
|
|
128
|
+
state = self._get_state()
|
|
129
|
+
state.message = message
|
|
130
|
+
state.progress = progress
|
|
131
|
+
state.status = status
|
|
132
|
+
self._save_state(state, **kwargs)
|
|
29
133
|
|
|
30
|
-
|
|
134
|
+
def _calculate_overall_progress(self, state: TrackerState) -> int:
|
|
135
|
+
if not state.tasks:
|
|
136
|
+
return 0
|
|
31
137
|
|
|
32
|
-
|
|
33
|
-
|
|
138
|
+
total = sum(
|
|
139
|
+
task.progress
|
|
140
|
+
for task in state.tasks.values()
|
|
141
|
+
)
|
|
34
142
|
|
|
35
|
-
|
|
36
|
-
cache.delete(self.key)
|
|
143
|
+
return int(total / len(state.tasks))
|
|
37
144
|
|
|
38
|
-
def
|
|
39
|
-
|
|
145
|
+
def _create_state(self) -> None:
|
|
146
|
+
task_names = [task.name for task in self._tasks]
|
|
147
|
+
|
|
148
|
+
state = TrackerState(
|
|
149
|
+
task_order=task_names,
|
|
150
|
+
tasks={name: TaskState() for name in task_names}
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
cache.set(self._key, state.to_dict(), timeout=self._timeout)
|
|
154
|
+
|
|
155
|
+
def _execute_parallel_tasks(self, results: dict[str, TaskResult]) -> bool:
|
|
156
|
+
tasks = [
|
|
157
|
+
task
|
|
158
|
+
for task in self._tasks if task.parallel
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
if not tasks:
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
runners: list[TaskRunner] = []
|
|
165
|
+
threads: list[threading.Thread] = []
|
|
166
|
+
|
|
167
|
+
for task in tasks:
|
|
168
|
+
runner = TaskRunner(self, task)
|
|
169
|
+
runners.append(runner)
|
|
170
|
+
|
|
171
|
+
thread = threading.Thread(target=runner.run_parallel, args=(results,))
|
|
172
|
+
threads.append(thread)
|
|
173
|
+
thread.start()
|
|
174
|
+
|
|
175
|
+
for thread in threads:
|
|
176
|
+
thread.join(timeout=self._timeout)
|
|
177
|
+
|
|
178
|
+
if thread.is_alive():
|
|
179
|
+
for runner in runners:
|
|
180
|
+
runner.stop()
|
|
181
|
+
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
return any(
|
|
185
|
+
results.get(task.name, TaskResult()).failed
|
|
186
|
+
for task in tasks
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def _execute_sequential_tasks(self, results: dict[str, TaskResult]) -> bool:
|
|
190
|
+
tasks = [
|
|
191
|
+
task
|
|
192
|
+
for task in self._tasks
|
|
193
|
+
if not task.parallel
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
for task in tasks:
|
|
197
|
+
runner = TaskRunner(self, task)
|
|
198
|
+
runner.run_sequential(results)
|
|
199
|
+
|
|
200
|
+
if results.get(task.name, TaskResult()).failed:
|
|
201
|
+
return True
|
|
202
|
+
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
def _generate_label(self, name: str) -> str:
|
|
206
|
+
return name.replace('_', ' ').title()
|
|
207
|
+
|
|
208
|
+
def _get_state(self) -> TrackerState:
|
|
209
|
+
task_names = [task.name for task in self._tasks]
|
|
210
|
+
data = cache.get(self._key)
|
|
211
|
+
|
|
212
|
+
if data is None:
|
|
213
|
+
return TrackerState(
|
|
214
|
+
task_order=task_names,
|
|
215
|
+
tasks={name: TaskState() for name in task_names}
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
return TrackerState.from_dict(data)
|
|
219
|
+
|
|
220
|
+
def _save_state(self, state: TrackerState, **kwargs: Any) -> None:
|
|
221
|
+
data = state.to_dict()
|
|
222
|
+
data.update(kwargs)
|
|
223
|
+
cache.set(self._key, data, timeout=self._timeout)
|
|
224
|
+
|
|
225
|
+
def _update_task(
|
|
226
|
+
self,
|
|
227
|
+
name: str,
|
|
228
|
+
status: ProgressStatus,
|
|
229
|
+
message: str,
|
|
230
|
+
progress: int
|
|
231
|
+
) -> None:
|
|
232
|
+
with self._lock:
|
|
233
|
+
state = self._get_state()
|
|
234
|
+
|
|
235
|
+
state.tasks[name] = TaskState(
|
|
236
|
+
message=message,
|
|
237
|
+
progress=progress,
|
|
238
|
+
status=status,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
state.message = message
|
|
242
|
+
state.progress = self._calculate_overall_progress(state)
|
|
243
|
+
state.status = ProgressStatus.PROCESSING
|
|
244
|
+
|
|
245
|
+
self._save_state(state)
|
|
@@ -5,46 +5,60 @@ import time
|
|
|
5
5
|
|
|
6
6
|
from typing import TYPE_CHECKING
|
|
7
7
|
|
|
8
|
+
from django.core.cache import cache
|
|
8
9
|
from django.http import StreamingHttpResponse
|
|
9
10
|
|
|
10
|
-
from django_spire.contrib.progress.tracker import ProgressTracker
|
|
11
|
-
|
|
12
11
|
if TYPE_CHECKING:
|
|
13
|
-
from typing import Callable
|
|
12
|
+
from typing import Callable, Generator
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
def sse_stream_view(
|
|
17
16
|
key: str,
|
|
18
17
|
interval: float = 0.5,
|
|
19
|
-
should_continue: Callable[[dict], bool] | None = None
|
|
18
|
+
should_continue: Callable[[dict], bool] | None = None,
|
|
19
|
+
timeout: int = 300
|
|
20
20
|
) -> StreamingHttpResponse:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
cache_key = f'progress_tracker_{key}'
|
|
22
|
+
|
|
23
|
+
def event_stream() -> Generator[str, None, None]:
|
|
24
|
+
previous_data = None
|
|
25
|
+
start_time = time.time()
|
|
26
|
+
last_heartbeat = time.time()
|
|
27
|
+
|
|
28
|
+
yield ': connected\n\n'
|
|
24
29
|
|
|
25
30
|
while True:
|
|
26
|
-
time.
|
|
27
|
-
|
|
31
|
+
elapsed = time.time() - start_time
|
|
32
|
+
|
|
33
|
+
if elapsed > timeout:
|
|
34
|
+
yield f'data: {json.dumps({"step": "error", "message": "Timeout", "progress": 0})}\n\n'
|
|
35
|
+
break
|
|
28
36
|
|
|
29
|
-
|
|
30
|
-
current_progress = data.get('progress', 0)
|
|
37
|
+
data = cache.get(cache_key)
|
|
31
38
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
if data and data != previous_data:
|
|
40
|
+
yield f'data: {json.dumps(data)}\n\n'
|
|
41
|
+
previous_data = data
|
|
42
|
+
last_heartbeat = time.time()
|
|
35
43
|
|
|
36
44
|
if should_continue and not should_continue(data):
|
|
37
|
-
|
|
45
|
+
break
|
|
38
46
|
|
|
39
|
-
if
|
|
47
|
+
if data.get('progress', 0) >= 100 or data.get('step') == 'error':
|
|
40
48
|
break
|
|
41
49
|
|
|
50
|
+
if time.time() - last_heartbeat > 10:
|
|
51
|
+
yield ': heartbeat\n\n'
|
|
52
|
+
last_heartbeat = time.time()
|
|
53
|
+
|
|
54
|
+
time.sleep(interval)
|
|
55
|
+
|
|
42
56
|
response = StreamingHttpResponse(
|
|
43
57
|
event_stream(),
|
|
44
58
|
content_type='text/event-stream'
|
|
45
59
|
)
|
|
46
60
|
|
|
47
|
-
response['Cache-Control'] = 'no-cache'
|
|
61
|
+
response['Cache-Control'] = 'no-cache, no-store'
|
|
48
62
|
response['X-Accel-Buffering'] = 'no'
|
|
49
63
|
|
|
50
64
|
return response
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
from abc import abstractmethod
|
|
4
5
|
from typing import Type
|
|
5
6
|
|
|
6
7
|
from django.core.handlers.wsgi import WSGIRequest
|
|
7
8
|
from django.db.models import QuerySet
|
|
8
9
|
from django.forms import Form
|
|
9
|
-
|
|
10
10
|
from django_spire.contrib.form.utils import show_form_errors
|
|
11
11
|
from django_spire.contrib.queryset.enums import SessionFilterActionEnum
|
|
12
12
|
from django_spire.contrib.session.controller import SessionController
|
|
@@ -18,16 +18,19 @@ class SessionFilterQuerySetMixin(QuerySet):
|
|
|
18
18
|
self,
|
|
19
19
|
request: WSGIRequest,
|
|
20
20
|
session_key: str,
|
|
21
|
-
form_class: Type[Form]
|
|
21
|
+
form_class: Type[Form],
|
|
22
|
+
is_from_body: bool = False,
|
|
22
23
|
) -> QuerySet:
|
|
23
24
|
# Session keys must match to process new queryset data
|
|
24
25
|
|
|
26
|
+
data = json.loads(request.body.decode('utf-8')) if is_from_body else request.GET
|
|
27
|
+
|
|
25
28
|
try:
|
|
26
|
-
action = SessionFilterActionEnum(
|
|
29
|
+
action = SessionFilterActionEnum(data.get('action'))
|
|
27
30
|
except ValueError:
|
|
28
31
|
action = None
|
|
29
32
|
|
|
30
|
-
form = form_class(
|
|
33
|
+
form = form_class(data)
|
|
31
34
|
|
|
32
35
|
if form.is_valid():
|
|
33
36
|
session = SessionController(request=request, session_key=session_key)
|
|
@@ -39,8 +42,9 @@ class SessionFilterQuerySetMixin(QuerySet):
|
|
|
39
42
|
# Apply filters when the user submits the filter form
|
|
40
43
|
if (
|
|
41
44
|
action == SessionFilterActionEnum.FILTER
|
|
42
|
-
and session_key ==
|
|
45
|
+
and session_key == data.get('session_filter_key')
|
|
43
46
|
):
|
|
47
|
+
|
|
44
48
|
# Update session data
|
|
45
49
|
for key, value in form.cleaned_data.items():
|
|
46
50
|
session.add_data(key, value)
|
|
@@ -56,7 +60,6 @@ class SessionFilterQuerySetMixin(QuerySet):
|
|
|
56
60
|
show_form_errors(request, form)
|
|
57
61
|
return self
|
|
58
62
|
|
|
59
|
-
|
|
60
63
|
@abstractmethod
|
|
61
64
|
def bulk_filter(self, filter_data: dict) -> QuerySet:
|
|
62
65
|
pass
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div
|
|
2
2
|
x-data="{
|
|
3
|
-
batch_size: parseInt('{{ batch_size|default:
|
|
3
|
+
batch_size: parseInt('{{ batch_size|default:50 }}'),
|
|
4
4
|
current_page: parseInt('{{ current_page|default:0 }}'),
|
|
5
5
|
endpoint: '{{ endpoint }}',
|
|
6
6
|
has_next: {{ has_next|default:'true'|yesno:'true,false' }},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div
|
|
2
2
|
x-data="{
|
|
3
|
-
batch_size:
|
|
3
|
+
batch_size: 50,
|
|
4
4
|
current_page: 0,
|
|
5
5
|
endpoint: '',
|
|
6
6
|
has_next: true,
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
async init() {
|
|
17
17
|
this.scroll_id = $id('scroll');
|
|
18
18
|
this.endpoint = this.$el.dataset.endpoint || '';
|
|
19
|
-
this.batch_size = parseInt(this.$el.dataset.batchSize || '
|
|
19
|
+
this.batch_size = parseInt(this.$el.dataset.batchSize || '50');
|
|
20
20
|
|
|
21
21
|
await this.$nextTick();
|
|
22
22
|
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
this.observer.observe(trigger);
|
|
117
117
|
},
|
|
118
118
|
}"
|
|
119
|
-
data-batch-size="{{ batch_size|default:
|
|
119
|
+
data-batch-size="{{ batch_size|default:50 }}"
|
|
120
120
|
data-endpoint="{{ endpoint }}"
|
|
121
121
|
:data-scroll-id="scroll_id"
|
|
122
122
|
@item-mounted.window="handle_item_mounted($event)"
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
<template x-for="i in skeleton_count" :key="i">
|
|
4
4
|
<tr class="skeleton-row" :style="average_row_height > 0 ? `height: ${average_row_height}px;` : ''">
|
|
5
5
|
<template x-for="j in column_count" :key="j">
|
|
6
|
-
<td class="align-middle">
|
|
7
|
-
<div class="
|
|
6
|
+
<td class="align-middle h-100">
|
|
7
|
+
<div class="align-items-center d-flex h-100">
|
|
8
|
+
<div class="skeleton-box" style="height: 20px; width: 90%;"></div>
|
|
9
|
+
</div>
|
|
8
10
|
</td>
|
|
9
11
|
</template>
|
|
10
12
|
</tr>
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
<template x-for="i in skeleton_count" :key="i">
|
|
4
4
|
<tr class="skeleton-row" :style="average_row_height > 0 ? `height: ${average_row_height}px;` : ''">
|
|
5
5
|
<template x-for="j in column_count" :key="j">
|
|
6
|
-
<td class="align-middle">
|
|
7
|
-
<div class="
|
|
6
|
+
<td class="align-middle h-100">
|
|
7
|
+
<div class="align-items-center d-flex h-100">
|
|
8
|
+
<div class="skeleton-box" style="height: 20px; width: 90%;"></div>
|
|
9
|
+
</div>
|
|
8
10
|
</td>
|
|
9
11
|
</template>
|
|
10
12
|
</tr>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-spire
|
|
3
|
-
Version: 0.23.
|
|
3
|
+
Version: 0.23.5
|
|
4
4
|
Summary: A project for Django Spire
|
|
5
5
|
Author-email: Brayden Carlson <braydenc@stratusadv.com>, Nathan Johnson <nathanj@stratusadv.com>
|
|
6
6
|
License: Copyright (c) 2024 Stratus Advanced Technologies and Contributors.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
django_spire/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
django_spire/conf.py,sha256=c5Hs-7lk9T15254tOasiQ2ZTFLQIVJof9_QJDfm1PAI,933
|
|
3
|
-
django_spire/consts.py,sha256=
|
|
3
|
+
django_spire/consts.py,sha256=YR9sX6FXBUPmKDLHD7b1mDlQp6jwxIOoAF39q4rJoiM,171
|
|
4
4
|
django_spire/exceptions.py,sha256=L5ndRO5ftMmh0pHkO2z_NG3LSGZviJ-dDHNT73SzTNw,48
|
|
5
5
|
django_spire/settings.py,sha256=B4GPqBGt_dmkt0Ay0j-IP-SZ6mY44m2Ap5kVSON5YLA,1005
|
|
6
6
|
django_spire/urls.py,sha256=mKeZszb5U4iIGqddMb5Tt5fRC72U2wABEOi6mvOfEBU,656
|
|
@@ -168,6 +168,7 @@ django_spire/auth/group/templates/django_spire/auth/group/tab/user_list_card_tab
|
|
|
168
168
|
django_spire/auth/group/templates/django_spire/auth/group/tab/user_list_card_tabs.html,sha256=qQRlAdB8TxfR0Z-crGGW5e5qgRx9qOCyYWfSX2EgpiM,548
|
|
169
169
|
django_spire/auth/group/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
170
170
|
django_spire/auth/group/templatetags/permission_tags.py,sha256=nK73ZKVhgCnIAQFpD_e4TZ_hHFt9GyPJCd1tZ_56CgY,256
|
|
171
|
+
django_spire/auth/group/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
171
172
|
django_spire/auth/group/urls/__init__.py,sha256=efpuo6oZPZJsDS3-UjwkBOrcxKmyWk8M81OwsmZ4fJs,342
|
|
172
173
|
django_spire/auth/group/urls/form_urls.py,sha256=bGofmsYfaWi7XRmaWS7zdFXgd8jNrIqNjVQVV_VdfvA,584
|
|
173
174
|
django_spire/auth/group/urls/json_urls.py,sha256=uOQqYiYwtFN1KafQSW6vFzzvkMFaRB5jOjFtw21NpMM,423
|
|
@@ -198,6 +199,7 @@ django_spire/auth/permissions/consts.py,sha256=Sm7Oqq-lfOLpViar6yiZI2DPBYmNvbYBG
|
|
|
198
199
|
django_spire/auth/permissions/decorators.py,sha256=LA169sW50UUDlnpuAJuUmRzb_nvGWrR_AsyNlKXWiBE,1502
|
|
199
200
|
django_spire/auth/permissions/permissions.py,sha256=089AidHUVx0DcpGtfJ9DAN38dyUmoUuEp8cIem8O-Is,6187
|
|
200
201
|
django_spire/auth/permissions/tools.py,sha256=vfoI6KNLHVSaEzo1YSdNoStna0OLaoY5Xb3UBnt-DRo,2460
|
|
202
|
+
django_spire/auth/permissions/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
201
203
|
django_spire/auth/seeding/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
202
204
|
django_spire/auth/seeding/seed.py,sha256=TDbGgJb5XADk4STs5jU7VvHb9qWFWE1hhqjqsCCgn74,92
|
|
203
205
|
django_spire/auth/seeding/seeder.py,sha256=t50d-13t2oEAHG1vgQ41qXMyq0tmPQ3jjZuFlR-BZug,722
|
|
@@ -319,7 +321,7 @@ django_spire/contrib/gamification/static/django_spire/css/contrib/gamification/g
|
|
|
319
321
|
django_spire/contrib/gamification/templates/django_spire/contrib/gamification/effect/kapow.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
320
322
|
django_spire/contrib/generic_views/__init__.py,sha256=fJU5nY61oqFCK-osl1Hnr7W2FKc_ZWeFuEkADWqCKzA,245
|
|
321
323
|
django_spire/contrib/generic_views/modal_views.py,sha256=nDiVW9uIqNGYQx6hbY_H1YoiXg-FEKOCFow2z0qQk4A,2041
|
|
322
|
-
django_spire/contrib/generic_views/portal_views.py,sha256=
|
|
324
|
+
django_spire/contrib/generic_views/portal_views.py,sha256=03_CUGvdAC6TKp9d_wg5TO_mykhfTJLdSSlKsjzUzkA,7894
|
|
323
325
|
django_spire/contrib/help/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
324
326
|
django_spire/contrib/help/apps.py,sha256=UM1IoIMFNaVaQdUfACTMfUkm541TqNhOMqlp9X47zBc,193
|
|
325
327
|
django_spire/contrib/help/templates/django_spire/contrib/help/help_button.html,sha256=ikym1hVCy1OuBJnU0CWhi-aCPCrLsfP3stCs0OV3PFk,162
|
|
@@ -346,18 +348,22 @@ django_spire/contrib/pagination/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TI
|
|
|
346
348
|
django_spire/contrib/pagination/templatetags/pagination_tags.py,sha256=xE3zTDJZDdDvWf6D7fMyg_Vi0PW37OA3aZxCjOHDp4I,1173
|
|
347
349
|
django_spire/contrib/performance/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
348
350
|
django_spire/contrib/performance/decorators.py,sha256=xOjXX-3FVOEWqr2TTTqdh4lkIhmHTlROevOVgXDeyt8,507
|
|
349
|
-
django_spire/contrib/progress/__init__.py,sha256=
|
|
350
|
-
django_spire/contrib/progress/
|
|
351
|
-
django_spire/contrib/progress/
|
|
352
|
-
django_spire/contrib/progress/
|
|
351
|
+
django_spire/contrib/progress/__init__.py,sha256=YEXnnbomc32c2J7e3sXHjtLboZ-PijiZOguoVHPfoUQ,719
|
|
352
|
+
django_spire/contrib/progress/enums.py,sha256=nwyfKxQno_ghGGTbpK8lRO1cY7dCfbxnnsQ4cSbkXBw,194
|
|
353
|
+
django_spire/contrib/progress/mixins.py,sha256=V84LRtniFDLK2E5dKcSZNh-a1zTf9Ee7QXg1rDfoFHo,895
|
|
354
|
+
django_spire/contrib/progress/runner.py,sha256=3jSqpdk2MJ7HPNutwF43Hs81O0NlzX4E9gnVaenhXUs,4445
|
|
355
|
+
django_spire/contrib/progress/states.py,sha256=V7JU_nzpZN77TQFW0S51Gjil8xn8CKLKkAWDOpqzA8k,1908
|
|
356
|
+
django_spire/contrib/progress/task.py,sha256=4pMbzOsjRRNqPKNohh08D39DRFpC3QomTCH5IdqQS0A,780
|
|
357
|
+
django_spire/contrib/progress/tracker.py,sha256=xjGlEyKe5aiKW_KC8NKZ8F0BjQdFpLUqHz5Gr_C7jws,6490
|
|
358
|
+
django_spire/contrib/progress/views.py,sha256=CwJu-L09tTiCpsjVuqzvMEKVqp6-etLDi7L9ibmlq8U,1694
|
|
353
359
|
django_spire/contrib/progress/static/django_spire/css/contrib/progress/progress.css,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
354
360
|
django_spire/contrib/progress/static/django_spire/js/contrib/progress/progress.js,sha256=2Zuwiu1nWJpghzU-6fmf6s4qKLLyiZUcAJwB9-v5xWI,1701
|
|
355
361
|
django_spire/contrib/progress/templates/django_spire/contrib/progress/card/card.html,sha256=2Ajv0UBJEGwc4InVe69aLe2u4K56yh2JAuvO6uH24j8,3353
|
|
356
|
-
django_spire/contrib/progress/templates/django_spire/contrib/progress/modal/content.html,sha256=
|
|
362
|
+
django_spire/contrib/progress/templates/django_spire/contrib/progress/modal/content.html,sha256=Qxer-q4GBC5h8onzno2NUlPrXluw_tO1kWlGComQ98E,5232
|
|
357
363
|
django_spire/contrib/queryset/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
358
364
|
django_spire/contrib/queryset/enums.py,sha256=nGUHwCVOCBfVXo-lrgyqRoEaMq0ZQAGOWnjALSFPjJU,108
|
|
359
365
|
django_spire/contrib/queryset/filter_tools.py,sha256=o7OBB3Jjtyoj7aaWspkFKS2bv0BeMIigXz7DVvt6Ueo,1087
|
|
360
|
-
django_spire/contrib/queryset/mixins.py,sha256
|
|
366
|
+
django_spire/contrib/queryset/mixins.py,sha256=-fnk0nmfTUBa7dbA0Eb6ou82x0b5Kj5NnDtH654yDf8,2217
|
|
361
367
|
django_spire/contrib/seeding/__init__.py,sha256=mHKO8a7fCAf70BWYjgyGgBDfssPtb3Pp9IxCVVI_4-M,112
|
|
362
368
|
django_spire/contrib/seeding/field/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
363
369
|
django_spire/contrib/seeding/field/base.py,sha256=SK8EYSsLgMRgosY8bRb3X27RpL7cq55VETf6Q_pYYr4,1198
|
|
@@ -664,8 +670,8 @@ django_spire/core/templates/django_spire/forms/elements/dict_element.html,sha256
|
|
|
664
670
|
django_spire/core/templates/django_spire/forms/elements/list_element.html,sha256=NbKavjm9qlSUnl5sao3xZILT-bW-TmIFB3Bqy0NfZkg,682
|
|
665
671
|
django_spire/core/templates/django_spire/forms/elements/value_element.html,sha256=LaOR2ggmf-J_cPCVHjNoD4l_858zHCRA58T_ZB3EIkE,57
|
|
666
672
|
django_spire/core/templates/django_spire/forms/widgets/json_tree_widget.html,sha256=dykf6xYRZ477i6SgcGR3TSnxDwLsu2p3FR45hEr4DF8,1489
|
|
667
|
-
django_spire/core/templates/django_spire/infinite_scroll/base.html,sha256
|
|
668
|
-
django_spire/core/templates/django_spire/infinite_scroll/scroll.html,sha256=
|
|
673
|
+
django_spire/core/templates/django_spire/infinite_scroll/base.html,sha256=--8DAhRYwAfowKWKLrnfdvh4rNTeBXyziE82fReFfnc,10875
|
|
674
|
+
django_spire/core/templates/django_spire/infinite_scroll/scroll.html,sha256=5new-JbLIo6D9zurYN39hy41moJDj_2obeY1v9VWk_Q,4311
|
|
669
675
|
django_spire/core/templates/django_spire/infinite_scroll/element/footer.html,sha256=22yxDuYB2vKqaDaMsARStrYTYoHtPGYmK6Xl9XQdwH8,391
|
|
670
676
|
django_spire/core/templates/django_spire/item/infinite_scroll_item.html,sha256=NMx3jQwtcKJiKtzbQXaXI3eWOtnJFiZhCI13tLTxSnc,1011
|
|
671
677
|
django_spire/core/templates/django_spire/item/item.html,sha256=9AD6qRcYIb7q-EcmWlRUFiK8XfnmPBSS5NOsXdKEAAA,1217
|
|
@@ -711,8 +717,8 @@ django_spire/core/templates/django_spire/table/element/child_row.html,sha256=B0f
|
|
|
711
717
|
django_spire/core/templates/django_spire/table/element/expandable_row.html,sha256=sAzzUeGishdYRUqs9t4lduxuxZaOzUYNhMfMytRLq90,1516
|
|
712
718
|
django_spire/core/templates/django_spire/table/element/footer.html,sha256=fVxbDrVj4C9jIswkri7B2Dr4nEr2Lf_EPPULMU3mRs0,312
|
|
713
719
|
django_spire/core/templates/django_spire/table/element/header.html,sha256=NBx9Lfn3Dav5uEb-SlyB-vhZapCzxbfc2xxmrg7Bu3c,647
|
|
714
|
-
django_spire/core/templates/django_spire/table/element/loading_skeleton.html,sha256=
|
|
715
|
-
django_spire/core/templates/django_spire/table/element/refreshing_skeleton.html,sha256=
|
|
720
|
+
django_spire/core/templates/django_spire/table/element/loading_skeleton.html,sha256=4YIsTLGjAC6T0idBjjkeqTkfXBb7sKJAUXWVbhla6ng,688
|
|
721
|
+
django_spire/core/templates/django_spire/table/element/refreshing_skeleton.html,sha256=nShQpQPP00z70Kvpm3CDpXpq_J_bjpBjv1BD4aVEmPE,673
|
|
716
722
|
django_spire/core/templates/django_spire/table/element/row.html,sha256=BoNZTjRs_cVG11CKK5VVocAq4p-UtB79FnhGPfSXKEo,3379
|
|
717
723
|
django_spire/core/templates/django_spire/table/element/trigger.html,sha256=KixtqIshbs8V1QSupIMxTe0UYJbK2oJIsqD8fZYxAOs,66
|
|
718
724
|
django_spire/core/templates/django_spire/table/item/no_data_item.html,sha256=PxTyI98edGtMxRdRzuEh1P_uOKRY_X5awrWXQsVb1jI,441
|
|
@@ -1195,8 +1201,8 @@ django_spire/theme/urls/page_urls.py,sha256=S8nkKkgbhG3XHI3uMUL-piOjXIrRkuY2UlM_
|
|
|
1195
1201
|
django_spire/theme/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
1196
1202
|
django_spire/theme/views/json_views.py,sha256=PWwVTaty0BVGbj65L5cxex6JNhc-xVAI_rEYjbJWqEM,1893
|
|
1197
1203
|
django_spire/theme/views/page_views.py,sha256=WenjOa6Welpu3IMolY56ZwBjy4aK9hpbiMNuygjAl1A,3922
|
|
1198
|
-
django_spire-0.23.
|
|
1199
|
-
django_spire-0.23.
|
|
1200
|
-
django_spire-0.23.
|
|
1201
|
-
django_spire-0.23.
|
|
1202
|
-
django_spire-0.23.
|
|
1204
|
+
django_spire-0.23.5.dist-info/licenses/LICENSE.md,sha256=tlTbOtgKoy-xAQpUk9gPeh9O4oRXCOzoWdW3jJz0wnA,1091
|
|
1205
|
+
django_spire-0.23.5.dist-info/METADATA,sha256=NsK-z_MdElXotF4ZnWMERuHNpLZODw6_kRI_lO7bt-Q,5127
|
|
1206
|
+
django_spire-0.23.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
1207
|
+
django_spire-0.23.5.dist-info/top_level.txt,sha256=xf3QV1e--ONkVpgMDQE9iqjQ1Vg4--_6C8wmO-KxPHQ,13
|
|
1208
|
+
django_spire-0.23.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|