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): | ||||
|     default_auto_field = 'django.db.models.BigAutoField' | ||||
|     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 | ||||
| from django.conf import settings | ||||
| import django.core.files.storage | ||||
| import django.core.validators | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
| import pathlib | ||||
| import uuid | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| @ -23,7 +21,7 @@ class Migration(migrations.Migration): | ||||
|             name='Platform', | ||||
|             fields=[ | ||||
|                 ('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)), | ||||
|             ], | ||||
|         ), | ||||
| @ -31,29 +29,26 @@ class Migration(migrations.Migration): | ||||
|             name='Ticket', | ||||
|             fields=[ | ||||
|                 ('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)), | ||||
|                 ('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_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)), | ||||
|             ], | ||||
|             options={ | ||||
|                 'ordering': ['-time_create'], | ||||
|             }, | ||||
|         ), | ||||
|         migrations.CreateModel( | ||||
|             name='Archive', | ||||
|             fields=[ | ||||
|                 ('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)), | ||||
|                 ('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_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): | ||||
|         # 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: | ||||
|             md5 = hashlib.md5() | ||||
|             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) | ||||
|     resolved = models.BooleanField(default=False) | ||||
|     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=[ | ||||
|             MaxValueValidator(10), | ||||
|             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 delUrl = $(this).attr("href"); | ||||
|         $.ajax({ | ||||
|             type: "DELETE", | ||||
|             type: "delete", | ||||
|             url: delUrl, | ||||
|             headers: { | ||||
|                 "X-CSRFToken":CSRF, | ||||
| @ -21,12 +21,12 @@ $(function () { | ||||
|             // beforeSend: function(xhr) {
 | ||||
|             //     xhr.setRequestHeader("X-CSRFToken", csrf);
 | ||||
|             // },
 | ||||
|             success: function (data, textStatus, jqXHR) { | ||||
|                 console.log(jqXHR.status); | ||||
|             success: function (response) { | ||||
|                 console.log(response.status); | ||||
|                 $(archiveListElement).hide(1500); | ||||
|             }, | ||||
|             error: function (data, textStatus, jqXHR) { | ||||
|                 console.log(jqXHR.status); | ||||
|             error: function (response) { | ||||
|                 console.log(response.status); | ||||
|             } | ||||
|         }); | ||||
|     }); | ||||
| @ -37,13 +37,16 @@ $(function () { | ||||
|         let resolved = false; | ||||
|         let ticketStateUrl = $(this).attr("ticket-state-url") | ||||
|         if ($(this).attr("ticket-state-switch") === "1") { | ||||
|             console.log('Find it!!!') | ||||
|             resolved = true; | ||||
|             $(this).attr("ticket-state-switch", "0");  // disable
 | ||||
|         } else { | ||||
|             resolved = true; | ||||
|             resolved = false; | ||||
|             $(this).attr("ticket-state-switch", "1");  // enable
 | ||||
|         } | ||||
|         console.log(resolved) | ||||
|         $.ajax({ | ||||
|             type: "PATCH", | ||||
|             type: "POST", | ||||
|             url: ticketStateUrl, | ||||
|             headers: { | ||||
|                 "X-CSRFToken":CSRF, | ||||
| @ -54,12 +57,11 @@ $(function () { | ||||
|             data: JSON.stringify({ | ||||
|                 resolved: resolved, | ||||
|             }), | ||||
|             success: function (data, textStatus, jqXHR) { | ||||
|                 console.log(jqXHR.status) | ||||
|             success: function (response) { | ||||
|                 console.log(response.resolved) | ||||
|             }, | ||||
|             error: function (data, textStatus, jqXHR) { | ||||
|                 console.log(data) | ||||
|                 console.log(jqXHR.status) | ||||
|             error: function (response) { | ||||
|                 console.log(response.resolved) | ||||
|             } | ||||
|         }); | ||||
|     }); | ||||
| @ -78,16 +80,16 @@ $(function () { | ||||
|                 'X-CSRFToken':CSRF, | ||||
|                 'Content-Type':'application/json' | ||||
|             }, | ||||
|             success: function (data, textStatus, jqXHR) { | ||||
|                 console.log(jqXHR.status); | ||||
|             success: function (response) { | ||||
|                 console.log(response.status); | ||||
|                 if (delDiv.length) { | ||||
|                     delDiv.hide(1500); | ||||
|                 } else { | ||||
|                     window.location.href = redirectUrl; | ||||
|                 } | ||||
|             }, | ||||
|             error: function (data, textStatus, jqXHR) { | ||||
|                 console.log(jqXHR.status); | ||||
|             error: function (response) { | ||||
|                 console.log(response.status); | ||||
|             } | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
| @ -1,30 +1,61 @@ | ||||
| {% extends 'base.html' %} | ||||
| {% load static %} | ||||
| 
 | ||||
| {% block collector_head %} | ||||
|   <title>{% block title %}{% endblock title %}</title> | ||||
| {% endblock collector_head %} | ||||
| 
 | ||||
| {% block collector_content %} | ||||
|   <header> | ||||
|     <section> | ||||
|       {% include 'includes/navigation.html' %} | ||||
|     </section> | ||||
|   </header> | ||||
|   <main> | ||||
|     <section> | ||||
|       {% block main %}{% endblock main %} | ||||
|     </section> | ||||
|   </main> | ||||
|   <footer> | ||||
|     <section> | ||||
|       {% block footer %}{% endblock footer %} | ||||
|     </section> | ||||
|   </footer> | ||||
| {% endblock collector_content %} | ||||
| 
 | ||||
| {% block collector_scripts %} | ||||
|   <script src="{% static 'collector/js/jquery-3.7.0.min.js' %}"></script> | ||||
|   {% block bs %}{% endblock bs %} | ||||
|   {% block jquery %}{% endblock jquery %} | ||||
| {% endblock collector_scripts %} | ||||
| <!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 '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> | ||||
|       <section> | ||||
|         {% include 'collector/includes/navigation.html' %} | ||||
|       </section> | ||||
|     </header> | ||||
|     <main> | ||||
|       <section> | ||||
|         {% block main %}{% endblock main %} | ||||
|       </section> | ||||
|     </main> | ||||
|     <footer> | ||||
|       <section> | ||||
|         {% block footer %}{% endblock footer %} | ||||
|       </section> | ||||
|     </footer> | ||||
|     <script src="{% static 'collector/js/bootstrap.bundle.min.js' %}"></script> | ||||
|     <script src="{% static 'collector/js/bs.theme.mode.js' %}"></script> | ||||
|     <script src="{% static 'collector/js/jquery-3.7.0.min.js' %}"></script> | ||||
|     {% block bs %}{% endblock bs %} | ||||
|     {% block jquery %}{% endblock jquery %} | ||||
|   </body> | ||||
| </html> | ||||
|  | ||||
| @ -30,7 +30,7 @@ | ||||
|         >Cancel | ||||
|         </button> | ||||
|         <a | ||||
|           href="{% url 'collector_api:archive-detail' archive.id %}" | ||||
|           href="{% url 'collector:download' archive.file %}" | ||||
|           type="button" | ||||
|           class="btn btn-danger btn-archive-eraser" | ||||
|           data-bs-dismiss="modal" | ||||
|  | ||||
| @ -27,7 +27,7 @@ | ||||
|         </button> | ||||
|         <a | ||||
|           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" | ||||
|           data-bs-dismiss="modal" | ||||
|           data-jq-ticket-del-target="#div-ticket-{{ ticket.number }}" | ||||
|  | ||||
| @ -1,5 +1,20 @@ | ||||
| {% load collector_extras %} | ||||
| {% 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"> | ||||
|   <div class="container"> | ||||
|     <a  | ||||
| @ -90,29 +105,12 @@ | ||||
|               {% if request.user.is_staff %} | ||||
|               <li> | ||||
|                 <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> | ||||
|               {% endif %} | ||||
|               <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><button class="dropdown-item" type="button"><i class="bi bi-gear"></i> Settings</button></li> | ||||
|               <li><hr class="dropdown-divider" /></li> | ||||
|               <li> | ||||
|                 <a | ||||
|                   href="{% url 'account:logout' %}" | ||||
|                   class="dropdown-item" | ||||
|                   type="button"><i class="bi bi-door-closed"></i> Logout | ||||
|                 </a> | ||||
|               </li>            | ||||
|               <li><button class="dropdown-item" type="button"><i class="bi bi-door-closed"></i> Logout</button></li>            | ||||
|             </ul> | ||||
|           </div>               | ||||
|         </li> | ||||
| @ -121,7 +119,7 @@ | ||||
|           <div class="btn-group"> | ||||
|             <a | ||||
|               type="button" | ||||
|               href="{% url 'two_factor:login' %}" | ||||
|               href="{% url 'admin:index' %}" | ||||
|               class="btn btn-outline-secondary" | ||||
|             > | ||||
|               <i class="bi bi-box-arrow-in-right"></i></i> Login | ||||
| @ -137,7 +135,42 @@ | ||||
|         <!-- Theme switcher--> | ||||
|         <li class="nav-item dropdown"> | ||||
|           <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> | ||||
|         </li> | ||||
|       </ul> | ||||
| @ -9,7 +9,7 @@ | ||||
|     type="checkbox" | ||||
|     role="switch" | ||||
|     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 %} checked {% endif %}> | ||||
| </div> | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| {% extends 'collector/base.html' %} | ||||
| {% load static %} | ||||
| {% load collector_extras %} | ||||
| {% block title %} {{ title }} {% endblock title %} | ||||
| {% block title %}<title>{{ title }}</title>{% endblock title %} | ||||
| {% block main %}  | ||||
| <div class="container mt-3"> | ||||
|   <div class="row"> | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| {% extends 'collector/base.html' %} | ||||
| {% load static %} | ||||
| {% load crispy_forms_tags %} | ||||
| {% block title %} {{ title }} {% endblock title %} | ||||
| {% block title %}<title>{{ title }}</title>{% endblock title %} | ||||
| {% block main %} | ||||
| <div class="container mt-3"> | ||||
|     <div class="card"> | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| {% extends 'collector/base.html' %} | ||||
| {% load static %} | ||||
| {% load collector_extras %} | ||||
| {% block title %} {{ title }} {% endblock title %} | ||||
| {% block title %}<title>{{ title }}</title>{% endblock title %} | ||||
| {% block main %} | ||||
| <div class="container mt-3"> | ||||
|   {% 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 | ||||
| 
 | ||||
| app_name = 'collector' | ||||
| 
 | ||||
| # █░█░█ █▀▀ █▄▄ | ||||
| # ▀▄▀▄▀ ██▄ █▄█ | ||||
| # -- -- -- -- -- | ||||
| router = routers.DefaultRouter() | ||||
| router.register(r'archives', views.PublicArchiveUploadViewSet) | ||||
| 
 | ||||
| urlpatterns = [ | ||||
| 
 | ||||
|     # █░█░█ █▀▀ █▄▄ | ||||
|     # ▀▄▀▄▀ ██▄ █▄█ | ||||
|     # -- -- -- -- -- | ||||
| 
 | ||||
|     # CREATE: | ||||
|     path( | ||||
|         'tickets/create/', | ||||
| @ -47,4 +53,28 @@ urlpatterns = [ | ||||
|         views.UpdateTicket.as_view(), | ||||
|         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): | ||||
|     """ | ||||
|         file will be uploaded to | ||||
|         MEDIA_ROOT_FOR_SENSITIVE_FILES/<ticket-token>/<filename> | ||||
|     """ | ||||
|     return f'{instance.ticket.number}/{filename}' | ||||
|     # file will be uploaded to | ||||
|     # MEDIA_ROOT_FOR_SENSITIVE_FILES/<ticket>/<filename> | ||||
|     return f'{instance.ticket}/{filename}' | ||||
| 
 | ||||
| 
 | ||||
| # deprecated | ||||
| def get_file_size(file_path, unit='bytes'): | ||||
|     file_size = os.path.getsize(file_path) | ||||
|     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) | ||||
| 
 | ||||
| 
 | ||||
| # deprecated | ||||
| def is_ajax(request): | ||||
|     if request.headers.get('X-Requested-With') == 'XMLHttpRequest': | ||||
|         return True | ||||
|  | ||||
| @ -1,21 +1,27 @@ | ||||
| import json | ||||
| from django.core.exceptions import ValidationError | ||||
| 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.generic.detail import SingleObjectMixin | ||||
| from django.urls import reverse_lazy | ||||
| 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 .utils import PageTitleViewMixin | ||||
| from .forms import TicketForm | ||||
| from .utils import PageTitleViewMixin, is_ajax | ||||
| 
 | ||||
| from .serializers import PublicArchiveUploadSerializer | ||||
| 
 | ||||
| 
 | ||||
| class ArchiveHandlerView( | ||||
|         OTPRequiredMixin, | ||||
|         LoginRequiredMixin, | ||||
|         SingleObjectMixin, | ||||
|         generic.View): | ||||
| class ArchiveHandlerView(LoginRequiredMixin, SingleObjectMixin, generic.View): | ||||
|     model = Archive | ||||
|     slug_field = 'file' | ||||
|     slug_url_kwarg = 'path' | ||||
| @ -24,6 +30,12 @@ class ArchiveHandlerView( | ||||
|         self.object = self.get_object() | ||||
|         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): | ||||
|     model = Ticket | ||||
| @ -49,11 +61,12 @@ class UpdateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.UpdateView): | ||||
|         return f'{self.title} - {self.kwargs.get("ticket", "update")}' | ||||
| 
 | ||||
|     def form_valid(self, form): | ||||
|         print(self.request.user) | ||||
|         form.instance.user = self.request.user | ||||
|         return super().form_valid(form) | ||||
| 
 | ||||
| 
 | ||||
| class ListAllTickets(LoginRequiredMixin, PageTitleViewMixin, generic.ListView): | ||||
| class ListAllTickets(PageTitleViewMixin, generic.ListView): | ||||
|     model = Ticket | ||||
|     template_name = 'collector/tickets.html' | ||||
|     context_object_name = 'tickets' | ||||
| @ -72,17 +85,13 @@ class ListAllTickets(LoginRequiredMixin, PageTitleViewMixin, generic.ListView): | ||||
|             queryset = self.model.objects.filter( | ||||
|                 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 super().get_queryset() | ||||
| 
 | ||||
| 
 | ||||
| class ListPlatformTickets( | ||||
|         LoginRequiredMixin, | ||||
|         PageTitleViewMixin, | ||||
|         generic.ListView | ||||
|         ): | ||||
| class ListPlatformTickets(PageTitleViewMixin, generic.ListView): | ||||
|     model = Ticket | ||||
|     template_name = 'collector/tickets.html' | ||||
|     context_object_name = 'tickets' | ||||
| @ -98,7 +107,7 @@ class ListPlatformTickets( | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| class DetailTicket(LoginRequiredMixin, PageTitleViewMixin, generic.DetailView): | ||||
| class DetailTicket(PageTitleViewMixin, generic.DetailView): | ||||
|     model = Ticket | ||||
|     template_name = 'collector/ticket.html' | ||||
|     context_object_name = 'ticket' | ||||
| @ -107,3 +116,109 @@ class DetailTicket(LoginRequiredMixin, PageTitleViewMixin, generic.DetailView): | ||||
| 
 | ||||
|     def get_title(self, **kwargs): | ||||
|         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 | ||||
| from pathlib import Path | ||||
| from datetime import timedelta | ||||
| 
 | ||||
| 
 | ||||
| env = environ.Env( | ||||
|     # set casting default value | ||||
|     DEBUG=(bool, False), | ||||
|     SECRET_KEY=(str, 'j9QGbvM9Z4otb47') | ||||
|     DEBUG=(bool, False) | ||||
| ) | ||||
| # Build paths inside the project like this: BASE_DIR / 'subdir'. | ||||
| BASE_DIR = Path(__file__).resolve().parent.parent | ||||
| @ -36,18 +34,9 @@ INSTALLED_APPS = [ | ||||
|     'django.contrib.messages', | ||||
|     'django.contrib.staticfiles', | ||||
|     'collector.apps.CollectorConfig',  # main app | ||||
|     'account.apps.AccountConfig',  # account app | ||||
|     'rest_framework', | ||||
|     'rest_framework_simplejwt', | ||||
|     'django_filters', | ||||
|     'drf_spectacular', | ||||
|     "crispy_forms", | ||||
|     "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 | ||||
| ] | ||||
| 
 | ||||
| @ -57,7 +46,6 @@ MIDDLEWARE = [ | ||||
|     'django.middleware.common.CommonMiddleware', | ||||
|     'django.middleware.csrf.CsrfViewMiddleware', | ||||
|     'django.contrib.auth.middleware.AuthenticationMiddleware', | ||||
|     'django_otp.middleware.OTPMiddleware', | ||||
|     'django.contrib.messages.middleware.MessageMiddleware', | ||||
|     'django.middleware.clickjacking.XFrameOptionsMiddleware', | ||||
| ] | ||||
| @ -67,7 +55,7 @@ ROOT_URLCONF = 'logs_collector.urls' | ||||
| TEMPLATES = [ | ||||
|     { | ||||
|         'BACKEND': 'django.template.backends.django.DjangoTemplates', | ||||
|         'DIRS': [BASE_DIR / 'templates'], | ||||
|         'DIRS': [], | ||||
|         'APP_DIRS': True, | ||||
|         'OPTIONS': { | ||||
|             'context_processors': [ | ||||
| @ -118,7 +106,7 @@ AUTH_PASSWORD_VALIDATORS = [ | ||||
| 
 | ||||
| LANGUAGE_CODE = 'en-us' | ||||
| 
 | ||||
| TIME_ZONE = env('TZ', default='UTC') | ||||
| TIME_ZONE = 'Asia/Irkutsk' | ||||
| 
 | ||||
| USE_I18N = True | ||||
| 
 | ||||
| @ -129,9 +117,6 @@ USE_TZ = True | ||||
| # https://docs.djangoproject.com/en/4.2/howto/static-files/ | ||||
| 
 | ||||
| STATIC_URL = 'static/' | ||||
| STATICFILES_DIRS = [ | ||||
|     BASE_DIR / "static", | ||||
| ] | ||||
| 
 | ||||
| # Default primary key field type | ||||
| # 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_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.contrib import admin | ||||
| 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 account.utils import AdminSiteOTPRequiredMixinRedirectSetup | ||||
| 
 | ||||
| 
 | ||||
| # ? 2FA patch (Admin site protection) | ||||
| admin.site.__class__ = AdminSiteOTPRequiredMixinRedirectSetup | ||||
| 
 | ||||
| 
 | ||||
| urlpatterns = [ | ||||
|     path( | ||||
|         'admin/', | ||||
|         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') | ||||
|     ), | ||||
|     path('admin/', admin.site.urls), | ||||
|     path('', include('collector.urls', namespace='collector')), | ||||
| ] | ||||
| 
 | ||||
| # 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: | ||||
|     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] | ||||
| 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]] | ||||
| name = "crispy-bootstrap5" | ||||
| 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"] | ||||
| 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]] | ||||
| name = "djangorestframework" | ||||
| version = "3.14.0" | ||||
| @ -254,29 +135,6 @@ lint = ["flake8", "isort", "pep8"] | ||||
| python-jose = ["python-jose (==3.3.0)"] | ||||
| 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]] | ||||
| name = "flake8" | ||||
| version = "6.0.0" | ||||
| @ -293,52 +151,6 @@ mccabe = ">=0.7.0,<0.8.0" | ||||
| pycodestyle = ">=2.10.0,<2.11.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]] | ||||
| name = "markdown" | ||||
| version = "3.4.4" | ||||
| @ -365,17 +177,6 @@ files = [ | ||||
|     {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]] | ||||
| name = "pycodestyle" | ||||
| 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"] | ||||
| 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]] | ||||
| name = "pytz" | ||||
| version = "2023.3" | ||||
| @ -437,199 +227,6 @@ files = [ | ||||
|     {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]] | ||||
| name = "sqlparse" | ||||
| version = "0.4.4" | ||||
| @ -668,18 +265,7 @@ files = [ | ||||
|     {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] | ||||
| lock-version = "2.0" | ||||
| python-versions = "^3.10" | ||||
| content-hash = "69aa50d072f03697f4d11a333694d01f2528e433185216e3dc8d32b2debe7432" | ||||
| content-hash = "1fc8f57d5dd1b53de4879607d1c7e7c8d9acafd9cbafe4ff64763766d69a4d51" | ||||
|  | ||||
| @ -16,9 +16,6 @@ django-cleanup = "^8.0.0" | ||||
| django-crispy-forms = "^2.0" | ||||
| crispy-bootstrap5 = "^0.7" | ||||
| 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] | ||||
| flake8 = "^6.0.0" | ||||
|  | ||||