Compare commits

...

4 Commits

12 changed files with 223 additions and 37 deletions

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

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