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