Refactoring: new multi app structure

This commit is contained in:
2023-08-15 03:13:07 +09:00
parent 30b3efa5fc
commit e45d1af857
94 changed files with 634 additions and 1548 deletions

View File

@@ -1,25 +0,0 @@
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)

View File

@@ -1,6 +0,0 @@
from django.apps import AppConfig
class CollectorConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'collector'

View File

@@ -1,39 +0,0 @@
from django_filters.rest_framework import (
CharFilter,
FilterSet,
NumberFilter,
)
from django_filters import widgets
from .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']
}

View File

@@ -1,31 +0,0 @@
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'),
)

View File

@@ -1,54 +0,0 @@
# Generated by Django 4.2 on 2023-07-28 14:40
import collector.utils
from django.conf import settings
import django.core.files.storage
from django.db import migrations, models
import django.db.models.deletion
import pathlib
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)),
('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()),
('resolved', models.BooleanField(default=False)),
('note', models.TextField(blank=True)),
('time_create', models.DateTimeField(auto_now_add=True)),
('time_update', models.DateTimeField(auto_now=True)),
('platform', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='collector.platform')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
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=collector.utils.logs_dir_path)),
('size', models.CharField(blank=True, max_length=50)),
('sha1', 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(on_delete=django.db.models.deletion.CASCADE, to='collector.ticket')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@@ -1,27 +0,0 @@
# Generated by Django 4.2 on 2023-08-05 11:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('collector', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='ticket',
options={'ordering': ['-time_create']},
),
migrations.AlterField(
model_name='archive',
name='size',
field=models.CharField(blank=True, editable=False, max_length=50),
),
migrations.AlterField(
model_name='ticket',
name='number',
field=models.IntegerField(db_index=True, unique=True),
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 4.2 on 2023-08-08 05:52
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('collector', '0002_alter_ticket_options_alter_archive_size_and_more'),
]
operations = [
migrations.AlterField(
model_name='archive',
name='ticket',
field=models.ForeignKey(db_column='ticket_number', on_delete=django.db.models.deletion.CASCADE, to='collector.ticket', to_field='number'),
),
]

View File

@@ -1,22 +0,0 @@
# Generated by Django 4.2 on 2023-08-08 09:17
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('collector', '0003_alter_archive_ticket'),
]
operations = [
migrations.RenameField(
model_name='archive',
old_name='sha1',
new_name='md5',
),
migrations.RemoveField(
model_name='archive',
name='size',
),
]

View File

@@ -1,33 +0,0 @@
# Generated by Django 4.2 on 2023-08-08 11:16
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('collector', '0004_rename_sha1_archive_md5_remove_archive_size'),
]
operations = [
migrations.CreateModel(
name='Token',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('expires', models.IntegerField(default=5, validators=[django.core.validators.MaxValueValidator(100), django.core.validators.MinValueValidator(1)])),
('blocked', models.BooleanField(default=False)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.AddField(
model_name='archive',
name='token',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='collector.token'),
preserve_default=False,
),
]

View File

@@ -1,47 +0,0 @@
# Generated by Django 4.2 on 2023-08-08 16:52
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('collector', '0005_token_archive_token'),
]
operations = [
migrations.RemoveField(
model_name='archive',
name='token',
),
migrations.RemoveField(
model_name='archive',
name='user',
),
migrations.AddField(
model_name='ticket',
name='token',
field=models.UUIDField(default=uuid.uuid4, editable=False),
),
migrations.AddField(
model_name='ticket',
name='upload',
field=models.IntegerField(default=5, validators=[django.core.validators.MaxValueValidator(10), django.core.validators.MinValueValidator(1)]),
),
migrations.AlterField(
model_name='platform',
name='name',
field=models.CharField(max_length=20, unique=True),
),
migrations.AlterField(
model_name='ticket',
name='platform',
field=models.ForeignKey(db_column='platform_name', on_delete=django.db.models.deletion.CASCADE, to='collector.platform', to_field='name'),
),
migrations.DeleteModel(
name='Token',
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 4.2 on 2023-08-08 17:08
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('collector', '0006_remove_archive_token_remove_archive_user_and_more'),
]
operations = [
migrations.RenameField(
model_name='ticket',
old_name='upload',
new_name='attempts',
),
]

View File

@@ -1,25 +0,0 @@
# Generated by Django 4.2 on 2023-08-10 03:24
import django.core.validators
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('collector', '0007_rename_upload_ticket_attempts'),
]
operations = [
migrations.AlterField(
model_name='ticket',
name='attempts',
field=models.IntegerField(default=5, validators=[django.core.validators.MaxValueValidator(10), django.core.validators.MinValueValidator(0)]),
),
migrations.AlterField(
model_name='ticket',
name='token',
field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
),
]

View File

@@ -1,100 +0,0 @@
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)

View File

@@ -1,13 +0,0 @@
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

View File

