Compare commits
	
		
			No commits in common. "fd19181eff26f9d011a1df2c438aab5aebb86301" and "90d7e64db3fd4252e8554c15c4d5b5c418a8cdaa" have entirely different histories.
		
	
	
		
			fd19181eff
			...
			90d7e64db3
		
	
		
| @ -1,3 +0,0 @@ | |||||||
| from django.contrib import admin |  | ||||||
| 
 |  | ||||||
| # Register your models here. |  | ||||||
| @ -1,7 +0,0 @@ | |||||||
| from django.apps import AppConfig |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class AccountConfig(AppConfig): |  | ||||||
|     default_auto_field = 'django.db.models.BigAutoField' |  | ||||||
|     name = 'account' |  | ||||||
|     verbose_name = 'Auth and account management' |  | ||||||
| @ -1,3 +0,0 @@ | |||||||
| from django.db import models |  | ||||||
| 
 |  | ||||||
| # Create your models here. |  | ||||||
| @ -1,3 +0,0 @@ | |||||||
| from django.test import TestCase |  | ||||||
| 
 |  | ||||||
| # Create your tests here. |  | ||||||
| @ -1,40 +0,0 @@ | |||||||
| from django.conf import settings |  | ||||||
| from django.urls import path |  | ||||||
| from django.contrib.auth.views import LogoutView |  | ||||||
| 
 |  | ||||||
