Compare commits
	
		
			4 Commits
		
	
	
		
			51d1c09540
			...
			90d7e64db3
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 90d7e64db3 | |||
| 02906554dd | |||
| cc7043f013 | |||
| 116c0e4f41 | 
| @ -16,6 +16,10 @@ class ArchiveAdmin(admin.ModelAdmin): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class TokenAdmin(admin.ModelAdmin): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| admin.site.register(Platform, PlatformAdmin) | ||||
| admin.site.register(Ticket, TicketAdmin) | ||||
| admin.site.register(Archive, ArchiveAdmin) | ||||
|  | ||||
| @ -10,7 +10,7 @@ class TicketForm(forms.ModelForm): | ||||
| 
 | ||||
|     class Meta: | ||||
|         model = Ticket | ||||
|         fields = ['number', 'platform', 'note'] | ||||
|         fields = ['number', 'attempts', 'platform', 'note'] | ||||
|         widgets = { | ||||
|             'platform': forms.RadioSelect() | ||||
|         } | ||||
| @ -21,7 +21,11 @@ class TicketForm(forms.ModelForm): | ||||
|         # self.helper.attrs = {"novalidate": ''} | ||||
| 
 | ||||
|         self.helper.layout = Layout( | ||||
|             Div(FloatingField('number'), 'platform', css_class='col-lg-2'), | ||||
|             Div( | ||||
|                 FloatingField('number', 'attempts'), | ||||
|                 'platform', | ||||
|                 css_class='col-lg-2' | ||||
|             ), | ||||
|             Div('note', css_class='col-lg-6'), | ||||
|             Submit('submit', 'Save', css_class='btn btn-primary'), | ||||
|         ) | ||||
|  | ||||
| @ -0,0 +1,33 @@ | ||||
| # Generated by Django 4.2 on 2023-08-08 11:16 | ||||
| 
 | ||||
| from django.conf import settings | ||||
| import django.core.validators | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
| import uuid | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||||
|         ('collector', '0004_rename_sha1_archive_md5_remove_archive_size'), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.CreateModel( | ||||
|             name='Token', | ||||
|             fields=[ | ||||
|                 ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), | ||||
|                 ('expires', models.IntegerField(default=5, validators=[django.core.validators.MaxValueValidator(100), django.core.validators.MinValueValidator(1)])), | ||||
|                 ('blocked', models.BooleanField(default=False)), | ||||
|                 ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), | ||||
|             ], | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='archive', | ||||
|             name='token', | ||||
|             field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='collector.token'), | ||||
|             preserve_default=False, | ||||
|         ), | ||||
|     ] | ||||
| @ -0,0 +1,47 @@ | ||||
| # Generated by Django 4.2 on 2023-08-08 16:52 | ||||
| 
 | ||||
| import django.core.validators | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
| import uuid | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ('collector', '0005_token_archive_token'), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.RemoveField( | ||||
|             model_name='archive', | ||||
|             name='token', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='archive', | ||||
|             name='user', | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='ticket', | ||||
|             name='token', | ||||
|             field=models.UUIDField(default=uuid.uuid4, editable=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='ticket', | ||||
|             name='upload', | ||||
|             field=models.IntegerField(default=5, validators=[django.core.validators.MaxValueValidator(10), django.core.validators.MinValueValidator(1)]), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='platform', | ||||
|             name='name', | ||||
|             field=models.CharField(max_length=20, unique=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='ticket', | ||||
|             name='platform', | ||||
|             field=models.ForeignKey(db_column='platform_name', on_delete=django.db.models.deletion.CASCADE, to='collector.platform', to_field='name'), | ||||
|         ), | ||||
|         migrations.DeleteModel( | ||||
|             name='Token', | ||||
|         ), | ||||
|     ] | ||||
| @ -0,0 +1,18 @@ | ||||
| # Generated by Django 4.2 on 2023-08-08 17:08 | ||||
| 
 | ||||
| from django.db import migrations | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ('collector', '0006_remove_archive_token_remove_archive_user_and_more'), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.RenameField( | ||||
|             model_name='ticket', | ||||
|             old_name='upload', | ||||
|             new_name='attempts', | ||||
|         ), | ||||
|     ] | ||||
| @ -1,6 +1,8 @@ | ||||
| import uuid | ||||
| import hashlib | ||||
| from functools import partial | ||||
| 
 | ||||
