Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b0b22b79dc | |||
| 6471413681 | |||
| 50b10689fc | |||
| 85bddb6cba | |||
| c285b0d3b3 | |||
| 671fe8d86f |
68
PROJECT.org
68
PROJECT.org
@ -93,7 +93,7 @@ fetching and simple saving.
|
|||||||
:LOGBOOK:
|
:LOGBOOK:
|
||||||
CLOCK: [2025-07-09 Wed 09:55]--[2025-07-09 Wed 10:15] => 0:20
|
CLOCK: [2025-07-09 Wed 09:55]--[2025-07-09 Wed 10:15] => 0:20
|
||||||
:END:
|
:END:
|
||||||
* Backlog [0/15] :vrobbler:project:personal:
|
* Backlog [0/13] :vrobbler:project:personal:
|
||||||
** TODO [#C] Add sentiment parsing for Scrobbles with notes :vrobbler:project:scrobbles:sentiment:
|
** TODO [#C] Add sentiment parsing for Scrobbles with notes :vrobbler:project:scrobbles:sentiment:
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:ID: 37781d6a-f3b0-48b2-bf98-33c2c791cf85
|
:ID: 37781d6a-f3b0-48b2-bf98-33c2c791cf85
|
||||||
@ -459,11 +459,11 @@ https://app.todoist.com/app/task/add-a-csv-endpoint-for-users-book-reads-that-li
|
|||||||
- Note taken on [2025-09-25 Thu 10:51]
|
- Note taken on [2025-09-25 Thu 10:51]
|
||||||
|
|
||||||
As an example https://comicbookroundup.com/comic-books/reviews/humanoids-publishing/the-history-of-science-fiction
|
As an example https://comicbookroundup.com/comic-books/reviews/humanoids-publishing/the-history-of-science-fiction
|
||||||
** TODO [#B] Find page numbers for comic books from ComicVine :vrobbler:feature:books:personal:project:
|
** TODO [#B] Find page numbers for comic books from ComicVine :feature:books:
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:ID: 79f867c3-1288-4143-b6bf-2a452983ee9f
|
:ID: 79f867c3-1288-4143-b6bf-2a452983ee9f
|
||||||
:END:
|
:END:
|
||||||
** TODO [#B] Fix koreader scrobble imports to use DST properly :vrobbler:personal:bug:books:imports:
|
** TODO [#B] Fix koreader scrobble imports to use DST properly :bug:books:imports:
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:ID: 79758cba-a440-48b6-a637-efb88827acf2
|
:ID: 79758cba-a440-48b6-a637-efb88827acf2
|
||||||
:END:
|
:END:
|
||||||
@ -489,7 +489,36 @@ whatever time KoReader reports, we need to know, given the date and the user
|
|||||||
profile's historic timezone, how many hours to adjust the KoReader time to get
|
profile's historic timezone, how many hours to adjust the KoReader time to get
|
||||||
to GMT to save it in the database.
|
to GMT to save it in the database.
|
||||||
|
|
||||||
** TODO [#A] Orgmode tasks are not updated if in progress :tasks:orgmode:bug:
|
* Version 43.0 [5/5]
|
||||||
|
** DONE [#B] Can we show a graph of all past Weigh-in tasks :scale:tasks:graphs:javascript:
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: ae499d87-03bf-4e48-9b2c-1a421a46af11
|
||||||
|
:END:
|
||||||
|
|
||||||
|
*** Description
|
||||||
|
|
||||||
|
I wonder if, as a special type of task, Weigh-in's could show a graph of the
|
||||||
|
metrics that are stored against all the past weigh-ins?
|
||||||
|
|
||||||
|
The graph would contain all Weigh-in scrobbles for that user, no matter which
|
||||||
|
date is being viewed, and the highlighted value on the graph would be the date
|
||||||
|
being viewed.
|
||||||
|
|
||||||
|
Probably could use something like chart.js although maybe that's too heavy?
|
||||||
|
|
||||||
|
And can we have each metric overlayed on the same graph?
|
||||||
|
|
||||||
|
** DONE [#B] When viewing scrobbles by tag, sum the total time :scrobbles:tags:
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: d51f23df-c2c5-4e1a-b000-67c89032af02
|
||||||
|
:END:
|
||||||
|
|
||||||
|
*** Description
|
||||||
|
|
||||||
|
On scrobbles filtered by tags, we should see a sum of the time spent doing those tasks, in a human
|
||||||
|
readable format like "X days, X hours, X minutes and X seconds"
|
||||||
|
|
||||||
|
** DONE [#A] Orgmode tasks are not updated if in progress :tasks:orgmode:bug:
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:ID: 7dcebb2c-7c4c-4ac5-bee6-c2e36c3811f9
|
:ID: 7dcebb2c-7c4c-4ac5-bee6-c2e36c3811f9
|
||||||
:END:
|
:END:
|
||||||
@ -505,7 +534,10 @@ different. And the same for comments. If a comment (by timestamp key) is
|
|||||||
different in the webhook than what's in the scrobble.log, update the comment in
|
different in the webhook than what's in the scrobble.log, update the comment in
|
||||||
the scrobble.log
|
the scrobble.log
|
||||||
|
|
||||||
** TODO [#A] Ignore tag 'inprogress' for Tasks :bug:tasks:tags:
|
** DONE [#A] Ignore tag 'inprogress' for Tasks :bug:tasks:tags:
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: cd37c1ec-e2fc-b93c-daf8-6b329712c3f1
|
||||||
|
:END:
|
||||||
|
|
||||||
*** Description
|
*** Description
|
||||||
|
|
||||||
@ -515,6 +547,32 @@ payload, because that's how we parse tasks starting from the Todoist webhooks.
|
|||||||
But we don't really need anything tagged as `inprogress` Can we ignore this tag
|
But we don't really need anything tagged as `inprogress` Can we ignore this tag
|
||||||
when applying tags to Task scrobbles coming from Todoist?`
|
when applying tags to Task scrobbles coming from Todoist?`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
** DONE [#A] Deploys are now throwing an unknown version error :bug:tooling:releases:
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: 3870f9d3-b5ed-4b87-9e8c-9bf905bfb766
|
||||||
|
:END:
|
||||||
|
|
||||||
|
*** Description
|
||||||
|
|
||||||
|
Almost everything is working, but for some reason `__version__` does not seem to
|
||||||
|
exist.
|
||||||
|
|
||||||
|
#+begin_src sh
|
||||||
|
out: Installing collected packages: vrobbler
|
||||||
|
out: Successfully installed vrobbler-42.0
|
||||||
|
err: WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv
|
||||||
|
err: Traceback (most recent call last):
|
||||||
|
err: File "<string>", line 1, in <module>
|
||||||
|
err: AttributeError: module 'vrobbler' has no attribute '__version__'
|
||||||
|
2026/06/04 17:18:15 Process exited with status 1
|
||||||
|
failed to remove container: Error response from daemon: removal of container c8ac64bee9b6bf5978d2c16f299e5ac271d8bbf7192b7a4023c3712bc2444f8b is already in progress
|
||||||
|
❌ Failure - Main Install wheel and restart services
|
||||||
|
exit with `FAILURE`: 1
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
|
||||||
* Version 42.0 [1/1]
|
* Version 42.0 [1/1]
|
||||||
** DONE [#B] Add ability to add track to current Mopidy queue :feature:mopidy:tracks:
|
** DONE [#B] Add ability to add track to current Mopidy queue :feature:mopidy:tracks:
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "vrobbler"
|
name = "vrobbler"
|
||||||
version = "42.0"
|
version = "43.0"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Colin Powell <colin@unbl.ink>"]
|
authors = ["Colin Powell <colin@unbl.ink>"]
|
||||||
|
|
||||||
@ -107,6 +107,8 @@ exclude_dirs = ["*/tests/*", "*/migrations/*"]
|
|||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
vrobbler = "vrobbler.cli:main"
|
vrobbler = "vrobbler.cli:main"
|
||||||
|
|
||||||
|
[tool.poetry_bumpversion.file."vrobbler/__init__.py"]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=1.0.0"]
|
requires = ["poetry-core>=1.0.0"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|||||||
@ -2,4 +2,5 @@
|
|||||||
# Django starts so that shared_task will use this app.
|
# Django starts so that shared_task will use this app.
|
||||||
from .celery import app as celery_app
|
from .celery import app as celery_app
|
||||||
|
|
||||||
__all__ = ("celery_app",)
|
__version__ = "42.0"
|
||||||
|
__all__ = ("celery_app", "__version__")
|
||||||
|
|||||||
@ -761,7 +761,10 @@ def todoist_scrobble_task(
|
|||||||
|
|
||||||
todoist_task["title"] = todoist_task.pop("description")
|
todoist_task["title"] = todoist_task.pop("description")
|
||||||
todoist_task["description"] = todoist_task.pop("details")
|
todoist_task["description"] = todoist_task.pop("details")
|
||||||
todoist_task["labels"] = todoist_task.pop("todoist_label_list", [])
|
labels = todoist_task.pop("todoist_label_list", [])
|
||||||
|
todoist_task["labels"] = [
|
||||||
|
l for l in labels if l.lower() != "inprogress"
|
||||||
|
]
|
||||||
todoist_task.pop("todoist_type")
|
todoist_task.pop("todoist_type")
|
||||||
todoist_task.pop("todoist_event")
|
todoist_task.pop("todoist_event")
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@ from django.contrib import messages
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
|
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
|
||||||
from django.db.models import Count, Max, Q
|
from django.db.models import Count, Max, Q, Sum
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
from rest_framework.authentication import TokenAuthentication
|
from rest_framework.authentication import TokenAuthentication
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
@ -367,6 +367,7 @@ class ScrobbleListView(LoginRequiredMixin, ListView):
|
|||||||
else:
|
else:
|
||||||
tag_list = []
|
tag_list = []
|
||||||
self.tag_list = tag_list
|
self.tag_list = tag_list
|
||||||
|
self._full_queryset = qs
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
def _compute_overlap_groups(self, scrobbles):
|
def _compute_overlap_groups(self, scrobbles):
|
||||||
@ -433,6 +434,13 @@ class ScrobbleListView(LoginRequiredMixin, ListView):
|
|||||||
ctx["tag_list"] = getattr(self, "tag_list", [])
|
ctx["tag_list"] = getattr(self, "tag_list", [])
|
||||||
scrobbles = list(ctx.get("object_list", []))
|
scrobbles = list(ctx.get("object_list", []))
|
||||||
ctx["overlap_map"] = self._compute_overlap_groups(scrobbles)
|
ctx["overlap_map"] = self._compute_overlap_groups(scrobbles)
|
||||||
|
full_qs = getattr(self, "_full_queryset", None)
|
||||||
|
if full_qs is not None and getattr(self, "tag_list", []):
|
||||||
|
total = (
|
||||||
|
full_qs.aggregate(total=Sum("playback_position_seconds"))["total"]
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
ctx["total_time_seconds"] = total
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import pendulum
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
@ -8,6 +9,7 @@ from rest_framework import status
|
|||||||
from rest_framework.decorators import api_view, permission_classes
|
from rest_framework.decorators import api_view, permission_classes
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
from scrobbles.models import Scrobble
|
||||||
from scrobbles.views import ScrobbleableDetailView, ScrobbleableListView
|
from scrobbles.views import ScrobbleableDetailView, ScrobbleableListView
|
||||||
from tasks.models import Task
|
from tasks.models import Task
|
||||||
|
|
||||||
@ -23,6 +25,72 @@ class TaskListView(ScrobbleableListView):
|
|||||||
class TaskDetailView(ScrobbleableDetailView):
|
class TaskDetailView(ScrobbleableDetailView):
|
||||||
model = Task
|
model = Task
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
if self.object.title != "Weigh-in":
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
scrobbles = list(
|
||||||
|
Scrobble.objects.filter(
|
||||||
|
user=self.request.user,
|
||||||
|
task=self.object,
|
||||||
|
log__weight__isnull=False,
|
||||||
|
).order_by("timestamp")
|
||||||
|
)
|
||||||
|
if not scrobbles:
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
labels = []
|
||||||
|
weight_data = []
|
||||||
|
body_fat_data = []
|
||||||
|
bmi_data = []
|
||||||
|
for s in scrobbles:
|
||||||
|
ts = s.timestamp
|
||||||
|
if isinstance(ts, str):
|
||||||
|
ts = pendulum.parse(ts)
|
||||||
|
labels.append(ts.strftime("%Y-%m-%d"))
|
||||||
|
log = s.log if isinstance(s.log, dict) else {}
|
||||||
|
raw_weight = log.get("weight")
|
||||||
|
weight_data.append(
|
||||||
|
float(raw_weight) if raw_weight is not None else None
|
||||||
|
)
|
||||||
|
raw_bf = log.get("body_fat")
|
||||||
|
body_fat_data.append(
|
||||||
|
float(raw_bf) if raw_bf is not None else None
|
||||||
|
)
|
||||||
|
raw_bmi = log.get("bmi")
|
||||||
|
bmi_data.append(
|
||||||
|
float(raw_bmi) if raw_bmi is not None else None
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx["weighin_chart"] = {
|
||||||
|
"labels": labels,
|
||||||
|
"datasets": [
|
||||||
|
{
|
||||||
|
"label": "Weight",
|
||||||
|
"data": weight_data,
|
||||||
|
"borderColor": "#4bc0c0",
|
||||||
|
"fill": False,
|
||||||
|
"yAxisID": "y",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Body Fat %",
|
||||||
|
"data": body_fat_data,
|
||||||
|
"borderColor": "#ff6384",
|
||||||
|
"fill": False,
|
||||||
|
"yAxisID": "y1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "BMI",
|
||||||
|
"data": bmi_data,
|
||||||
|
"borderColor": "#36a2eb",
|
||||||
|
"fill": False,
|
||||||
|
"yAxisID": "y2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
@api_view(["GET"])
|
@api_view(["GET"])
|
||||||
@permission_classes([IsAuthenticated])
|
@permission_classes([IsAuthenticated])
|
||||||
|
|||||||
@ -232,7 +232,7 @@ class EmacsWebhookView(APIView):
|
|||||||
status=status.HTTP_304_NOT_MODIFIED,
|
status=status.HTTP_304_NOT_MODIFIED,
|
||||||
)
|
)
|
||||||
|
|
||||||
if task_in_progress:
|
if scrobble and scrobble.in_progress:
|
||||||
emacs_scrobble_update_task(
|
emacs_scrobble_update_task(
|
||||||
post_data.get("source_id"),
|
post_data.get("source_id"),
|
||||||
post_data.get("notes") or [],
|
post_data.get("notes") or [],
|
||||||
|
|||||||
@ -8,21 +8,20 @@ def natural_duration(value):
|
|||||||
if not value:
|
if not value:
|
||||||
return
|
return
|
||||||
value = int(value)
|
value = int(value)
|
||||||
total_minutes = int(value / 60)
|
days = int(value / 86400)
|
||||||
hours = int(total_minutes / 60)
|
remainder = value % 86400
|
||||||
minutes = total_minutes - (hours * 60)
|
hours = int(remainder / 3600)
|
||||||
seconds = value % 60
|
minutes = int((remainder % 3600) / 60)
|
||||||
value_str = ""
|
seconds = remainder % 60
|
||||||
if seconds:
|
parts = []
|
||||||
value_str = f"{seconds} seconds"
|
if days:
|
||||||
if minutes:
|
parts.append(f"{days} day{'s' if days != 1 else ''}")
|
||||||
if value_str:
|
|
||||||
value_str = f"{minutes} minutes, " + value_str
|
|
||||||
else:
|
|
||||||
value_str = f"{minutes} minutes"
|
|
||||||
if hours:
|
if hours:
|
||||||
if value_str:
|
parts.append(f"{hours} hour{'s' if hours != 1 else ''}")
|
||||||
value_str = f"{hours} hours, " + value_str
|
if minutes:
|
||||||
else:
|
parts.append(f"{minutes} minute{'s' if minutes != 1 else ''}")
|
||||||
value_str = f"{hours} hours"
|
if seconds or not parts:
|
||||||
return value_str
|
parts.append(f"{seconds} second{'s' if seconds != 1 else ''}")
|
||||||
|
if len(parts) == 1:
|
||||||
|
return parts[0]
|
||||||
|
return ", ".join(parts[:-1]) + " and " + parts[-1]
|
||||||
|
|||||||
@ -9,6 +9,9 @@
|
|||||||
<h1 class="h2">All Scrobbles</h1>
|
<h1 class="h2">All Scrobbles</h1>
|
||||||
{% if tag_list %}
|
{% if tag_list %}
|
||||||
<h6 class="text-muted">Tagged {{ tag_list|join:", " }}</h6>
|
<h6 class="text-muted">Tagged {{ tag_list|join:", " }}</h6>
|
||||||
|
{% if total_time_seconds %}
|
||||||
|
<p class="text-muted small mb-0">Total time: {{ total_time_seconds|natural_duration }}</p>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
{% load mathfilters %}
|
{% load mathfilters %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load naturalduration %}
|
{% load naturalduration %}
|
||||||
|
{% load l10n %}
|
||||||
|
|
||||||
{% block title %}{{object.title}}{% endblock %}
|
{% block title %}{{object.title}}{% endblock %}
|
||||||
|
|
||||||
@ -39,6 +40,15 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if weighin_chart %}
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<canvas id="weighinChart" width="800" height="300"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>{{scrobbles.count}} scrobbles</p>
|
<p>{{scrobbles.count}} scrobbles</p>
|
||||||
<p>
|
<p>
|
||||||
@ -94,3 +104,61 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
{% if weighin_chart %}
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js"></script>
|
||||||
|
<script>
|
||||||
|
var ctx = document.getElementById('weighinChart').getContext('2d');
|
||||||
|
new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: {{ weighin_chart.labels|safe }},
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Weight',
|
||||||
|
data: {{ weighin_chart.datasets.0.data|safe }},
|
||||||
|
borderColor: '{{ weighin_chart.datasets.0.borderColor }}',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
pointRadius: 2,
|
||||||
|
fill: false,
|
||||||
|
yAxisID: 'y',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Body Fat %',
|
||||||
|
data: {{ weighin_chart.datasets.1.data|safe }},
|
||||||
|
borderColor: '{{ weighin_chart.datasets.1.borderColor }}',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
pointRadius: 2,
|
||||||
|
fill: false,
|
||||||
|
yAxisID: 'y1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'BMI',
|
||||||
|
data: {{ weighin_chart.datasets.2.data|safe }},
|
||||||
|
borderColor: '{{ weighin_chart.datasets.2.borderColor }}',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
pointRadius: 2,
|
||||||
|
fill: false,
|
||||||
|
yAxisID: 'y2',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
scales: {
|
||||||
|
xAxes: [{
|
||||||
|
ticks: { maxTicksLimit: 25, maxRotation: 45 },
|
||||||
|
}],
|
||||||
|
yAxes: [
|
||||||
|
{ id: 'y', type: 'linear', position: 'left', scaleLabel: { display: true, labelString: 'Weight' } },
|
||||||
|
{ id: 'y1', type: 'linear', position: 'right', scaleLabel: { display: true, labelString: 'Body Fat %' }, gridLines: { display: false } },
|
||||||
|
{ id: 'y2', type: 'linear', position: 'right', scaleLabel: { display: true, labelString: 'BMI' }, gridLines: { display: false } },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
legend: { position: 'bottom' },
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user