| from rest_framework_simplejwt.views import ( |  | ||||||
|     TokenObtainPairView, |  | ||||||
|     TokenRefreshView, |  | ||||||
|     TokenVerifyView |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| app_name = 'account' |  | ||||||
| 
 |  | ||||||
| urlpatterns = [ |  | ||||||
|     # WEB LOGOUT: |  | ||||||
|     path( |  | ||||||
|         'account/logout/', |  | ||||||
|         LogoutView.as_view(next_page=settings.LOGOUT_REDIRECT_URL), |  | ||||||
|         name='logout' |  | ||||||
|     ) |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| urlpatterns += [ |  | ||||||
|     # JWT AUTH: |  | ||||||
|     path( |  | ||||||
|         'api/v1/auth/token/', |  | ||||||
|         TokenObtainPairView.as_view(), |  | ||||||
|         name='token_obtain_pair' |  | ||||||
|     ), |  | ||||||
|     path( |  | ||||||
|         'api/v1/auth/token/refresh/', |  | ||||||
|         TokenRefreshView.as_view(), |  | ||||||
|         name='token_refresh' |  | ||||||
|     ), |  | ||||||
|     path( |  | ||||||
|         'api/v1/auth/token/verify/', |  | ||||||
|         TokenVerifyView.as_view(), |  | ||||||
|         name='token_verify' |  | ||||||
|     ), |  | ||||||
| ] |  | ||||||
| @ -1,46 +0,0 @@ | |||||||
| from django.conf import settings |  | ||||||
| from django.contrib.auth import REDIRECT_FIELD_NAME |  | ||||||
| from django.contrib.auth.views import redirect_to_login |  | ||||||
| from django.http import HttpResponseRedirect |  | ||||||
| from django.shortcuts import resolve_url |  | ||||||
| from django.urls import reverse |  | ||||||
| from django.utils.http import url_has_allowed_host_and_scheme  # renamed Dj^3.* |  | ||||||
| from two_factor.admin import AdminSiteOTPRequired, AdminSiteOTPRequiredMixin |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| # https://stackoverflow.com/questions/48600737/django-two-factor-auth-cant-access-admin-site |  | ||||||
| class AdminSiteOTPRequiredMixinRedirectSetup(AdminSiteOTPRequired): |  | ||||||
|     """ |  | ||||||
|     Fixes the current implementation of django-two-factor-auth = 1.15.3 |  | ||||||
|     when admin page is patched for 2fa |  | ||||||
|     (circular redirect - super user created with manage.py |  | ||||||
|     and cannot log in because he does not have a device configured). |  | ||||||
|     The class redirects to the setup page. |  | ||||||
|     After that, you can log in as usual. |  | ||||||
|     """ |  | ||||||
|     def login(self, request, extra_context=None): |  | ||||||
|         redirect_to = request.POST.get( |  | ||||||
|             REDIRECT_FIELD_NAME, request.GET.get(REDIRECT_FIELD_NAME) |  | ||||||
|         ) |  | ||||||
|         # For users not yet verified the AdminSiteOTPRequired.has_permission |  | ||||||
|         # will fail. So use the standard admin has_permission check: |  | ||||||
|         # (is_active and is_staff) and then check for verification. |  | ||||||
|         # Go to index if they pass, otherwise make them setup OTP device. |  | ||||||
|         if request.method == "GET" and super( |  | ||||||
|             AdminSiteOTPRequiredMixin, self |  | ||||||
|         ).has_permission(request): |  | ||||||
|             # Already logged-in and verified by OTP |  | ||||||
|             if request.user.is_verified(): |  | ||||||
|                 # User has permission |  | ||||||
|                 index_path = reverse("admin:index", current_app=self.name) |  | ||||||
|             else: |  | ||||||
|                 # User has permission but no OTP set: |  | ||||||
|                 index_path = reverse("two_factor:setup", current_app=self.name) |  | ||||||
|             return HttpResponseRedirect(index_path) |  | ||||||
| 
 |  | ||||||
|         if not redirect_to or not url_has_allowed_host_and_scheme( |  | ||||||
|             url=redirect_to, allowed_hosts=[request.get_host()] |  | ||||||
|         ): |  | ||||||
|             redirect_to = resolve_url(settings.LOGIN_REDIRECT_URL) |  | ||||||
| 
 |  | ||||||
|         return redirect_to_login(redirect_to) |  | ||||||
| @ -1,39 +0,0 @@ | |||||||
| from django_filters.rest_framework import ( |  | ||||||
|     CharFilter, |  | ||||||
|     FilterSet, |  | ||||||
|     NumberFilter, |  | ||||||
| ) |  | ||||||
| from django_filters import widgets |  | ||||||
| 
 |  | ||||||
| from collector.models import Archive, Ticket |  | ||||||
| from .utils import DateTimeFilterMixin |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class ArchiveFilter(DateTimeFilterMixin, FilterSet): |  | ||||||
| 
 |  | ||||||
|     class Meta: |  | ||||||
|         model = Archive |  | ||||||
|         fields = { |  | ||||||
|             'id': ['exact', 'in', 'lte', 'gte'], |  | ||||||
|             'ticket': ['exact', 'in', 'lte', 'gte'], |  | ||||||
|             'time_create': ['exact', 'lte', 'gte'] |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class TicketFilter(DateTimeFilterMixin, FilterSet): |  | ||||||
|     number = NumberFilter( |  | ||||||
|         field_name='number', |  | ||||||
|         widget=widgets.CSVWidget(), |  | ||||||
|     ) |  | ||||||
|     user = CharFilter( |  | ||||||
|         field_name='user__username' |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     class Meta: |  | ||||||
|         model = Ticket |  | ||||||
|         fields = { |  | ||||||
|             'id': ['exact', 'in', 'lte', 'gte'], |  | ||||||
|             'number': ['exact', 'contains', 'in', 'lte', 'gte'], |  | ||||||
|             'resolved': ['exact'], |  | ||||||
|             'user': ['exact'] |  | ||||||
|         } |  | ||||||
| @ -1,13 +0,0 @@ | |||||||
| from rest_framework import permissions |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class IsGuestUpload(permissions.BasePermission): |  | ||||||
|     """ |  | ||||||
|     Special permission class for the ability to upload attachments |  | ||||||
|     to an unauthorized user using a ticket token |  | ||||||
|     """ |  | ||||||
|     def has_permission(self, request, view): |  | ||||||
|         if request.method in ('HEAD', 'OPTIONS', 'POST',): |  | ||||||
|             return True |  | ||||||
| 
 |  | ||||||
|         return request.user.is_authenticated |  | ||||||
| @ -1,60 +0,0 @@ | |||||||
| from rest_framework import serializers |  | ||||||
| 
 |  | ||||||
| from drf_spectacular.utils import extend_schema_field |  | ||||||
| from drf_spectacular.openapi import OpenApiTypes |  | ||||||
| 
 |  | ||||||
| from collector.models import Archive, Platform, Ticket |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| @extend_schema_field(OpenApiTypes.NUMBER) |  | ||||||
| class TimestampField(serializers.Field): |  | ||||||
|     def to_representation(self, value) -> int: |  | ||||||
|         return value.timestamp() |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| @extend_schema_field(OpenApiTypes.NUMBER) |  | ||||||
| class JsTimestampField(serializers.Field): |  | ||||||
|     def to_representation(self, value) -> int: |  | ||||||
|         return round(value.timestamp()*1000) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class PublicArchiveUploadSerializer(serializers.ModelSerializer): |  | ||||||
|     class Meta: |  | ||||||
|         model = Archive |  | ||||||
|         fields = ['file', 'ticket'] |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class ArchiveSerializer(serializers.ModelSerializer): |  | ||||||
|     time_create = JsTimestampField(read_only=True) |  | ||||||
| 
 |  | ||||||
|     class Meta: |  | ||||||
|         model = Archive |  | ||||||
|         fields = ['id', 'file', 'ticket', 'time_create'] |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class PlatformSerializer(serializers.ModelSerializer): |  | ||||||
| 
 |  | ||||||
|     class Meta: |  | ||||||
|         model = Platform |  | ||||||
|         fields = ['id', 'name', 'pretty_name'] |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class TicketSerializer(serializers.ModelSerializer): |  | ||||||
|     time_create = JsTimestampField(read_only=True) |  | ||||||
|     time_update = JsTimestampField(read_only=True) |  | ||||||
|     token = serializers.UUIDField(read_only=True) |  | ||||||
|     user = serializers.ReadOnlyField(source='user.username') |  | ||||||
| 
 |  | ||||||
|     class Meta: |  | ||||||
|         model = Ticket |  | ||||||
|         fields = [ |  | ||||||
|             'id', |  | ||||||
|             'number', |  | ||||||
|             'resolved', |  | ||||||
|             'token', |  | ||||||
|             'attempts', |  | ||||||
|             'platform', |  | ||||||
|             'time_create', |  | ||||||
|             'time_update', |  | ||||||
|             'user' |  | ||||||
|         ] |  | ||||||
| @ -1,21 +0,0 @@ | |||||||
| from django.urls import path, include |  | ||||||
| 
 |  | ||||||
| from rest_framework import routers |  | ||||||
| 
 |  | ||||||
| from . import views |  | ||||||
| 
 |  | ||||||
| # ▄▀█ █▀█ █ |  | ||||||
| # █▀█ █▀▀ █ |  | ||||||
| # -- -- -- |  | ||||||
| 
 |  | ||||||
| app_name = 'collector_api' |  | ||||||
| 
 |  | ||||||
| router = routers.DefaultRouter() |  | ||||||
| router.register(r'archives', views.ArchiveViewSet) |  | ||||||
| router.register(r'platforms', views.PlatformViewSet) |  | ||||||
| router.register(r'tickets', views.TicketViewSet) |  | ||||||
| 
 |  | ||||||
| urlpatterns = [ |  | ||||||
|     # CRUD: |  | ||||||
|     path('v1/', include(router.urls)), |  | ||||||
| ] |  | ||||||
| @ -1,20 +0,0 @@ | |||||||
| from django_filters import NumberFilter |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class DateTimeFilterMixin: |  | ||||||
|     year__gte = NumberFilter( |  | ||||||
|         field_name='time_create', |  | ||||||
|         lookup_expr='year__gte' |  | ||||||
|     ) |  | ||||||
|     year__lte = NumberFilter( |  | ||||||
|         field_name='time_create', |  | ||||||
|         lookup_expr='year__lte' |  | ||||||
|     ) |  | ||||||
|     month__gte = NumberFilter( |  | ||||||
|         field_name='time_create', |  | ||||||
|         lookup_expr='month__gte' |  | ||||||
|     ) |  | ||||||
|     month__lte = NumberFilter( |  | ||||||
|         field_name='time_create', |  | ||||||
|         lookup_expr='month__lte' |  | ||||||
|     ) |  | ||||||
| @ -1,94 +0,0 @@ | |||||||
| from django.core.exceptions import ValidationError, ObjectDoesNotExist |  | ||||||
| 
 |  | ||||||
| from rest_framework import status |  | ||||||
| # from rest_framework.decorators import action |  | ||||||
| from rest_framework.parsers import FormParser, MultiPartParser |  | ||||||
| from rest_framework.permissions import IsAuthenticated |  | ||||||
| from rest_framework.response import Response |  | ||||||
| from rest_framework import viewsets |  | ||||||
| from rest_framework import filters |  | ||||||
| 
 |  | ||||||
| from django_filters.rest_framework import DjangoFilterBackend |  | ||||||
| 
 |  | ||||||
| from collector.models import Archive, Ticket, Platform |  | ||||||
| 
 |  | ||||||
| from .filters import ArchiveFilter, TicketFilter |  | ||||||
| from .permissions import IsGuestUpload |  | ||||||
| from .serializers import ( |  | ||||||
|     PublicArchiveUploadSerializer, |  | ||||||
|     ArchiveSerializer, |  | ||||||
|     PlatformSerializer, |  | ||||||
|     TicketSerializer |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class ArchiveViewSet(viewsets.ModelViewSet): |  | ||||||
|     queryset = Archive.objects.order_by('-time_create') |  | ||||||
|     serializer_class = ArchiveSerializer |  | ||||||
|     parser_classes = (MultiPartParser, FormParser) |  | ||||||
|     permission_classes = (IsGuestUpload, ) |  | ||||||
|     filter_backends = [DjangoFilterBackend] |  | ||||||
|     filterset_class = ArchiveFilter |  | ||||||
| 
 |  | ||||||
|     def create(self, request, *args, **kwargs): |  | ||||||
|         # ! upload-token protection: |  | ||||||
|         upload_token = request.headers.get('upload-token', '') |  | ||||||
|         if not request.user.is_authenticated and 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 number to request.data from user |  | ||||||
|                 request.data['ticket'] = bound_ticket.number |  | ||||||
|                 # ? change serializer for guest user |  | ||||||
|                 self.serializer_class = PublicArchiveUploadSerializer |  | ||||||
|             except (ValidationError, ObjectDoesNotExist,): |  | ||||||
|                 return Response( |  | ||||||
|                     {'error': f'token {upload_token} is not valid'}, |  | ||||||
|                     status=status.HTTP_403_FORBIDDEN |  | ||||||
|                 ) |  | ||||||
|         elif not request.user.is_authenticated: |  | ||||||
|             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 |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class PlatformViewSet(viewsets.ModelViewSet): |  | ||||||
|     queryset = Platform.objects.all() |  | ||||||
|     lookup_field = 'name' |  | ||||||
|     serializer_class = PlatformSerializer |  | ||||||
|     permission_classes = (IsAuthenticated, ) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class TicketViewSet(viewsets.ModelViewSet): |  | ||||||
|     queryset = Ticket.objects.order_by('-time_create') |  | ||||||
|     lookup_field = 'number' |  | ||||||
|     serializer_class = TicketSerializer |  | ||||||
|     permission_classes = (IsAuthenticated, ) |  | ||||||
|     filter_backends = [DjangoFilterBackend, filters.SearchFilter] |  | ||||||
|     filterset_class = TicketFilter |  | ||||||
|     search_fields = ['number'] |  | ||||||
| 
 |  | ||||||
|     def perform_create(self, serializer): |  | ||||||
|         serializer.save(user=self.request.user) |  | ||||||
| @ -4,4 +4,3 @@ from django.apps import AppConfig | |||||||
| class CollectorConfig(AppConfig): | class CollectorConfig(AppConfig): | ||||||
|     default_auto_field = 'django.db.models.BigAutoField' |     default_auto_field = 'django.db.models.BigAutoField' | ||||||
|     name = 'collector' |     name = 'collector' | ||||||
|     verbose_name = 'Collector archives for analyse' |  | ||||||
|  | |||||||
| @ -1,13 +1,11 @@ | |||||||
| # Generated by Django 4.2 on 2023-08-15 03:58 | # Generated by Django 4.2 on 2023-07-28 14:40 | ||||||
| 
 | 
 | ||||||
| import collector.utils | import collector.utils | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| import django.core.files.storage | import django.core.files.storage | ||||||
| import django.core.validators |  | ||||||
| from django.db import migrations, models | from django.db import migrations, models | ||||||
| import django.db.models.deletion | import django.db.models.deletion | ||||||
| import pathlib | import pathlib | ||||||
| import uuid |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Migration(migrations.Migration): | class Migration(migrations.Migration): | ||||||
| @ -23,7 +21,7 @@ class Migration(migrations.Migration): | |||||||
|             name='Platform', |             name='Platform', | ||||||
|             fields=[ |             fields=[ | ||||||
|                 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |                 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|                 ('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)), | ||||||
|             ], |             ], | ||||||
|         ), |         ), | ||||||
| @ -31,29 +29,26 @@ class Migration(migrations.Migration): | |||||||
|             name='Ticket', |             name='Ticket', | ||||||
|             fields=[ |             fields=[ | ||||||
|                 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |                 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|                 ('number', models.IntegerField(db_index=True, unique=True)), |                 ('number', models.IntegerField()), | ||||||
|                 ('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, unique=True)), |  | ||||||
|                 ('attempts', models.IntegerField(default=5, validators=[django.core.validators.MaxValueValidator(10), django.core.validators.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(db_column='platform_name', on_delete=django.db.models.deletion.CASCADE, to='collector.platform', to_field='name')), |                 ('platform', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='collector.platform')), | ||||||
|                 ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), |                 ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), | ||||||
|             ], |             ], | ||||||
|             options={ |  | ||||||
|                 'ordering': ['-time_create'], |  | ||||||
|             }, |  | ||||||
|         ), |         ), | ||||||
|         migrations.CreateModel( |         migrations.CreateModel( | ||||||
|             name='Archive', |             name='Archive', | ||||||
|             fields=[ |             fields=[ | ||||||
|                 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |                 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|                 ('file', models.FileField(blank=True, null=True, storage=django.core.files.storage.FileSystemStorage(base_url='/archives/', location=pathlib.PurePosixPath('/home/stepan/Documents/Dev/ISPsystem/logs-collector/logs_collector/archives')), upload_to=collector.utils.logs_dir_path)), |                 ('file', models.FileField(blank=True, null=True, storage=django.core.files.storage.FileSystemStorage(base_url='/archives/', location=pathlib.PurePosixPath('/home/stepan/Documents/Dev/ISPsystem/logs-collector/logs_collector/archives')), upload_to=collector.utils.logs_dir_path)), | ||||||
|                 ('md5', models.CharField(editable=False, max_length=1024)), |                 ('size', models.CharField(blank=True, max_length=50)), | ||||||
|  |                 ('sha1', models.CharField(editable=False, max_length=1024)), | ||||||
|                 ('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)), | ||||||
|                 ('ticket', models.ForeignKey(db_column='ticket_number', on_delete=django.db.models.deletion.CASCADE, to='collector.ticket', to_field='number')), |                 ('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='collector.ticket')), | ||||||
|  |                 ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), | ||||||
|             ], |             ], | ||||||
|         ), |         ), | ||||||
|     ] |     ] | ||||||
|  | |||||||
| @ -0,0 +1,27 @@ | |||||||
|  | # Generated by Django 4.2 on 2023-08-05 11:14 | ||||||
|  | 
 | ||||||
|  | from django.db import migrations, models | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  | 
 | ||||||
|  |     dependencies = [ | ||||||
|  |         ('collector', '0001_initial'), | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AlterModelOptions( | ||||||
|  |             name='ticket', | ||||||
|  |             options={'ordering': ['-time_create']}, | ||||||
|  |         ), | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name='archive', | ||||||
|  |             name='size', | ||||||
|  |             field=models.CharField(blank=True, editable=False, max_length=50), | ||||||
|  |         ), | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name='ticket', | ||||||
|  |             name='number', | ||||||
|  |             field=models.IntegerField(db_index=True, unique=True), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -0,0 +1,19 @@ | |||||||
|  | # Generated by Django 4.2 on 2023-08-08 05:52 | ||||||
|  | 
 | ||||||
|  | from django.db import migrations, models | ||||||
|  | import django.db.models.deletion | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  | 
 | ||||||
|  |     dependencies = [ | ||||||
|  |         ('collector', '0002_alter_ticket_options_alter_archive_size_and_more'), | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name='archive', | ||||||
|  |             name='ticket', | ||||||
|  |             field=models.ForeignKey(db_column='ticket_number', on_delete=django.db.models.deletion.CASCADE, to='collector.ticket', to_field='number'), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -0,0 +1,22 @@ | |||||||
|  | # Generated by Django 4.2 on 2023-08-08 09:17 | ||||||
|  | 
 | ||||||
|  | from django.db import migrations | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  | 
 | ||||||
|  |     dependencies = [ | ||||||
|  |         ('collector', '0003_alter_archive_ticket'), | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     operations = [ | ||||||
|  |         migrations.RenameField( | ||||||
|  |             model_name='archive', | ||||||
|  |             old_name='sha1', | ||||||
|  |             new_name='md5', | ||||||
|  |         ), | ||||||
|  |         migrations.RemoveField( | ||||||
|  |             model_name='archive', | ||||||
|  |             name='size', | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -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', | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -41,7 +41,7 @@ class Archive(models.Model): | |||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|     def save(self, *args, **kwargs): |     def save(self, *args, **kwargs): | ||||||
|         # calculate md5 hash sum and write md5 field to db |         # calculate sha 1 hash sum and write md5 field to db | ||||||
|         with self.file.open('rb') as f: |         with self.file.open('rb') as f: | ||||||
|             md5 = hashlib.md5() |             md5 = hashlib.md5() | ||||||
|             for byte_block in iter(lambda: f.read(4096), b""): |             for byte_block in iter(lambda: f.read(4096), b""): | ||||||
| @ -72,7 +72,7 @@ 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(unique=True, default=uuid.uuid4, editable=False) |     token = models.UUIDField(default=uuid.uuid4, editable=False) | ||||||
|     attempts = models.IntegerField(default=5, validators=[ |     attempts = models.IntegerField(default=5, validators=[ | ||||||
|             MaxValueValidator(10), |             MaxValueValidator(10), | ||||||
|             MinValueValidator(0) |             MinValueValidator(0) | ||||||
|  | |||||||
							
								
								
									
										11
									
								
								logs_collector/collector/serializers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,11 @@ | |||||||
|  | from rest_framework import serializers | ||||||
|  | 
 | ||||||
|  | from .models import Archive | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class PublicArchiveUploadSerializer(serializers.ModelSerializer): | ||||||
|  |     ticket = serializers.ReadOnlyField(source='ticket.token') | ||||||
|  | 
 | ||||||
|  |     class Meta: | ||||||
|  |         model = Archive | ||||||
|  |         fields = ['file', 'ticket'] | ||||||
| @ -12,7 +12,7 @@ $(function () { | |||||||
|         const archiveListElement = $(this).attr("data-jq-archive-target"); |         const archiveListElement = $(this).attr("data-jq-archive-target"); | ||||||
|         const delUrl = $(this).attr("href"); |         const delUrl = $(this).attr("href"); | ||||||
|         $.ajax({ |         $.ajax({ | ||||||
|             type: "DELETE", |             type: "delete", | ||||||
|             url: delUrl, |             url: delUrl, | ||||||
|             headers: { |             headers: { | ||||||
|                 "X-CSRFToken":CSRF, |                 "X-CSRFToken":CSRF, | ||||||
| @ -21,12 +21,12 @@ $(function () { | |||||||
|             // beforeSend: function(xhr) {
 |             // beforeSend: function(xhr) {
 | ||||||
|             //     xhr.setRequestHeader("X-CSRFToken", csrf);
 |             //     xhr.setRequestHeader("X-CSRFToken", csrf);
 | ||||||
|             // },
 |             // },
 | ||||||
|             success: function (data, textStatus, jqXHR) { |             success: function (response) { | ||||||
|                 console.log(jqXHR.status); |                 console.log(response.status); | ||||||
|                 $(archiveListElement).hide(1500); |                 $(archiveListElement).hide(1500); | ||||||
|             }, |             }, | ||||||
|             error: function (data, textStatus, jqXHR) { |             error: function (response) { | ||||||
|                 console.log(jqXHR.status); |                 console.log(response.status); | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|     }); |     }); | ||||||
| @ -37,13 +37,16 @@ $(function () { | |||||||
|         let resolved = false; |         let resolved = false; | ||||||
|         let ticketStateUrl = $(this).attr("ticket-state-url") |         let ticketStateUrl = $(this).attr("ticket-state-url") | ||||||
|         if ($(this).attr("ticket-state-switch") === "1") { |         if ($(this).attr("ticket-state-switch") === "1") { | ||||||
|  |             console.log('Find it!!!') | ||||||
|  |             resolved = true; | ||||||
|             $(this).attr("ticket-state-switch", "0");  // disable
 |             $(this).attr("ticket-state-switch", "0");  // disable
 | ||||||
|         } else { |         } else { | ||||||
|             resolved = true; |             resolved = false; | ||||||
|             $(this).attr("ticket-state-switch", "1");  // enable
 |             $(this).attr("ticket-state-switch", "1");  // enable
 | ||||||
|         } |         } | ||||||
|  |         console.log(resolved) | ||||||
|         $.ajax({ |         $.ajax({ | ||||||
|             type: "PATCH", |             type: "POST", | ||||||
|             url: ticketStateUrl, |             url: ticketStateUrl, | ||||||
|             headers: { |             headers: { | ||||||
|                 "X-CSRFToken":CSRF, |                 "X-CSRFToken":CSRF, | ||||||
| @ -54,12 +57,11 @@ $(function () { | |||||||
|             data: JSON.stringify({ |             data: JSON.stringify({ | ||||||
|                 resolved: resolved, |                 resolved: resolved, | ||||||
|             }), |             }), | ||||||
|             success: function (data, textStatus, jqXHR) { |             success: function (response) { | ||||||
|                 console.log(jqXHR.status) |                 console.log(response.resolved) | ||||||
|             }, |             }, | ||||||
|             error: function (data, textStatus, jqXHR) { |             error: function (response) { | ||||||
|                 console.log(data) |                 console.log(response.resolved) | ||||||
|                 console.log(jqXHR.status) |  | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|     }); |     }); | ||||||
| @ -78,16 +80,16 @@ $(function () { | |||||||
|                 'X-CSRFToken':CSRF, |                 'X-CSRFToken':CSRF, | ||||||
|                 'Content-Type':'application/json' |                 'Content-Type':'application/json' | ||||||
|             }, |             }, | ||||||
|             success: function (data, textStatus, jqXHR) { |             success: function (response) { | ||||||
|                 console.log(jqXHR.status); |                 console.log(response.status); | ||||||
|                 if (delDiv.length) { |                 if (delDiv.length) { | ||||||
|                     delDiv.hide(1500); |                     delDiv.hide(1500); | ||||||
|                 } else { |                 } else { | ||||||
|                     window.location.href = redirectUrl; |                     window.location.href = redirectUrl; | ||||||
|                 } |                 } | ||||||
|             }, |             }, | ||||||
|             error: function (data, textStatus, jqXHR) { |             error: function (response) { | ||||||
|                 console.log(jqXHR.status); |                 console.log(response.status); | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|     }); |     }); | ||||||
|  | |||||||
| @ -1,14 +1,45 @@ | |||||||
| {% extends 'base.html' %} |  | ||||||
| {% load static %} | {% load static %} | ||||||
| 
 | <!DOCTYPE html> | ||||||
| {% block collector_head %} | <html lang="en" data-bs-theme="auto"> | ||||||
|   <title>{% block title %}{% endblock title %}</title> |   <head> | ||||||
| {% endblock collector_head %} |     <meta charset="UTF-8" /> | ||||||
| 
 |     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||||
| {% block collector_content %} |     <link | ||||||
|  |       rel="stylesheet" | ||||||
|  |       href="{% static 'collector/css/bootstrap.min.css' %}" | ||||||
|  |       rel="stylesheet" | ||||||
|  |     > | ||||||
|  |     <link | ||||||
|  |       rel="apple-touch-icon" | ||||||
|  |       sizes="180x180" | ||||||
|  |       href="{% static 'collector/img/apple-touch-icon.png' %}" | ||||||
|  |     > | ||||||
|  |     <link | ||||||
|  |       rel="icon" | ||||||
|  |       type="image/png" | ||||||
|  |       sizes="32x32" | ||||||
|  |       href="{% static 'collector/img/favicon-32x32.png' %}" | ||||||
|  |     > | ||||||
|  |     <link | ||||||
|  |       rel="icon" | ||||||
|  |       type="image/png" | ||||||
|  |       sizes="16x16" | ||||||
|  |       href="{% static 'collector/img/favicon-16x16.png' %}" | ||||||
|  |     > | ||||||
|  |     <link  | ||||||
|  |       rel="manifest" | ||||||
|  |       href="{% static 'collector/img/site.webmanifest' %}" | ||||||
|  |     > | ||||||
|  |     <link  | ||||||
|  |       rel="stylesheet" | ||||||
|  |       href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" | ||||||
|  |     > | ||||||
|  |     {% block title %}{% endblock title %} | ||||||
|  |   </head> | ||||||
|  |   <body> | ||||||
|     <header> |     <header> | ||||||
|       <section> |       <section> | ||||||
|       {% include 'includes/navigation.html' %} |         {% include 'collector/includes/navigation.html' %} | ||||||
|       </section> |       </section> | ||||||
|     </header> |     </header> | ||||||
|     <main> |     <main> | ||||||
| @ -21,10 +52,10 @@ | |||||||
|         {% block footer %}{% endblock footer %} |         {% block footer %}{% endblock footer %} | ||||||
|       </section> |       </section> | ||||||
|     </footer> |     </footer> | ||||||
| {% endblock collector_content %} |     <script src="{% static 'collector/js/bootstrap.bundle.min.js' %}"></script> | ||||||
| 
 |     <script src="{% static 'collector/js/bs.theme.mode.js' %}"></script> | ||||||
| {% block collector_scripts %} |  | ||||||
|     <script src="{% static 'collector/js/jquery-3.7.0.min.js' %}"></script> |     <script src="{% static 'collector/js/jquery-3.7.0.min.js' %}"></script> | ||||||
|     {% block bs %}{% endblock bs %} |     {% block bs %}{% endblock bs %} | ||||||
|     {% block jquery %}{% endblock jquery %} |     {% block jquery %}{% endblock jquery %} | ||||||
| {% endblock collector_scripts %} |   </body> | ||||||
|  | </html> | ||||||
|  | |||||||
| @ -30,7 +30,7 @@ | |||||||
|         >Cancel |         >Cancel | ||||||
|         </button> |         </button> | ||||||
|         <a |         <a | ||||||
|           href="{% url 'collector_api:archive-detail' archive.id %}" |           href="{% url 'collector:download' archive.file %}" | ||||||
|           type="button" |           type="button" | ||||||
|           class="btn btn-danger btn-archive-eraser" |           class="btn btn-danger btn-archive-eraser" | ||||||
|           data-bs-dismiss="modal" |           data-bs-dismiss="modal" | ||||||
|  | |||||||
| @ -27,7 +27,7 @@ | |||||||
|         </button> |         </button> | ||||||
|         <a |         <a | ||||||
|           type="button" |           type="button" | ||||||
|           href="{% url 'collector_api:ticket-detail' ticket.number %}" |           href="{% url 'collector:ajax_delete_ticket' ticket.number %}" | ||||||
|           class="btn btn-danger btn-ticket-del" |           class="btn btn-danger btn-ticket-del" | ||||||
|           data-bs-dismiss="modal" |           data-bs-dismiss="modal" | ||||||
|           data-jq-ticket-del-target="#div-ticket-{{ ticket.number }}" |           data-jq-ticket-del-target="#div-ticket-{{ ticket.number }}" | ||||||
|  | |||||||
| @ -1,5 +1,20 @@ | |||||||
| {% load collector_extras %} | {% load collector_extras %} | ||||||
| {% get_platforms as platforms %} | {% get_platforms as platforms %} | ||||||
|  | <svg xmlns="http://www.w3.org/2000/svg" class="d-none"> | ||||||
|  |   <symbol id="check2" viewBox="0 0 16 16"> | ||||||
|  |     <path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/> | ||||||
|  |   </symbol> | ||||||
|  |   <symbol id="circle-half" fill="currentColor" viewBox="0 0 16 16"> | ||||||
|  |     <path d="M8 15A7 7 0 1 0 8 1v14zm0 1A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/> | ||||||
|  |   </symbol> | ||||||
|  |   <symbol id="moon-stars-fill" fill="currentColor" viewBox="0 0 16 16"> | ||||||
|  |     <path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"/> | ||||||
|  |     <path d="M10.794 3.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387a1.734 1.734 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.734 1.734 0 0 0 9.31 6.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387a1.734 1.734 0 0 0 1.097-1.097l.387-1.162zM13.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732l-.774-.258a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L13.863.1z"/> | ||||||
|  |   </symbol> | ||||||
|  |   <symbol id="sun-fill" fill="currentColor" viewBox="0 0 16 16"> | ||||||
|  |     <path d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"/> | ||||||
|  |   </symbol> | ||||||
|  | </svg> | ||||||
| <nav class="navbar navbar-expand-lg bg-body-tertiary"> | <nav class="navbar navbar-expand-lg bg-body-tertiary"> | ||||||
|   <div class="container"> |   <div class="container"> | ||||||
|     <a  |     <a  | ||||||
| @ -90,29 +105,12 @@ | |||||||
|               {% if request.user.is_staff %} |               {% if request.user.is_staff %} | ||||||
|               <li> |               <li> | ||||||
|                 <a class="dropdown-item" type="button" href="{% url 'admin:index' %}" |                 <a class="dropdown-item" type="button" href="{% url 'admin:index' %}" | ||||||
|                 ><i class="bi bi-shield-shaded"></i> Admin</a> |                 ><i class="bi bi-shield-shaded"></i></i> Admin</a> | ||||||
|               </li> |               </li> | ||||||
|               {% endif %} |               {% endif %} | ||||||
|               <li> |               <li><button class="dropdown-item" type="button"><i class="bi bi-gear"></i> Settings</button></li> | ||||||
|                 <a class="dropdown-item" type="button" href="{% url 'swagger-ui' %}" |  | ||||||
|                 ><i class="bi bi-braces-asterisk"></i> Swagger</a> |  | ||||||
|               </li> |  | ||||||
|               <li> |  | ||||||
|                 <a |  | ||||||
|                   href="{% url 'two_factor:profile' %}" |  | ||||||
|                   class="dropdown-item" |  | ||||||
|                   type="button"> |  | ||||||
|                     <i class="bi bi-gear"></i> Settings |  | ||||||
|                 </a> |  | ||||||
|               </li> |  | ||||||
|               <li><hr class="dropdown-divider" /></li> |               <li><hr class="dropdown-divider" /></li> | ||||||
|               <li> |               <li><button class="dropdown-item" type="button"><i class="bi bi-door-closed"></i> Logout</button></li>            | ||||||
|                 <a |  | ||||||
|                   href="{% url 'account:logout' %}" |  | ||||||
|                   class="dropdown-item" |  | ||||||
|                   type="button"><i class="bi bi-door-closed"></i> Logout |  | ||||||
|                 </a> |  | ||||||
|               </li>            |  | ||||||
|             </ul> |             </ul> | ||||||
|           </div>               |           </div>               | ||||||
|         </li> |         </li> | ||||||
| @ -121,7 +119,7 @@ | |||||||
|           <div class="btn-group"> |           <div class="btn-group"> | ||||||
|             <a |             <a | ||||||
|               type="button" |               type="button" | ||||||
|               href="{% url 'two_factor:login' %}" |               href="{% url 'admin:index' %}" | ||||||
|               class="btn btn-outline-secondary" |               class="btn btn-outline-secondary" | ||||||
|             > |             > | ||||||
|               <i class="bi bi-box-arrow-in-right"></i></i> Login |               <i class="bi bi-box-arrow-in-right"></i></i> Login | ||||||
| @ -137,7 +135,42 @@ | |||||||
|         <!-- Theme switcher--> |         <!-- Theme switcher--> | ||||||
|         <li class="nav-item dropdown"> |         <li class="nav-item dropdown"> | ||||||
|           <div class="dropdown bd-mode-toggle"> |           <div class="dropdown bd-mode-toggle"> | ||||||
|             {% include 'includes/theme_switcher.html' %} |             <button class="btn btn-bd-primary py-2 dropdown-toggle d-flex align-items-center" | ||||||
|  |                     id="bd-theme" | ||||||
|  |                     type="button" | ||||||
|  |                     aria-expanded="false" | ||||||
|  |                     data-bs-toggle="dropdown" | ||||||
|  |                     aria-label="Toggle theme (auto)"> | ||||||
|  |               <svg class="bi my-1 theme-icon-active" width="1em" height="1em"><use href="#circle-half"></use></svg> | ||||||
|  |               <span class="visually-hidden" id="bd-theme-text">Toggle theme</span> | ||||||
|  |             </button> | ||||||
|  |             <ul class="dropdown-menu dropdown-menu-end shadow" aria-labelledby="bd-theme-text"> | ||||||
|  |               <li> | ||||||
|  |                 <button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="light" aria-pressed="false"> | ||||||
|  |                   <svg class="bi me-2 opacity-50 theme-icon" width="1em" height="1em"><use href="#sun-fill"></use></svg> | ||||||
|  |                   Light | ||||||
|  |                   <svg class="bi ms-auto d-none" width="1em" height="1em"><use href="#check2"></use></svg> | ||||||
|  |                 </button> | ||||||
|  |               </li> | ||||||
|  |               <li> | ||||||
|  |                 <button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="dark" aria-pressed="false"> | ||||||
|  |                   <svg class="bi me-2 opacity-50 theme-icon" width="1em" height="1em"><use href="#moon-stars-fill"></use></svg> | ||||||
|  |                   Dark | ||||||
|  |                   <svg class="bi ms-auto d-none" width="1em" height="1em"><use href="#check2"></use></svg> | ||||||
|  |                 </button> | ||||||
|  |               </li> | ||||||
|  |               <li> | ||||||
|  |                 <button | ||||||
|  |                   type="button" | ||||||
|  |                   class="dropdown-item d-flex align-items-center active" | ||||||
|  |                   data-bs-theme-value="auto" aria-pressed="true" | ||||||
|  |                 > | ||||||
|  |                   <svg class="bi me-2 opacity-50 theme-icon" width="1em" height="1em"><use href="#circle-half"></use></svg> | ||||||
|  |                   Auto | ||||||
|  |                   <svg class="bi ms-auto d-none" width="1em" height="1em"><use href="#check2"></use></svg> | ||||||
|  |                 </button> | ||||||
|  |               </li> | ||||||
|  |             </ul> | ||||||
|           </div> |           </div> | ||||||
|         </li> |         </li> | ||||||
|       </ul> |       </ul> | ||||||
| @ -9,7 +9,7 @@ | |||||||
|     type="checkbox" |     type="checkbox" | ||||||
|     role="switch" |     role="switch" | ||||||
|     name="ticket-state" |     name="ticket-state" | ||||||
|     ticket-state-url="{% url 'collector_api:ticket-detail' ticket.number %}" |     ticket-state-url="{% url 'collector:ajax_update_state_ticket' ticket.platform.name ticket.number %}" | ||||||
|     {% if ticket.resolved %} ticket-state-switch="1" {% endif %} |     {% if ticket.resolved %} ticket-state-switch="1" {% endif %} | ||||||
|     {% if ticket.resolved %} checked {% endif %}> |     {% if ticket.resolved %} checked {% endif %}> | ||||||
| </div> | </div> | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| {% extends 'collector/base.html' %} | {% extends 'collector/base.html' %} | ||||||
| {% load static %} | {% load static %} | ||||||
| {% load collector_extras %} | {% load collector_extras %} | ||||||
| {% block title %} {{ title }} {% endblock title %} | {% block title %}<title>{{ title }}</title>{% endblock title %} | ||||||
| {% block main %}  | {% block main %}  | ||||||
| <div class="container mt-3"> | <div class="container mt-3"> | ||||||
|   <div class="row"> |   <div class="row"> | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| {% extends 'collector/base.html' %} | {% extends 'collector/base.html' %} | ||||||
| {% load static %} | {% load static %} | ||||||
| {% load crispy_forms_tags %} | {% load crispy_forms_tags %} | ||||||
| {% block title %} {{ title }} {% endblock title %} | {% block title %}<title>{{ title }}</title>{% endblock title %} | ||||||
| {% block main %} | {% block main %} | ||||||
| <div class="container mt-3"> | <div class="container mt-3"> | ||||||
|     <div class="card"> |     <div class="card"> | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| {% extends 'collector/base.html' %} | {% extends 'collector/base.html' %} | ||||||
| {% load static %} | {% load static %} | ||||||
| {% load collector_extras %} | {% load collector_extras %} | ||||||
| {% block title %} {{ title }} {% endblock title %} | {% block title %}<title>{{ title }}</title>{% endblock title %} | ||||||
| {% block main %} | {% block main %} | ||||||
| <div class="container mt-3"> | <div class="container mt-3"> | ||||||
|   {% csrf_token %} |   {% csrf_token %} | ||||||
|  | |||||||
| @ -1,14 +1,20 @@ | |||||||
| from django.urls import path | from django.urls import path, include | ||||||
|  | 
 | ||||||
|  | from rest_framework import routers | ||||||
| 
 | 
 | ||||||
| from . import views | from . import views | ||||||
| 
 | 
 | ||||||
| app_name = 'collector' | app_name = 'collector' | ||||||
| 
 | 
 | ||||||
|  | router = routers.DefaultRouter() | ||||||
|  | router.register(r'archives', views.PublicArchiveUploadViewSet) | ||||||
|  | 
 | ||||||
|  | urlpatterns = [ | ||||||
|  | 
 | ||||||
|     # █░█░█ █▀▀ █▄▄ |     # █░█░█ █▀▀ █▄▄ | ||||||
|     # ▀▄▀▄▀ ██▄ █▄█ |     # ▀▄▀▄▀ ██▄ █▄█ | ||||||
|     # -- -- -- -- -- |     # -- -- -- -- -- | ||||||
| 
 | 
 | ||||||
| urlpatterns = [ |  | ||||||
|     # CREATE: |     # CREATE: | ||||||
|     path( |     path( | ||||||
|         'tickets/create/', |         'tickets/create/', | ||||||
| @ -47,4 +53,28 @@ urlpatterns = [ | |||||||
|         views.UpdateTicket.as_view(), |         views.UpdateTicket.as_view(), | ||||||
|         name='update' |         name='update' | ||||||
|     ), |     ), | ||||||
|  | 
 | ||||||
|  |     # ▄▀█ ░░█ ▄▀█ ▀▄▀ | ||||||
|  |     # █▀█ █▄█ █▀█ █░█ | ||||||
|  |     # -- -- -- -- -- | ||||||
|  | 
 | ||||||
|  |     # UPDATE: | ||||||
|  |     path( | ||||||
|  |         'ajax/tickets/update/<slug:platform>/<int:ticket>/', | ||||||
|  |         views.UpdateTicketStateHandler.as_view(), | ||||||
|  |         name='ajax_update_state_ticket' | ||||||
|  |     ), | ||||||
|  |     # DELETE: | ||||||
|  |     path( | ||||||
|  |         'ajax/tickets/delete/<int:ticket>/', | ||||||
|  |         views.DeleteTicketHandler.as_view(), | ||||||
|  |         name='ajax_delete_ticket' | ||||||
|  |     ), | ||||||
|  | 
 | ||||||
|  |     # ▄▀█ █▀█ █ | ||||||
|  |     # █▀█ █▀▀ █ | ||||||
|  |     # -- -- -- | ||||||
|  | 
 | ||||||
|  |     # CREATE: | ||||||
|  |     path('api/v1/', include(router.urls)) | ||||||
| ] | ] | ||||||
|  | |||||||
| @ -2,14 +2,11 @@ import os | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def logs_dir_path(instance, filename): | def logs_dir_path(instance, filename): | ||||||
|     """ |     # file will be uploaded to | ||||||
|         file will be uploaded to |     # MEDIA_ROOT_FOR_SENSITIVE_FILES/<ticket>/<filename> | ||||||
|         MEDIA_ROOT_FOR_SENSITIVE_FILES/<ticket-token>/<filename> |     return f'{instance.ticket}/{filename}' | ||||||
|     """ |  | ||||||
|     return f'{instance.ticket.number}/{filename}' |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| # deprecated |  | ||||||
| def get_file_size(file_path, unit='bytes'): | def get_file_size(file_path, unit='bytes'): | ||||||
|     file_size = os.path.getsize(file_path) |     file_size = os.path.getsize(file_path) | ||||||
|     exponents_map = {'bytes': 0, 'kb': 1, 'mb': 2, 'gb': 3} |     exponents_map = {'bytes': 0, 'kb': 1, 'mb': 2, 'gb': 3} | ||||||
| @ -21,7 +18,6 @@ def get_file_size(file_path, unit='bytes'): | |||||||
|         return round(size, 3) |         return round(size, 3) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| # deprecated |  | ||||||
| def is_ajax(request): | def is_ajax(request): | ||||||
|     if request.headers.get('X-Requested-With') == 'XMLHttpRequest': |     if request.headers.get('X-Requested-With') == 'XMLHttpRequest': | ||||||
|         return True |         return True | ||||||
|  | |||||||
| @ -1,21 +1,27 @@ | |||||||
|  | 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 | from django.http import FileResponse, JsonResponse | ||||||
| from django.views import generic | from django.views import generic | ||||||
| from django.views.generic.detail import SingleObjectMixin | from django.views.generic.detail import SingleObjectMixin | ||||||
|  | from django.urls import reverse_lazy | ||||||
| from django.db.models import Q | from django.db.models import Q | ||||||
| 
 | 
 | ||||||
| from two_factor.views import OTPRequiredMixin | from rest_framework import status | ||||||
|  | from rest_framework.response import Response | ||||||
|  | from rest_framework.parsers import FormParser, MultiPartParser | ||||||
|  | 
 | ||||||
|  | from rest_framework import mixins | ||||||
|  | from rest_framework.viewsets import GenericViewSet | ||||||
| 
 | 
 | ||||||
| from .forms import TicketForm |  | ||||||
| from .models import Archive, Ticket | from .models import Archive, Ticket | ||||||
| from .utils import PageTitleViewMixin | from .forms import TicketForm | ||||||
|  | from .utils import PageTitleViewMixin, is_ajax | ||||||
|  | 
 | ||||||
|  | from .serializers import PublicArchiveUploadSerializer | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ArchiveHandlerView( | class ArchiveHandlerView(LoginRequiredMixin, SingleObjectMixin, generic.View): | ||||||
|         OTPRequiredMixin, |  | ||||||
|         LoginRequiredMixin, |  | ||||||
|         SingleObjectMixin, |  | ||||||
|         generic.View): |  | ||||||
|     model = Archive |     model = Archive | ||||||
|     slug_field = 'file' |     slug_field = 'file' | ||||||
|     slug_url_kwarg = 'path' |     slug_url_kwarg = 'path' | ||||||
| @ -24,6 +30,12 @@ class ArchiveHandlerView( | |||||||
|         self.object = self.get_object() |         self.object = self.get_object() | ||||||
|         return FileResponse(self.object.file) |         return FileResponse(self.object.file) | ||||||
| 
 | 
 | ||||||
|  |     def delete(self, request, path): | ||||||
|  |         if is_ajax(request): | ||||||
|  |             self.object = self.get_object() | ||||||
|  |             self.object.delete() | ||||||
|  |             return JsonResponse({'file': path}, status=status.HTTP_200_OK) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| class CreateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.CreateView): | class CreateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.CreateView): | ||||||
|     model = Ticket |     model = Ticket | ||||||
| @ -49,11 +61,12 @@ class UpdateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.UpdateView): | |||||||
|         return f'{self.title} - {self.kwargs.get("ticket", "update")}' |         return f'{self.title} - {self.kwargs.get("ticket", "update")}' | ||||||
| 
 | 
 | ||||||
|     def form_valid(self, form): |     def form_valid(self, form): | ||||||
|  |         print(self.request.user) | ||||||
|         form.instance.user = self.request.user |         form.instance.user = self.request.user | ||||||
|         return super().form_valid(form) |         return super().form_valid(form) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ListAllTickets(LoginRequiredMixin, PageTitleViewMixin, generic.ListView): | class ListAllTickets(PageTitleViewMixin, generic.ListView): | ||||||
|     model = Ticket |     model = Ticket | ||||||
|     template_name = 'collector/tickets.html' |     template_name = 'collector/tickets.html' | ||||||
|     context_object_name = 'tickets' |     context_object_name = 'tickets' | ||||||
| @ -72,17 +85,13 @@ class ListAllTickets(LoginRequiredMixin, PageTitleViewMixin, generic.ListView): | |||||||
|             queryset = self.model.objects.filter( |             queryset = self.model.objects.filter( | ||||||
|                 Q(number__in=query_list) | Q(number__icontains=query_list[0]) |                 Q(number__in=query_list) | Q(number__icontains=query_list[0]) | ||||||
|             ) |             ) | ||||||
|             self.paginate_by = 100  # ? fake disable pagination) |             self.paginate_by = 100  # fake disable pagination) | ||||||
|             return queryset |             return queryset | ||||||
| 
 | 
 | ||||||
|         return super().get_queryset() |         return super().get_queryset() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ListPlatformTickets( | class ListPlatformTickets(PageTitleViewMixin, generic.ListView): | ||||||
|         LoginRequiredMixin, |  | ||||||
|         PageTitleViewMixin, |  | ||||||
|         generic.ListView |  | ||||||
|         ): |  | ||||||
|     model = Ticket |     model = Ticket | ||||||
|     template_name = 'collector/tickets.html' |     template_name = 'collector/tickets.html' | ||||||
|     context_object_name = 'tickets' |     context_object_name = 'tickets' | ||||||
| @ -98,7 +107,7 @@ class ListPlatformTickets( | |||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class DetailTicket(LoginRequiredMixin, PageTitleViewMixin, generic.DetailView): | class DetailTicket(PageTitleViewMixin, generic.DetailView): | ||||||
|     model = Ticket |     model = Ticket | ||||||
|     template_name = 'collector/ticket.html' |     template_name = 'collector/ticket.html' | ||||||
|     context_object_name = 'ticket' |     context_object_name = 'ticket' | ||||||
| @ -107,3 +116,109 @@ class DetailTicket(LoginRequiredMixin, PageTitleViewMixin, generic.DetailView): | |||||||
| 
 | 
 | ||||||
|     def get_title(self, **kwargs): |     def get_title(self, **kwargs): | ||||||
|         return f'{self.title} - {self.kwargs.get("ticket", "show")}' |         return f'{self.title} - {self.kwargs.get("ticket", "show")}' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class DeleteTicket(PageTitleViewMixin, generic.DeleteView): | ||||||
|  |     model = Ticket | ||||||
|  |     template_name = 'collector/ticket_delete.html' | ||||||
|  |     context_object_name = 'ticket' | ||||||
|  |     slug_field = 'number' | ||||||
|  |     slug_url_kwarg = 'ticket' | ||||||
|  |     success_url = reverse_lazy('tickets') | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class UpdateTicketStateHandler(SingleObjectMixin, generic.View): | ||||||
|  |     model = Ticket | ||||||
|  |     slug_field = 'number' | ||||||
|  |     slug_url_kwarg = 'ticket' | ||||||
|  | 
 | ||||||
|  |     def post(self, request, **kwargs): | ||||||
|  |         if is_ajax(request): | ||||||
|  |             self.object = self.get_object() | ||||||
|  |             if request.body: | ||||||
|  |                 data = json.loads(request.body) | ||||||
|  |                 resolved_field = data.get('resolved') | ||||||
|  |                 if isinstance(resolved_field, bool): | ||||||
|  |                     self.object.resolved = not resolved_field | ||||||
|  |                     self.object.save() | ||||||
|  |                     return JsonResponse( | ||||||
|  |                         {'resolved': not resolved_field}, | ||||||
|  |                         status=status.HTTP_201_CREATED | ||||||
|  |                     ) | ||||||
|  |                 return JsonResponse( | ||||||
|  |                     {'resolved': 'must be a boolean'}, | ||||||
|  |                     status=status.HTTP_400_BAD_REQUEST | ||||||
|  |                 ) | ||||||
|  |         return JsonResponse( | ||||||
|  |             {'error': 'header XMLHttpRequest is required'}, | ||||||
|  |             status=status.HTTP_406_NOT_ACCEPTABLE | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class DeleteTicketHandler(SingleObjectMixin, generic.View): | ||||||
|  |     model = Ticket | ||||||
|  |     slug_field = 'number' | ||||||
|  |     slug_url_kwarg = 'ticket' | ||||||
|  | 
 | ||||||
|  |     def delete(self, request, ticket): | ||||||
|  |         if is_ajax(request): | ||||||
|  |             self.object = self.get_object() | ||||||
|  |             self.object.delete() | ||||||
|  |             return JsonResponse( | ||||||
|  |                 {'status': status.HTTP_200_OK}, | ||||||
|  |                 status=status.HTTP_200_OK | ||||||
|  |             ) | ||||||
|  |         return JsonResponse( | ||||||
|  |             {'error': 'header XMLHttpRequest is required'}, | ||||||
|  |             status=status.HTTP_406_NOT_ACCEPTABLE | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class PublicArchiveUploadViewSet(mixins.CreateModelMixin, GenericViewSet): | ||||||
|  |     queryset = Archive.objects.order_by('-time_create') | ||||||
|  |     serializer_class = PublicArchiveUploadSerializer | ||||||
|  |     parser_classes = (MultiPartParser, FormParser) | ||||||
|  | 
 | ||||||
|  |     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(ticket=self.request.data['ticket']) | ||||||
|  | |||||||
| @ -1,12 +1,10 @@ | |||||||
| import environ | import environ | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from datetime import timedelta |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| env = environ.Env( | env = environ.Env( | ||||||
|     # set casting default value |     # set casting default value | ||||||
|     DEBUG=(bool, False), |     DEBUG=(bool, False) | ||||||
|     SECRET_KEY=(str, 'j9QGbvM9Z4otb47') |  | ||||||
| ) | ) | ||||||
| # Build paths inside the project like this: BASE_DIR / 'subdir'. | # Build paths inside the project like this: BASE_DIR / 'subdir'. | ||||||
| BASE_DIR = Path(__file__).resolve().parent.parent | BASE_DIR = Path(__file__).resolve().parent.parent | ||||||
| @ -36,18 +34,9 @@ INSTALLED_APPS = [ | |||||||
|     'django.contrib.messages', |     'django.contrib.messages', | ||||||
|     'django.contrib.staticfiles', |     'django.contrib.staticfiles', | ||||||
|     'collector.apps.CollectorConfig',  # main app |     'collector.apps.CollectorConfig',  # main app | ||||||
|     'account.apps.AccountConfig',  # account app |  | ||||||
|     'rest_framework', |     'rest_framework', | ||||||
|     'rest_framework_simplejwt', |  | ||||||
|     'django_filters', |  | ||||||
|     'drf_spectacular', |  | ||||||
|     "crispy_forms", |     "crispy_forms", | ||||||
|     "crispy_bootstrap5", |     "crispy_bootstrap5", | ||||||
|     'django_otp', |  | ||||||
|     'django_otp.plugins.otp_static', |  | ||||||
|     'django_otp.plugins.otp_totp', |  | ||||||
|     'two_factor.plugins.phonenumber',  # <- if you want phone number capability |  | ||||||
|     'two_factor', |  | ||||||
|     'django_cleanup.apps.CleanupConfig',  # required bottom |     'django_cleanup.apps.CleanupConfig',  # required bottom | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| @ -57,7 +46,6 @@ MIDDLEWARE = [ | |||||||
|     'django.middleware.common.CommonMiddleware', |     'django.middleware.common.CommonMiddleware', | ||||||
|     'django.middleware.csrf.CsrfViewMiddleware', |     'django.middleware.csrf.CsrfViewMiddleware', | ||||||
|     'django.contrib.auth.middleware.AuthenticationMiddleware', |     'django.contrib.auth.middleware.AuthenticationMiddleware', | ||||||
|     'django_otp.middleware.OTPMiddleware', |  | ||||||
|     'django.contrib.messages.middleware.MessageMiddleware', |     'django.contrib.messages.middleware.MessageMiddleware', | ||||||
|     'django.middleware.clickjacking.XFrameOptionsMiddleware', |     'django.middleware.clickjacking.XFrameOptionsMiddleware', | ||||||
| ] | ] | ||||||
| @ -67,7 +55,7 @@ ROOT_URLCONF = 'logs_collector.urls' | |||||||
| TEMPLATES = [ | TEMPLATES = [ | ||||||
|     { |     { | ||||||
|         'BACKEND': 'django.template.backends.django.DjangoTemplates', |         'BACKEND': 'django.template.backends.django.DjangoTemplates', | ||||||
|         'DIRS': [BASE_DIR / 'templates'], |         'DIRS': [], | ||||||
|         'APP_DIRS': True, |         'APP_DIRS': True, | ||||||
|         'OPTIONS': { |         'OPTIONS': { | ||||||
|             'context_processors': [ |             'context_processors': [ | ||||||
| @ -118,7 +106,7 @@ AUTH_PASSWORD_VALIDATORS = [ | |||||||
| 
 | 
 | ||||||
| LANGUAGE_CODE = 'en-us' | LANGUAGE_CODE = 'en-us' | ||||||
| 
 | 
 | ||||||
| TIME_ZONE = env('TZ', default='UTC') | TIME_ZONE = 'Asia/Irkutsk' | ||||||
| 
 | 
 | ||||||
| USE_I18N = True | USE_I18N = True | ||||||
| 
 | 
 | ||||||
| @ -129,9 +117,6 @@ USE_TZ = True | |||||||
| # https://docs.djangoproject.com/en/4.2/howto/static-files/ | # https://docs.djangoproject.com/en/4.2/howto/static-files/ | ||||||
| 
 | 
 | ||||||
| STATIC_URL = 'static/' | STATIC_URL = 'static/' | ||||||
| STATICFILES_DIRS = [ |  | ||||||
|     BASE_DIR / "static", |  | ||||||
| ] |  | ||||||
| 
 | 
 | ||||||
| # Default primary key field type | # Default primary key field type | ||||||
| # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field | # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field | ||||||
| @ -150,83 +135,3 @@ MEDIA_URL_FOR_SENSITIVE_FILES = '/archives/' | |||||||
| CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" | CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" | ||||||
| 
 | 
 | ||||||
| CRISPY_TEMPLATE_PACK = "bootstrap5" | CRISPY_TEMPLATE_PACK = "bootstrap5" | ||||||
| 
 |  | ||||||
| # https://www.django-rest-framework.org/api-guide/settings/ |  | ||||||
| 
 |  | ||||||
| REST_FRAMEWORK = { |  | ||||||
|     'DEFAULT_RENDERER_CLASSES': [ |  | ||||||
|         'rest_framework.renderers.JSONRenderer', |  | ||||||
|         'rest_framework.renderers.BrowsableAPIRenderer', |  | ||||||
|     ], |  | ||||||
|     'DEFAULT_PARSER_CLASSES': [ |  | ||||||
|         'rest_framework.parsers.JSONParser', |  | ||||||
|         'rest_framework.renderers.BrowsableAPIRenderer', |  | ||||||
|         'rest_framework.parsers.MultiPartParser' |  | ||||||
|     ], |  | ||||||
|     'DEFAULT_AUTHENTICATION_CLASSES': [ |  | ||||||
|         'rest_framework_simplejwt.authentication.JWTAuthentication', |  | ||||||
|         'rest_framework.authentication.SessionAuthentication', |  | ||||||
|         'rest_framework.authentication.BasicAuthentication' |  | ||||||
|     ], |  | ||||||
|     'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend', ],  # noqa:E501 |  | ||||||
|     'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', |  | ||||||
|     # 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',  # noqa:E501 |  | ||||||
|     # 'PAGE_SIZE': 3, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| # https://drf-spectacular.readthedocs.io/en/latest/readme.html |  | ||||||
| # TODO: set environ vars config! |  | ||||||
| 
 |  | ||||||
| SPECTACULAR_SETTINGS = { |  | ||||||
|     'TITLE': 'Logs collector API', |  | ||||||
|     'DESCRIPTION': 'Collector of archives with log files for further analysis', |  | ||||||
|     'VERSION': '0.1.0', |  | ||||||
|     'SERVE_INCLUDE_SCHEMA': True, |  | ||||||
|     'SERVE_PUBLIC': False, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| # https://django-rest-framework-simplejwt.readthedocs.io/en/latest/ |  | ||||||
| 
 |  | ||||||
| SIMPLE_JWT = { |  | ||||||
|     "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60), |  | ||||||
|     "REFRESH_TOKEN_LIFETIME": timedelta(days=1), |  | ||||||
|     "ROTATE_REFRESH_TOKENS": False, |  | ||||||
|     "BLACKLIST_AFTER_ROTATION": False, |  | ||||||
|     "UPDATE_LAST_LOGIN": False, |  | ||||||
| 
 |  | ||||||
|     "ALGORITHM": "HS256", |  | ||||||
|     "SIGNING_KEY": SECRET_KEY, |  | ||||||
|     "VERIFYING_KEY": "", |  | ||||||
|     "AUDIENCE": None, |  | ||||||
|     "ISSUER": None, |  | ||||||
|     "JSON_ENCODER": None, |  | ||||||
|     "JWK_URL": None, |  | ||||||
|     "LEEWAY": 0, |  | ||||||
| 
 |  | ||||||
|     "AUTH_HEADER_TYPES": ("Bearer",), |  | ||||||
|     "AUTH_HEADER_NAME": "HTTP_AUTHORIZATION", |  | ||||||
|     "USER_ID_FIELD": "id", |  | ||||||
|     "USER_ID_CLAIM": "user_id", |  | ||||||
|     "USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule",  # noqa:E501 |  | ||||||
| 
 |  | ||||||
|     "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",), |  | ||||||
|     "TOKEN_TYPE_CLAIM": "token_type", |  | ||||||
|     "TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser", |  | ||||||
| 
 |  | ||||||
|     "JTI_CLAIM": "jti", |  | ||||||
| 
 |  | ||||||
|     "SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp", |  | ||||||
|     "SLIDING_TOKEN_LIFETIME": timedelta(minutes=5), |  | ||||||
|     "SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1), |  | ||||||
| 
 |  | ||||||
|     "TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainPairSerializer",  # noqa:E501 |  | ||||||
|     "TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSerializer",  # noqa:E501 |  | ||||||
|     "TOKEN_VERIFY_SERIALIZER": "rest_framework_simplejwt.serializers.TokenVerifySerializer",  # noqa:E501 |  | ||||||
|     "TOKEN_BLACKLIST_SERIALIZER": "rest_framework_simplejwt.serializers.TokenBlacklistSerializer",  # noqa:E501 |  | ||||||
|     "SLIDING_TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainSlidingSerializer",  # noqa:E501 |  | ||||||
|     "SLIDING_TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSlidingSerializer",  # noqa:E501 |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| LOGIN_URL = 'two_factor:login' |  | ||||||
| LOGIN_REDIRECT_URL = 'collector:index' |  | ||||||
| LOGOUT_REDIRECT_URL = 'two_factor:login' |  | ||||||
|  | |||||||
| @ -1,62 +1,31 @@ | |||||||
|  | """ | ||||||
|  | URL configuration for logs_collector project. | ||||||
|  | 
 | ||||||
|  | The `urlpatterns` list routes URLs to views. For more information please see: | ||||||
|  |     https://docs.djangoproject.com/en/4.2/topics/http/urls/ | ||||||
|  | Examples: | ||||||
|  | Function views | ||||||
|  |     1. Add an import:  from my_app import views | ||||||
|  |     2. Add a URL to urlpatterns:  path('', views.home, name='home') | ||||||
|  | Class-based views | ||||||
|  |     1. Add an import:  from other_app.views import Home | ||||||
|  |     2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home') | ||||||
|  | Including another URLconf | ||||||
|  |     1. Import the include() function: from django.urls import include, path | ||||||
|  |     2. Add a URL to urlpatterns:  path('blog/', include('blog.urls')) | ||||||
|  | """ | ||||||
| from django.conf.urls.static import static | from django.conf.urls.static import static | ||||||
| from django.contrib import admin | from django.contrib import admin | ||||||
| from django.urls import path, include | from django.urls import path, include | ||||||
| 
 | 
 | ||||||
| from drf_spectacular.views import ( |  | ||||||
|     SpectacularAPIView, |  | ||||||
|     SpectacularRedocView, |  | ||||||
|     SpectacularSwaggerView |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| from two_factor.urls import urlpatterns as tf_urls |  | ||||||
| 
 |  | ||||||
| from logs_collector import settings | from logs_collector import settings | ||||||
| from account.utils import AdminSiteOTPRequiredMixinRedirectSetup |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| # ? 2FA patch (Admin site protection) |  | ||||||
| admin.site.__class__ = AdminSiteOTPRequiredMixinRedirectSetup |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| urlpatterns = [ | urlpatterns = [ | ||||||
|     path( |     path('admin/', admin.site.urls), | ||||||
|         'admin/', |     path('', include('collector.urls', namespace='collector')), | ||||||
|         admin.site.urls |  | ||||||
|     ), |  | ||||||
|     path( |  | ||||||
|         '', |  | ||||||
|         include('collector.urls', namespace='collector') |  | ||||||
|     ), |  | ||||||
|     path( |  | ||||||
|         '', |  | ||||||
|         include(tf_urls) |  | ||||||
|     ), |  | ||||||
|     path( |  | ||||||
|         '', |  | ||||||
|         include('account.urls', namespace='account') |  | ||||||
|     ), |  | ||||||
|     path( |  | ||||||
|         'api/', |  | ||||||
|         include('collector.api.urls', namespace='collector_api') |  | ||||||
|     ), |  | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| # SWAGGER URLS: |  | ||||||
| urlpatterns += [ |  | ||||||
|     # API PATTERNS |  | ||||||
|     path('api/v1/schema/', SpectacularAPIView.as_view(), name='schema'), |  | ||||||
|     # Optional UI: |  | ||||||
|     path( |  | ||||||
|         'api/v1/schema/swagger/', |  | ||||||
|         SpectacularSwaggerView.as_view(url_name='schema'), |  | ||||||
|         name='swagger-ui' |  | ||||||
|     ), |  | ||||||
|     path( |  | ||||||
|         'api/v1/schema/redoc/', |  | ||||||
|         SpectacularRedocView.as_view(url_name='schema'), |  | ||||||
|         name='redoc' |  | ||||||
|     ), |  | ||||||
| ] |  | ||||||
| 
 | 
 | ||||||
| if settings.DEBUG: | if settings.DEBUG: | ||||||
|     urlpatterns += static( |     urlpatterns += static( | ||||||
|  | |||||||
							
								
								
									
										6
									
								
								logs_collector/static/css/bootstrap.min.css
									
									
									
									
										vendored
									
									
								
							
							
						
						| Before Width: | Height: | Size: 7.0 KiB | 
| Before Width: | Height: | Size: 23 KiB | 
| Before Width: | Height: | Size: 6.9 KiB | 
| Before Width: | Height: | Size: 495 B | 
| Before Width: | Height: | Size: 930 B | 
| Before Width: | Height: | Size: 15 KiB | 
| @ -1 +0,0 @@ | |||||||
| {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} |  | ||||||
| @ -1,90 +0,0 @@ | |||||||
| /*! |  | ||||||
|  * Color mode toggler for Bootstrap's docs (https://getbootstrap.com/)
 |  | ||||||
|  * Copyright 2011-2023 The Bootstrap Authors |  | ||||||
|  * Licensed under the Creative Commons Attribution 3.0 Unported License. |  | ||||||
|  */ |  | ||||||
| 
 |  | ||||||
| ;(() => { |  | ||||||
|   "use strict" |  | ||||||
| 
 |  | ||||||
|   const getStoredTheme = () => localStorage.getItem("theme") |  | ||||||
|   const setStoredTheme = (theme) => localStorage.setItem("theme", theme) |  | ||||||
| 
 |  | ||||||
|   const getPreferredTheme = () => { |  | ||||||
|     const storedTheme = getStoredTheme() |  | ||||||
|     if (storedTheme) { |  | ||||||
|       return storedTheme |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return window.matchMedia("(prefers-color-scheme: dark)").matches |  | ||||||
|       ? "dark" |  | ||||||
|       : "light" |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   const setTheme = (theme) => { |  | ||||||
|     if ( |  | ||||||
|       theme === "auto" && |  | ||||||
|       window.matchMedia("(prefers-color-scheme: dark)").matches |  | ||||||
|     ) { |  | ||||||
|       document.documentElement.setAttribute("data-bs-theme", "dark") |  | ||||||
|     } else { |  | ||||||
|       document.documentElement.setAttribute("data-bs-theme", theme) |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   setTheme(getPreferredTheme()) |  | ||||||
| 
 |  | ||||||
|   const showActiveTheme = (theme, focus = false) => { |  | ||||||
|     const themeSwitcher = document.querySelector("#bd-theme") |  | ||||||
| 
 |  | ||||||
|     if (!themeSwitcher) { |  | ||||||
|       return |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const themeSwitcherText = document.querySelector("#bd-theme-text") |  | ||||||
|     const activeThemeIcon = document.querySelector(".theme-icon-active use") |  | ||||||
|     const btnToActive = document.querySelector( |  | ||||||
|       `[data-bs-theme-value="${theme}"]` |  | ||||||
|     ) |  | ||||||
|     const svgOfActiveBtn = btnToActive |  | ||||||
|       .querySelector("svg use") |  | ||||||
|       .getAttribute("href") |  | ||||||
| 
 |  | ||||||
|     document.querySelectorAll("[data-bs-theme-value]").forEach((element) => { |  | ||||||
|       element.classList.remove("active") |  | ||||||
|       element.setAttribute("aria-pressed", "false") |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     btnToActive.classList.add("active") |  | ||||||
|     btnToActive.setAttribute("aria-pressed", "true") |  | ||||||
|     activeThemeIcon.setAttribute("href", svgOfActiveBtn) |  | ||||||
|     const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})` |  | ||||||
|     themeSwitcher.setAttribute("aria-label", themeSwitcherLabel) |  | ||||||
| 
 |  | ||||||
|     if (focus) { |  | ||||||
|       themeSwitcher.focus() |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   window |  | ||||||
|     .matchMedia("(prefers-color-scheme: dark)") |  | ||||||
|     .addEventListener("change", () => { |  | ||||||
|       const storedTheme = getStoredTheme() |  | ||||||
|       if (storedTheme !== "light" && storedTheme !== "dark") { |  | ||||||
|         setTheme(getPreferredTheme()) |  | ||||||
|       } |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|   window.addEventListener("DOMContentLoaded", () => { |  | ||||||
|     showActiveTheme(getPreferredTheme()) |  | ||||||
| 
 |  | ||||||
|     document.querySelectorAll("[data-bs-theme-value]").forEach((toggle) => { |  | ||||||
|       toggle.addEventListener("click", () => { |  | ||||||
|         const theme = toggle.getAttribute("data-bs-theme-value") |  | ||||||
|         setStoredTheme(theme) |  | ||||||
|         setTheme(theme) |  | ||||||
|         showActiveTheme(theme, true) |  | ||||||
|       }) |  | ||||||
|     }) |  | ||||||
|   }) |  | ||||||
| })() |  | ||||||
| @ -1,6 +0,0 @@ | |||||||
| const tooltipTriggerList = document.querySelectorAll( |  | ||||||
|     '[data-bs-toggle="tooltip"]' |  | ||||||
| ) |  | ||||||
| const tooltipList = [...tooltipTriggerList].map( |  | ||||||
|     (tooltipTriggerEl) => new bootstrap.Tooltip(tooltipTriggerEl) |  | ||||||
| ) |  | ||||||
| @ -1,50 +0,0 @@ | |||||||
| {% load static %} |  | ||||||
| <!DOCTYPE html> |  | ||||||
| <html lang="en" data-bs-theme="auto"> |  | ||||||
|   <head> |  | ||||||
|     <meta charset="UTF-8" /> |  | ||||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |  | ||||||
|     <link |  | ||||||
|       rel="stylesheet" |  | ||||||
|       href="{% static '/css/bootstrap.min.css' %}" |  | ||||||
|       rel="stylesheet" |  | ||||||
|     > |  | ||||||
|     <link |  | ||||||
|       rel="apple-touch-icon" |  | ||||||
|       sizes="180x180" |  | ||||||
|       href="{% static '/img/apple-touch-icon.png' %}" |  | ||||||
|     > |  | ||||||
|     <link |  | ||||||
|       rel="icon" |  | ||||||
|       type="image/png" |  | ||||||
|       sizes="32x32" |  | ||||||
|       href="{% static '/img/favicon-32x32.png' %}" |  | ||||||
|     > |  | ||||||
|     <link |  | ||||||
|       rel="icon" |  | ||||||
|       type="image/png" |  | ||||||
|       sizes="16x16" |  | ||||||
|       href="{% static '/img/favicon-16x16.png' %}" |  | ||||||
|     > |  | ||||||
|     <link  |  | ||||||
|       rel="manifest" |  | ||||||
|       href="{% static '/img/site.webmanifest' %}" |  | ||||||
|     > |  | ||||||
|     <link  |  | ||||||
|     rel="stylesheet" |  | ||||||
|     href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" |  | ||||||
|     > |  | ||||||
|     {% block collector_head %}{% endblock collector_head %} |  | ||||||
|     {% block account_head %}{% endblock account_head %} |  | ||||||
|   </head> |  | ||||||
|   <body> |  | ||||||
|     {% block collector_content %}{% endblock collector_content %} |  | ||||||
|     {% block account_content %}{% endblock account_content %} |  | ||||||
|      |  | ||||||
|     <script src="{% static '/js/bootstrap.bundle.min.js' %}"></script> |  | ||||||
|     <script src="{% static '/js/bs.theme.mode.js' %}"></script> |  | ||||||
|      |  | ||||||
|     {% block collector_scripts %}{% endblock collector_scripts %} |  | ||||||
|     {% block account_scripts %}{% endblock account_scripts %} |  | ||||||
|   </body> |  | ||||||
| </html> |  | ||||||
| @ -1,91 +0,0 @@ | |||||||
| <!--Theme switcher icons--> |  | ||||||
| <svg xmlns="http://www.w3.org/2000/svg" class="d-none"> |  | ||||||
|   <symbol id="check2" viewBox="0 0 16 16"> |  | ||||||
|     <path |  | ||||||
|       d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" |  | ||||||
|     /> |  | ||||||
|   </symbol> |  | ||||||
|   <symbol id="circle-half" fill="currentColor" viewBox="0 0 16 16"> |  | ||||||
|     <path d="M8 15A7 7 0 1 0 8 1v14zm0 1A8 8 0 1 1 8 0a8 8 0 0 1 0 16z" /> |  | ||||||
|   </symbol> |  | ||||||
|   <symbol id="moon-stars-fill" fill="currentColor" viewBox="0 0 16 16"> |  | ||||||
|     <path |  | ||||||
|       d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z" |  | ||||||
|     /> |  | ||||||
|     <path |  | ||||||
|       d="M10.794 3.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387a1.734 1.734 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.734 1.734 0 0 0 9.31 6.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387a1.734 1.734 0 0 0 1.097-1.097l.387-1.162zM13.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732l-.774-.258a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L13.863.1z" |  | ||||||
|     /> |  | ||||||
|   </symbol> |  | ||||||
|   <symbol id="sun-fill" fill="currentColor" viewBox="0 0 16 16"> |  | ||||||
|     <path |  | ||||||
|       d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z" |  | ||||||
|     /> |  | ||||||
|   </symbol> |  | ||||||
| </svg> |  | ||||||
| <!-- Theme switcher dropdown buttons --> |  | ||||||
| <button |  | ||||||
|   class="btn btn-bd-primary py-2 dropdown-toggle d-flex align-items-center" |  | ||||||
|   id="bd-theme" |  | ||||||
|   type="button" |  | ||||||
|   aria-expanded="false" |  | ||||||
|   data-bs-toggle="dropdown" |  | ||||||
|   aria-label="Toggle theme (auto)" |  | ||||||
| > |  | ||||||
|   <svg class="bi my-1 theme-icon-active" width="1em" height="1em"> |  | ||||||
|     <use href="#circle-half"></use> |  | ||||||
|   </svg> |  | ||||||
|   <span class="visually-hidden" id="bd-theme-text">Toggle theme</span> |  | ||||||
| </button> |  | ||||||
| <ul |  | ||||||
|   class="dropdown-menu dropdown-menu-end shadow" |  | ||||||
|   aria-labelledby="bd-theme-text" |  | ||||||
| > |  | ||||||
|   <li> |  | ||||||
|     <button |  | ||||||
|       type="button" |  | ||||||
|       class="dropdown-item d-flex align-items-center" |  | ||||||
|       data-bs-theme-value="light" |  | ||||||
|       aria-pressed="false" |  | ||||||
|     > |  | ||||||
|       <svg class="bi me-2 opacity-50 theme-icon" width="1em" height="1em"> |  | ||||||
|         <use href="#sun-fill"></use> |  | ||||||
|       </svg> |  | ||||||
|       Light |  | ||||||
|       <svg class="bi ms-auto d-none" width="1em" height="1em"> |  | ||||||
|         <use href="#check2"></use> |  | ||||||
|       </svg> |  | ||||||
|     </button> |  | ||||||
|   </li> |  | ||||||
|   <li> |  | ||||||
|     <button |  | ||||||
|       type="button" |  | ||||||
|       class="dropdown-item d-flex align-items-center" |  | ||||||
|       data-bs-theme-value="dark" |  | ||||||
|       aria-pressed="false" |  | ||||||
|     > |  | ||||||
|       <svg class="bi me-2 opacity-50 theme-icon" width="1em" height="1em"> |  | ||||||
|         <use href="#moon-stars-fill"></use> |  | ||||||
|       </svg> |  | ||||||
|       Dark |  | ||||||
|       <svg class="bi ms-auto d-none" width="1em" height="1em"> |  | ||||||
|         <use href="#check2"></use> |  | ||||||
|       </svg> |  | ||||||
|     </button> |  | ||||||
|   </li> |  | ||||||
|   <li> |  | ||||||
|     <button |  | ||||||
|       type="button" |  | ||||||
|       class="dropdown-item d-flex align-items-center active" |  | ||||||
|       data-bs-theme-value="auto" |  | ||||||
|       aria-pressed="true" |  | ||||||
|     > |  | ||||||
|       <svg class="bi me-2 opacity-50 theme-icon" width="1em" height="1em"> |  | ||||||
|         <use href="#circle-half"></use> |  | ||||||
|       </svg> |  | ||||||
|       Auto |  | ||||||
|       <svg class="bi ms-auto d-none" width="1em" height="1em"> |  | ||||||
|         <use href="#check2"></use> |  | ||||||
|       </svg> |  | ||||||
|     </button> |  | ||||||
|   </li> |  | ||||||
| </ul> |  | ||||||
| Before Width: | Height: | Size: 3.9 KiB | 
| @ -1,18 +0,0 @@ | |||||||
| {% extends 'base.html' %} |  | ||||||
| {% load static %} |  | ||||||
| 
 |  | ||||||
| {% block account_head %} |  | ||||||
|   <title>Collector - {% block title %}{% endblock %}</title> |  | ||||||
|   {% block extra_media %}{% endblock %} |  | ||||||
| {% endblock account_head %} |  | ||||||
| 
 |  | ||||||
| {% block account_content %} |  | ||||||
|   {% block content_wrapper %} |  | ||||||
|     {% block content %}{% endblock %} |  | ||||||
|   {% endblock %} |  | ||||||
| <section> |  | ||||||
|   <footer> |  | ||||||
|     {% block theme_switcher %}{% endblock %} |  | ||||||
|   </footer> |  | ||||||
| </section> |  | ||||||
| {% endblock account_content %} |  | ||||||
| @ -1,22 +0,0 @@ | |||||||
| {% extends "two_factor/_base.html" %} |  | ||||||
| 
 |  | ||||||
| {% block content_wrapper %} |  | ||||||
|   <section> |  | ||||||
|     <header> |  | ||||||
|       {% block nav %}{% endblock nav %} |  | ||||||
|     </header> |  | ||||||
|   </section> |  | ||||||
|   <section> |  | ||||||
|     <main> |  | ||||||
|       <div class="container mt-3"> |  | ||||||
|         <div class="card"> |  | ||||||
|           <div class="card-body"> |  | ||||||
|             <div class="text-center"> |  | ||||||
|               {% block content %}{% endblock %} |  | ||||||
|             </div> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|     </main> |  | ||||||
|   </section> |  | ||||||
| {% endblock %} |  | ||||||
| @ -1,16 +0,0 @@ | |||||||
| {% load i18n %} |  | ||||||
| 
 |  | ||||||
| {% if cancel_url %} |  | ||||||
|   <a href="{{ cancel_url }}" class="float-right btn btn-link">{% trans "Cancel" %}</a> |  | ||||||
| {% endif %} |  | ||||||
| {% if wizard.steps.prev %} |  | ||||||
|   <button  |  | ||||||
|     name="wizard_goto_step" |  | ||||||
|     type="submit" |  | ||||||
|     value="{{ wizard.steps.prev }}" |  | ||||||
|     class="btn btn-secondary">{% trans "Back" %} |  | ||||||
|   </button> |  | ||||||
| {% else %} |  | ||||||
|   <button disabled name="" type="button" class="btn">{% trans "Back" %}</button> |  | ||||||
| {% endif %} |  | ||||||
| <button type="submit" class="btn btn-primary">{% trans "Next" %}</button> |  | ||||||
| @ -1,5 +0,0 @@ | |||||||
| {% load crispy_forms_tags %} |  | ||||||
| <div class="mb-3"> |  | ||||||
|   {{ wizard.management_form }} |  | ||||||
|   {{ wizard.form|crispy }} |  | ||||||
| </div> |  | ||||||
| @ -1,35 +0,0 @@ | |||||||
| {% extends "two_factor/_base_focus.html" %} |  | ||||||
| {% load i18n %} |  | ||||||
| {% load crispy_forms_tags %} |  | ||||||
| 
 |  | ||||||
| {% block nav %}{% include 'includes/navigation.html' %}{% endblock nav %} |  | ||||||
| 
 |  | ||||||
| {% block content %} |  | ||||||
| 
 |  | ||||||
|   <h1>{% block title %}{% trans "Backup Tokens" %}{% endblock %}</h1> |  | ||||||
|   <div class="mb-3 d-flex justify-content-center"> |  | ||||||
|     <p>{% blocktrans trimmed %}Backup tokens can be used when your primary and backup |  | ||||||
|         phone numbers aren't available. The backup tokens below can be used |  | ||||||
|         for login verification. If you've used up all your backup tokens, you |  | ||||||
|         can generate a new set of backup tokens. Only the backup tokens shown |  | ||||||
|         below will be valid.{% endblocktrans %}</p> |  | ||||||
|   </div> |  | ||||||
|   {% if device.token_set.count %} |  | ||||||
|     <ul class="list-group"> |  | ||||||
|       {% for token in device.token_set.all %} |  | ||||||
|         <li class="list-group-item list-group-item-action">{{ token.token }}</li> |  | ||||||
|       {% endfor %} |  | ||||||
|     </ul> |  | ||||||
|     <p>{% blocktrans %}Print these tokens and keep them somewhere safe.{% endblocktrans %}</p> |  | ||||||
|   {% else %} |  | ||||||
|     <p>{% trans "You don't have any backup codes yet." %}</p> |  | ||||||
|   {% endif %} |  | ||||||
| 
 |  | ||||||
|   <form method="post">{% csrf_token %}{{ form|crispy }} |  | ||||||
|     <a  |  | ||||||
|       href="{% url 'two_factor:profile'%}" |  | ||||||
|       class="float-right btn btn-link">{% trans "Back to Account Security" %} |  | ||||||
|     </a> |  | ||||||
|     <button class="btn btn-outline-primary" type="submit">{% trans "Generate Tokens" %}</button> |  | ||||||
|   </form> |  | ||||||
| {% endblock %} |  | ||||||
| @ -1,71 +0,0 @@ | |||||||
| {% extends "two_factor/_base.html" %} |  | ||||||
| {% load i18n %} |  | ||||||
| {% load two_factor_tags %} |  | ||||||
| 
 |  | ||||||
| {% block extra_media %} |  | ||||||
|   {{ form.media }} |  | ||||||
| {% endblock %} |  | ||||||
| 
 |  | ||||||
| {% block content %} |  | ||||||
| <div class="d-flex min-vh-100 align-items-center py-4" cz-shortcut-listen="true"> |  | ||||||
|   <div class="container"> |  | ||||||
|     <div class="row"> |  | ||||||
|       <div class="col-md-3 offset-md-4"> |  | ||||||
|         <h1>{% block title %}{% trans "Login" %}{% endblock %}</h1> |  | ||||||
|         {% if wizard.steps.current == 'auth' %} |  | ||||||
|           <p>{% blocktrans %}Enter your credentials.{% endblocktrans %}</p> |  | ||||||
|         {% elif wizard.steps.current == 'token' %} |  | ||||||
|           <p>{{ device|as_verbose_action }}</p> |  | ||||||
|         {% elif wizard.steps.current == 'backup' %} |  | ||||||
|           <p>{% blocktrans trimmed %}Use this form for entering backup tokens for logging in. |  | ||||||
|             These tokens have been generated for you to print and keep safe. Please |  | ||||||
|             enter one of these backup tokens to login to your account.{% endblocktrans %}</p> |  | ||||||
|         {% endif %} |  | ||||||
|        |  | ||||||
|         <form action="" method="post">{% csrf_token %} |  | ||||||
|           {% include "two_factor/_wizard_forms.html" %} |  | ||||||
|        |  | ||||||
|           {# hidden submit button to enable [enter] key #} |  | ||||||
|           <input type="submit" value="" hidden /> |  | ||||||
|        |  | ||||||
|           {% if other_devices %} |  | ||||||
|             <p>{% trans "Or, alternatively, use one of your other authentication methods:" %}</p> |  | ||||||
|             <p> |  | ||||||
|               {% for other in other_devices %} |  | ||||||
|               <button name="challenge_device" value="{{ other.persistent_id }}" |  | ||||||
|                       class="btn btn-secondary btn-block" type="submit"> |  | ||||||
|                 {{ other|as_action }} |  | ||||||
|               </button> |  | ||||||
|             {% endfor %}</p> |  | ||||||
|           {% endif %} |  | ||||||
|        |  | ||||||
|           {% include "two_factor/_wizard_actions.html" %} |  | ||||||
|         </form> |  | ||||||
|        |  | ||||||
|         {% block 'backup_tokens' %} |  | ||||||
|           {% if backup_tokens %} |  | ||||||
|             <hr> |  | ||||||
|             <div class="backup_tokens_form"> |  | ||||||
|             <form action="" method="post"> |  | ||||||
|                 {% csrf_token %} |  | ||||||
|                   <p>{% trans "As a last resort, you can use a backup token:" %}</p> |  | ||||||
|                   <p> |  | ||||||
|                       <button name="wizard_goto_step" type="submit" value="backup" |  | ||||||
|                           class="btn btn-sm btn-secondary btn-block">{% trans "Use Backup Token" %}</button> |  | ||||||
|                   </p> |  | ||||||
|             </form> |  | ||||||
|             </div> |  | ||||||
|           {% endif %} |  | ||||||
|         {% endblock %} |  | ||||||
|         </div> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| </div> |  | ||||||
| {% endblock %} |  | ||||||
| 
 |  | ||||||
| <!-- Theme switcher --> |  | ||||||
| {% block theme_switcher %} |  | ||||||
| <div class="dropdown position-fixed bottom-0 end-0 mb-3 me-3 bd-mode-toggle"> |  | ||||||
|   {% include 'includes/theme_switcher.html' %} |  | ||||||
| </div> |  | ||||||
| {% endblock theme_switcher %} |  | ||||||
| @ -1,35 +0,0 @@ | |||||||
| {% extends "two_factor/_base.html" %} |  | ||||||
| {% load i18n %} |  | ||||||
| 
 |  | ||||||
| {% block content %} |  | ||||||
| <div class="d-flex min-vh-100 align-items-center py-4" cz-shortcut-listen="true"> |  | ||||||
|   <div class="container"> |  | ||||||
|     <div class="row"> |  | ||||||
|       <div class="col-md-4 offset-md-4"> |  | ||||||
|         <h1>{% block title %}{% trans "Permission Denied" %}{% endblock %}</h1> |  | ||||||
| 
 |  | ||||||
|         <p>{% blocktrans trimmed %}The page you requested, enforces users to verify using |  | ||||||
|           two-factor authentication for security reasons. You need to enable these |  | ||||||
|           security features in order to access this page.{% endblocktrans %}</p> |  | ||||||
|            |  | ||||||
|         <p>{% blocktrans trimmed %}Two-factor authentication is not enabled for your |  | ||||||
|           account. Enable two-factor authentication for enhanced account |  | ||||||
|           security.{% endblocktrans %}</p> |  | ||||||
|         <p> |  | ||||||
|           <a href="javascript:history.go(-1)" |  | ||||||
|             class="float-right btn btn-link">{% trans "Go back" %}</a> |  | ||||||
|           <a href="{% url 'two_factor:setup' %}" class="btn btn-primary"> |  | ||||||
|           {% trans "Enable Two-Factor Authentication" %}</a> |  | ||||||
|         </p> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| </div> |  | ||||||
| {% endblock %} |  | ||||||
| 
 |  | ||||||
| <!-- Theme switcher --> |  | ||||||
| {% block theme_switcher %} |  | ||||||
| <div class="dropdown position-fixed bottom-0 end-0 mb-3 me-3 bd-mode-toggle"> |  | ||||||
|   {% include 'includes/theme_switcher.html' %} |  | ||||||
| </div> |  | ||||||
| {% endblock theme_switcher %} |  | ||||||
| @ -1,24 +0,0 @@ | |||||||
| {% extends "two_factor/_base_focus.html" %} |  | ||||||
| {% load i18n %} |  | ||||||
| 
 |  | ||||||
| {% block content %} |  | ||||||
|   <h1>{% block title %}{% trans "Add Backup Phone" %}{% endblock %}</h1> |  | ||||||
| 
 |  | ||||||
|   {% if wizard.steps.current == 'setup' %} |  | ||||||
|       <p>{% blocktrans trimmed %}You'll be adding a backup phone number to your |  | ||||||
|         account. This number will be used if your primary method of |  | ||||||
|         registration is not available.{% endblocktrans %}</p> |  | ||||||
|   {% elif wizard.steps.current == 'validation' %} |  | ||||||
|       <p>{% blocktrans trimmed %}We've sent a token to your phone number. Please |  | ||||||
|         enter the token you've received.{% endblocktrans %}</p> |  | ||||||
|   {% endif %} |  | ||||||
| 
 |  | ||||||
|   <form action="" method="post">{% csrf_token %} |  | ||||||
|     {% include "two_factor/_wizard_forms.html" %} |  | ||||||
| 
 |  | ||||||
|     {# hidden submit button to enable [enter] key #} |  | ||||||
|     <input type="submit" value="" hidden /> |  | ||||||
| 
 |  | ||||||
|     {% include "two_factor/_wizard_actions.html" %} |  | ||||||
|   </form> |  | ||||||
| {% endblock %} |  | ||||||
| @ -1,67 +0,0 @@ | |||||||
| {% extends "two_factor/_base_focus.html" %} |  | ||||||
| {% load i18n %} |  | ||||||
| 
 |  | ||||||
| {% block extra_media %} |  | ||||||
|   {{ form.media }} |  | ||||||
| {% endblock %} |  | ||||||
| 
 |  | ||||||
| {% block nav %}{% include 'includes/navigation.html' %}{% endblock nav %} |  | ||||||
| 
 |  | ||||||
| {% block content %} |  | ||||||
|   <h1>{% block title %}{% trans "Enable Two-Factor Authentication" %}{% endblock %}</h1> |  | ||||||
|   {% if wizard.steps.current == 'welcome' %} |  | ||||||
|     <p>{% blocktrans trimmed %}You are about to take your account security to the |  | ||||||
|         next level. Follow the steps in this wizard to enable two-factor |  | ||||||
|         authentication.{% endblocktrans %}</p> |  | ||||||
|   {% elif wizard.steps.current == 'method' %} |  | ||||||
|     <p>{% blocktrans trimmed %}Please select which authentication method you would |  | ||||||
|         like to use.{% endblocktrans %}</p> |  | ||||||
|   {% elif wizard.steps.current == 'generator' %} |  | ||||||
|     <p>{% blocktrans trimmed %}To start using a token generator, please use your |  | ||||||
|         smartphone to scan the QR code below. For example, use Google |  | ||||||
|         Authenticator.{% endblocktrans %}</p> |  | ||||||
|     <p><img src="{{ QR_URL }}" alt="QR Code" class="bg-white"/></p> |  | ||||||
|     <p>{% blocktrans trimmed %}Alternatively you can use the following secret to |  | ||||||
|         setup TOTP in your authenticator or password manager manually.{% endblocktrans %}</p> |  | ||||||
|     <p>{% translate "TOTP Secret:" %} <a href="{{ otpauth_url }}">{{ secret_key }}</a></p> |  | ||||||
|     <p>{% blocktrans %}Then, enter the token generated by the app.{% endblocktrans %}</p> |  | ||||||
| 
 |  | ||||||
|   {% elif wizard.steps.current == 'sms' %} |  | ||||||
|     <p>{% blocktrans trimmed %}Please enter the phone number you wish to receive the |  | ||||||
|       text messages on. This number will be validated in the next step. |  | ||||||
|       {% endblocktrans %}</p> |  | ||||||
|   {% elif wizard.steps.current == 'call' %} |  | ||||||
|     <p>{% blocktrans trimmed %}Please enter the phone number you wish to be called on. |  | ||||||
|       This number will be validated in the next step. {% endblocktrans %}</p> |  | ||||||
|   {% elif wizard.steps.current == 'validation' %} |  | ||||||
|     {% if challenge_succeeded %} |  | ||||||
|       {% if device.method == 'call' %} |  | ||||||
|         <p>{% blocktrans trimmed %}We are calling your phone right now, please enter the |  | ||||||
|           digits you hear.{% endblocktrans %}</p> |  | ||||||
|       {% elif device.method == 'sms' %} |  | ||||||
|         <p>{% blocktrans trimmed %}We sent you a text message, please enter the tokens we |  | ||||||
|           sent.{% endblocktrans %}</p> |  | ||||||
|       {% endif %} |  | ||||||
|     {% else %} |  | ||||||
|       <p class="alert alert-warning" role="alert">{% blocktrans trimmed %}We've |  | ||||||
|         encountered an issue with the selected authentication method. Please |  | ||||||
|         go back and verify that you entered your information correctly, try |  | ||||||
|         again, or use a different authentication method instead. If the issue |  | ||||||
|         persists, contact the site administrator.{% endblocktrans %}</p> |  | ||||||
|     {% endif %} |  | ||||||
|   {% elif wizard.steps.current == 'yubikey' %} |  | ||||||
|     <p>{% blocktrans trimmed %}To identify and verify your YubiKey, please insert a |  | ||||||
|       token in the field below. Your YubiKey will be linked to your |  | ||||||
|       account.{% endblocktrans %}</p> |  | ||||||
|   {% endif %} |  | ||||||
|   <div class="input-group mb-3 d-flex justify-content-center"> |  | ||||||
|     <form action="" method="post">{% csrf_token %} |  | ||||||
|       {% include "two_factor/_wizard_forms.html" %} |  | ||||||
| 
 |  | ||||||
|       {# hidden submit button to enable [enter] key #} |  | ||||||
|       <input type="submit" value="" hidden /> |  | ||||||
| 
 |  | ||||||
|       {% include "two_factor/_wizard_actions.html" %} |  | ||||||
|     </form> |  | ||||||
|   </div> |  | ||||||
| {% endblock %} |  | ||||||
| @ -1,26 +0,0 @@ | |||||||
| {% extends "two_factor/_base_focus.html" %} |  | ||||||
| {% load i18n %} |  | ||||||
| 
 |  | ||||||
| {% block nav %}{% include 'includes/navigation.html' %}{% endblock nav %} |  | ||||||
| 
 |  | ||||||
| {% block content %} |  | ||||||
|   <h1>{% block title %}{% trans "Enable Two-Factor Authentication" %}{% endblock %}</h1> |  | ||||||
| 
 |  | ||||||
|   <p>{% blocktrans trimmed %}Congratulations, you've successfully enabled two-factor |  | ||||||
|       authentication.{% endblocktrans %}</p> |  | ||||||
| 
 |  | ||||||
|   {% if not phone_methods %} |  | ||||||
|     <p><a href="{% url 'two_factor:profile' %}" |  | ||||||
|         class="btn btn-block btn-secondary">{% trans "Back to Account Security" %}</a></p> |  | ||||||
|   {% else %} |  | ||||||
|     <p>{% blocktrans trimmed %}However, it might happen that you don't have access to |  | ||||||
|       your primary token device. To enable account recovery, add a phone |  | ||||||
|       number.{% endblocktrans %}</p> |  | ||||||
| 
 |  | ||||||
|     <a href="{% url 'two_factor:profile' %}" |  | ||||||
|         class="float-right btn btn-link">{% trans "Back to Account Security" %}</a> |  | ||||||
|     <p><a href="{% url 'two_factor:phone_create' %}" |  | ||||||
|         class="btn btn-success">{% trans "Add Phone Number" %}</a></p> |  | ||||||
|   {% endif %} |  | ||||||
| 
 |  | ||||||
| {% endblock %} |  | ||||||
| @ -1,19 +0,0 @@ | |||||||
| {% extends "two_factor/_base_focus.html" %} |  | ||||||
| {% load i18n %} |  | ||||||
| {% load crispy_forms_tags %} |  | ||||||
| 
 |  | ||||||
| {% block nav %}{% include 'includes/navigation.html' %}{% endblock nav %} |  | ||||||
| 
 |  | ||||||
| {% block content %} |  | ||||||
| <h1>{% block title %}{% trans "Disable Two-factor Authentication" %}{% endblock %}</h1> |  | ||||||
| <p>{% blocktrans trimmed %}You are about to disable two-factor authentication. This |  | ||||||
|   weakens your account security, are you sure?{% endblocktrans %}</p> |  | ||||||
| <div class="input-group mb-3 d-flex justify-content-center"> |  | ||||||
|   <form method="post"> |  | ||||||
|     {% csrf_token %} |  | ||||||
|     <div>{{ form|crispy }}</div> |  | ||||||
|     <button class="btn btn-danger" type="submit">{% trans "Disable" %}</button> |  | ||||||
|   </form> |  | ||||||
| </div> |  | ||||||
| 
 |  | ||||||
| {% endblock %} |  | ||||||
| @ -1,59 +0,0 @@ | |||||||
| {% extends "two_factor/_base_focus.html" %} |  | ||||||
| {% load i18n %} |  | ||||||
| {% load two_factor_tags %} |  | ||||||
| 
 |  | ||||||
| {% block nav %}{% include 'includes/navigation.html' %}{% endblock nav %} |  | ||||||
| 
 |  | ||||||
| {% block content %} |  | ||||||
|       <h1>{% block title %}{% trans "Account Security" %}{% endblock %}</h1> |  | ||||||
|       {% if default_device %} |  | ||||||
|         <p>{% blocktrans with primary=default_device|as_action %}Primary method: {{ primary }}{% endblocktrans %}</p> |  | ||||||
| 
 |  | ||||||
|         {% if available_phone_methods %} |  | ||||||
|           <h2>{% trans "Backup Phone Numbers" %}</h2> |  | ||||||
|           <p>{% blocktrans trimmed %}If your primary method is not available, we are able to |  | ||||||
|             send backup tokens to the phone numbers listed below.{% endblocktrans %}</p> |  | ||||||
|           <ul> |  | ||||||
|             {% for phone in backup_phones %} |  | ||||||
|               <li> |  | ||||||
|                 {{ phone|as_action }} |  | ||||||
|                 <form method="post" action="{% url 'two_factor:phone_delete' phone.id %}" |  | ||||||
|                       onsubmit="return confirm({% trans 'Are you sure?' %})"> |  | ||||||
|                   {% csrf_token %} |  | ||||||
|                   <button class="btn btn-sm btn-warning" |  | ||||||
|                           type="submit">{% trans "Unregister" %}</button> |  | ||||||
|                 </form> |  | ||||||
|               </li> |  | ||||||
|             {% endfor %} |  | ||||||
|           </ul> |  | ||||||
|           <p><a href="{% url 'two_factor:phone_create' %}" |  | ||||||
|             class="btn btn-info">{% trans "Add Phone Number" %}</a></p> |  | ||||||
|         {% endif %} |  | ||||||
|            |  | ||||||
|         <h2>{% trans "Backup Tokens" %}</h2> |  | ||||||
|         <p> |  | ||||||
|           {% blocktrans trimmed %}If you don't have any device with you, you can access |  | ||||||
|             your account using backup tokens.{% endblocktrans %} |  | ||||||
|           {% blocktrans trimmed count counter=backup_tokens %} |  | ||||||
|             You have only one backup token remaining. |  | ||||||
|           {% plural %} |  | ||||||
|             You have {{ counter }} backup tokens remaining. |  | ||||||
|           {% endblocktrans %} |  | ||||||
|         </p> |  | ||||||
|         <p><a href="{% url 'two_factor:backup_tokens' %}" |  | ||||||
|               class="btn btn-info">{% trans "Show Codes" %}</a></p> |  | ||||||
| 
 |  | ||||||
|         <h3>{% trans "Disable Two-Factor Authentication" %}</h3> |  | ||||||
|         <p>{% blocktrans trimmed %}However we strongly discourage you to do so, you can |  | ||||||
|           also disable two-factor authentication for your account.{% endblocktrans %}</p> |  | ||||||
|         <p><a class="btn btn-secondary" href="{% url 'two_factor:disable' %}"> |  | ||||||
|           {% trans "Disable Two-Factor Authentication" %}</a></p> |  | ||||||
|       {% else %} |  | ||||||
|         <p>{% blocktrans trimmed %}Two-factor authentication is not enabled for your |  | ||||||
|           account. Enable two-factor authentication for enhanced account |  | ||||||
|           security.{% endblocktrans %}</p> |  | ||||||
|         <p><a href="{% url 'two_factor:setup' %}" class="btn btn-primary"> |  | ||||||
|           {% trans "Enable Two-Factor Authentication" %}</a> |  | ||||||
|         </p> |  | ||||||
|       {% endif %} |  | ||||||
| {% endblock %} |  | ||||||
| @ -1,7 +0,0 @@ | |||||||
| {% load i18n %}<?xml version="1.0" encoding="UTF-8" ?> |  | ||||||
| <Response> |  | ||||||
|   <Gather timeout="15" numDigits="1" finishOnKey=""> |  | ||||||
|     <Say language="{{ locale }}">{% blocktrans %}Hi, this is {{ site_name }} calling. Press any key to continue.{% endblocktrans %}</Say> |  | ||||||
|   </Gather> |  | ||||||
|   <Say language="{{ locale }}">{% trans "You didn’t press any keys. Good bye." %}</Say> |  | ||||||
| </Response> |  | ||||||
| @ -1,5 +0,0 @@ | |||||||
| {% load i18n %} |  | ||||||
| {% blocktrans trimmed %} |  | ||||||
|     Your OTP token is {{ token }} |  | ||||||
| {% endblocktrans %} |  | ||||||
| 
 |  | ||||||
| @ -1,12 +0,0 @@ | |||||||
| {% load i18n %}<?xml version="1.0" encoding="UTF-8" ?> |  | ||||||
| <Response> |  | ||||||
|   <Say language="{{ locale }}">{% trans "Your token is:" %}</Say> |  | ||||||
|   <Pause> |  | ||||||
| {% for digit in token %}  <Say language="{{ locale }}">{{ digit }}</Say> |  | ||||||
|   <Pause> |  | ||||||
| {% endfor %}  <Say language="{{ locale }}">{% trans "Repeat:" %}</Say> |  | ||||||
|   <Pause> |  | ||||||
| {% for digit in token %}  <Say language="{{ locale }}">{{ digit }}</Say> |  | ||||||
|   <Pause> |  | ||||||
| {% endfor %}  <Say language="{{ locale }}">{% trans "Good bye." %}</Say> |  | ||||||
| </Response> |  | ||||||
							
								
								
									
										416
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						| @ -17,35 +17,6 @@ typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} | |||||||
| [package.extras] | [package.extras] | ||||||
| tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] | tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] | ||||||
| 
 | 
 | ||||||
| [[package]] |  | ||||||
| name = "attrs" |  | ||||||
| version = "23.1.0" |  | ||||||
| description = "Classes Without Boilerplate" |  | ||||||
| optional = false |  | ||||||
| python-versions = ">=3.7" |  | ||||||
| files = [ |  | ||||||
|     {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, |  | ||||||
|     {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [package.extras] |  | ||||||
| cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] |  | ||||||
| dev = ["attrs[docs,tests]", "pre-commit"] |  | ||||||
| docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] |  | ||||||
| tests = ["attrs[tests-no-zope]", "zope-interface"] |  | ||||||
| tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] |  | ||||||
| 
 |  | ||||||
| [[package]] |  | ||||||
| name = "colorama" |  | ||||||
| version = "0.4.6" |  | ||||||
| description = "Cross-platform colored terminal text." |  | ||||||
| optional = false |  | ||||||
| python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" |  | ||||||
| files = [ |  | ||||||
|     {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, |  | ||||||
|     {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "crispy-bootstrap5" | name = "crispy-bootstrap5" | ||||||
| version = "0.7" | version = "0.7" | ||||||
| @ -125,96 +96,6 @@ develop = ["coverage[toml] (>=5.0a4)", "furo (>=2021.8.17b43,<2021.9.dev0)", "py | |||||||
| docs = ["furo (>=2021.8.17b43,<2021.9.dev0)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] | docs = ["furo (>=2021.8.17b43,<2021.9.dev0)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] | ||||||
| testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)"] | testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)"] | ||||||
| 
 | 
 | ||||||
| [[package]] |  | ||||||
| name = "django-filter" |  | ||||||
| version = "23.2" |  | ||||||
| description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically." |  | ||||||
| optional = false |  | ||||||
| python-versions = ">=3.7" |  | ||||||
| files = [ |  | ||||||
|     {file = "django-filter-23.2.tar.gz", hash = "sha256:2fe15f78108475eda525692813205fa6f9e8c1caf1ae65daa5862d403c6dbf00"}, |  | ||||||
|     {file = "django_filter-23.2-py3-none-any.whl", hash = "sha256:d12d8e0fc6d3eb26641e553e5d53b191eb8cec611427d4bdce0becb1f7c172b5"}, |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [package.dependencies] |  | ||||||
| Django = ">=3.2" |  | ||||||
| 
 |  | ||||||
| [[package]] |  | ||||||
| name = "django-formtools" |  | ||||||
| version = "2.4.1" |  | ||||||
| description = "A set of high-level abstractions for Django forms" |  | ||||||
| optional = false |  | ||||||
| python-versions = ">=3.7" |  | ||||||
| files = [ |  | ||||||
|     {file = "django-formtools-2.4.1.tar.gz", hash = "sha256:21f8d5dac737f1e636fa8a0a10969c1c32f525a6dfa27c29592827ba70d9643a"}, |  | ||||||
|     {file = "django_formtools-2.4.1-py3-none-any.whl", hash = "sha256:49ea8a64ddef4728a558bf5f8f622c0f4053b979edcf193bf00dd80432ab2f12"}, |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [package.dependencies] |  | ||||||
| Django = ">=3.2" |  | ||||||
| 
 |  | ||||||
| [[package]] |  | ||||||
| name = "django-otp" |  | ||||||
| version = "1.2.2" |  | ||||||
| description = "A pluggable framework for adding two-factor authentication to Django using one-time passwords." |  | ||||||
| optional = false |  | ||||||
| python-versions = ">=3.7" |  | ||||||
| files = [ |  | ||||||
|     {file = "django_otp-1.2.2-py3-none-any.whl", hash = "sha256:90765d5dac238a719f9550ac05681dd6307f513a81a10b6adb879b4abc6bc1a3"}, |  | ||||||
|     {file = "django_otp-1.2.2.tar.gz", hash = "sha256:007a6354dabb3a1a54574bf73abf045ebbde0bb8734a38e2ed7845ba450f345e"}, |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [package.dependencies] |  | ||||||
| django = ">=3.2" |  | ||||||
| 
 |  | ||||||
| [package.extras] |  | ||||||
| qrcode = ["qrcode"] |  | ||||||
| 
 |  | ||||||
| [[package]] |  | ||||||
| name = "django-phonenumber-field" |  | ||||||
| version = "6.4.0" |  | ||||||
| description = "An international phone number field for django models." |  | ||||||
| optional = false |  | ||||||
| python-versions = ">=3.7" |  | ||||||
| files = [ |  | ||||||
|     {file = "django-phonenumber-field-6.4.0.tar.gz", hash = "sha256:72a3e7a3e7493bf2a12c07a3bc77ce89813acc16592bf04d0eee3b5a452097ed"}, |  | ||||||
|     {file = "django_phonenumber_field-6.4.0-py3-none-any.whl", hash = "sha256:a31b4f05ac0ff898661516c84940f83adb5cdcf0ae4b9b1d31bb8ad3ff345b58"}, |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [package.dependencies] |  | ||||||
| Django = ">=3.2" |  | ||||||
| 
 |  | ||||||
| [package.extras] |  | ||||||
| phonenumbers = ["phonenumbers (>=7.0.2)"] |  | ||||||
| phonenumberslite = ["phonenumberslite (>=7.0.2)"] |  | ||||||
| 
 |  | ||||||
| [[package]] |  | ||||||
| name = "django-two-factor-auth" |  | ||||||
| version = "1.15.3" |  | ||||||
| description = "Complete Two-Factor Authentication for Django" |  | ||||||
| optional = false |  | ||||||
| python-versions = "*" |  | ||||||
| files = [ |  | ||||||
|     {file = "django-two-factor-auth-1.15.3.tar.gz", hash = "sha256:311b7f0c5ee47ae5c3734f7810f90c9390b3aef556f58a767b0d80d6b54013fb"}, |  | ||||||
|     {file = "django_two_factor_auth-1.15.3-py3-none-any.whl", hash = "sha256:a5752732225304ba0461ec6f5347def517bdde98685e90bb7879aa066c91f3a4"}, |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [package.dependencies] |  | ||||||
| Django = ">=3.2" |  | ||||||
| django-formtools = "*" |  | ||||||
| django-otp = ">=0.8.0" |  | ||||||
| django-phonenumber-field = ">=1.1.0,<7" |  | ||||||
| phonenumberslite = {version = ">=7.0.9,<8.99", optional = true, markers = "extra == \"phonenumberslite\""} |  | ||||||
| qrcode = ">=4.0.0,<7.99" |  | ||||||
| 
 |  | ||||||
| [package.extras] |  | ||||||
| call = ["twilio (>=6.0)"] |  | ||||||
| phonenumbers = ["phonenumbers (>=7.0.9,<8.99)"] |  | ||||||
| phonenumberslite = ["phonenumberslite (>=7.0.9,<8.99)"] |  | ||||||
| sms = ["twilio (>=6.0)"] |  | ||||||
| webauthn = ["pydantic (>=1.9.0,<1.99)", "webauthn (>=1.6.0,<1.99)"] |  | ||||||
| yubikey = ["django-otp-yubikey"] |  | ||||||
| 
 |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "djangorestframework" | name = "djangorestframework" | ||||||
| version = "3.14.0" | version = "3.14.0" | ||||||
| @ -254,29 +135,6 @@ lint = ["flake8", "isort", "pep8"] | |||||||
| python-jose = ["python-jose (==3.3.0)"] | python-jose = ["python-jose (==3.3.0)"] | ||||||
| test = ["cryptography", "pytest", "pytest-cov", "pytest-django", "pytest-xdist", "tox"] | test = ["cryptography", "pytest", "pytest-cov", "pytest-django", "pytest-xdist", "tox"] | ||||||
| 
 | 
 | ||||||
| [[package]] |  | ||||||
| name = "drf-spectacular" |  | ||||||
| version = "0.26.4" |  | ||||||
| description = "Sane and flexible OpenAPI 3 schema generation for Django REST framework" |  | ||||||
| optional = false |  | ||||||
| python-versions = ">=3.6" |  | ||||||
| files = [ |  | ||||||
|     {file = "drf-spectacular-0.26.4.tar.gz", hash = "sha256:8f5a8f87353d1bb8dcb3f3909b7109b2dcbe1d91f3e069409cf322963e140bd6"}, |  | ||||||
|     {file = "drf_spectacular-0.26.4-py3-none-any.whl", hash = "sha256:afeccc6533dcdb4e78afbfcc49f3c5e9c369aeb62f965e4d1a43b165449c147a"}, |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [package.dependencies] |  | ||||||
| Django = ">=2.2" |  | ||||||
| djangorestframework = ">=3.10.3" |  | ||||||
| inflection = ">=0.3.1" |  | ||||||
| jsonschema = ">=2.6.0" |  | ||||||
| PyYAML = ">=5.1" |  | ||||||
| uritemplate = ">=2.0.0" |  | ||||||
| 
 |  | ||||||
| [package.extras] |  | ||||||
| offline = ["drf-spectacular-sidecar"] |  | ||||||
| sidecar = ["drf-spectacular-sidecar"] |  | ||||||
| 
 |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "flake8" | name = "flake8" | ||||||
| version = "6.0.0" | version = "6.0.0" | ||||||
| @ -293,52 +151,6 @@ mccabe = ">=0.7.0,<0.8.0" | |||||||
| pycodestyle = ">=2.10.0,<2.11.0" | pycodestyle = ">=2.10.0,<2.11.0" | ||||||
| pyflakes = ">=3.0.0,<3.1.0" | pyflakes = ">=3.0.0,<3.1.0" | ||||||
| 
 | 
 | ||||||
| [[package]] |  | ||||||
| name = "inflection" |  | ||||||
| version = "0.5.1" |  | ||||||
| description = "A port of Ruby on Rails inflector to Python" |  | ||||||
| optional = false |  | ||||||
| python-versions = ">=3.5" |  | ||||||
| files = [ |  | ||||||
|     {file = "inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"}, |  | ||||||
|     {file = "inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417"}, |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [[package]] |  | ||||||
| name = "jsonschema" |  | ||||||
| version = "4.19.0" |  | ||||||
| description = "An implementation of JSON Schema validation for Python" |  | ||||||
| optional = false |  | ||||||
| python-versions = ">=3.8" |  | ||||||
| files = [ |  | ||||||
|     {file = "jsonschema-4.19.0-py3-none-any.whl", hash = "sha256:043dc26a3845ff09d20e4420d6012a9c91c9aa8999fa184e7efcfeccb41e32cb"}, |  | ||||||
|     {file = "jsonschema-4.19.0.tar.gz", hash = "sha256:6e1e7569ac13be8139b2dd2c21a55d350066ee3f80df06c608b398cdc6f30e8f"}, |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [package.dependencies] |  | ||||||
| attrs = ">=22.2.0" |  | ||||||
| jsonschema-specifications = ">=2023.03.6" |  | ||||||
| referencing = ">=0.28.4" |  | ||||||
| rpds-py = ">=0.7.1" |  | ||||||
| 
 |  | ||||||
| [package.extras] |  | ||||||
| format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] |  | ||||||
| format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] |  | ||||||
| 
 |  | ||||||
| [[package]] |  | ||||||
| name = "jsonschema-specifications" |  | ||||||
| version = "2023.7.1" |  | ||||||
| description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" |  | ||||||
| optional = false |  | ||||||
| python-versions = ">=3.8" |  | ||||||
| files = [ |  | ||||||
|     {file = "jsonschema_specifications-2023.7.1-py3-none-any.whl", hash = "sha256:05adf340b659828a004220a9613be00fa3f223f2b82002e273dee62fd50524b1"}, |  | ||||||
|     {file = "jsonschema_specifications-2023.7.1.tar.gz", hash = "sha256:c91a50404e88a1f6ba40636778e2ee08f6e24c5613fe4c53ac24578a5a7f72bb"}, |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [package.dependencies] |  | ||||||
| referencing = ">=0.28.0" |  | ||||||
| 
 |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "markdown" | name = "markdown" | ||||||
| version = "3.4.4" | version = "3.4.4" | ||||||
| @ -365,17 +177,6 @@ files = [ | |||||||
|     {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, |     {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] |  | ||||||
| name = "phonenumberslite" |  | ||||||
| version = "8.13.18" |  | ||||||
| description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers." |  | ||||||
| optional = false |  | ||||||
| python-versions = "*" |  | ||||||
| files = [ |  | ||||||
|     {file = "phonenumberslite-8.13.18-py2.py3-none-any.whl", hash = "sha256:40cef03b24f2bc5711fed2b53b72770ff58f6b7dbfff749822c91078d6e82481"}, |  | ||||||
|     {file = "phonenumberslite-8.13.18.tar.gz", hash = "sha256:a321f0decf3e4e080f005fda3fba5a791d9d14a3ca217974345ff452923c31e2"}, |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "pycodestyle" | name = "pycodestyle" | ||||||
| version = "2.10.0" | version = "2.10.0" | ||||||
| @ -415,17 +216,6 @@ dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pyte | |||||||
| docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] | docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] | ||||||
| tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] | tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] | ||||||
| 
 | 
 | ||||||
| [[package]] |  | ||||||
| name = "pypng" |  | ||||||
| version = "0.20220715.0" |  | ||||||
| description = "Pure Python library for saving and loading PNG images" |  | ||||||
| optional = false |  | ||||||
| python-versions = "*" |  | ||||||
| files = [ |  | ||||||
|     {file = "pypng-0.20220715.0-py3-none-any.whl", hash = "sha256:4a43e969b8f5aaafb2a415536c1a8ec7e341cd6a3f957fd5b5f32a4cfeed902c"}, |  | ||||||
|     {file = "pypng-0.20220715.0.tar.gz", hash = "sha256:739c433ba96f078315de54c0db975aee537cbc3e1d0ae4ed9aab0ca1e427e2c1"}, |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "pytz" | name = "pytz" | ||||||
| version = "2023.3" | version = "2023.3" | ||||||
| @ -437,199 +227,6 @@ files = [ | |||||||
|     {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, |     {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] |  | ||||||
| name = "pyyaml" |  | ||||||
| version = "6.0.1" |  | ||||||
| description = "YAML parser and emitter for Python" |  | ||||||
| optional = false |  | ||||||
| python-versions = ">=3.6" |  | ||||||
| files = [ |  | ||||||
|     {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, |  | ||||||
|     {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, |  | ||||||
|     {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [[package]] |  | ||||||
| name = "qrcode" |  | ||||||
| version = "7.4.2" |  | ||||||
| description = "QR Code image generator" |  | ||||||
| optional = false |  | ||||||
| python-versions = ">=3.7" |  | ||||||
| files = [ |  | ||||||
|     {file = "qrcode-7.4.2-py3-none-any.whl", hash = "sha256:581dca7a029bcb2deef5d01068e39093e80ef00b4a61098a2182eac59d01643a"}, |  | ||||||
|     {file = "qrcode-7.4.2.tar.gz", hash = "sha256:9dd969454827e127dbd93696b20747239e6d540e082937c90f14ac95b30f5845"}, |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [package.dependencies] |  | ||||||
| colorama = {version = "*", markers = "platform_system == \"Windows\""} |  | ||||||
| pypng = "*" |  | ||||||
| typing-extensions = "*" |  | ||||||
| 
 |  | ||||||
| [package.extras] |  | ||||||
| all = ["pillow (>=9.1.0)", "pytest", "pytest-cov", "tox", "zest.releaser[recommended]"] |  | ||||||
| dev = ["pytest", "pytest-cov", "tox"] |  | ||||||
| maintainer = ["zest.releaser[recommended]"] |  | ||||||
| pil = ["pillow (>=9.1.0)"] |  | ||||||
| test = ["coverage", "pytest"] |  | ||||||
| 
 |  | ||||||
| [[package]] |  | ||||||
| name = "referencing" |  | ||||||
| version = "0.30.2" |  | ||||||
| description = "JSON Referencing + Python" |  | ||||||
| optional = false |  | ||||||
| python-versions = ">=3.8" |  | ||||||
| files = [ |  | ||||||
|     {file = "referencing-0.30.2-py3-none-any.whl", hash = "sha256:449b6669b6121a9e96a7f9e410b245d471e8d48964c67113ce9afe50c8dd7bdf"}, |  | ||||||
|     {file = "referencing-0.30.2.tar.gz", hash = "sha256:794ad8003c65938edcdbc027f1933215e0d0ccc0291e3ce20a4d87432b59efc0"}, |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [package.dependencies] |  | ||||||
| attrs = ">=22.2.0" |  | ||||||
| rpds-py = ">=0.7.0" |  | ||||||
| 
 |  | ||||||
| [[package]] |  | ||||||
| name = "rpds-py" |  | ||||||
| version = "0.9.2" |  | ||||||
| description = "Python bindings to Rust's persistent data structures (rpds)" |  | ||||||
| optional = false |  | ||||||
| python-versions = ">=3.8" |  | ||||||
| files = [ |  | ||||||
|     {file = "rpds_py-0.9.2-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:ab6919a09c055c9b092798ce18c6c4adf49d24d4d9e43a92b257e3f2548231e7"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d55777a80f78dd09410bd84ff8c95ee05519f41113b2df90a69622f5540c4f8b"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a216b26e5af0a8e265d4efd65d3bcec5fba6b26909014effe20cd302fd1138fa"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29cd8bfb2d716366a035913ced99188a79b623a3512292963d84d3e06e63b496"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44659b1f326214950a8204a248ca6199535e73a694be8d3e0e869f820767f12f"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:745f5a43fdd7d6d25a53ab1a99979e7f8ea419dfefebcab0a5a1e9095490ee5e"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a987578ac5214f18b99d1f2a3851cba5b09f4a689818a106c23dbad0dfeb760f"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf4151acb541b6e895354f6ff9ac06995ad9e4175cbc6d30aaed08856558201f"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:03421628f0dc10a4119d714a17f646e2837126a25ac7a256bdf7c3943400f67f"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:13b602dc3e8dff3063734f02dcf05111e887f301fdda74151a93dbbc249930fe"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fae5cb554b604b3f9e2c608241b5d8d303e410d7dfb6d397c335f983495ce7f6"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp310-none-win32.whl", hash = "sha256:47c5f58a8e0c2c920cc7783113df2fc4ff12bf3a411d985012f145e9242a2764"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp310-none-win_amd64.whl", hash = "sha256:4ea6b73c22d8182dff91155af018b11aac9ff7eca085750455c5990cb1cfae6e"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:e564d2238512c5ef5e9d79338ab77f1cbbda6c2d541ad41b2af445fb200385e3"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f411330a6376fb50e5b7a3e66894e4a39e60ca2e17dce258d53768fea06a37bd"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e7521f5af0233e89939ad626b15278c71b69dc1dfccaa7b97bd4cdf96536bb7"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8d3335c03100a073883857e91db9f2e0ef8a1cf42dc0369cbb9151c149dbbc1b"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d25b1c1096ef0447355f7293fbe9ad740f7c47ae032c2884113f8e87660d8f6e"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a5d3fbd02efd9cf6a8ffc2f17b53a33542f6b154e88dd7b42ef4a4c0700fdad"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5934e2833afeaf36bd1eadb57256239785f5af0220ed8d21c2896ec4d3a765f"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:095b460e117685867d45548fbd8598a8d9999227e9061ee7f012d9d264e6048d"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:91378d9f4151adc223d584489591dbb79f78814c0734a7c3bfa9c9e09978121c"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:24a81c177379300220e907e9b864107614b144f6c2a15ed5c3450e19cf536fae"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:de0b6eceb46141984671802d412568d22c6bacc9b230174f9e55fc72ef4f57de"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp311-none-win32.whl", hash = "sha256:700375326ed641f3d9d32060a91513ad668bcb7e2cffb18415c399acb25de2ab"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp311-none-win_amd64.whl", hash = "sha256:0766babfcf941db8607bdaf82569ec38107dbb03c7f0b72604a0b346b6eb3298"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:b1440c291db3f98a914e1afd9d6541e8fc60b4c3aab1a9008d03da4651e67386"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0f2996fbac8e0b77fd67102becb9229986396e051f33dbceada3debaacc7033f"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f30d205755566a25f2ae0382944fcae2f350500ae4df4e795efa9e850821d82"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:159fba751a1e6b1c69244e23ba6c28f879a8758a3e992ed056d86d74a194a0f3"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1f044792e1adcea82468a72310c66a7f08728d72a244730d14880cd1dabe36b"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9251eb8aa82e6cf88510530b29eef4fac825a2b709baf5b94a6094894f252387"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01899794b654e616c8625b194ddd1e5b51ef5b60ed61baa7a2d9c2ad7b2a4238"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0c43f8ae8f6be1d605b0465671124aa8d6a0e40f1fb81dcea28b7e3d87ca1e1"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:207f57c402d1f8712618f737356e4b6f35253b6d20a324d9a47cb9f38ee43a6b"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b52e7c5ae35b00566d244ffefba0f46bb6bec749a50412acf42b1c3f402e2c90"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:978fa96dbb005d599ec4fd9ed301b1cc45f1a8f7982d4793faf20b404b56677d"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:6aa8326a4a608e1c28da191edd7c924dff445251b94653988efb059b16577a4d"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:aad51239bee6bff6823bbbdc8ad85136c6125542bbc609e035ab98ca1e32a192"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bd4dc3602370679c2dfb818d9c97b1137d4dd412230cfecd3c66a1bf388a196"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd9da77c6ec1f258387957b754f0df60766ac23ed698b61941ba9acccd3284d1"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:190ca6f55042ea4649ed19c9093a9be9d63cd8a97880106747d7147f88a49d18"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:876bf9ed62323bc7dcfc261dbc5572c996ef26fe6406b0ff985cbcf460fc8a4c"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa2818759aba55df50592ecbc95ebcdc99917fa7b55cc6796235b04193eb3c55"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9ea4d00850ef1e917815e59b078ecb338f6a8efda23369677c54a5825dbebb55"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:5855c85eb8b8a968a74dc7fb014c9166a05e7e7a8377fb91d78512900aadd13d"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:14c408e9d1a80dcb45c05a5149e5961aadb912fff42ca1dd9b68c0044904eb32"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:65a0583c43d9f22cb2130c7b110e695fff834fd5e832a776a107197e59a1898e"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp38-none-win32.whl", hash = "sha256:71f2f7715935a61fa3e4ae91d91b67e571aeb5cb5d10331ab681256bda2ad920"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp38-none-win_amd64.whl", hash = "sha256:674c704605092e3ebbbd13687b09c9f78c362a4bc710343efe37a91457123044"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:07e2c54bef6838fa44c48dfbc8234e8e2466d851124b551fc4e07a1cfeb37260"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f7fdf55283ad38c33e35e2855565361f4bf0abd02470b8ab28d499c663bc5d7c"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:890ba852c16ace6ed9f90e8670f2c1c178d96510a21b06d2fa12d8783a905193"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:50025635ba8b629a86d9d5474e650da304cb46bbb4d18690532dd79341467846"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:517cbf6e67ae3623c5127206489d69eb2bdb27239a3c3cc559350ef52a3bbf0b"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0836d71ca19071090d524739420a61580f3f894618d10b666cf3d9a1688355b1"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c439fd54b2b9053717cca3de9583be6584b384d88d045f97d409f0ca867d80f"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f68996a3b3dc9335037f82754f9cdbe3a95db42bde571d8c3be26cc6245f2324"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7d68dc8acded354c972116f59b5eb2e5864432948e098c19fe6994926d8e15c3"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f963c6b1218b96db85fc37a9f0851eaf8b9040aa46dec112611697a7023da535"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5a46859d7f947061b4010e554ccd1791467d1b1759f2dc2ec9055fa239f1bc26"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp39-none-win32.whl", hash = "sha256:e07e5dbf8a83c66783a9fe2d4566968ea8c161199680e8ad38d53e075df5f0d0"}, |  | ||||||
|     {file = "rpds_py-0.9.2-cp39-none-win_amd64.whl", hash = "sha256:682726178138ea45a0766907957b60f3a1bf3acdf212436be9733f28b6c5af3c"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:196cb208825a8b9c8fc360dc0f87993b8b260038615230242bf18ec84447c08d"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c7671d45530fcb6d5e22fd40c97e1e1e01965fc298cbda523bb640f3d923b387"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83b32f0940adec65099f3b1c215ef7f1d025d13ff947975a055989cb7fd019a4"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f67da97f5b9eac838b6980fc6da268622e91f8960e083a34533ca710bec8611"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03975db5f103997904c37e804e5f340c8fdabbb5883f26ee50a255d664eed58c"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:987b06d1cdb28f88a42e4fb8a87f094e43f3c435ed8e486533aea0bf2e53d931"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c861a7e4aef15ff91233751619ce3a3d2b9e5877e0fcd76f9ea4f6847183aa16"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:02938432352359805b6da099c9c95c8a0547fe4b274ce8f1a91677401bb9a45f"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:ef1f08f2a924837e112cba2953e15aacfccbbfcd773b4b9b4723f8f2ddded08e"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:35da5cc5cb37c04c4ee03128ad59b8c3941a1e5cd398d78c37f716f32a9b7f67"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:141acb9d4ccc04e704e5992d35472f78c35af047fa0cfae2923835d153f091be"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:79f594919d2c1a0cc17d1988a6adaf9a2f000d2e1048f71f298b056b1018e872"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:a06418fe1155e72e16dddc68bb3780ae44cebb2912fbd8bb6ff9161de56e1798"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b2eb034c94b0b96d5eddb290b7b5198460e2d5d0c421751713953a9c4e47d10"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b08605d248b974eb02f40bdcd1a35d3924c83a2a5e8f5d0fa5af852c4d960af"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a0805911caedfe2736935250be5008b261f10a729a303f676d3d5fea6900c96a"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab2299e3f92aa5417d5e16bb45bb4586171c1327568f638e8453c9f8d9e0f020"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c8d7594e38cf98d8a7df25b440f684b510cf4627fe038c297a87496d10a174f"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b9ec12ad5f0a4625db34db7e0005be2632c1013b253a4a60e8302ad4d462afd"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1fcdee18fea97238ed17ab6478c66b2095e4ae7177e35fb71fbe561a27adf620"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:933a7d5cd4b84f959aedeb84f2030f0a01d63ae6cf256629af3081cf3e3426e8"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:686ba516e02db6d6f8c279d1641f7067ebb5dc58b1d0536c4aaebb7bf01cdc5d"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:0173c0444bec0a3d7d848eaeca2d8bd32a1b43f3d3fde6617aac3731fa4be05f"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:d576c3ef8c7b2d560e301eb33891d1944d965a4d7a2eacb6332eee8a71827db6"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed89861ee8c8c47d6beb742a602f912b1bb64f598b1e2f3d758948721d44d468"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1054a08e818f8e18910f1bee731583fe8f899b0a0a5044c6e680ceea34f93876"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99e7c4bb27ff1aab90dcc3e9d37ee5af0231ed98d99cb6f5250de28889a3d502"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c545d9d14d47be716495076b659db179206e3fd997769bc01e2d550eeb685596"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9039a11bca3c41be5a58282ed81ae422fa680409022b996032a43badef2a3752"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fb39aca7a64ad0c9490adfa719dbeeb87d13be137ca189d2564e596f8ba32c07"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2d8b3b3a2ce0eaa00c5bbbb60b6713e94e7e0becab7b3db6c5c77f979e8ed1f1"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:99b1c16f732b3a9971406fbfe18468592c5a3529585a45a35adbc1389a529a03"}, |  | ||||||
|     {file = "rpds_py-0.9.2-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:c27ee01a6c3223025f4badd533bea5e87c988cb0ba2811b690395dfe16088cfe"}, |  | ||||||
|     {file = "rpds_py-0.9.2.tar.gz", hash = "sha256:8d70e8f14900f2657c249ea4def963bed86a29b81f81f5b76b5a9215680de945"}, |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "sqlparse" | name = "sqlparse" | ||||||
| version = "0.4.4" | version = "0.4.4" | ||||||
| @ -668,18 +265,7 @@ files = [ | |||||||
|     {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, |     {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] |  | ||||||
| name = "uritemplate" |  | ||||||
| version = "4.1.1" |  | ||||||
| description = "Implementation of RFC 6570 URI Templates" |  | ||||||
| optional = false |  | ||||||
| python-versions = ">=3.6" |  | ||||||
| files = [ |  | ||||||
|     {file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"}, |  | ||||||
|     {file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"}, |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [metadata] | [metadata] | ||||||
| lock-version = "2.0" | lock-version = "2.0" | ||||||
| python-versions = "^3.10" | python-versions = "^3.10" | ||||||
| content-hash = "69aa50d072f03697f4d11a333694d01f2528e433185216e3dc8d32b2debe7432" | content-hash = "1fc8f57d5dd1b53de4879607d1c7e7c8d9acafd9cbafe4ff64763766d69a4d51" | ||||||
|  | |||||||
| @ -16,9 +16,6 @@ django-cleanup = "^8.0.0" | |||||||
| django-crispy-forms = "^2.0" | django-crispy-forms = "^2.0" | ||||||
| crispy-bootstrap5 = "^0.7" | crispy-bootstrap5 = "^0.7" | ||||||
| markdown = "^3.4.4" | markdown = "^3.4.4" | ||||||
| django-filter = "^23.2" |  | ||||||
| drf-spectacular = "^0.26.4" |  | ||||||
| django-two-factor-auth = {extras = ["phonenumberslite"], version = "^1.15.3"} |  | ||||||
| 
 | 
 | ||||||
| [tool.poetry.group.dev.dependencies] | [tool.poetry.group.dev.dependencies] | ||||||
| flake8 = "^6.0.0" | flake8 = "^6.0.0" | ||||||
|  | |||||||