mirror of
https://github.com/MOIS3Y/logs-collector.git
synced 2025-02-01 09:20:52 +01:00
Create: views to upload files by ajax
This commit is contained in:
parent
fd19181eff
commit
87a6ca06e6
@ -19,6 +19,7 @@ class JsTimestampField(serializers.Field):
|
|||||||
|
|
||||||
|
|
||||||
class PublicArchiveUploadSerializer(serializers.ModelSerializer):
|
class PublicArchiveUploadSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Archive
|
model = Archive
|
||||||
fields = ['file', 'ticket']
|
fields = ['file', 'ticket']
|
||||||
|
@ -10,6 +10,9 @@ from rest_framework import filters
|
|||||||
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
|
||||||
|
from drf_spectacular.utils import extend_schema
|
||||||
|
from drf_spectacular.openapi import OpenApiParameter
|
||||||
|
|
||||||
from collector.models import Archive, Ticket, Platform
|
from collector.models import Archive, Ticket, Platform
|
||||||
|
|
||||||
from .filters import ArchiveFilter, TicketFilter
|
from .filters import ArchiveFilter, TicketFilter
|
||||||
@ -30,15 +33,37 @@ class ArchiveViewSet(viewsets.ModelViewSet):
|
|||||||
filter_backends = [DjangoFilterBackend]
|
filter_backends = [DjangoFilterBackend]
|
||||||
filterset_class = ArchiveFilter
|
filterset_class = ArchiveFilter
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
operation_id='upload_file',
|
||||||
|
request={
|
||||||
|
'multipart/form-data': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'file': {
|
||||||
|
'type': 'string',
|
||||||
|
'format': 'binary'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parameters=[
|
||||||
|
OpenApiParameter(
|
||||||
|
name='Upload-Token',
|
||||||
|
type=str,
|
||||||
|
location=OpenApiParameter.HEADER,
|
||||||
|
description="upload permission token",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
# ! upload-token protection:
|
# ! upload-token protection:
|
||||||
upload_token = request.headers.get('upload-token', '')
|
upload_token = request.headers.get('upload-token', '')
|
||||||
if not request.user.is_authenticated and upload_token:
|
if upload_token:
|
||||||
try:
|
try:
|
||||||
bound_ticket = Ticket.objects.get(token=upload_token)
|
bound_ticket = Ticket.objects.get(token=upload_token)
|
||||||
if bound_ticket.resolved:
|
if bound_ticket.resolved:
|
||||||
return Response(
|
return Response(
|
||||||
{'error': f'ticket {upload_token} already resolved'},
|
{'error': f'ticket {bound_ticket} already resolved'},
|
||||||
status=status.HTTP_423_LOCKED
|
status=status.HTTP_423_LOCKED
|
||||||
)
|
)
|
||||||
if bound_ticket.attempts <= 0:
|
if bound_ticket.attempts <= 0:
|
||||||
@ -51,13 +76,14 @@ class ArchiveViewSet(viewsets.ModelViewSet):
|
|||||||
# ? mixin bound ticket number to request.data from user
|
# ? mixin bound ticket number to request.data from user
|
||||||
request.data['ticket'] = bound_ticket.number
|
request.data['ticket'] = bound_ticket.number
|
||||||
# ? change serializer for guest user
|
# ? change serializer for guest user
|
||||||
|
if not request.user.is_authenticated:
|
||||||
self.serializer_class = PublicArchiveUploadSerializer
|
self.serializer_class = PublicArchiveUploadSerializer
|
||||||
except (ValidationError, ObjectDoesNotExist,):
|
except (ValidationError, ObjectDoesNotExist,):
|
||||||
return Response(
|
return Response(
|
||||||
{'error': f'token {upload_token} is not valid'},
|
{'error': f'token {upload_token} is not valid'},
|
||||||
status=status.HTTP_403_FORBIDDEN
|
status=status.HTTP_403_FORBIDDEN
|
||||||
)
|
)
|
||||||
elif not request.user.is_authenticated:
|
else:
|
||||||
return Response(
|
return Response(
|
||||||
{'error': 'Header Upload-Token is required'},
|
{'error': 'Header Upload-Token is required'},
|
||||||
status=status.HTTP_401_UNAUTHORIZED
|
status=status.HTTP_401_UNAUTHORIZED
|
||||||
|
@ -3,7 +3,7 @@ from crispy_forms.helper import FormHelper
|
|||||||
from crispy_forms.layout import Layout, Submit, Div
|
from crispy_forms.layout import Layout, Submit, Div
|
||||||
from crispy_bootstrap5.bootstrap5 import FloatingField
|
from crispy_bootstrap5.bootstrap5 import FloatingField
|
||||||
|
|
||||||
from .models import Ticket
|
from .models import Ticket, Archive
|
||||||
|
|
||||||
|
|
||||||
class TicketForm(forms.ModelForm):
|
class TicketForm(forms.ModelForm):
|
||||||
@ -29,3 +29,25 @@ class TicketForm(forms.ModelForm):
|
|||||||
Div('note', css_class='col-lg-6'),
|
Div('note', css_class='col-lg-6'),
|
||||||
Submit('submit', 'Save', css_class='btn btn-primary'),
|
Submit('submit', 'Save', css_class='btn btn-primary'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ArchiveForm(forms.ModelForm):
|
||||||
|
token = forms.UUIDField(required=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Archive
|
||||||
|
fields = ['token', 'file']
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(ArchiveForm, self).__init__(*args, **kwargs)
|
||||||
|
self.helper = FormHelper(self)
|
||||||
|
self.helper.form_id = 'upload_form'
|
||||||
|
|
||||||
|
self.helper.layout = Layout(
|
||||||
|
Div(
|
||||||
|
FloatingField('token'),
|
||||||
|
'file',
|
||||||
|
css_class='col-lg-6'
|
||||||
|
),
|
||||||
|
Submit('submit', 'Upload', css_class='btn btn-primary'),
|
||||||
|
)
|
||||||
|
@ -0,0 +1,80 @@
|
|||||||
|
$(function () {
|
||||||
|
const uploadForm = document.getElementById('upload_form');
|
||||||
|
const input_file = document.getElementById('id_file');
|
||||||
|
const progress_bar = document.getElementById('progress');
|
||||||
|
const alert_container = document.getElementById('alert');
|
||||||
|
|
||||||
|
$("#upload_form").submit(function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
$form = $(this)
|
||||||
|
let formData = new FormData(this);
|
||||||
|
let upload_token = formData.get("token")
|
||||||
|
const media_data = input_file.files[0];
|
||||||
|
if(media_data != null){
|
||||||
|
progress_bar.classList.remove("not-visible");
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: progress_bar.getAttribute("upload-url"),
|
||||||
|
data: formData,
|
||||||
|
dataType: 'json',
|
||||||
|
xhr:function(){
|
||||||
|
const xhr = new window.XMLHttpRequest();
|
||||||
|
xhr.upload.addEventListener('progress', e=>{
|
||||||
|
if(e.lengthComputable){
|
||||||
|
const percentProgress = (e.loaded/e.total)*100;
|
||||||
|
console.log(percentProgress);
|
||||||
|
progress_bar.innerHTML = `
|
||||||
|
<div
|
||||||
|
class="progress-bar progress-bar-striped progress-bar-animated"
|
||||||
|
style="width: ${percentProgress}%"
|
||||||
|
>
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return xhr
|
||||||
|
},
|
||||||
|
beforeSend: function(xhr) {
|
||||||
|
if (upload_token) {
|
||||||
|
xhr.setRequestHeader("Upload-Token", upload_token);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
success: function(data, textStatus, jqXHR){
|
||||||
|
console.log(jqXHR.status);
|
||||||
|
let type = "success";
|
||||||
|
alert_container.innerHTML = [
|
||||||
|
`<div class="alert alert-${type} alert-dismissible col-lg-6" role="alert">`,
|
||||||
|
` <div>The file has been successfully uploaded to the server. Thank you!</div>`,
|
||||||
|
' <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>',
|
||||||
|
'</div>'
|
||||||
|
].join('')
|
||||||
|
uploadForm.reset()
|
||||||
|
progress_bar.classList.add('not-visible')
|
||||||
|
},
|
||||||
|
error: function(data, textStatus, jqXHR){
|
||||||
|
console.log(data.responseJSON.error);
|
||||||
|
let type = "danger";
|
||||||
|
let error_message = "Unexpected error. Try again please"
|
||||||
|
if (data.status === 423) {
|
||||||
|
error_message = `Error ${data.status}: ${data.responseJSON.error}`
|
||||||
|
}
|
||||||
|
if (data.status === 403) {
|
||||||
|
error_message = `Error ${data.status}: ${data.responseJSON.error}`
|
||||||
|
}
|
||||||
|
if (data.status === 401) {
|
||||||
|
error_message = 'The token field cannot be empty'
|
||||||
|
}
|
||||||
|
alert_container.innerHTML = [
|
||||||
|
`<div class="alert alert-${type} alert-dismissible col-lg-6" role="alert">`,
|
||||||
|
` <div>${error_message}</div>`,
|
||||||
|
' <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>',
|
||||||
|
'</div>'
|
||||||
|
].join('')
|
||||||
|
progress_bar.classList.add('not-visible')
|
||||||
|
},
|
||||||
|
cache: false,
|
||||||
|
contentType: false,
|
||||||
|
processData: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,44 @@
|
|||||||
|
{% extends 'collector/base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
{% block title %} {{ title }} {% endblock title %}
|
||||||
|
{% block main %}
|
||||||
|
<style>
|
||||||
|
.not-visible{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="container mt-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Archive upload:</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="alert" class="container"></div>
|
||||||
|
<div class="container">
|
||||||
|
{% crispy form %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<div
|
||||||
|
id="progress"
|
||||||
|
upload-url="{% url 'collector_api:archive-list' %}"
|
||||||
|
class="progress"
|
||||||
|
role="progressbar"
|
||||||
|
aria-label="Example 20px high"
|
||||||
|
aria-valuenow="25"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100"
|
||||||
|
style="height: 20px"
|
||||||
|
>
|
||||||
|
<div class="progress-bar"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock main %}
|
||||||
|
|
||||||
|
{% block jquery %}
|
||||||
|
<script src="{% static 'collector/js/jq.upload.progress.js' %}"></script>
|
||||||
|
{% endblock jquery %}
|
@ -15,6 +15,11 @@ urlpatterns = [
|
|||||||
views.CreateTicket.as_view(),
|
views.CreateTicket.as_view(),
|
||||||
name='create'
|
name='create'
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
'archives/upload/',
|
||||||
|
views.ArchiveUploadView.as_view(),
|
||||||
|
name='upload'
|
||||||
|
),
|
||||||
# READ:
|
# READ:
|
||||||
path(
|
path(
|
||||||
'',
|
'',
|
||||||
@ -37,7 +42,7 @@ urlpatterns = [
|
|||||||
name='ticket'
|
name='ticket'
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
'archives/<path:path>',
|
'archives/download/<path:path>',
|
||||||
views.ArchiveHandlerView.as_view(),
|
views.ArchiveHandlerView.as_view(),
|
||||||
name="download"
|
name="download"
|
||||||
),
|
),
|
||||||
|
@ -3,14 +3,30 @@ from django.http import FileResponse
|
|||||||
from django.views import generic
|
from django.views import generic
|
||||||
from django.views.generic.detail import SingleObjectMixin
|
from django.views.generic.detail import SingleObjectMixin
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
from two_factor.views import OTPRequiredMixin
|
from two_factor.views import OTPRequiredMixin
|
||||||
|
|
||||||
from .forms import TicketForm
|
from .forms import TicketForm, ArchiveForm
|
||||||
from .models import Archive, Ticket
|
from .models import Archive, Ticket
|
||||||
from .utils import PageTitleViewMixin
|
from .utils import PageTitleViewMixin
|
||||||
|
|
||||||
|
|
||||||
|
class ArchiveUploadView(PageTitleViewMixin, generic.View):
|
||||||
|
form_class = ArchiveForm()
|
||||||
|
template = 'collector/archive_upload.html',
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
self.template,
|
||||||
|
context={'form': self.form_class}
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_title(self):
|
||||||
|
return f'{self.title} - upload'
|
||||||
|
|
||||||
|
|
||||||
class ArchiveHandlerView(
|
class ArchiveHandlerView(
|
||||||
OTPRequiredMixin,
|
OTPRequiredMixin,
|
||||||
LoginRequiredMixin,
|
LoginRequiredMixin,
|
||||||
|
@ -17,8 +17,24 @@
|
|||||||
>
|
>
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||||
<ul class="navbar-nav ml-auto mb-2 mb-lg-0 me-md-auto">
|
<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"
|
||||||
|
>Archives</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{% url 'collector:upload' %}">
|
||||||
|
<i class="bi bi-archive"></i> Upload archive
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<button
|
<button
|
||||||
class="nav-link dropdown-toggle"
|
class="nav-link dropdown-toggle"
|
||||||
@ -142,5 +158,6 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
Loading…
Reference in New Issue
Block a user