@@ -1,60 +0,0 @@
from rest_framework import serializers
from drf_spectacular.utils import extend_schema_field
from drf_spectacular.openapi import OpenApiTypes
from .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'
]

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.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 495 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 930 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1 +0,0 @@
{"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"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,90 +0,0 @@
/*!
* 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)
})
})
})
})()

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

@@ -1,108 +0,0 @@
$(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)
});
});

File diff suppressed because one or more lines are too long

View File

@@ -1,30 +0,0 @@
{% load static %}
<!DOCTYPE html>
<html lang="en" data-bs-theme="auto">
<head>
{% include 'collector/includes/metalinks.html' %}
<title>{% block title %}{% endblock title %}</title>
</head>
<body>
<header>
<section>
{% include 'collector/includes/navigation.html' %}
</section>
</header>
<main>
<section>
{% block main %}{% endblock main %}
</section>
</main>
<footer>
<section>
{% block footer %}{% endblock footer %}
</section>
</footer>
<script src="{% static 'collector/js/bootstrap.bundle.min.js' %}"></script>
<script src="{% static 'collector/js/bs.theme.mode.js' %}"></script>
<script src="{% static 'collector/js/jquery-3.7.0.min.js' %}"></script>
{% block bs %}{% endblock bs %}
{% block jquery %}{% endblock jquery %}
</body>
</html>

View File

@@ -1,33 +0,0 @@
{% load static %}
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="stylesheet"
href="{% static 'collector/css/bootstrap.min.css' %}"
rel="stylesheet"
>
<link
rel="apple-touch-icon"
sizes="180x180"
href="{% static 'collector/img/apple-touch-icon.png' %}"
>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="{% static 'collector/img/favicon-32x32.png' %}"
>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="{% static 'collector/img/favicon-16x16.png' %}"
>
<link
rel="manifest"
href="{% static 'collector/img/site.webmanifest' %}"
>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css"
>

View File

@@ -1,43 +0,0 @@
<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: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>

View File

@@ -1,40 +0,0 @@
<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: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>

View File

@@ -1,139 +0,0 @@
{% load collector_extras %}
{% get_platforms as platforms %}
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container">
<a
class="navbar-brand"
href="{% url 'collector:index' %}">Logs Collector <i class="bi bi-file-earmark-zip-fill"></i>
</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Переключатель навигации"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ml-auto mb-2 mb-lg-0 me-md-auto">
<li class="nav-item dropdown">
<button
class="nav-link dropdown-toggle"
role="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>Tickets</button>
<ul class="dropdown-menu">
{% for platform in platforms %}
<li>
<a
class="dropdown-item {% if request.resolver_match.kwargs.platform == platform.name %}active{% endif %}"
href="{{ platform.get_absolute_url }}"
>{{ platform.pretty_name}}
</a>
</li>
{% endfor %}
<li><hr class="dropdown-divider" /></li>
<li>
<a class="dropdown-item" href="{% url 'collector:tickets' %}">
<i class="bi bi-funnel"></i> Reset filter
</a>
</li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link" aria-current="page" href="{% url 'collector:create' %}">
<i class="bi bi-pencil-square"></i> New
</a>
</li>
</ul>
<!-- Search -->
<ul class="navbar-nav flex-row flex-wrap me-md-auto">
<li class="nav-item py-2 col-12 col-lg-auto">
<form class="d-flex" role="search" action="{% url 'collector:tickets' %}">
<input
class="form-control me-2"
type="search"
placeholder="Search"
aria-label="Search"
name="search"
/>
<button
class="btn btn-outline-success"
type="submit">
<i class="bi bi-search"></i>
</button>
</form>
</li>
</ul>
<!-- Right fields -->
<ul class="navbar-nav flex-row flex-wrap ms-md-auto">
<!-- User settings -->
{% if request.user.is_authenticated %}
<li class="nav-item py-2 py-lg-1 col-12 col-lg-auto">
<div class="btn-group">
<button type="button" class="btn btn-outline-secondary">
<i class="bi bi-person-square"></i> {{ request.user }}
</button>
<button
type="button"
class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu dropdown-menu-end">
{% if request.user.is_staff %}
<li>
<a class="dropdown-item" type="button" href="{% url 'admin:index' %}"
><i class="bi bi-shield-shaded"></i> Admin</a>
</li>
{% endif %}
<li>
<a class="dropdown-item" type="button" href="{% url 'swagger-ui' %}"
><i class="bi bi-braces-asterisk"></i> Swagger</a>
</li>
<li><button class="dropdown-item" type="button"><i class="bi bi-gear"></i> Settings</button></li>
<li><hr class="dropdown-divider" /></li>
<li>
<a
href="{% url 'account:logout' %}"
class="dropdown-item"
type="button"><i class="bi bi-door-closed"></i> Logout
</a>
</li>
</ul>
</div>
</li>
{% else %}
<li class="nav-item py-2 py-lg-1 col-12 col-lg-auto">
<div class="btn-group">
<a
type="button"
href="{% url 'two_factor:login' %}"
class="btn btn-outline-secondary"
>
<i class="bi bi-box-arrow-in-right"></i></i> Login
</a>
</div>
</li>
{% endif %}
<!-- Separator -->
<li class="nav-item py-2 py-lg-1 col-12 col-lg-auto">
<div class="vr d-none d-lg-flex h-100 mx-lg-2 text-white"></div>
<hr class="d-lg-none my-2 text-white-50">
</li>
<!-- Theme switcher-->
<li class="nav-item dropdown">
<div class="dropdown bd-mode-toggle">
{% include 'collector/includes/theme_swither.html' %}
</div>
</li>
</ul>
</div>
</div>
</nav>

View File

@@ -1,41 +0,0 @@
{% 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 %}

View File

@@ -1,91 +0,0 @@
<!--Theme switcher icons-->
<svg xmlns="http://www.w3.org/2000/svg" class="d-none">
<symbol id="check2" viewBox="0 0 16 16">
<path
d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"
/>
</symbol>
<symbol id="circle-half" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 0 8 1v14zm0 1A8 8 0 1 1 8 0a8 8 0 0 1 0 16z" />
</symbol>
<symbol id="moon-stars-fill" fill="currentColor" viewBox="0 0 16 16">
<path
d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"
/>
<path
d="M10.794 3.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387a1.734 1.734 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.734 1.734 0 0 0 9.31 6.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387a1.734 1.734 0 0 0 1.097-1.097l.387-1.162zM13.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732l-.774-.258a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L13.863.1z"
/>
</symbol>
<symbol id="sun-fill" fill="currentColor" viewBox="0 0 16 16">
<path
d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"
/>
</symbol>
</svg>
<!--Theme switcher buttons-->
<button
class="btn btn-bd-primary py-2 dropdown-toggle d-flex align-items-center"
id="bd-theme"
type="button"
aria-expanded="false"
data-bs-toggle="dropdown"
aria-label="Toggle theme (auto)"
>
<svg class="bi my-1 theme-icon-active" width="1em" height="1em">
<use href="#circle-half"></use>
</svg>
<span class="visually-hidden" id="bd-theme-text">Toggle theme</span>
</button>
<ul
class="dropdown-menu dropdown-menu-end shadow"
aria-labelledby="bd-theme-text"
>
<li>
<button
type="button"
class="dropdown-item d-flex align-items-center"
data-bs-theme-value="light"
aria-pressed="false"
>
<svg class="bi me-2 opacity-50 theme-icon" width="1em" height="1em">
<use href="#sun-fill"></use>
</svg>
Light
<svg class="bi ms-auto d-none" width="1em" height="1em">
<use href="#check2"></use>
</svg>
</button>
</li>
<li>
<button
type="button"
class="dropdown-item d-flex align-items-center"
data-bs-theme-value="dark"
aria-pressed="false"
>
<svg class="bi me-2 opacity-50 theme-icon" width="1em" height="1em">
<use href="#moon-stars-fill"></use>
</svg>
Dark
<svg class="bi ms-auto d-none" width="1em" height="1em">
<use href="#check2"></use>
</svg>
</button>
</li>
<li>
<button
type="button"
class="dropdown-item d-flex align-items-center active"
data-bs-theme-value="auto"
aria-pressed="true"
>
<svg class="bi me-2 opacity-50 theme-icon" width="1em" height="1em">
<use href="#circle-half"></use>
</svg>
Auto
<svg class="bi ms-auto d-none" width="1em" height="1em">
<use href="#check2"></use>
</svg>
</button>
</li>
</ul>

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -1,38 +0,0 @@
{% 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>

View File

@@ -1,49 +0,0 @@
<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: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>

View File

@@ -1,60 +0,0 @@
{% 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 %}

View File

@@ -1,16 +0,0 @@
{% 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 %}

View File

@@ -1,22 +0,0 @@
{% 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 %}

View File

@@ -1,94 +0,0 @@
{% 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 %}

View File

@@ -1,57 +0,0 @@
import markdown as md
from django import template
from django.template.defaultfilters import stringfilter
from 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'])

View File

@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -1,66 +0,0 @@
from django.urls import path, include
from rest_framework import routers
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'
),
]
# ▄▀█ █▀█ █
# █▀█ █▀▀ █
# -- -- --
router = routers.DefaultRouter()
router.register(r'archives', views.ArchiveViewSet)
router.register(r'platforms', views.PlatformViewSet)
router.register(r'tickets', views.TicketViewSet)
urlpatterns += [
# CRUD:
path('api/v1/', include(router.urls)),
]

View File

@@ -1,61 +0,0 @@
import os
from django_filters import NumberFilter
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 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)
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
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'
)

View File

@@ -1,201 +0,0 @@
from django.core.exceptions import ValidationError, ObjectDoesNotExist
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 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 two_factor.views import OTPRequiredMixin
from .models import Archive, Ticket, Platform
from .forms import TicketForm
from .filters import ArchiveFilter, TicketFilter
from .utils import PageTitleViewMixin
from .permissions import IsGuestUpload
from .serializers import (
PublicArchiveUploadSerializer,
ArchiveSerializer,
PlatformSerializer,
TicketSerializer
)
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")}'
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)