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 Meta:
model = Archive
fields = ['file', 'ticket']

View File

@ -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
self.serializer_class = PublicArchiveUploadSerializer
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

View File

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

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

View File

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

View File

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