| from django.core.validators import MaxValueValidator, MinValueValidator | ||||
| from django.contrib.auth.models import User | ||||
| from django.db import models | ||||
| from django.conf import settings | ||||
| @ -37,7 +39,6 @@ class Archive(models.Model): | ||||
|         db_column='ticket_number', | ||||
|         on_delete=models.CASCADE | ||||
|     ) | ||||
|     user = models.ForeignKey(User, on_delete=models.CASCADE) | ||||
| 
 | ||||
|     def save(self, *args, **kwargs): | ||||
|         # calculate sha 1 hash sum and write md5 field to db | ||||
| @ -57,7 +58,7 @@ class Archive(models.Model): | ||||
| 
 | ||||
| 
 | ||||
| class Platform(models.Model): | ||||
|     name = models.CharField(max_length=20) | ||||
|     name = models.CharField(max_length=20, unique=True) | ||||
|     pretty_name = models.CharField(max_length=20) | ||||
| 
 | ||||
|     def get_absolute_url(self): | ||||
| @ -71,9 +72,19 @@ class Ticket(models.Model): | ||||
|     number = models.IntegerField(unique=True, db_index=True) | ||||
|     resolved = models.BooleanField(default=False) | ||||
|     note = models.TextField(blank=True) | ||||
|     token = models.UUIDField(default=uuid.uuid4, editable=False) | ||||
|     attempts = models.IntegerField(default=5, validators=[ | ||||
|             MaxValueValidator(10), | ||||
|             MinValueValidator(0) | ||||
|         ]) | ||||
|     time_create = models.DateTimeField(auto_now_add=True) | ||||
|     time_update = models.DateTimeField(auto_now=True) | ||||
|     platform = models.ForeignKey('Platform', on_delete=models.CASCADE) | ||||
|     platform = models.ForeignKey( | ||||
|         'Platform', | ||||
|         to_field='name', | ||||
|         db_column='platform_name', | ||||
|         on_delete=models.CASCADE | ||||
|     ) | ||||
|     user = models.ForeignKey(User, on_delete=models.CASCADE) | ||||
| 
 | ||||
|     class Meta: | ||||
|  | ||||
| @ -1,17 +1,11 @@ | ||||
| from rest_framework import serializers | ||||
| 
 | ||||
| from .models import Archive, Ticket | ||||
| from .models import Archive | ||||
| 
 | ||||
| 
 | ||||
| class ArchiveUploadSerializer(serializers.ModelSerializer): | ||||
| class PublicArchiveUploadSerializer(serializers.ModelSerializer): | ||||
|     ticket = serializers.ReadOnlyField(source='ticket.token') | ||||
| 
 | ||||
|     class Meta: | ||||
|         model = Archive | ||||
|         fields = ['file', 'ticket'] | ||||
| 
 | ||||
| 
 | ||||
| class TicketSerializer(serializers.ModelSerializer): | ||||
| 
 | ||||
|     class Meta: | ||||
|         model = Ticket | ||||
|         fields = ['number', 'platform', 'note'] | ||||
|  | ||||
| @ -93,4 +93,18 @@ $(function () { | ||||
|             } | ||||
|         }); | ||||
|     }); | ||||
|     // copy token to clipboard:
 | ||||
|     // -- -- -- -- -- -- -- --
 | ||||
