Add: storage info widget and storage api endpoint refactoring project structure add version app

This commit is contained in:
2023-09-07 13:07:18 +09:00
parent e95de1b553
commit 016994d594
28 changed files with 487 additions and 216 deletions

View File

@@ -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):

View File

@@ -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'),
]

View File

@@ -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))

View 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)

View File

@@ -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)),

View File

@@ -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):

View File

@@ -1,6 +0,0 @@
const tooltipTriggerList = document.querySelectorAll(
'[data-bs-toggle="tooltip"]'
)
const tooltipList = [...tooltipTriggerList].map(
(tooltipTriggerEl) => new bootstrap.Tooltip(tooltipTriggerEl)
)

View 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};

View File

@@ -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);
}
});

View File

@@ -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);

View File

@@ -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 %}

View 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>

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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

View 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}

View 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

View File

@@ -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'