Compare commits

...

4 Commits

12 changed files with 223 additions and 37 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)
});
}); });

View File

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

View File

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

View File

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

View File

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