mirror of
https://github.com/MOIS3Y/logs-collector.git
synced 2025-09-13 13:13:01 +02:00
Refactoring: new multi app structure
This commit is contained in:
0
logs_collector/apps/collector/__init__.py
Normal file
0
logs_collector/apps/collector/__init__.py
Normal file
25
logs_collector/apps/collector/admin.py
Normal file
25
logs_collector/apps/collector/admin.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Platform, Archive, Ticket
|
||||
|
||||
|
||||
# Register your models here.
|
||||
class PlatformAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
class TicketAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
class ArchiveAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
class TokenAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
admin.site.register(Platform, PlatformAdmin)
|
||||
admin.site.register(Ticket, TicketAdmin)
|
||||
admin.site.register(Archive, ArchiveAdmin)
|
0
logs_collector/apps/collector/api/__init__.py
Normal file
0
logs_collector/apps/collector/api/__init__.py
Normal file
39
logs_collector/apps/collector/api/filters.py
Normal file
39
logs_collector/apps/collector/api/filters.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from django_filters.rest_framework import (
|
||||
CharFilter,
|
||||
FilterSet,
|
||||
NumberFilter,
|
||||
)
|
||||
from django_filters import widgets
|
||||
|
||||
from apps.collector.models import Archive, Ticket
|
||||
from .utils import DateTimeFilterMixin
|
||||
|
||||
|
||||
class ArchiveFilter(DateTimeFilterMixin, FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Archive
|
||||
fields = {
|
||||
'id': ['exact', 'in', 'lte', 'gte'],
|
||||
'ticket': ['exact', 'in', 'lte', 'gte'],
|
||||
'time_create': ['exact', 'lte', 'gte']
|
||||
}
|
||||
|
||||
|
||||
class TicketFilter(DateTimeFilterMixin, FilterSet):
|
||||
number = NumberFilter(
|
||||
field_name='number',
|
||||
widget=widgets.CSVWidget(),
|
||||
)
|
||||
user = CharFilter(
|
||||
field_name='user__username'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Ticket
|
||||
fields = {
|
||||
'id': ['exact', 'in', 'lte', 'gte'],
|
||||
'number': ['exact', 'contains', 'in', 'lte', 'gte'],
|
||||
'resolved': ['exact'],
|
||||
'user': ['exact']
|
||||
}
|
13
logs_collector/apps/collector/api/permissions.py
Normal file
13
logs_collector/apps/collector/api/permissions.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
|
||||
class IsGuestUpload(permissions.BasePermission):
|
||||
"""
|
||||
Special permission class for the ability to upload attachments
|
||||
to an unauthorized user using a ticket token
|
||||
"""
|
||||
def has_permission(self, request, view):
|
||||
if request.method in ('HEAD', 'OPTIONS', 'POST',):
|
||||
return True
|
||||
|
||||
return request.user.is_authenticated
|
60
logs_collector/apps/collector/api/serializers.py
Normal file
60
logs_collector/apps/collector/api/serializers.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from drf_spectacular.openapi import OpenApiTypes
|
||||
|
||||
from apps.collector.models import Archive, Platform, Ticket
|
||||
|
||||
|
||||
@extend_schema_field(OpenApiTypes.NUMBER)
|
||||
class TimestampField(serializers.Field):
|
||||
def to_representation(self, value) -> int:
|
||||
return value.timestamp()
|
||||
|
||||
|
||||
@extend_schema_field(OpenApiTypes.NUMBER)
|
||||
class JsTimestampField(serializers.Field):
|
||||
def to_representation(self, value) -> int:
|
||||
return round(value.timestamp()*1000)
|
||||
|
||||
|
||||
class PublicArchiveUploadSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Archive
|
||||
fields = ['file', 'ticket']
|
||||
|
||||
|
||||
class ArchiveSerializer(serializers.ModelSerializer):
|
||||
time_create = JsTimestampField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Archive
|
||||
fields = ['id', 'file', 'ticket', 'time_create']
|
||||
|
||||
|
||||
class PlatformSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = ['id', 'name', 'pretty_name']
|
||||
|
||||
|
||||
class TicketSerializer(serializers.ModelSerializer):
|
||||
time_create = JsTimestampField(read_only=True)
|
||||
time_update = JsTimestampField(read_only=True)
|
||||
token = serializers.UUIDField(read_only=True)
|
||||
user = serializers.ReadOnlyField(source='user.username')
|
||||
|
||||
class Meta:
|
||||
model = Ticket
|
||||
fields = [
|
||||
'id',
|
||||
'number',
|
||||
'resolved',
|
||||
'token',
|
||||
'attempts',
|
||||
'platform',
|
||||
'time_create',
|
||||
'time_update',
|
||||
'user'
|
||||
]
|
21
logs_collector/apps/collector/api/urls.py
Normal file
21
logs_collector/apps/collector/api/urls.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from django.urls import path, include
|
||||
|
||||
from rest_framework import routers
|
||||
|
||||
from . import views
|
||||
|
||||
# ▄▀█ █▀█ █
|
||||
# █▀█ █▀▀ █
|
||||
# -- -- --
|
||||
|
||||
app_name = 'collector_api'
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
router.register(r'archives', views.ArchiveViewSet)
|
||||
router.register(r'platforms', views.PlatformViewSet)
|
||||
router.register(r'tickets', views.TicketViewSet)
|
||||
|
||||
urlpatterns = [
|
||||
# CRUD:
|
||||
path('v1/', include(router.urls)),
|
||||
]
|
20
logs_collector/apps/collector/api/utils.py
Normal file
20
logs_collector/apps/collector/api/utils.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from django_filters import NumberFilter
|
||||
|
||||
|
||||
class DateTimeFilterMixin:
|
||||
year__gte = NumberFilter(
|
||||
field_name='time_create',
|
||||
lookup_expr='year__gte'
|
||||
)
|
||||
year__lte = NumberFilter(
|
||||
field_name='time_create',
|
||||
lookup_expr='year__lte'
|
||||
)
|
||||
month__gte = NumberFilter(
|
||||
field_name='time_create',
|
||||
lookup_expr='month__gte'
|
||||
)
|
||||
month__lte = NumberFilter(
|
||||
field_name='time_create',
|
||||
lookup_expr='month__lte'
|
||||
)
|
94
logs_collector/apps/collector/api/views.py
Normal file
94
logs_collector/apps/collector/api/views.py
Normal file
@@ -0,0 +1,94 @@
|
||||
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
||||
|
||||
from rest_framework import status
|
||||
# from rest_framework.decorators import action
|
||||
from rest_framework.parsers import FormParser, MultiPartParser
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import viewsets
|
||||
from rest_framework import filters
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
|
||||
from apps.collector.models import Archive, Ticket, Platform # ???????
|
||||
|
||||
from .filters import ArchiveFilter, TicketFilter
|
||||
from .permissions import IsGuestUpload
|
||||
from .serializers import (
|
||||
PublicArchiveUploadSerializer,
|
||||
ArchiveSerializer,
|
||||
PlatformSerializer,
|
||||
TicketSerializer
|
||||
)
|
||||
|
||||
|
||||
class ArchiveViewSet(viewsets.ModelViewSet):
|
||||
queryset = Archive.objects.order_by('-time_create')
|
||||
serializer_class = ArchiveSerializer
|
||||
parser_classes = (MultiPartParser, FormParser)
|
||||
permission_classes = (IsGuestUpload, )
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_class = ArchiveFilter
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
# ! upload-token protection:
|
||||
upload_token = request.headers.get('upload-token', '')
|
||||
if not request.user.is_authenticated and upload_token:
|
||||
try:
|
||||
bound_ticket = Ticket.objects.get(token=upload_token)
|
||||
if bound_ticket.resolved:
|
||||
return Response(
|
||||
{'error': f'ticket {upload_token} already resolved'},
|
||||
status=status.HTTP_423_LOCKED
|
||||
)
|
||||
if bound_ticket.attempts <= 0:
|
||||
return Response(
|
||||
{'error': f'token {upload_token} expired'},
|
||||
status=status.HTTP_423_LOCKED
|
||||
)
|
||||
bound_ticket.attempts -= 1
|
||||
bound_ticket.save()
|
||||
# ? mixin bound ticket number to request.data from user
|
||||
request.data['ticket'] = bound_ticket.number
|
||||
# ? change serializer for guest user
|
||||
self.serializer_class = PublicArchiveUploadSerializer
|
||||
except (ValidationError, ObjectDoesNotExist,):
|
||||
return Response(
|
||||
{'error': f'token {upload_token} is not valid'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
elif not request.user.is_authenticated:
|
||||
return Response(
|
||||
{'error': 'Header Upload-Token is required'},
|
||||
status=status.HTTP_401_UNAUTHORIZED
|
||||
)
|
||||
# ! default create method:
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_create(serializer)
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(
|
||||
serializer.data,
|
||||
status=status.HTTP_201_CREATED,
|
||||
headers=headers
|
||||
)
|
||||
|
||||
|
||||
class PlatformViewSet(viewsets.ModelViewSet):
|
||||
queryset = Platform.objects.all()
|
||||
lookup_field = 'name'
|
||||
serializer_class = PlatformSerializer
|
||||
permission_classes = (IsAuthenticated, )
|
||||
|
||||
|
||||
class TicketViewSet(viewsets.ModelViewSet):
|
||||
queryset = Ticket.objects.order_by('-time_create')
|
||||
lookup_field = 'number'
|
||||
serializer_class = TicketSerializer
|
||||
permission_classes = (IsAuthenticated, )
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter]
|
||||
filterset_class = TicketFilter
|
||||
search_fields = ['number']
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(user=self.request.user)
|
7
logs_collector/apps/collector/apps.py
Normal file
7
logs_collector/apps/collector/apps.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CollectorConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.collector'
|
||||
verbose_name = 'Collector archives for analyse'
|
31
logs_collector/apps/collector/forms.py
Normal file
31
logs_collector/apps/collector/forms.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from django import forms
|
||||
from crispy_forms.helper import FormHelper
|
||||
from crispy_forms.layout import Layout, Submit, Div
|
||||
from crispy_bootstrap5.bootstrap5 import FloatingField
|
||||
|
||||
from .models import Ticket
|
||||
|
||||
|
||||
class TicketForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Ticket
|
||||
fields = ['number', 'attempts', 'platform', 'note']
|
||||
widgets = {
|
||||
'platform': forms.RadioSelect()
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TicketForm, self).__init__(*args, **kwargs)
|
||||
self.helper = FormHelper(self)
|
||||
# self.helper.attrs = {"novalidate": ''}
|
||||
|
||||
self.helper.layout = Layout(
|
||||
Div(
|
||||
FloatingField('number', 'attempts'),
|
||||
'platform',
|
||||
css_class='col-lg-2'
|
||||
),
|
||||
Div('note', css_class='col-lg-6'),
|
||||
Submit('submit', 'Save', css_class='btn btn-primary'),
|
||||
)
|
59
logs_collector/apps/collector/migrations/0001_initial.py
Normal file
59
logs_collector/apps/collector/migrations/0001_initial.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# Generated by Django 4.2 on 2023-08-14 09:07
|
||||
|
||||
import apps.collector.utils
|
||||
from django.conf import settings
|
||||
import django.core.files.storage
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import pathlib
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Platform',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=20, unique=True)),
|
||||
('pretty_name', models.CharField(max_length=20)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Ticket',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('number', models.IntegerField(db_index=True, unique=True)),
|
||||
('resolved', models.BooleanField(default=False)),
|
||||
('note', models.TextField(blank=True)),
|
||||
('token', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
||||
('attempts', models.IntegerField(default=5, validators=[django.core.validators.MaxValueValidator(10), django.core.validators.MinValueValidator(0)])),
|
||||
('time_create', models.DateTimeField(auto_now_add=True)),
|
||||
('time_update', models.DateTimeField(auto_now=True)),
|
||||
('platform', models.ForeignKey(db_column='platform_name', on_delete=django.db.models.deletion.CASCADE, to='collector.platform', to_field='name')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-time_create'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Archive',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('file', models.FileField(blank=True, null=True, storage=django.core.files.storage.FileSystemStorage(base_url='/archives/', location=pathlib.PurePosixPath('/home/stepan/Documents/Dev/ISPsystem/logs-collector/logs_collector/archives')), upload_to=apps.collector.utils.logs_dir_path)),
|
||||
('md5', models.CharField(editable=False, max_length=1024)),
|
||||
('time_create', models.DateTimeField(auto_now_add=True)),
|
||||
('time_update', models.DateTimeField(auto_now=True)),
|
||||
('ticket', models.ForeignKey(db_column='ticket_number', on_delete=django.db.models.deletion.CASCADE, to='collector.ticket', to_field='number')),
|
||||
],
|
||||
),
|
||||
]
|
100
logs_collector/apps/collector/models.py
Normal file
100
logs_collector/apps/collector/models.py
Normal file
@@ -0,0 +1,100 @@
|
||||
import uuid
|
||||
import hashlib
|
||||
from functools import partial
|
||||
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.core.files.storage import FileSystemStorage
|
||||
from django.urls import reverse
|
||||
|
||||
from .utils import logs_dir_path
|
||||
|
||||
|
||||
# Create a custom storage location, using a value from your settings file
|
||||
sensitive_upload_storage = FileSystemStorage(
|
||||
location=settings.MEDIA_ROOT_FOR_SENSITIVE_FILES,
|
||||
base_url=settings.MEDIA_URL_FOR_SENSITIVE_FILES
|
||||
)
|
||||
# ... and a file field that will use the custom storage
|
||||
AuthenticatedFileField = partial(
|
||||
models.FileField,
|
||||
storage=sensitive_upload_storage
|
||||
)
|
||||
|
||||
|
||||
class Archive(models.Model):
|
||||
file = AuthenticatedFileField(
|
||||
upload_to=logs_dir_path,
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
md5 = models.CharField(max_length=1024, editable=False)
|
||||
time_create = models.DateTimeField(auto_now_add=True)
|
||||
time_update = models.DateTimeField(auto_now=True)
|
||||
ticket = models.ForeignKey(
|
||||
'Ticket',
|
||||
to_field='number',
|
||||
db_column='ticket_number',
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# calculate md5 hash sum and write md5 field to db
|
||||
with self.file.open('rb') as f:
|
||||
md5 = hashlib.md5()
|
||||
for byte_block in iter(lambda: f.read(4096), b""):
|
||||
md5.update(byte_block)
|
||||
self.md5 = md5.hexdigest()
|
||||
# Call the "real" save() method
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('collector:download', kwargs={'path': self.file})
|
||||
|
||||
def __str__(self):
|
||||
return str(self.file)
|
||||
|
||||
|
||||
class Platform(models.Model):
|
||||
name = models.CharField(max_length=20, unique=True)
|
||||
pretty_name = models.CharField(max_length=20)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('collector:platform', kwargs={'platform': self.name})
|
||||
|
||||
def __str__(self):
|
||||
return self.pretty_name
|
||||
|
||||
|
||||
class Ticket(models.Model):
|
||||
number = models.IntegerField(unique=True, db_index=True)
|
||||
resolved = models.BooleanField(default=False)
|
||||
note = models.TextField(blank=True)
|
||||
token = models.UUIDField(unique=True, default=uuid.uuid4, editable=False)
|
||||
attempts = models.IntegerField(default=5, validators=[
|
||||
MaxValueValidator(10),
|
||||
MinValueValidator(0)
|
||||
])
|
||||
time_create = models.DateTimeField(auto_now_add=True)
|
||||
time_update = models.DateTimeField(auto_now=True)
|
||||
platform = models.ForeignKey(
|
||||
'Platform',
|
||||
to_field='name',
|
||||
db_column='platform_name',
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-time_create']
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
'collector:ticket',
|
||||
kwargs={'platform': self.platform.name, 'ticket': self.number}
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.number)
|
6
logs_collector/apps/collector/static/collector/css/bootstrap.min.css
vendored
Normal file
6
logs_collector/apps/collector/static/collector/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
After Width: | Height: | Size: 7.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
After Width: | Height: | Size: 6.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 495 B |
Binary file not shown.
After Width: | Height: | Size: 930 B |
BIN
logs_collector/apps/collector/static/collector/img/favicon.ico
Normal file
BIN
logs_collector/apps/collector/static/collector/img/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1 @@
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
7
logs_collector/apps/collector/static/collector/js/bootstrap.bundle.min.js
vendored
Normal file
7
logs_collector/apps/collector/static/collector/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,90 @@
|
||||
/*!
|
||||
* Color mode toggler for Bootstrap's docs (https://getbootstrap.com/)
|
||||
* Copyright 2011-2023 The Bootstrap Authors
|
||||
* Licensed under the Creative Commons Attribution 3.0 Unported License.
|
||||
*/
|
||||
|
||||
;(() => {
|
||||
"use strict"
|
||||
|
||||
const getStoredTheme = () => localStorage.getItem("theme")
|
||||
const setStoredTheme = (theme) => localStorage.setItem("theme", theme)
|
||||
|
||||
const getPreferredTheme = () => {
|
||||
const storedTheme = getStoredTheme()
|
||||
if (storedTheme) {
|
||||
return storedTheme
|
||||
}
|
||||
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light"
|
||||
}
|
||||
|
||||
const setTheme = (theme) => {
|
||||
if (
|
||||
theme === "auto" &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
) {
|
||||
document.documentElement.setAttribute("data-bs-theme", "dark")
|
||||
} else {
|
||||
document.documentElement.setAttribute("data-bs-theme", theme)
|
||||
}
|
||||
}
|
||||
|
||||
setTheme(getPreferredTheme())
|
||||
|
||||
const showActiveTheme = (theme, focus = false) => {
|
||||
const themeSwitcher = document.querySelector("#bd-theme")
|
||||
|
||||
if (!themeSwitcher) {
|
||||
return
|
||||
}
|
||||
|
||||
const themeSwitcherText = document.querySelector("#bd-theme-text")
|
||||
const activeThemeIcon = document.querySelector(".theme-icon-active use")
|
||||
const btnToActive = document.querySelector(
|
||||
`[data-bs-theme-value="${theme}"]`
|
||||
)
|
||||
const svgOfActiveBtn = btnToActive
|
||||
.querySelector("svg use")
|
||||
.getAttribute("href")
|
||||
|
||||
document.querySelectorAll("[data-bs-theme-value]").forEach((element) => {
|
||||
element.classList.remove("active")
|
||||
element.setAttribute("aria-pressed", "false")
|
||||
})
|
||||
|
||||
btnToActive.classList.add("active")
|
||||
btnToActive.setAttribute("aria-pressed", "true")
|
||||
activeThemeIcon.setAttribute("href", svgOfActiveBtn)
|
||||
const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`
|
||||
themeSwitcher.setAttribute("aria-label", themeSwitcherLabel)
|
||||
|
||||
if (focus) {
|
||||
themeSwitcher.focus()
|
||||
}
|
||||
}
|
||||
|
||||
window
|
||||
.matchMedia("(prefers-color-scheme: dark)")
|
||||
.addEventListener("change", () => {
|
||||
const storedTheme = getStoredTheme()
|
||||
if (storedTheme !== "light" && storedTheme !== "dark") {
|
||||
setTheme(getPreferredTheme())
|
||||
}
|
||||
})
|
||||
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
showActiveTheme(getPreferredTheme())
|
||||
|
||||
document.querySelectorAll("[data-bs-theme-value]").forEach((toggle) => {
|
||||
toggle.addEventListener("click", () => {
|
||||
const theme = toggle.getAttribute("data-bs-theme-value")
|
||||
setStoredTheme(theme)
|
||||
setTheme(theme)
|
||||
showActiveTheme(theme, true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})()
|
@@ -0,0 +1,6 @@
|
||||
const tooltipTriggerList = document.querySelectorAll(
|
||||
'[data-bs-toggle="tooltip"]'
|
||||
)
|
||||
const tooltipList = [...tooltipTriggerList].map(
|
||||
(tooltipTriggerEl) => new bootstrap.Tooltip(tooltipTriggerEl)
|
||||
)
|
@@ -0,0 +1,108 @@
|
||||
$(function () {
|
||||
console.log("JQ is ready to work");
|
||||
|
||||
// CSRF token:
|
||||
const CSRF = $("input[name=csrfmiddlewaretoken]").val()
|
||||
// -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
|
||||
|
||||
// delete one attachment
|
||||
// -- -- -- -- -- -- --
|
||||
$(".btn-archive-eraser").click(function (e) {
|
||||
e.preventDefault();
|
||||
const archiveListElement = $(this).attr("data-jq-archive-target");
|
||||
const delUrl = $(this).attr("href");
|
||||
$.ajax({
|
||||
type: "DELETE",
|
||||
url: delUrl,
|
||||
headers: {
|
||||
"X-CSRFToken":CSRF,
|
||||
"Content-Type":"application/json"
|
||||
},
|
||||
// beforeSend: function(xhr) {
|
||||
// xhr.setRequestHeader("X-CSRFToken", csrf);
|
||||
// },
|
||||
success: function (data, textStatus, jqXHR) {
|
||||
console.log(jqXHR.status);
|
||||
$(archiveListElement).hide(1500);
|
||||
},
|
||||
error: function (data, textStatus, jqXHR) {
|
||||
console.log(jqXHR.status);
|
||||
}
|
||||
});
|
||||
});
|
||||
// change ticket state
|
||||
// -- -- -- -- -- -- --
|
||||
$("input[name=ticket-state]").click(function () {
|
||||
console.log('Press');
|
||||
let resolved = false;
|
||||
let ticketStateUrl = $(this).attr("ticket-state-url")
|
||||
if ($(this).attr("ticket-state-switch") === "1") {
|
||||
$(this).attr("ticket-state-switch", "0"); // disable
|
||||
} else {
|
||||
resolved = true;
|
||||
$(this).attr("ticket-state-switch", "1"); // enable
|
||||
}
|
||||
$.ajax({
|
||||
type: "PATCH",
|
||||
url: ticketStateUrl,
|
||||
headers: {
|
||||
"X-CSRFToken":CSRF,
|
||||
"Content-Type":"application/json"
|
||||
},
|
||||
contentType: "application/json; charset=utf-8",
|
||||
dataType: "json",
|
||||
data: JSON.stringify({
|
||||
resolved: resolved,
|
||||
}),
|
||||
success: function (data, textStatus, jqXHR) {
|
||||
console.log(jqXHR.status)
|
||||
},
|
||||
error: function (data, textStatus, jqXHR) {
|
||||
console.log(data)
|
||||
console.log(jqXHR.status)
|
||||
}
|
||||
});
|
||||
});
|
||||
// delete ticket with attachments:
|
||||
// -- -- -- -- -- -- -- -- -- -- --
|
||||
$(".btn-ticket-del").click(function (e) {
|
||||
e.preventDefault();
|
||||
const delUrl = $(this).attr("href")
|
||||
const redirectUrl = $(this).attr("data-jq-ticket-del-redirect")
|
||||
const elementTarget = $(this).attr("data-jq-ticket-del-target")
|
||||
const delDiv = $(elementTarget)
|
||||
$.ajax({
|
||||
type: "DELETE",
|
||||
url: delUrl,
|
||||
headers: {
|
||||
'X-CSRFToken':CSRF,
|
||||
'Content-Type':'application/json'
|
||||
},
|
||||
success: function (data, textStatus, jqXHR) {
|
||||
console.log(jqXHR.status);
|
||||
if (delDiv.length) {
|
||||
delDiv.hide(1500);
|
||||
} else {
|
||||
window.location.href = redirectUrl;
|
||||
}
|
||||
},
|
||||
error: function (data, textStatus, jqXHR) {
|
||||
console.log(jqXHR.status);
|
||||
}
|
||||
});
|
||||
});
|
||||
// copy token to clipboard:
|
||||
// -- -- -- -- -- -- -- --
|
||||
$(".token-clipboard").click(function (e) {
|
||||
e.preventDefault();
|
||||
const btn = $(this)
|
||||
const tokenInput = btn.siblings("input[name=ticket-token]").val();
|
||||
const icon = btn.children(":first").get(0)
|
||||
navigator.clipboard.writeText(tokenInput);
|
||||
btn.html('<i class="bi bi-check-lg"></i>')
|
||||
// Revert button label after 500 milliseconds
|
||||
setTimeout(function(){
|
||||
btn.html(icon);
|
||||
}, 500)
|
||||
});
|
||||
});
|
2
logs_collector/apps/collector/static/collector/js/jquery-3.7.0.min.js
vendored
Normal file
2
logs_collector/apps/collector/static/collector/js/jquery-3.7.0.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
30
logs_collector/apps/collector/templates/collector/base.html
Normal file
30
logs_collector/apps/collector/templates/collector/base.html
Normal file
@@ -0,0 +1,30 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block collector_head %}
|
||||
<title>{% block title %}{% endblock title %}</title>
|
||||
{% endblock collector_head %}
|
||||
|
||||
{% block collector_content %}
|
||||
<header>
|
||||
<section>
|
||||
{% include 'includes/navigation.html' %}
|
||||
</section>
|
||||
</header>
|
||||
<main>
|
||||
<section>
|
||||
{% block main %}{% endblock main %}
|
||||
</section>
|
||||
</main>
|
||||
<footer>
|
||||
<section>
|
||||
{% block footer %}{% endblock footer %}
|
||||
</section>
|
||||
</footer>
|
||||
{% endblock collector_content %}
|
||||
|
||||
{% block collector_scripts %}
|
||||
<script src="{% static 'collector/js/jquery-3.7.0.min.js' %}"></script>
|
||||
{% block bs %}{% endblock bs %}
|
||||
{% block jquery %}{% endblock jquery %}
|
||||
{% endblock collector_scripts %}
|
@@ -0,0 +1,43 @@
|
||||
<div
|
||||
class="modal fade"
|
||||
id="modal-archive-del-{{ archive.id }}"
|
||||
tabindex="-1"
|
||||
aria-labelledby="LabelArchive-{{ archive.id }}"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5
|
||||
class="modal-title"
|
||||
id="LabelArchive-{{ archive.id }}">Delete this file?
|
||||
</h5>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close">
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p style="word-wrap: break-word">{{ archive.file }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
data-bs-dismiss="modal"
|
||||
>Cancel
|
||||
</button>
|
||||
<a
|
||||
href="{% url 'collector_api:archive-detail' archive.id %}"
|
||||
type="button"
|
||||
class="btn btn-danger btn-archive-eraser"
|
||||
data-bs-dismiss="modal"
|
||||
data-jq-archive-target="#li-archive-{{ archive.id }}"
|
||||
>Delete
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,40 @@
|
||||
<div
|
||||
class="modal fade"
|
||||
id="modal-ticket-del-{{ ticket.number }}"
|
||||
tabindex="-1"
|
||||
aria-labelledby="LabelTicket"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="LabelTicket">
|
||||
Delete ticket #{{ ticket.number }} ?
|
||||
</h5>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"
|
||||
></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Deleting a ticket will also permanently delete all files associated with it.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
Cancel
|
||||
</button>
|
||||
<a
|
||||
type="button"
|
||||
href="{% url 'collector_api:ticket-detail' ticket.number %}"
|
||||
class="btn btn-danger btn-ticket-del"
|
||||
data-bs-dismiss="modal"
|
||||
data-jq-ticket-del-target="#div-ticket-{{ ticket.number }}"
|
||||
data-jq-ticket-del-redirect="{% url 'collector:tickets' %}"
|
||||
>Delete</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,41 @@
|
||||
{% if page_obj.has_other_pages %}
|
||||
<div class="row">
|
||||
<nav class="d-flex justify-content-center mt-3" aria-label="...">
|
||||
<ul class="pagination">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a
|
||||
class="page-link"
|
||||
href="?page={{ page_obj.previous_page_number }}"
|
||||
>Back
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled"><a class="page-link">Back</a></li>
|
||||
{% endif %}
|
||||
{% for page in paginator.page_range %}
|
||||
{% if page_obj.number == page %}
|
||||
<li class="page-item active" aria-current="page">
|
||||
<button class="page-link">{{ page }}</button>
|
||||
</li>
|
||||
{% elif page >= page_obj.number|add:-2 and page <= page_obj.number|add:2%}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page }}">{{ page }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a
|
||||
class="page-link"
|
||||
href="?page={{ page_obj.next_page_number }}"
|
||||
>Next
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled"><a class="page-link">Next</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
{% endif %}
|
@@ -0,0 +1,38 @@
|
||||
{% load collector_extras %}
|
||||
<li
|
||||
id="li-archive-{{ archive.id }}"
|
||||
class="list-group-item list-group-item-action">
|
||||
<smal>
|
||||
<b>File:</b>
|
||||
<span style="word-wrap: break-word">{{ archive.file.name|clean_filename }}</span>
|
||||
</small>
|
||||
<small>
|
||||
<br>
|
||||
<b>MD5:</b>
|
||||
<span style="word-wrap: break-word">{{ archive.md5 }}</span>
|
||||
</small>
|
||||
<small>
|
||||
<br>
|
||||
<b>Uploaded:</b>
|
||||
<span style="word-wrap: break-word">{{ archive.time_update|date:"D d.m.y H:i" }}</span>
|
||||
</small>
|
||||
<br>
|
||||
<small>
|
||||
<b>Size:</b>
|
||||
<span style="word-wrap: break-word">{{ archive.file.size|sizify }}</span>
|
||||
</small>
|
||||
<div class="row">
|
||||
<div class="d-flex justify-content-sm-start justify-content-between" >
|
||||
<a
|
||||
class="btn btn-outline-success btn-sm mt-2"
|
||||
href="{{ archive.get_absolute_url }}"
|
||||
><i class="bi bi-download"></i> GET</a>
|
||||
<button
|
||||
button type="button"
|
||||
class="btn btn-outline-danger btn-sm ms-2 mt-2"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#modal-archive-del-{{ archive.id }}"
|
||||
><i class="bi bi-trash"></i> DEL</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
@@ -0,0 +1,49 @@
|
||||
<div class="d-sm-flex w-100 justify-content-between mb-2">
|
||||
<h4 class="card-title mb-1">Ticket: {{ ticket.number }}</h4>
|
||||
<small><i class="bi bi-clock-history"></i> {{ ticket.time_create|date:"D d.m.y H:i" }}</small>
|
||||
</div>
|
||||
<div class="form-check form-switch form-check-reverse d-flex w-100 justify-content-left">
|
||||
<label class="form-check-label" for="ticket-state">Resolved:</label>
|
||||
<input
|
||||
class="form-check-input ms-2 mb-2"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
name="ticket-state"
|
||||
ticket-state-url="{% url 'collector_api:ticket-detail' ticket.number %}"
|
||||
{% if ticket.resolved %} ticket-state-switch="1" {% endif %}
|
||||
{% if ticket.resolved %} checked {% endif %}>
|
||||
</div>
|
||||
<div class="col-xl-6 mb-2">
|
||||
<h6 class="card-title mb-1">Platform: {{ ticket.platform.pretty_name }}</h6>
|
||||
<h6 class="card-title mb-3">Owner: {{ ticket.user.username }}</h6>
|
||||
<!-- Token -->
|
||||
<div class="input-group input-group mb-3">
|
||||
<span class="input-group-text" id="inputGroup-sizing-sm"><i class="bi bi-key"></i></span>
|
||||
<!--Token attempts-->
|
||||
<span class="input-group-text" id="inputGroup-sizing-sm">
|
||||
<span
|
||||
class="badge
|
||||
{% if ticket.attempts <= 0 %}
|
||||
bg-danger
|
||||
{% elif ticket.attempts < 5 %}
|
||||
text-dark bg-warning
|
||||
{% else %}
|
||||
bg-primary
|
||||
{% endif %} rounded-pill">{{ ticket.attempts }}
|
||||
</span>
|
||||
</span>
|
||||
<input
|
||||
name="ticket-token"
|
||||
class="form-control"
|
||||
type="text"
|
||||
value="{{ ticket.token }}"
|
||||
aria-label="Disabled input example"
|
||||
aria-describedby="inputGroup-sizing-sm"
|
||||
disabled
|
||||
readonly>
|
||||
<button
|
||||
class="input-group-text token-clipboard"
|
||||
id="inputGroup-sizing-sm"><i class="bi bi-clipboard"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,60 @@
|
||||
{% extends 'collector/base.html' %}
|
||||
{% load static %}
|
||||
{% load collector_extras %}
|
||||
{% block title %} {{ title }} {% endblock title %}
|
||||
{% block main %}
|
||||
<div class="container mt-3">
|
||||
<div class="row">
|
||||
{% csrf_token %}
|
||||
<div class="card">
|
||||
<div class="card-body" aria-current="true">
|
||||
{% include 'collector/includes/ticket_info.html' %}
|
||||
<div class="col-xl-6 mt-1 mb-2">
|
||||
{% if ticket.note %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Note:
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="card-text">
|
||||
{{ ticket.note | markdown | safe }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- Logs -->
|
||||
{% if ticket.archive_set.all %}
|
||||
<ul class="list-group col-xl-6 mb-2 mt-2">
|
||||
{% for archive in ticket.archive_set.all %}
|
||||
{% include 'collector/includes/ticket_archives.html' %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<!-- Card buttons -->
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<a
|
||||
href="{% url 'collector:update' ticket.platform.name ticket.number %}"
|
||||
class="btn btn-outline-warning mb-1 mt-1"
|
||||
><i class="bi bi-pencil-square"></i> Edit</a>
|
||||
<button
|
||||
class="btn btn-outline-danger mb-1 mt-1"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#modal-ticket-del-{{ ticket.number }}"
|
||||
><i class="bi bi-trash"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Modal Archive -->
|
||||
{% for archive in ticket.archive_set.all %}
|
||||
{% include 'collector/includes/modal_archive.html' %}
|
||||
{% endfor %}
|
||||
<!-- Modal Ticket -->
|
||||
{% include 'collector/includes/modal_ticket.html' %}
|
||||
</div>
|
||||
{% endblock main %}
|
||||
{% block jquery %}
|
||||
<script src="{% static 'collector/js/jq.ticket.detail.js' %}"></script>
|
||||
{% endblock jquery %}
|
@@ -0,0 +1,16 @@
|
||||
{% extends 'collector/base.html' %}
|
||||
{% load static %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% block title %} {{ title }} {% endblock title %}
|
||||
{% block main %}
|
||||
<div class="container mt-3">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Ticket:</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% crispy form %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock main %}
|
@@ -0,0 +1,22 @@
|
||||
{% extends 'collector/base.html' %}
|
||||
{% load static %}
|
||||
{% block title %} {{ title }} {% endblock title %}
|
||||
{% block content %}
|
||||
<div class="container mt-3">
|
||||
<div class="row">
|
||||
<form method="post" action="{% url 'collector:delete' ticket.number %}">
|
||||
{% csrf_token %}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
data-bs-dismiss="modal"
|
||||
>Cancel</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-danger btn-archive-eraser"
|
||||
data-bs-dismiss="modal"
|
||||
>Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@@ -0,0 +1,94 @@
|
||||
{% extends 'collector/base.html' %}
|
||||
{% load static %}
|
||||
{% load collector_extras %}
|
||||
{% block title %} {{ title }} {% endblock title %}
|
||||
{% block main %}
|
||||
<div class="container mt-3">
|
||||
{% csrf_token %}
|
||||
<!-- Ticket -->
|
||||
{% for ticket in tickets %}
|
||||
<ul id="div-ticket-{{ ticket.number }}" class="list-group mb-2">
|
||||
<li class="list-group-item list-group-item-action disable" aria-current="true">
|
||||
{% include 'collector/includes/ticket_info.html' %}
|
||||
<div class="col-xl-6 mt-1 mb-2">
|
||||
<div class="accordion" id="#archive_{{ ticket.number }}">
|
||||
{% if ticket.note %}
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button
|
||||
class="accordion-button collapsed"
|
||||
type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#collapse_{{ ticket.number}}_note"
|
||||
aria-expanded="false"
|
||||
aria-controls="collapse_{{ ticket.number }}"
|
||||
><i class="bi bi-journal-text me-2"></i> Note</button>
|
||||
</h2>
|
||||
<div id="collapse_{{ ticket.number }}_note"
|
||||
class="accordion-collapse collapse"
|
||||
data-bs-parent="#archive_{{ ticket.number }}_note"
|
||||
>
|
||||
<div class="accordion-body">
|
||||
<p class="mb-1">{{ ticket.note |markdown |safe }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if ticket.archive_set.all %}
|
||||
<!-- Logs -->
|
||||
<div class="accordion-item">
|
||||
<h3 class="accordion-header">
|
||||
<button
|
||||
class="accordion-button collapsed"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#collapse_{{ ticket.number }}"
|
||||
aria-expanded="true" aria-controls="collapse_{{ ticket.number }}"
|
||||
><i class="bi bi-file-zip me-2"></i> Logs</button>
|
||||
</h3>
|
||||
<div
|
||||
id="collapse_{{ ticket.number }}"
|
||||
class="accordion-collapse collapse"
|
||||
data-bs-parent="#archive_{{ ticket.number }}"
|
||||
>
|
||||
<div class="accordion-body">
|
||||
<ul class="list-group col mb-2 mt-2">
|
||||
{% for archive in ticket.archive_set.all %}
|
||||
{% include 'collector/includes/ticket_archives.html' %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<a
|
||||
href="{{ ticket.get_absolute_url }}"
|
||||
class="btn btn-outline-primary mb-1 mt-1"
|
||||
><i class="bi bi-arrow-return-right"></i> Open</a>
|
||||
<button
|
||||
class="btn btn-outline-danger mb-1 mt-1"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#modal-ticket-del-{{ ticket.number }}"
|
||||
><i class="bi bi-trash"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- Modal ticket -->
|
||||
{% include 'collector/includes/modal_ticket.html' %}
|
||||
<!-- Modal archive -->
|
||||
{% for archive in ticket.archive_set.all %}
|
||||
{% include 'collector/includes/modal_archive.html' %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% 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>
|
||||
{% endblock jquery %}
|
@@ -0,0 +1,57 @@
|
||||
import markdown as md
|
||||
from django import template
|
||||
from django.template.defaultfilters import stringfilter
|
||||
|
||||
from apps.collector.models import Platform
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def get_platforms():
|
||||
return Platform.objects.all()
|
||||
|
||||
|
||||
@register.filter(name='sizify')
|
||||
def sizify(value: int) -> str:
|
||||
"""Simple kb/mb/gb size snippet for templates:
|
||||
|
||||
{{ Archive.file.size|sizify }}
|
||||
|
||||
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}'
|
||||
|
||||
|
||||
@register.filter(name='clean_filename')
|
||||
def clean_filename(filename: str) -> str:
|
||||
"""delete prefix ticket number folder for template
|
||||
|
||||
Args:
|
||||
filename (str): filename from Filefield
|
||||
|
||||
Returns:
|
||||
str: only filename
|
||||
"""
|
||||
return filename.rpartition('/')[-1]
|
||||
|
||||
|
||||
@register.filter(name='markdown')
|
||||
@stringfilter
|
||||
def markdown(value):
|
||||
return md.markdown(value, extensions=['markdown.extensions.fenced_code'])
|
3
logs_collector/apps/collector/tests.py
Normal file
3
logs_collector/apps/collector/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
50
logs_collector/apps/collector/urls.py
Normal file
50
logs_collector/apps/collector/urls.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = 'collector'
|
||||
|
||||
# █░█░█ █▀▀ █▄▄
|
||||
# ▀▄▀▄▀ ██▄ █▄█
|
||||
# -- -- -- -- --
|
||||
|
||||
urlpatterns = [
|
||||
# CREATE:
|
||||
path(
|
||||
'tickets/create/',
|
||||
views.CreateTicket.as_view(),
|
||||
name='create'
|
||||
),
|
||||
# READ:
|
||||
path(
|
||||
'',
|
||||
views.ListAllTickets.as_view(),
|
||||
name='index'
|
||||
),
|
||||
path(
|
||||
'tickets/',
|
||||
views.ListAllTickets.as_view(),
|
||||
name='tickets'
|
||||
),
|
||||
path(
|
||||
'tickets/show/<slug:platform>/',
|
||||
views.ListPlatformTickets.as_view(),
|
||||
name='platform'
|
||||
),
|
||||
path(
|
||||
'tickets/show/<slug:platform>/<int:ticket>/',
|
||||
views.DetailTicket.as_view(),
|
||||
name='ticket'
|
||||
),
|
||||
path(
|
||||
'archives/<path:path>',
|
||||
views.ArchiveHandlerView.as_view(),
|
||||
name="download"
|
||||
),
|
||||
# UPDATE:
|
||||
path(
|
||||
'tickets/update/<slug:platform>/<int:ticket>/',
|
||||
views.UpdateTicket.as_view(),
|
||||
name='update'
|
||||
),
|
||||
]
|
43
logs_collector/apps/collector/utils.py
Normal file
43
logs_collector/apps/collector/utils.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import os
|
||||
|
||||
|
||||
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}'
|
||||
|
||||
|
||||
# deprecated
|
||||
def get_file_size(file_path, unit='bytes'):
|
||||
file_size = os.path.getsize(file_path)
|
||||
exponents_map = {'bytes': 0, 'kb': 1, 'mb': 2, 'gb': 3}
|
||||
if unit not in exponents_map:
|
||||
raise ValueError("Must select from \
|
||||
['bytes', 'kb', 'mb', 'gb']")
|
||||
else:
|
||||
size = file_size / 1024 ** exponents_map[unit]
|
||||
return round(size, 3)
|
||||
|
||||
|
||||
# deprecated
|
||||
def is_ajax(request):
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
return True
|
||||
|
||||
|
||||
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
|
109
logs_collector/apps/collector/views.py
Normal file
109
logs_collector/apps/collector/views.py
Normal file
@@ -0,0 +1,109 @@
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import FileResponse
|
||||
from django.views import generic
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
from django.db.models import Q
|
||||
|
||||
from two_factor.views import OTPRequiredMixin
|
||||
|
||||
from .forms import TicketForm
|
||||
from .models import Archive, Ticket
|
||||
from .utils import PageTitleViewMixin
|
||||
|
||||
|
||||
class ArchiveHandlerView(
|
||||
OTPRequiredMixin,
|
||||
LoginRequiredMixin,
|
||||
SingleObjectMixin,
|
||||
generic.View):
|
||||
model = Archive
|
||||
slug_field = 'file'
|
||||
slug_url_kwarg = 'path'
|
||||
|
||||
def get(self, request, path):
|
||||
self.object = self.get_object()
|
||||
return FileResponse(self.object.file)
|
||||
|
||||
|
||||
class CreateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.CreateView):
|
||||
model = Ticket
|
||||
form_class = TicketForm
|
||||
template_name = 'collector/ticket_create.html'
|
||||
|
||||
def get_title(self):
|
||||
return f'{self.title} - create'
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.user = self.request.user
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class UpdateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.UpdateView):
|
||||
model = Ticket
|
||||
form_class = TicketForm
|
||||
template_name = 'collector/ticket_create.html'
|
||||
slug_field = 'number'
|
||||
slug_url_kwarg = 'ticket'
|
||||
|
||||
def get_title(self, **kwargs):
|
||||
return f'{self.title} - {self.kwargs.get("ticket", "update")}'
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.user = self.request.user
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class ListAllTickets(LoginRequiredMixin, PageTitleViewMixin, generic.ListView):
|
||||
model = Ticket
|
||||
template_name = 'collector/tickets.html'
|
||||
context_object_name = 'tickets'
|
||||
paginate_by = 5
|
||||
title = 'Collector - tickets'
|
||||
|
||||
def get_queryset(self):
|
||||
search_query = self.request.GET.get('search', '')
|
||||
if search_query:
|
||||
query_list = []
|
||||
try:
|
||||
for item in search_query.split(','):
|
||||
query_list.append(int(item))
|
||||
except ValueError:
|
||||
return super().get_queryset()
|
||||
queryset = self.model.objects.filter(
|
||||
Q(number__in=query_list) | Q(number__icontains=query_list[0])
|
||||
)
|
||||
self.paginate_by = 100 # ? fake disable pagination)
|
||||
return queryset
|
||||
|
||||
return super().get_queryset()
|
||||
|
||||
|
||||
class ListPlatformTickets(
|
||||
LoginRequiredMixin,
|
||||
PageTitleViewMixin,
|
||||
generic.ListView
|
||||
):
|
||||
model = Ticket
|
||||
template_name = 'collector/tickets.html'
|
||||
context_object_name = 'tickets'
|
||||
# allow_empty = False
|
||||
paginate_by = 5
|
||||
|
||||
def get_title(self, **kwargs):
|
||||
return f'{self.title} - {self.kwargs.get("platform", "tickets")}'
|
||||
|
||||
def get_queryset(self):
|
||||
return Ticket.objects.filter(
|
||||
platform__name=self.kwargs.get('platform')
|
||||
)
|
||||
|
||||
|
||||
class DetailTicket(LoginRequiredMixin, PageTitleViewMixin, generic.DetailView):
|
||||
model = Ticket
|
||||
template_name = 'collector/ticket.html'
|
||||
context_object_name = 'ticket'
|
||||
slug_field = 'number'
|
||||
slug_url_kwarg = 'ticket'
|
||||
|
||||
def get_title(self, **kwargs):
|
||||
return f'{self.title} - {self.kwargs.get("ticket", "show")}'
|
Reference in New Issue
Block a user