Compare commits
	
		
			No commits in common. "90d7e64db3fd4252e8554c15c4d5b5c418a8cdaa" and "51d1c095405d014a1088de054c96208702a8f4d8" have entirely different histories.
		
	
	
		
			90d7e64db3
			...
			51d1c09540
		
	
		
| @ -16,10 +16,6 @@ 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', 'attempts', 'platform', 'note'] |         fields = ['number', 'platform', 'note'] | ||||||
|         widgets = { |         widgets = { | ||||||
|             'platform': forms.RadioSelect() |             'platform': forms.RadioSelect() | ||||||
|         } |         } | ||||||
| @ -21,11 +21,7 @@ class TicketForm(forms.ModelForm): | |||||||
|         # self.helper.attrs = {"novalidate": ''} |         # self.helper.attrs = {"novalidate": ''} | ||||||
| 
 | 
 | ||||||
|         self.helper.layout = Layout( |         self.helper.layout = Layout( | ||||||
|             Div( |             Div(FloatingField('number'), 'platform', css_class='col-lg-2'), | ||||||
|                 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'), | ||||||
|         ) |         ) | ||||||
|  | |||||||
| @ -1,33 +0,0 @@ | |||||||
| # 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, |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,47 +0,0 @@ | |||||||
| # 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', |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,18 +0,0 @@ | |||||||
| # 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,8 +1,6 @@ | |||||||
| 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 | ||||||
| @ -39,6 +37,7 @@ 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 | ||||||
| @ -58,7 +57,7 @@ class Archive(models.Model): | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Platform(models.Model): | class Platform(models.Model): | ||||||
|     name = models.CharField(max_length=20, unique=True) |     name = models.CharField(max_length=20) | ||||||
|     pretty_name = models.CharField(max_length=20) |     pretty_name = models.CharField(max_length=20) | ||||||
| 
 | 
 | ||||||
|     def get_absolute_url(self): |     def get_absolute_url(self): | ||||||
| @ -72,19 +71,9 @@ 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 = models.ForeignKey('Platform', on_delete=models.CASCADE) | ||||||
|         '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,11 +1,17 @@ | |||||||
| from rest_framework import serializers | from rest_framework import serializers | ||||||
| 
 | 
 | ||||||
| from .models import Archive | from .models import Archive, Ticket | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class PublicArchiveUploadSerializer(serializers.ModelSerializer): | class ArchiveUploadSerializer(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,18 +93,4 @@ $(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,34 +16,4 @@ | |||||||
| <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 %} | ||||||
|     <ul id="div-ticket-{{ ticket.number }}" class="list-group mb-2"> |     <div id="div-ticket-{{ ticket.number }}" class="list-group mb-2"> | ||||||
|       <li class="list-group-item list-group-item-action disable" aria-current="true"> |         <div 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 }}"> | ||||||
| @ -66,7 +66,7 @@ | |||||||
|             <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" | ||||||
|             ><i class="bi bi-arrow-return-right"></i> Open</a> |             >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> | ||||||
|         </li> |         </div> | ||||||
|     </ul> |       </div> | ||||||
|     <!-- Modal ticket --> |     <!-- Modal ticket --> | ||||||
|     {% include 'collector/includes/modal_ticket.html' %} |     {% include 'collector/includes/modal_ticket.html' %} | ||||||
|     <!-- Modal archive --> |     <!-- Modal archive --> | ||||||
|  | |||||||
| @ -7,7 +7,9 @@ from . import views | |||||||
| app_name = 'collector' | app_name = 'collector' | ||||||
| 
 | 
 | ||||||
| router = routers.DefaultRouter() | router = routers.DefaultRouter() | ||||||
| router.register(r'archives', views.PublicArchiveUploadViewSet) | router.register(r'archives', views.ArchiveUploadViewSet) | ||||||
|  | router.register(r'tickets/create', views.TicketCreateViewSet) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| urlpatterns = [ | urlpatterns = [ | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,5 +1,4 @@ | |||||||
| 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 | ||||||
| @ -8,7 +7,6 @@ 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 | ||||||
| @ -18,7 +16,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 PublicArchiveUploadSerializer | from .serializers import ArchiveUploadSerializer, TicketSerializer | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ArchiveHandlerView(LoginRequiredMixin, SingleObjectMixin, generic.View): | class ArchiveHandlerView(LoginRequiredMixin, SingleObjectMixin, generic.View): | ||||||
| @ -174,51 +172,20 @@ class DeleteTicketHandler(SingleObjectMixin, generic.View): | |||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class PublicArchiveUploadViewSet(mixins.CreateModelMixin, GenericViewSet): | class ArchiveUploadViewSet(mixins.CreateModelMixin, GenericViewSet): | ||||||
|     queryset = Archive.objects.order_by('-time_create') |     queryset = Archive.objects.order_by('-time_create') | ||||||
|     serializer_class = PublicArchiveUploadSerializer |     serializer_class = ArchiveUploadSerializer | ||||||
|     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(ticket=self.request.data['ticket']) |         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) | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user