Compare commits
	
		
			4 Commits
		
	
	
		
			51d1c09540
			...
			90d7e64db3
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 90d7e64db3 | |||
| 02906554dd | |||
| cc7043f013 | |||
| 116c0e4f41 | 
| @ -16,6 +16,10 @@ class ArchiveAdmin(admin.ModelAdmin): | |||||||
|     pass |     pass | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | class TokenAdmin(admin.ModelAdmin): | ||||||
|  |     pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| admin.site.register(Platform, PlatformAdmin) | admin.site.register(Platform, PlatformAdmin) | ||||||
| admin.site.register(Ticket, TicketAdmin) | admin.site.register(Ticket, TicketAdmin) | ||||||
| admin.site.register(Archive, ArchiveAdmin) | admin.site.register(Archive, ArchiveAdmin) | ||||||
|  | |||||||
| @ -10,7 +10,7 @@ class TicketForm(forms.ModelForm): | |||||||
| 
 | 
 | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = Ticket |         model = Ticket | ||||||
|         fields = ['number', 'platform', 'note'] |         fields = ['number', 'attempts', 'platform', 'note'] | ||||||
|         widgets = { |         widgets = { | ||||||
|             'platform': forms.RadioSelect() |             'platform': forms.RadioSelect() | ||||||
|         } |         } | ||||||
| @ -21,7 +21,11 @@ class TicketForm(forms.ModelForm): | |||||||
|         # self.helper.attrs = {"novalidate": ''} |         # self.helper.attrs = {"novalidate": ''} | ||||||
| 
 | 
 | ||||||
|         self.helper.layout = Layout( |         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'), |             Div('note', css_class='col-lg-6'), | ||||||
|             Submit('submit', 'Save', css_class='btn btn-primary'), |             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 | import hashlib | ||||||
| from functools import partial | from functools import partial | ||||||
| 
 | 
 | ||||||
|  | from django.core.validators import MaxValueValidator, MinValueValidator | ||||||
| from django.contrib.auth.models import User | from django.contrib.auth.models import User | ||||||
| from django.db import models | from django.db import models | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| @ -37,7 +39,6 @@ class Archive(models.Model): | |||||||
|         db_column='ticket_number', |         db_column='ticket_number', | ||||||
|         on_delete=models.CASCADE |         on_delete=models.CASCADE | ||||||
|     ) |     ) | ||||||
|     user = models.ForeignKey(User, on_delete=models.CASCADE) |  | ||||||
| 
 | 
 | ||||||
|     def save(self, *args, **kwargs): |     def save(self, *args, **kwargs): | ||||||
|         # calculate sha 1 hash sum and write md5 field to db |         # calculate sha 1 hash sum and write md5 field to db | ||||||
| @ -57,7 +58,7 @@ class Archive(models.Model): | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Platform(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) |     pretty_name = models.CharField(max_length=20) | ||||||
| 
 | 
 | ||||||
|     def get_absolute_url(self): |     def get_absolute_url(self): | ||||||
| @ -71,9 +72,19 @@ class Ticket(models.Model): | |||||||
|     number = models.IntegerField(unique=True, db_index=True) |     number = models.IntegerField(unique=True, db_index=True) | ||||||
|     resolved = models.BooleanField(default=False) |     resolved = models.BooleanField(default=False) | ||||||
|     note = models.TextField(blank=True) |     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_create = models.DateTimeField(auto_now_add=True) | ||||||
|     time_update = models.DateTimeField(auto_now=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) |     user = models.ForeignKey(User, on_delete=models.CASCADE) | ||||||
| 
 | 
 | ||||||
