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 Meta:
|
||||
model = Archive
|
||||
fields = ['file', 'ticket']
|
||||
|
@ -10,6 +10,9 @@ from rest_framework import filters
|
||||
|
||||
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 .filters import ArchiveFilter, TicketFilter
|
||||
@ -30,15 +33,37 @@ class ArchiveViewSet(viewsets.ModelViewSet):
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
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):
|
||||
# ! upload-token protection:
|
||||
upload_token = request.headers.get('upload-token', '')
|
||||
if not request.user.is_authenticated and upload_token:
|
||||
if upload_token:
|
||||
try:
|
||||
bound_ticket = Ticket.objects.get(token=upload_token)
|
||||
if bound_ticket.resolved:
|
||||
return Response(
|
||||
{'error': f'ticket {upload_token} already resolved'},
|
||||
{'error': f'ticket {bound_ticket} already resolved'},
|
||||
status=status.HTTP_423_LOCKED
|
||||
)
|
||||
if bound_ticket.attempts <= 0:
|
||||
@ -51,13 +76,14 @@ class ArchiveViewSet(viewsets.ModelViewSet):
|
||||
# ? mixin bound ticket number to request.data from user
|
||||
request.data['ticket'] = bound_ticket.number
|
||||
# ? change serializer for guest user
|
||||
if not request.user.is_authenticated:
|
||||
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:
|
||||
else:
|
||||
return Response(
|
||||
{'error': 'Header Upload-Token is required'},
|
||||
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_bootstrap5.bootstrap5 import FloatingField
|
||||
|
||||
from .models import Ticket
|
||||
from .models import Ticket, Archive
|
||||
|
||||
|
||||
class TicketForm(forms.ModelForm):
|
||||
@ -29,3 +29,25 @@ class TicketForm(forms.ModelForm):
|
||||
Div('note', css_class='col-lg-6'),
|
||||
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(),
|
||||
name='create'
|
||||
),
|
||||
path(
|
||||
'archives/upload/',
|
||||
views.ArchiveUploadView.as_view(),
|
||||
name='upload'
|
||||
),
|
||||
# READ:
|
||||
path(
|
||||
'',
|
||||
@ -37,7 +42,7 @@ urlpatterns = [
|
||||
name='ticket'
|
||||
),
|
||||
path(
|
||||
'archives/<path:path>',
|
||||
'archives/download/<path:path>',
|
||||
views.ArchiveHandlerView.as_view(),
|
||||
name="download"
|
||||
),
|
||||
|
@ -3,14 +3,30 @@ from django.http import FileResponse
|
||||
from django.views import generic
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import render
|
||||
|
||||
from two_factor.views import OTPRequiredMixin
|
||||
|
||||
from .forms import TicketForm
|
||||
from .forms import TicketForm, ArchiveForm
|
||||
from .models import Archive, Ticket
|
||||
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(
|
||||
OTPRequiredMixin,
|
||||
LoginRequiredMixin,
|
||||
|
@ -17,8 +17,24 @@
|
||||
>
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
{% if request.user.is_authenticated %}
|
||||
<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"
|
||||
>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">
|
||||
<button
|
||||
class="nav-link dropdown-toggle"
|
||||
@ -142,5 +158,6 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
|
Loading…
Reference in New Issue
Block a user