|     $(".token-clipboard").click(function (e) {  | ||||
|         e.preventDefault(); | ||||
|         const btn = $(this) | ||||
|         const tokenInput = btn.siblings("input[name=ticket-token]").val(); | ||||
|         const icon = btn.children(":first").get(0) | ||||
|         navigator.clipboard.writeText(tokenInput); | ||||
|         btn.html('<i class="bi bi-check-lg"></i>') | ||||
|         // Revert button label after 500 milliseconds
 | ||||
|         setTimeout(function(){ | ||||
|             btn.html(icon); | ||||
|         }, 500) | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| @ -16,4 +16,34 @@ | ||||
| <div class="col-xl-6 mb-2"> | ||||
|   <h6 class="card-title mb-1">Platform: {{ ticket.platform.pretty_name }}</h6> | ||||
|   <h6 class="card-title mb-3">Owner: {{ ticket.user.username }}</h6> | ||||
|   <!-- Token --> | ||||
|   <div class="input-group input-group mb-3"> | ||||
|     <span class="input-group-text" id="inputGroup-sizing-sm"><i class="bi bi-key"></i></span> | ||||
|     <!--Token attempts--> | ||||
|     <span class="input-group-text" id="inputGroup-sizing-sm"> | ||||
|       <span | ||||
|         class="badge | ||||
|         {% if ticket.attempts <= 0 %} | ||||
|           bg-danger  | ||||
|         {% elif ticket.attempts < 5 %} | ||||
|           text-dark bg-warning | ||||
|         {% else %} | ||||
|           bg-primary | ||||
|         {% endif %} rounded-pill">{{ ticket.attempts }} | ||||
|       </span> | ||||
|     </span> | ||||
|     <input | ||||
|       name="ticket-token" | ||||
|       class="form-control" | ||||
|       type="text" | ||||
|       value="{{ ticket.token }}" | ||||
|       aria-label="Disabled input example" | ||||
|       aria-describedby="inputGroup-sizing-sm" | ||||
|       disabled | ||||
|       readonly> | ||||
|     <button | ||||
|       class="input-group-text token-clipboard" | ||||
|       id="inputGroup-sizing-sm"><i class="bi bi-clipboard"></i> | ||||
|   </button> | ||||
|   </div>   | ||||
| </div> | ||||
|  | ||||
| @ -7,8 +7,8 @@ | ||||
|   {% csrf_token %} | ||||
|   <!-- Ticket --> | ||||
|   {% for ticket in tickets %} | ||||
|     <div id="div-ticket-{{ ticket.number }}" class="list-group mb-2"> | ||||
|         <div class="list-group-item list-group-item-action disable" aria-current="true"> | ||||
|     <ul id="div-ticket-{{ ticket.number }}" class="list-group mb-2"> | ||||
|       <li class="list-group-item list-group-item-action disable" aria-current="true"> | ||||
|           {% include 'collector/includes/ticket_info.html' %}  | ||||
|           <div class="col-xl-6 mt-1 mb-2"> | ||||
|             <div class="accordion" id="#archive_{{ ticket.number }}"> | ||||
| @ -61,12 +61,12 @@ | ||||
|               </div> | ||||
|               {% endif %} | ||||
|             </div>  | ||||
|         </div> | ||||
|           </div> | ||||
|           <div class="d-flex w-100 justify-content-between"> | ||||
|             <a | ||||
|               href="{{ ticket.get_absolute_url }}" | ||||
|               class="btn btn-outline-primary mb-1 mt-1" | ||||
|             >Open</a> | ||||
|             ><i class="bi bi-arrow-return-right"></i> Open</a> | ||||
|             <button | ||||
|               class="btn btn-outline-danger mb-1 mt-1" | ||||
|               data-bs-toggle="modal" | ||||
| @ -74,8 +74,8 @@ | ||||
|             ><i class="bi bi-trash"></i> Delete | ||||
|             </button> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|         </li> | ||||
|     </ul> | ||||
|     <!-- Modal ticket --> | ||||
|     {% include 'collector/includes/modal_ticket.html' %} | ||||
|     <!-- Modal archive --> | ||||
|  | ||||
| @ -7,9 +7,7 @@ from . import views | ||||
| app_name = 'collector' | ||||
| 
 | ||||
| router = routers.DefaultRouter() | ||||
| router.register(r'archives', views.ArchiveUploadViewSet) | ||||
| router.register(r'tickets/create', views.TicketCreateViewSet) | ||||
| 
 | ||||
| router.register(r'archives', views.PublicArchiveUploadViewSet) | ||||
| 
 | ||||
| urlpatterns = [ | ||||
| 
 | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| import json | ||||
| from django.core.exceptions import ValidationError | ||||
| from django.contrib.auth.mixins import LoginRequiredMixin | ||||
| from django.http import FileResponse, JsonResponse | ||||
| from django.views import generic | ||||
| @ -7,6 +8,7 @@ from django.urls import reverse_lazy | ||||
| from django.db.models import Q | ||||
| 
 | ||||
| from rest_framework import status | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.parsers import FormParser, MultiPartParser | ||||
| 
 | ||||
| from rest_framework import mixins | ||||
| @ -16,7 +18,7 @@ from .models import Archive, Ticket | ||||
| from .forms import TicketForm | ||||
| from .utils import PageTitleViewMixin, is_ajax | ||||
| 
 | ||||
| from .serializers import ArchiveUploadSerializer, TicketSerializer | ||||
| from .serializers import PublicArchiveUploadSerializer | ||||
| 
 | ||||
| 
 | ||||
| class ArchiveHandlerView(LoginRequiredMixin, SingleObjectMixin, generic.View): | ||||
| @ -172,20 +174,51 @@ class DeleteTicketHandler(SingleObjectMixin, generic.View): | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| class ArchiveUploadViewSet(mixins.CreateModelMixin, GenericViewSet): | ||||
| class PublicArchiveUploadViewSet(mixins.CreateModelMixin, GenericViewSet): | ||||
|     queryset = Archive.objects.order_by('-time_create') | ||||
|     serializer_class = ArchiveUploadSerializer | ||||
|     serializer_class = PublicArchiveUploadSerializer | ||||
|     parser_classes = (MultiPartParser, FormParser) | ||||
|     # permission_classes = [permissions.IsAuthenticatedOrReadOnly] | ||||
| 
 | ||||
|     def create(self, request, *args, **kwargs): | ||||
|         # ! upload-token protection: | ||||
|         upload_token = request.headers.get('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'}, | ||||
|                         status=status.HTTP_423_LOCKED | ||||
|                     ) | ||||
|                 if bound_ticket.attempts <= 0: | ||||
|                     return Response( | ||||
|                         {'error': f'token {upload_token} expired'}, | ||||
|                         status=status.HTTP_423_LOCKED | ||||
|                     ) | ||||
|                 bound_ticket.attempts -= 1 | ||||
|                 bound_ticket.save() | ||||
|                 # ? mixin bound ticket to request.data from user | ||||
|                 request.data['ticket'] = bound_ticket | ||||
|             except ValidationError: | ||||
|                 return Response( | ||||
|                     {'error': f'token {upload_token} is not valid'}, | ||||
|                     status=status.HTTP_403_FORBIDDEN | ||||
|                 ) | ||||
|         else: | ||||
|             return Response( | ||||
|                 {'error': 'Header Upload-Token is required'}, | ||||
|                 status=status.HTTP_401_UNAUTHORIZED | ||||
|             ) | ||||
|         # ! default create method: | ||||
|         serializer = self.get_serializer(data=request.data) | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         self.perform_create(serializer) | ||||
|         headers = self.get_success_headers(serializer.data) | ||||
|         return Response( | ||||
|             serializer.data, | ||||
|             status=status.HTTP_201_CREATED, | ||||
|             headers=headers | ||||
|         ) | ||||
| 
 | ||||
|     def perform_create(self, serializer): | ||||
|         serializer.save(user=self.request.user) | ||||
| 
 | ||||
| 
 | ||||
| class TicketCreateViewSet(mixins.CreateModelMixin, GenericViewSet): | ||||
|     queryset = Ticket.objects.order_by('-time_create') | ||||
|     serializer_class = TicketSerializer | ||||
|     # permission_classes = [permissions.IsAuthenticatedOrReadOnly] | ||||
| 
 | ||||
|     def perform_create(self, serializer): | ||||
|         serializer.save(user=self.request.user) | ||||
|         serializer.save(ticket=self.request.data['ticket']) | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user