mirror of
https://github.com/MOIS3Y/logs-collector.git
synced 2025-09-13 05:03:01 +02:00
Add: storage info widget and storage api endpoint refactoring project structure add version app
This commit is contained in:
@@ -5,7 +5,7 @@ from django.utils.html import format_html
|
||||
from django.utils.translation import ngettext
|
||||
|
||||
from .models import Platform, Archive, Ticket
|
||||
from .utils import sizify
|
||||
from .utils.helpers import sizify
|
||||
|
||||
|
||||
class PlatformAdmin(admin.ModelAdmin):
|
||||
|
@@ -18,4 +18,5 @@ router.register(r'tickets', views.TicketViewSet)
|
||||
urlpatterns = [
|
||||
# CRUD:
|
||||
path('v1/', include(router.urls)),
|
||||
path('v1/storage/', views.StorageInfo.as_view(), name='storage-info'),
|
||||
]
|
||||
|
@@ -1,4 +1,5 @@
|
||||
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
||||
from django.conf import settings
|
||||
|
||||
from rest_framework import status
|
||||
# from rest_framework.decorators import action
|
||||
@@ -10,6 +11,7 @@ from rest_framework.parsers import (
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import viewsets
|
||||
from rest_framework import views
|
||||
from rest_framework import filters
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
@@ -18,6 +20,7 @@ from drf_spectacular.utils import extend_schema
|
||||
from drf_spectacular.openapi import OpenApiParameter
|
||||
|
||||
from collector.models import Archive, Ticket, Platform
|
||||
from collector.utils.helpers import get_mount_fs_info
|
||||
|
||||
from .filters import ArchiveFilter, TicketFilter
|
||||
from .permissions import IsGuestUpload
|
||||
@@ -122,3 +125,10 @@ class TicketViewSet(viewsets.ModelViewSet):
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(user=self.request.user)
|
||||
|
||||
|
||||
class StorageInfo(views.APIView):
|
||||
"""Info about storage total/used/free space"""
|
||||
|
||||
def get(self, request):
|
||||
return Response(get_mount_fs_info(settings.MEDIA_ROOT))
|
||||
|
14
logs_collector/collector/context_processors.py
Normal file
14
logs_collector/collector/context_processors.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from django.conf import settings
|
||||
|
||||
from .utils.helpers import get_mount_fs_info
|
||||
|
||||
|
||||
def metadata(request):
|
||||
return {
|
||||
"version": settings.VERSION,
|
||||
"environment": settings.ENVIRONMENT,
|
||||
}
|
||||
|
||||
|
||||
def storage_info(request):
|
||||
return get_mount_fs_info(settings.MEDIA_ROOT)
|
@@ -47,7 +47,7 @@ class Migration(migrations.Migration):
|
||||
name='Archive',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('file', models.FileField(upload_to=collector.utils.logs_dir_path)),
|
||||
('file', models.FileField(upload_to=collector.utils.helpers.logs_dir_path)),
|
||||
('size', models.BigIntegerField(editable=False)),
|
||||
('md5', models.CharField(editable=False, max_length=1024)),
|
||||
('time_create', models.DateTimeField(auto_now_add=True)),
|
||||
|
@@ -6,7 +6,7 @@ from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
|
||||
from .utils import logs_dir_path
|
||||
from .utils.helpers import logs_dir_path
|
||||
|
||||
|
||||
class Archive(models.Model):
|
||||
|
@@ -1,6 +0,0 @@
|
||||
const tooltipTriggerList = document.querySelectorAll(
|
||||
'[data-bs-toggle="tooltip"]'
|
||||
)
|
||||
const tooltipList = [...tooltipTriggerList].map(
|
||||
(tooltipTriggerEl) => new bootstrap.Tooltip(tooltipTriggerEl)
|
||||
)
|
69
logs_collector/collector/static/collector/js/helpers.js
Normal file
69
logs_collector/collector/static/collector/js/helpers.js
Normal file
@@ -0,0 +1,69 @@
|
||||
// formatted byte size to human readable:
|
||||
const sizify = (value) => {
|
||||
let ext = ''
|
||||
if (value < 512000) {
|
||||
value = value / 1024.0
|
||||
ext = 'KB'
|
||||
} else if (value < 4194304000) {
|
||||
value = value / 1048576.0
|
||||
ext = 'MB'
|
||||
} else {
|
||||
value = value / 1073741824.0
|
||||
ext = 'GB'
|
||||
};
|
||||
return `${Math.round(value * 10) / 10} ${ext}`
|
||||
};
|
||||
|
||||
// fix update bootstrap tooltip func:
|
||||
const updateBsTooltip = (instance) => {
|
||||
let tt = bootstrap.Tooltip.getInstance(instance);
|
||||
tt.dispose();
|
||||
bootstrap.Tooltip.getOrCreateInstance(instance);
|
||||
};
|
||||
|
||||
// update storage info widget:
|
||||
const updateStorageInfo = () => {
|
||||
// set storage items vars:
|
||||
let storageIcon = $("#storage_icon")
|
||||
let storageProgressContainer = $("#storage_progress_container")
|
||||
let storage_progress = $("#storage_progress")
|
||||
// set API url:
|
||||
const storageUrl = storage_progress.attr("storage-url")
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url: storageUrl,
|
||||
headers: {
|
||||
"Content-Type":"application/json"
|
||||
},
|
||||
dataType: "json",
|
||||
success: function (data, textStatus, jqXHR) {
|
||||
// JSON answer:
|
||||
let storage = data.storage
|
||||
// set updated fields:
|
||||
let storageInfoNewFields = [
|
||||
`Total: ${sizify(storage.total)}`,
|
||||
'<br>',
|
||||
`Used: ${sizify(storage.used)}`,
|
||||
'<br>',
|
||||
`Free: ${sizify(storage.free)}`
|
||||
].join('')
|
||||
// progress bar update:
|
||||
storage_progress.attr("style", `width:${storage.used_percent}%`)
|
||||
// progress bar color update:
|
||||
if (storage.used_percent > 90) {
|
||||
storage_progress.attr("class", "progress-bar bg-danger");
|
||||
} else if (storage.used_percent > 80) {
|
||||
storage_progress.attr("class", "progress-bar bg-warning");
|
||||
} else {
|
||||
storage_progress.attr("class", "progress-bar bg-success");
|
||||
};
|
||||
// tooltips update:
|
||||
storageIcon.attr("data-bs-title", `Storage used: ${storage.used_percent}%`)
|
||||
storageProgressContainer.attr("data-bs-title", storageInfoNewFields)
|
||||
updateBsTooltip(storageIcon)
|
||||
updateBsTooltip(storageProgressContainer)
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export {sizify, updateBsTooltip, updateStorageInfo};
|
@@ -1,3 +1,6 @@
|
||||
import {updateStorageInfo} from "./helpers.js";
|
||||
|
||||
|
||||
$(function () {
|
||||
console.log("JQ is ready to work");
|
||||
|
||||
@@ -24,8 +27,11 @@ $(function () {
|
||||
success: function (data, textStatus, jqXHR) {
|
||||
console.log(jqXHR.status);
|
||||
$(archiveListElement).hide(1500);
|
||||
setTimeout(() => {
|
||||
updateStorageInfo();
|
||||
}, 3000);
|
||||
},
|
||||
error: function (data, textStatus, jqXHR) {
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
console.log(jqXHR.status);
|
||||
}
|
||||
});
|
||||
@@ -57,7 +63,7 @@ $(function () {
|
||||
success: function (data, textStatus, jqXHR) {
|
||||
console.log(jqXHR.status)
|
||||
},
|
||||
error: function (data, textStatus, jqXHR) {
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
console.log(data)
|
||||
console.log(jqXHR.status)
|
||||
}
|
||||
@@ -82,11 +88,14 @@ $(function () {
|
||||
console.log(jqXHR.status);
|
||||
if (delDiv.length) {
|
||||
delDiv.hide(1500);
|
||||
setTimeout(() => {
|
||||
updateStorageInfo();
|
||||
}, 3000);
|
||||
} else {
|
||||
window.location.href = redirectUrl;
|
||||
}
|
||||
},
|
||||
error: function (data, textStatus, jqXHR) {
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
console.log(jqXHR.status);
|
||||
}
|
||||
});
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import {updateStorageInfo} from "./helpers.js";
|
||||
|
||||
$(function () {
|
||||
const uploadForm = document.getElementById('upload_form');
|
||||
const input_file = document.getElementById('id_file');
|
||||
@@ -6,7 +8,7 @@ $(function () {
|
||||
|
||||
$("#upload_form").submit(function(e){
|
||||
e.preventDefault();
|
||||
$form = $(this)
|
||||
// $form = $(this)
|
||||
let formData = new FormData(this);
|
||||
let upload_token = formData.get("token")
|
||||
const media_data = input_file.files[0];
|
||||
@@ -51,6 +53,11 @@ $(function () {
|
||||
].join('')
|
||||
uploadForm.reset()
|
||||
progress_bar.classList.add('not-visible')
|
||||
try {
|
||||
updateStorageInfo();
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
};
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown){
|
||||
console.log(jqXHR);
|
||||
|
@@ -51,5 +51,5 @@
|
||||
{% endblock main %}
|
||||
|
||||
{% block jquery %}
|
||||
<script src="{% static 'collector/js/jq.upload.progress.js' %}"></script>
|
||||
<script type="module" src="{% static 'collector/js/jq.upload.progress.js' %}"></script>
|
||||
{% endblock jquery %}
|
||||
|
39
logs_collector/collector/templates/collector/storage.html
Normal file
39
logs_collector/collector/templates/collector/storage.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<li class="nav-item col-lg-auto d-flex align-items-center">
|
||||
<i
|
||||
class="nav-link me-1 bi bi-sd-card"
|
||||
aria-current="page"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-title="Storage used: {{ storage.used_percent }}%"
|
||||
>
|
||||
</i>
|
||||
<div
|
||||
class="progress"
|
||||
role="progressbar"
|
||||
aria-label="storage used"
|
||||
aria-valuenow="25"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
style="width: 125px"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-html="true"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-title="
|
||||
Total: {{ storage.total|filesizeformat }}
|
||||
<br>
|
||||
Used: {{ storage.used|filesizeformat }}
|
||||
<br>
|
||||
Free: {{ storage.free|filesizeformat }}
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="progress-bar
|
||||
{% if storage.used_percent > 90 %} bg-danger
|
||||
{% elif storage.used_percent > 80 %} bg-warning
|
||||
{% else %} bg-success
|
||||
{% endif %}"
|
||||
style="width: {{ storage.used_percent }}%"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
@@ -56,5 +56,5 @@
|
||||
</div>
|
||||
{% endblock main %}
|
||||
{% block jquery %}
|
||||
<script src="{% static 'collector/js/jq.ticket.detail.js' %}"></script>
|
||||
<script type="module" src="{% static 'collector/js/jq.ticket.detail.js' %}"></script>
|
||||
{% endblock jquery %}
|
||||
|
@@ -86,9 +86,6 @@
|
||||
{% include 'collector/includes/pagination.html' %}
|
||||
</div>
|
||||
{% endblock main %}
|
||||
{% block bs %}
|
||||
<script src="{% static 'collector/js/bs.tooltip.js' %}"></script>
|
||||
{% endblock bs %}
|
||||
{% block jquery %}
|
||||
<script src="{% static 'collector/js/jq.ticket.detail.js' %}"></script>
|
||||
<script type="module" src="{% static 'collector/js/jq.ticket.detail.js' %}"></script>
|
||||
{% endblock jquery %}
|
||||
|
@@ -1,43 +0,0 @@
|
||||
def logs_dir_path(instance, filename):
|
||||
"""
|
||||
file will be uploaded to
|
||||
MEDIA_ROOT_FOR_SENSITIVE_FILES/<ticket-token>/<filename>
|
||||
"""
|
||||
return f'{instance.ticket.number}/{filename}'
|
||||
|
||||
|
||||
def sizify(value: int) -> str:
|
||||
"""Simple kb/mb/gb size snippet for admin panel custom field:
|
||||
|
||||
Args:
|
||||
value (int): size of file from Filefield
|
||||
|
||||
Returns:
|
||||
str: format human readable size like 4.2 Gb
|
||||
"""
|
||||
if value < 512000:
|
||||
value = value / 1024.0
|
||||
ext = 'Kb'
|
||||
elif value < 4194304000:
|
||||
value = value / 1048576.0
|
||||
ext = 'Mb'
|
||||
else:
|
||||
value = value / 1073741824.0
|
||||
ext = 'Gb'
|
||||
return f'{round(value, 2)} {ext}'
|
||||
|
||||
|
||||
class PageTitleViewMixin:
|
||||
title = 'Collector'
|
||||
|
||||
def get_title(self, *args, **kwargs):
|
||||
"""
|
||||
Return the class title attr by default,
|
||||
but you can override this method to further customize
|
||||
"""
|
||||
return self.title
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['title'] = self.get_title()
|
||||
return context
|
0
logs_collector/collector/utils/__init__.py
Normal file
0
logs_collector/collector/utils/__init__.py
Normal file
38
logs_collector/collector/utils/helpers.py
Normal file
38
logs_collector/collector/utils/helpers.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import shutil
|
||||
|
||||
|
||||
def logs_dir_path(instance, filename):
|
||||
"""
|
||||
file will be uploaded to
|
||||
MEDIA_ROOT/view/<filename>
|
||||
"""
|
||||
return f'{instance.ticket.number}/{filename}'
|
||||
|
||||
|
||||
def sizify(value: int) -> str:
|
||||
"""Simple kb/mb/gb size snippet for admin panel custom field:
|
||||
|
||||
Args:
|
||||
value (int): size of file from Filefield
|
||||
|
||||
Returns:
|
||||
str: format human readable size like 4.2 Gb
|
||||
"""
|
||||
if value < 512000:
|
||||
value = value / 1024.0
|
||||
ext = 'KB'
|
||||
elif value < 4194304000:
|
||||
value = value / 1048576.0
|
||||
ext = 'MB'
|
||||
else:
|
||||
value = value / 1073741824.0
|
||||
ext = 'GB'
|
||||
return f'{round(value, 1)} {ext}'
|
||||
|
||||
|
||||
def get_mount_fs_info(path):
|
||||
mount_info = shutil.disk_usage(path)._asdict()
|
||||
mount_info['used_percent'] = round(
|
||||
mount_info['used'] / mount_info['total'] * 100
|
||||
)
|
||||
return {'storage': mount_info}
|
23
logs_collector/collector/utils/mixins.py
Normal file
23
logs_collector/collector/utils/mixins.py
Normal file
@@ -0,0 +1,23 @@
|
||||
class ExtraContextMixin:
|
||||
"""The class adds additional context
|
||||
to all child view classes that inherit from it.
|
||||
Overrides the get_context_data method for CBV
|
||||
"""
|
||||
|
||||
title = 'Collector'
|
||||
|
||||
def get_title(self, *args, **kwargs):
|
||||
"""
|
||||
Return the class title attr by default,
|
||||
but you can override this method to further customize
|
||||
"""
|
||||
return self.title
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {}
|
||||
try:
|
||||
context = super().get_context_data(**kwargs)
|
||||
except Exception:
|
||||
pass
|
||||
context['title'] = self.get_title()
|
||||
return context
|
@@ -3,25 +3,22 @@ from django.http import FileResponse
|
||||
from django.views import generic
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import render
|
||||
|
||||
from two_factor.views import OTPRequiredMixin
|
||||
|
||||
from .forms import TicketForm, ArchiveForm
|
||||
from .models import Archive, Ticket
|
||||
from .utils import PageTitleViewMixin
|
||||
from .utils.mixins import ExtraContextMixin
|
||||
|
||||
|
||||
class ArchiveUploadView(PageTitleViewMixin, generic.View):
|
||||
class ArchiveUploadView(ExtraContextMixin, generic.TemplateView):
|
||||
form_class = ArchiveForm()
|
||||
template = 'collector/archive_upload.html',
|
||||
template_name = 'collector/archive_upload.html'
|
||||
|
||||
def get(self, request):
|
||||
return render(
|
||||
request,
|
||||
self.template,
|
||||
context={'form': self.form_class}
|
||||
)
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['form'] = self.form_class
|
||||
return context
|
||||
|
||||
def get_title(self):
|
||||
return f'{self.title} - upload'
|
||||
@@ -41,7 +38,7 @@ class ArchiveHandlerView(
|
||||
return FileResponse(self.object.file)
|
||||
|
||||
|
||||
class CreateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.CreateView):
|
||||
class CreateTicket(LoginRequiredMixin, ExtraContextMixin, generic.CreateView):
|
||||
model = Ticket
|
||||
form_class = TicketForm
|
||||
template_name = 'collector/ticket_create.html'
|
||||
@@ -54,7 +51,7 @@ class CreateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.CreateView):
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class UpdateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.UpdateView):
|
||||
class UpdateTicket(LoginRequiredMixin, ExtraContextMixin, generic.UpdateView):
|
||||
model = Ticket
|
||||
form_class = TicketForm
|
||||
template_name = 'collector/ticket_create.html'
|
||||
@@ -69,7 +66,7 @@ class UpdateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.UpdateView):
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class ListAllTickets(LoginRequiredMixin, PageTitleViewMixin, generic.ListView):
|
||||
class ListAllTickets(LoginRequiredMixin, ExtraContextMixin, generic.ListView):
|
||||
model = Ticket
|
||||
template_name = 'collector/tickets.html'
|
||||
context_object_name = 'tickets'
|
||||
@@ -98,7 +95,7 @@ class ListAllTickets(LoginRequiredMixin, PageTitleViewMixin, generic.ListView):
|
||||
return super().get_queryset()
|
||||
|
||||
|
||||
class ListPlatformTickets(LoginRequiredMixin, PageTitleViewMixin, generic.ListView): # noqa:E501
|
||||
class ListPlatformTickets(LoginRequiredMixin, ExtraContextMixin, generic.ListView): # noqa:E501
|
||||
model = Ticket
|
||||
template_name = 'collector/tickets.html'
|
||||
context_object_name = 'tickets'
|
||||
@@ -114,7 +111,7 @@ class ListPlatformTickets(LoginRequiredMixin, PageTitleViewMixin, generic.ListVi
|
||||
)
|
||||
|
||||
|
||||
class DetailTicket(LoginRequiredMixin, PageTitleViewMixin, generic.DetailView):
|
||||
class DetailTicket(LoginRequiredMixin, ExtraContextMixin, generic.DetailView):
|
||||
model = Ticket
|
||||
template_name = 'collector/ticket.html'
|
||||
context_object_name = 'ticket'
|
||||
|
Reference in New Issue
Block a user