Create: views to upload files by ajax

This commit is contained in:
Stepan Zhukovsky 2023-08-17 00:53:13 +09:00
parent fd19181eff
commit 87a6ca06e6
8 changed files with 218 additions and 7 deletions

View File

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

View File

@ -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
self.serializer_class = PublicArchiveUploadSerializer if not request.user.is_authenticated:
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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