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 | ||||||
|                 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 | ||||||
|  | |||||||
| @ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user