|     class Meta: |     class Meta: | ||||||
|  | |||||||
| @ -1,17 +1,11 @@ | |||||||
| from rest_framework import serializers | 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: |     class Meta: | ||||||
|         model = Archive |         model = Archive | ||||||
|         fields = ['file', 'ticket'] |         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"> | <div class="col-xl-6 mb-2"> | ||||||
|   <h6 class="card-title mb-1">Platform: {{ ticket.platform.pretty_name }}</h6> |   <h6 class="card-title mb-1">Platform: {{ ticket.platform.pretty_name }}</h6> | ||||||
|   <h6 class="card-title mb-3">Owner: {{ ticket.user.username }}</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> | </div> | ||||||
|  | |||||||
| @ -7,8 +7,8 @@ | |||||||
|   {% csrf_token %} |   {% csrf_token %} | ||||||
|   <!-- Ticket --> |   <!-- Ticket --> | ||||||
|   {% for ticket in tickets %} |   {% for ticket in tickets %} | ||||||
|     <div id="div-ticket-{{ ticket.number }}" class="list-group mb-2"> |     <ul id="div-ticket-{{ ticket.number }}" class="list-group mb-2"> | ||||||
|         <div class="list-group-item list-group-item-action disable" aria-current="true"> |       <li class="list-group-item list-group-item-action disable" aria-current="true"> | ||||||
|           {% include 'collector/includes/ticket_info.html' %}  |           {% include 'collector/includes/ticket_info.html' %}  | ||||||
|           <div class="col-xl-6 mt-1 mb-2"> |           <div class="col-xl-6 mt-1 mb-2"> | ||||||
|             <div class="accordion" id="#archive_{{ ticket.number }}"> |             <div class="accordion" id="#archive_{{ ticket.number }}"> | ||||||
| @ -61,12 +61,12 @@ | |||||||
|               </div> |               </div> | ||||||
|               {% endif %} |               {% endif %} | ||||||
|             </div>  |             </div>  | ||||||
|         </div> |           </div> | ||||||
|           <div class="d-flex w-100 justify-content-between"> |           <div class="d-flex w-100 justify-content-between"> | ||||||
|             <a |             <a | ||||||
|               href="{{ ticket.get_absolute_url }}" |               href="{{ ticket.get_absolute_url }}" | ||||||
|               class="btn btn-outline-primary mb-1 mt-1" |               class="btn btn-outline-primary mb-1 mt-1" | ||||||
|             >Open</a> |             ><i class="bi bi-arrow-return-right"></i> Open</a> | ||||||
|             <button |             <button | ||||||
|               class="btn btn-outline-danger mb-1 mt-1" |               class="btn btn-outline-danger mb-1 mt-1" | ||||||
|               data-bs-toggle="modal" |               data-bs-toggle="modal" | ||||||
| @ -74,8 +74,8 @@ | |||||||
|             ><i class="bi bi-trash"></i> Delete |             ><i class="bi bi-trash"></i> Delete | ||||||
|             </button> |             </button> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </li> | ||||||
|       </div> |     </ul> | ||||||
|     <!-- Modal ticket --> |     <!-- Modal ticket --> | ||||||
|     {% include 'collector/includes/modal_ticket.html' %} |     {% include 'collector/includes/modal_ticket.html' %} | ||||||
|     <!-- Modal archive --> |     <!-- Modal archive --> | ||||||
|  | |||||||
| @ -7,9 +7,7 @@ from . import views | |||||||
| app_name = 'collector' | app_name = 'collector' | ||||||
| 
 | 
 | ||||||
| router = routers.DefaultRouter() | router = routers.DefaultRouter() | ||||||
| router.register(r'archives', views.ArchiveUploadViewSet) | router.register(r'archives', views.PublicArchiveUploadViewSet) | ||||||
| router.register(r'tickets/create', views.TicketCreateViewSet) |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| urlpatterns = [ | urlpatterns = [ | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| import json | import json | ||||||
|  | from django.core.exceptions import ValidationError | ||||||
| from django.contrib.auth.mixins import LoginRequiredMixin | from django.contrib.auth.mixins import LoginRequiredMixin | ||||||
| from django.http import FileResponse, JsonResponse | from django.http import FileResponse, JsonResponse | ||||||
| from django.views import generic | from django.views import generic | ||||||
| @ -7,6 +8,7 @@ from django.urls import reverse_lazy | |||||||
| from django.db.models import Q | from django.db.models import Q | ||||||
| 
 | 
 | ||||||
| from rest_framework import status | from rest_framework import status | ||||||
|  | from rest_framework.response import Response | ||||||
| from rest_framework.parsers import FormParser, MultiPartParser | from rest_framework.parsers import FormParser, MultiPartParser | ||||||
| 
 | 
 | ||||||
| from rest_framework import mixins | from rest_framework import mixins | ||||||
| @ -16,7 +18,7 @@ from .models import Archive, Ticket | |||||||
| from .forms import TicketForm | from .forms import TicketForm | ||||||
| from .utils import PageTitleViewMixin, is_ajax | from .utils import PageTitleViewMixin, is_ajax | ||||||
| 
 | 
 | ||||||
| from .serializers import ArchiveUploadSerializer, TicketSerializer | from .serializers import PublicArchiveUploadSerializer | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ArchiveHandlerView(LoginRequiredMixin, SingleObjectMixin, generic.View): | 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') |     queryset = Archive.objects.order_by('-time_create') | ||||||
|     serializer_class = ArchiveUploadSerializer |     serializer_class = PublicArchiveUploadSerializer | ||||||
|     parser_classes = (MultiPartParser, FormParser) |     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): |     def perform_create(self, serializer): | ||||||
|         serializer.save(user=self.request.user) |         serializer.save(ticket=self.request.data['ticket']) | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 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) |  | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user