diff --git a/.gitignore b/.gitignore index 4e042e3..3aa4657 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,7 @@ #--------------------------------------------------# # The following was generated by djinit: # #--------------------------------------------------# -.django_tailwind_cli/ -.django_tailwind_cli + # Python *.py[cod] *$py.class @@ -91,4 +90,4 @@ secrets.json *.sql # Cloud -.cloud +.cloud \ No newline at end of file diff --git a/config/settings/base.py b/config/settings/base.py index c45c72c..11cf99c 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -3,6 +3,7 @@ generated with djinit Common settings shared between development and production environment """ +import os from datetime import timedelta from pathlib import Path @@ -23,12 +24,11 @@ BASE_DIR = Path(__file__).resolve().parent.parent.parent THIRD_PARTY_APPS = [ "rest_framework", "rest_framework_simplejwt", + "rest_framework_simplejwt.token_blacklist", "corsheaders", - "drf_spectacular", + "drf_yasg", "django_tailwind_cli", "django_htmx", - "import_export", - "django_filters", ] USER_DEFINED_APPS = [ @@ -71,14 +71,13 @@ ROOT_URLCONF = "config.urls" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [BASE_DIR / "templates"], + "DIRS": [], "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", - "src.config.context_processors.site_stats", ], }, }, @@ -87,8 +86,6 @@ TEMPLATES = [ WSGI_APPLICATION = "config.wsgi.application" -AUTH_USER_MODEL = "accounts.User" - # Password validation # https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators @@ -107,14 +104,12 @@ AUTH_PASSWORD_VALIDATORS = [ }, ] -LOGIN_URL = "/login/" -LOGIN_REDIRECT_URL = "/dashboard/" # Internationalization # https://docs.djangoproject.com/en/5.2/topics/i18n/ LANGUAGE_CODE = "en-us" -TIME_ZONE = "Asia/Kathmandu" +TIME_ZONE = "UTC" USE_I18N = True USE_TZ = True @@ -138,45 +133,29 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" # REST Framework Settings REST_FRAMEWORK = { + "DEFAULT_RENDERER_CLASSES": [ + "rest_framework.renderers.JSONRenderer", + ], + "DEFAULT_PARSER_CLASSES": [ + "rest_framework.parsers.JSONParser", + ], "DEFAULT_AUTHENTICATION_CLASSES": ( "rest_framework_simplejwt.authentication.JWTAuthentication", "rest_framework.authentication.SessionAuthentication", ), - "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), + "DEFAULT_SCHEMA_CLASS": "rest_framework.schemas.coreapi.AutoSchema", "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", - "PAGE_SIZE": 50, - "DEFAULT_FILTER_BACKENDS": ( - "django_filters.rest_framework.DjangoFilterBackend", - "rest_framework.filters.SearchFilter", - "rest_framework.filters.OrderingFilter", - ), - "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", + "PAGE_SIZE": 20, } -# SimpleJWT +# JWT Settings SIMPLE_JWT = { - "ACCESS_TOKEN_LIFETIME": timedelta(hours=1), - "REFRESH_TOKEN_LIFETIME": timedelta(days=30), + "ACCESS_TOKEN_LIFETIME": timedelta(days=1), + "REFRESH_TOKEN_LIFETIME": timedelta(days=3), "ROTATE_REFRESH_TOKENS": True, - "AUTH_HEADER_TYPES": ("Bearer",), + "BLACKLIST_AFTER_ROTATION": True, } - -# Google OAuth -GOOGLE_CLIENT_ID = env("GOOGLE_CLIENT_ID", default="") -GOOGLE_CLIENT_SECRET = env("GOOGLE_CLIENT_SECRET", default="") -GOOGLE_REDIRECT_URI = env( - "GOOGLE_REDIRECT_URI", default="http://localhost:8000/api/auth/google/callback/" -) - -# Spectacular -SPECTACULAR_SETTINGS = { - "TITLE": "Project Management API", - "DESCRIPTION": "Django port of Project Management", - "VERSION": "1.0.0", -} - - # CORS Settings CORS_ALLOW_METHODS = [ "DELETE", diff --git a/config/settings/development.py b/config/settings/development.py index b00054b..fe67bef 100644 --- a/config/settings/development.py +++ b/config/settings/development.py @@ -2,7 +2,7 @@ from .base import * # noqa: F401, F403 -SECRET_KEY = "Z*xa0W7sE9x2A+6#GGR#%+z+!m1!g2WYdE%JY6cAH#B2U+k1xX" +SECRET_KEY = "u4lMI*X9ArV9+CUWTOdYdh_9FcAx%$VVzlK&Kb(%1WA@C4Dkqi" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True diff --git a/config/urls.py b/config/urls.py index c9aaeb3..1047589 100644 --- a/config/urls.py +++ b/config/urls.py @@ -1,232 +1,58 @@ -from django.conf import settings -from django.conf.urls.static import static +"""generated with djinit""" + from django.contrib import admin -from django.contrib.auth import views as auth_views -from django.contrib.auth.decorators import login_required -from django.urls import include, path -from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView - -from src.config import board_views, list_views, page_views, views - -api_patterns = [ - path("auth/", include("src.accounts.urls")), - path("projects/", include("src.projects.urls")), - path("tickets/", include("src.tickets.urls")), - path("epics/", include("src.epics.urls")), - path("notes/", include("src.notes.urls")), - path("notifications/", include("src.notifications.urls")), - path("external/", include("src.external.urls")), -] - -login_req = login_required -dashboard_patterns = [ - path("", login_req(views.dashboard), name="dashboard"), - path("stats-cards/", login_req(views.stats_cards), name="stats-cards"), - path( - "charts/tickets-per-project/", - login_req(views.tickets_per_project_chart), - name="chart-tickets-per-project", - ), - path( - "charts/user-statistics-data/", - login_req(views.user_statistics_data), - name="chart-user-statistics-data", - ), - path( - "charts/monthly-trend/", - login_req(views.monthly_trend_chart), - name="chart-monthly-trend", - ), - path("recent-activity/", login_req(views.recent_activity), name="recent-activity"), - path( - "project-timeline-widget/", - login_req(views.project_timeline_widget), - name="project-timeline-widget", - ), - path( - "notifications/unread-count/", - login_req(list_views.notifications_unread_count), - name="notif-unread-count", - ), - path( - "notifications/dropdown/", - login_req(page_views.notifications_dropdown), - name="notifications-dropdown", - ), - path( - "notifications/", - login_req(page_views.notifications_list), - name="notifications-list", - ), - path( - "notifications/mark-all-read/", - login_req(page_views.notification_mark_all_read), - name="notifications-mark-all-read", - ), - path( - "notifications//mark-read/", - login_req(page_views.notification_mark_read), - name="notification-mark-read", - ), - path("projects/", login_req(list_views.projects_list), name="projects-list"), - path( - "project-timeline/", - login_req(page_views.project_timeline), - name="project-timeline", - ), - path("project-board/", login_req(board_views.project_board), name="project-board"), - path( - "project-board//", - login_req(board_views.project_board), - name="project-board-project", - ), - path( - "board//data/", - login_req(board_views.board_data), - name="board-data", - ), - path( - "tickets/bulk/status/", - login_req(page_views.tickets_bulk_status), - name="tickets-bulk-status", - ), - path( - "tickets/bulk/priority/", - login_req(page_views.tickets_bulk_priority), - name="tickets-bulk-priority", - ), - path( - "tickets/bulk/assign/", - login_req(page_views.tickets_bulk_assign), - name="tickets-bulk-assign", - ), - path( - "tickets/bulk/epic/", - login_req(page_views.tickets_bulk_epic), - name="tickets-bulk-epic", - ), - path( - "tickets//", - login_req(page_views.ticket_detail), - name="ticket-detail", - ), - path( - "tickets//description/", - login_req(page_views.ticket_update_description), - name="ticket-update-description", - ), - path( - "tickets//field/", - login_req(page_views.ticket_update_field), - name="ticket-update-field", - ), - path( - "tickets//comments/add/", - login_req(page_views.ticket_comment_add), - name="ticket-comment-add", - ), - path( - "tickets/export/", login_req(page_views.tickets_export), name="tickets-export" - ), - path( - "tickets/import/", login_req(page_views.tickets_import), name="tickets-import" - ), - path("tickets/", login_req(list_views.tickets_list), name="tickets-list"), - path( - "ticket-timeline/", - login_req(page_views.ticket_timeline), - name="ticket-timeline", - ), - path( - "ticket-timeline//", - login_req(page_views.ticket_timeline), - name="ticket-timeline-project", - ), - path("epics/", login_req(page_views.epics_overview), name="epics-overview"), - path( - "epics//", - login_req(page_views.epics_overview), - name="epics-overview-project", - ), - path("leaderboard/", login_req(page_views.leaderboard), name="leaderboard"), - path( - "contributions/", login_req(page_views.user_contributions), name="contributions" - ), - path( - "ticket-priorities/", - login_req(page_views.ticket_priorities), - name="ticket-priorities", - ), - path( - "ticket-priorities/add/", - login_req(page_views.ticket_priority_add), - name="ticket-priority-add", - ), - path( - "ticket-priorities//edit/", - login_req(page_views.ticket_priority_edit), - name="ticket-priority-edit", - ), - path( - "ticket-priorities//delete/", - login_req(page_views.ticket_priority_delete), - name="ticket-priority-delete", - ), - path("users/", login_req(page_views.users_list), name="users-list"), - path("settings/ui/", login_req(page_views.ui_settings), name="ui-settings"), - path( - "external/", - login_req(page_views.external_access_list), - name="external-access-list", - ), - path( - "external/create//", - login_req(page_views.external_access_create), - name="external-access-create", - ), - path( - "external//regenerate/", - login_req(page_views.external_access_regenerate), - name="external-access-regenerate", - ), - path( - "external//toggle/", - login_req(page_views.external_access_toggle), - name="external-access-toggle", - ), -] +from django.urls import path, re_path +from django.conf import settings +from django.views.static import serve +from django.urls import include +from drf_yasg.views import get_schema_view +from drf_yasg import openapi +from rest_framework_simplejwt.views import ( + TokenObtainPairView, + TokenRefreshView, + TokenBlacklistView, +) urlpatterns = [ path("admin/", admin.site.urls), - path("api/", include(api_patterns)), - path("api/schema/", SpectacularAPIView.as_view(), name="schema"), - path( - "api/docs/", - SpectacularSwaggerView.as_view(url_name="schema"), - name="swagger-ui", - ), - path("dashboard/", include(dashboard_patterns)), - path( - "login/", - auth_views.LoginView.as_view( - template_name="auth/login.html", - redirect_authenticated_user=True, - ), - name="login", - ), - path("logout/", auth_views.LogoutView.as_view(next_page="login"), name="logout"), - path( - "external//", - page_views.external_access_view, - name="external-dashboard", - ), - path( - "external//logout/", - page_views.external_access_logout, - name="external-logout", - ), + # JWT tokens + path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"), + path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), + path("token/blacklist/", TokenBlacklistView.as_view(), name="token_blacklist"), + # App URLs + path("", include("src.epics.urls")), + path("", include("src.notes.urls")), + path("", include("src.projects.urls")), + path("", include("src.accounts.urls")), + path("", include("src.external.urls")), + path("", include("src.notifications.urls")), + path("", include("src.tickets.urls")), ] +# Don't show schema in production if settings.DEBUG: - urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + schema_view = get_schema_view( + openapi.Info( + title="config API", + default_version="v1", + ), + public=True, + ) + urlpatterns += [ + path("docs/", schema_view.with_ui("swagger", cache_timeout=0)), + path("schema/", schema_view.with_ui("redoc", cache_timeout=0)), + ] +# django-browser-reload for tailwind live reload (dev only) +if settings.DEBUG: + urlpatterns += [ + path("__reload__/", include("django_browser_reload.urls")), + ] + +urlpatterns += [ + re_path( + r"^media/(?P.*)$", + serve, + {"document_root": settings.MEDIA_ROOT}, + ), +] \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index e669d17..c054def 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,25 +8,21 @@ license = { text = "MIT" } dependencies = [ "django>=6.0.6", "django-environ>=0.14.0", - "django-jazzmin>=3.0.4", + "django-jazzmin>=3.0.5", "djangorestframework>=3.17.1", "djangorestframework-simplejwt>=5.5.1", + "drf-yasg>=1.21.15", "django-cors-headers>=4.9.0", "whitenoise>=6.12.0", "mysqlclient>=2.2.8", "django-tailwind-cli>=4.6.2", "django-htmx>=1.27.0", "gunicorn>=26.0.0", - "django-filter>=25.2", - "django-import-export>=4.4.1", - "requests>=2.34.2", - "openpyxl>=3.1.5", - "drf-spectacular>=0.29.0", ] [dependency-groups] dev = [ - "ruff>=0.15.19", + "ruff>=0.15.20", "pytest>=9.1.1", "pytest-django>=4.12.0", ] diff --git a/requirements.txt b/requirements.txt index c264ff8..5307aa1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,12 @@ django>=6.0.6 django-environ>=0.14.0 -django-jazzmin>=3.0.4 +django-jazzmin>=3.0.5 djangorestframework>=3.17.1 djangorestframework-simplejwt>=5.5.1 +drf-yasg>=1.21.15 django-cors-headers>=4.9.0 whitenoise>=6.12.0 mysqlclient>=2.2.8 django-tailwind-cli>=4.6.2 django-htmx>=1.27.0 gunicorn>=26.0.0 -django-filter>=25.2 -django-import-export>=4.4.1 -requests>=2.34.2 -openpyxl>=3.1.5 -drf-spectacular>=0.29.0 diff --git a/runtime.txt b/runtime.txt new file mode 100644 index 0000000..3bbfd95 --- /dev/null +++ b/runtime.txt @@ -0,0 +1 @@ +python-3.13 \ No newline at end of file diff --git a/src/accounts/admin.py b/src/accounts/admin.py index 58d14d0..0dc5f91 100644 --- a/src/accounts/admin.py +++ b/src/accounts/admin.py @@ -1,23 +1,5 @@ +"""generated with djinit""" + from django.contrib import admin -from django.contrib.auth.admin import UserAdmin as BaseUserAdmin -from src.accounts.models import Setting, User - - -@admin.register(User) -class UserAdmin(BaseUserAdmin): - list_display = ("id", "email", "name", "is_staff", "is_active") - search_fields = ("email", "name") - ordering = ("email",) - fieldsets = BaseUserAdmin.fieldsets + (("Extra", {"fields": ("google_id",)}),) - - @admin.display(description="Name") - def name(self, obj): - return obj.get_full_name() or obj.username - - -@admin.register(Setting) -class SettingAdmin(admin.ModelAdmin): - list_display = ("id", "user", "group", "key", "value") - list_filter = ("group",) - search_fields = ("key", "value", "user__email") +# Register your models here. \ No newline at end of file diff --git a/src/accounts/models.py b/src/accounts/models.py index 0d8459f..d7b9f75 100644 --- a/src/accounts/models.py +++ b/src/accounts/models.py @@ -1,37 +1,5 @@ -from django.contrib.auth.models import AbstractUser +"""generated with djinit""" + from django.db import models - -class User(AbstractUser): - google_id = models.CharField(max_length=255, blank=True, null=True, unique=True) - - class Meta: - db_table = "users" - verbose_name = "User" - verbose_name_plural = "Users" - - def __str__(self): - return self.email or self.username - - -class Setting(models.Model): - user = models.ForeignKey( - User, on_delete=models.CASCADE, blank=True, null=True, related_name="settings" - ) - group = models.CharField(max_length=255, blank=True, null=True) - key = models.CharField(max_length=255) - value = models.TextField(blank=True, null=True) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - db_table = "settings" - verbose_name = "Setting" - verbose_name_plural = "Settings" - constraints = [ - models.UniqueConstraint(fields=["user", "key"], name="unique_user_setting") - ] - - def __str__(self): - return f"{self.key}={self.value}" - +# Create your models here. \ No newline at end of file diff --git a/src/accounts/serializers.py b/src/accounts/serializers.py index 6f171b9..fbcbed8 100644 --- a/src/accounts/serializers.py +++ b/src/accounts/serializers.py @@ -1,49 +1,5 @@ -from django.contrib.auth import get_user_model +"""generated with djinit""" + from rest_framework import serializers -from src.accounts.models import Setting - -User = get_user_model() - - -class UserSerializer(serializers.ModelSerializer): - name = serializers.SerializerMethodField() - - class Meta: - model = User - fields = ( - "id", - "email", - "name", - "username", - "first_name", - "last_name", - "is_active", - "is_staff", - "date_joined", - "last_login", - ) - read_only_fields = ("id", "is_staff", "is_active", "date_joined", "last_login") - - def get_name(self, obj): - return obj.get_full_name() or obj.username - - -class RegisterSerializer(serializers.ModelSerializer): - password = serializers.CharField(write_only=True, min_length=8) - - class Meta: - model = User - fields = ("email", "username", "password", "first_name", "last_name") - - def create(self, validated_data): - user = User.objects.create_user(**validated_data) - return user - - -class SettingSerializer(serializers.ModelSerializer): - class Meta: - model = Setting - fields = ("id", "user", "group", "key", "value", "created_at", "updated_at") - read_only_fields = ("id", "user", "created_at", "updated_at") - +# Create your serializers here. \ No newline at end of file diff --git a/src/accounts/urls.py b/src/accounts/urls.py index 8075f8e..8020533 100644 --- a/src/accounts/urls.py +++ b/src/accounts/urls.py @@ -1,16 +1,7 @@ -from django.urls import path -from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView +"""generated with djinit""" -from src.accounts import views +from django.urls import path urlpatterns = [ - path("me/", views.MeView.as_view(), name="auth-me"), - path("register/", views.RegisterView.as_view(), name="auth-register"), - path("login/", TokenObtainPairView.as_view(), name="auth-login"), - path("refresh/", TokenRefreshView.as_view(), name="auth-refresh"), - path("google/", views.GoogleAuthUrlView.as_view(), name="auth-google"), - path( - "google/callback/", views.GoogleLoginView.as_view(), name="auth-google-callback" - ), - path("settings/", views.user_settings_view, name="auth-settings"), -] + # Add your URL patterns here +] \ No newline at end of file diff --git a/src/accounts/views.py b/src/accounts/views.py index e43c74e..6630beb 100644 --- a/src/accounts/views.py +++ b/src/accounts/views.py @@ -1,145 +1,5 @@ -import requests -from django.conf import settings -from django.contrib.auth import get_user_model -from rest_framework import generics, permissions, status -from rest_framework.decorators import api_view, permission_classes -from rest_framework.response import Response -from rest_framework.views import APIView -from rest_framework_simplejwt.tokens import RefreshToken +"""generated with djinit""" -from src.accounts.models import Setting -from src.accounts.serializers import ( - RegisterSerializer, - SettingSerializer, - UserSerializer, -) - -User = get_user_model() - - -class MeView(generics.RetrieveUpdateAPIView): - serializer_class = UserSerializer - permission_classes = (permissions.IsAuthenticated,) - - def get_object(self): - return self.request.user - - -class RegisterView(generics.CreateAPIView): - queryset = User.objects.all() - serializer_class = RegisterSerializer - permission_classes = (permissions.AllowAny,) - - -class GoogleLoginView(APIView): - permission_classes = (permissions.AllowAny,) - - def post(self, request): - code = request.data.get("code") - if not code: - return Response( - {"error": "Authorization code is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - token_url = "https://oauth2.googleapis.com/token" - data = { - "code": code, - "client_id": settings.GOOGLE_CLIENT_ID, - "client_secret": settings.GOOGLE_CLIENT_SECRET, - "redirect_uri": settings.GOOGLE_REDIRECT_URI, - "grant_type": "authorization_code", - } - - token_resp = requests.post(token_url, data=data, timeout=10) - if token_resp.status_code != 200: - return Response( - {"error": "Failed to exchange authorization code"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - tokens = token_resp.json() - access_token = tokens.get("access_token") - - userinfo_resp = requests.get( - "https://www.googleapis.com/oauth2/v2/userinfo", - headers={"Authorization": f"Bearer {access_token}"}, - timeout=10, - ) - if userinfo_resp.status_code != 200: - return Response( - {"error": "Failed to get user info"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - google_user = userinfo_resp.json() - google_id = google_user["id"] - email = google_user.get("email") - name = google_user.get("name", "") - first_name = google_user.get("given_name", "") - last_name = google_user.get("family_name", "") - - try: - user = User.objects.get(google_id=google_id) - except User.DoesNotExist: - user = User.objects.filter(email=email).first() - if user: - user.google_id = google_id - user.save() - else: - user = User.objects.create_user( - username=email.split("@")[0] if email else google_id, - email=email, - google_id=google_id, - first_name=first_name, - last_name=last_name, - ) - - refresh = RefreshToken.for_user(user) - return Response( - { - "refresh": str(refresh), - "access": str(refresh.access_token), - "user": UserSerializer(user).data, - } - ) - - -class GoogleAuthUrlView(APIView): - permission_classes = (permissions.AllowAny,) - - def get(self, request): - client_id = settings.GOOGLE_CLIENT_ID - redirect_uri = settings.GOOGLE_REDIRECT_URI - url = ( - f"https://accounts.google.com/o/oauth2/v2/auth" - f"?client_id={client_id}" - f"&redirect_uri={redirect_uri}" - f"&response_type=code" - f"&scope=email%20profile" - ) - return Response({"url": url}) - - -@api_view(["GET", "PUT"]) -@permission_classes([permissions.IsAuthenticated]) -def user_settings_view(request): - if request.method == "PUT": - data = request.data - Setting.objects.update_or_create( - user=request.user, - key=data.get("key"), - defaults={ - "group": data.get("group", ""), - "value": data.get("value", ""), - }, - ) - return Response({"status": "ok"}) - - group = request.query_params.get("group") - qs = Setting.objects.filter(user=request.user) - if group: - qs = qs.filter(group=group) - serializer = SettingSerializer(qs, many=True) - return Response(serializer.data) +from rest_framework import viewsets +# Create your views here. \ No newline at end of file diff --git a/src/config/board_views.py b/src/config/board_views.py deleted file mode 100644 index 24fc40c..0000000 --- a/src/config/board_views.py +++ /dev/null @@ -1,73 +0,0 @@ -from django.db.models import Count -from django.shortcuts import get_object_or_404, render -from django.views.decorators.http import require_http_methods - -from src.epics.models import Epic -from src.projects.models import Project -from src.tickets.models import Ticket, TicketPriority, TicketStatus - - -def project_board(request, project_id=None): - projects = Project.objects.annotate( - member_count=Count("members", distinct=True), - ticket_count=Count("tickets", distinct=True), - ) - return render( - request, - "projects/board.html", - { - "projects": projects, - "selected_project_id": project_id, - }, - ) - - -@require_http_methods(["GET"]) -def board_data(request, project_id): - project = get_object_or_404(Project, pk=project_id) - statuses = TicketStatus.objects.filter(project=project).order_by("sort_order") - tickets = ( - Ticket.objects.filter(project=project) - .select_related("status", "priority", "epic", "created_by") - .prefetch_related("assignees") - ) - - sort = request.GET.get("sort", "created_at_desc") - if sort == "created_at_asc": - tickets = tickets.order_by("created_at") - elif sort == "name_asc": - tickets = tickets.order_by("name") - elif sort == "name_desc": - tickets = tickets.order_by("-name") - elif sort == "due_date_asc": - tickets = tickets.order_by("due_date") - elif sort == "priority": - tickets = tickets.order_by("priority__name") - else: - tickets = tickets.order_by("-created_at") - - columns = [] - for status in statuses: - col_tickets = [t for t in tickets if t.status_id == status.id] - columns.append( - { - "status": status, - "tickets": col_tickets, - "count": len(col_tickets), - } - ) - - priorities = TicketPriority.objects.all() - epics = Epic.objects.filter(project=project) - - return render( - request, - "projects/partials/board_columns.html", - { - "project": project, - "columns": columns, - "priorities": priorities, - "epics": epics, - "sort": sort, - }, - ) diff --git a/src/config/context_processors.py b/src/config/context_processors.py deleted file mode 100644 index cbeb8b7..0000000 --- a/src/config/context_processors.py +++ /dev/null @@ -1,20 +0,0 @@ -from django.contrib.auth import get_user_model - -from src.accounts.models import Setting -from src.tickets.models import Ticket - - -def site_stats(request): - User = get_user_model() - ctx = { - "total_users": User.objects.filter(is_active=True).count(), - "total_tickets": Ticket.objects.count(), - } - user_ui_settings = {} - if request.user.is_authenticated: - for s in Setting.objects.filter(user=request.user, group="ui"): - user_ui_settings[s.key] = s.value - ctx["user_ui_settings"] = user_ui_settings - ctx["navigation_style"] = user_ui_settings.get("navigation_style", "sidebar") - ctx["color_theme"] = user_ui_settings.get("color_theme", "blue") - return ctx diff --git a/src/config/list_views.py b/src/config/list_views.py deleted file mode 100644 index 2eb0835..0000000 --- a/src/config/list_views.py +++ /dev/null @@ -1,112 +0,0 @@ -import json - -from django.contrib.auth import get_user_model -from django.db.models import Count -from django.shortcuts import render -from django.utils import timezone - -from src.epics.models import Epic -from src.notifications.models import Notification -from src.projects.models import Project -from src.tickets.models import Ticket, TicketPriority, TicketStatus - -User = get_user_model() - - -def projects_list(request): - projects = Project.objects.annotate( - member_count=Count("members", distinct=True), - ticket_count=Count("tickets", distinct=True), - ) - return render( - request, - "projects/list.html", - { - "projects": projects, - }, - ) - - -def tickets_list(request): - tickets = ( - Ticket.objects.select_related("status", "priority", "project", "created_by") - .prefetch_related("assignees") - .order_by("-created_at") - ) - - if not request.user.is_superuser: - user_project_ids = request.user.projects.values_list("id", flat=True) - tickets = tickets.filter(project_id__in=user_project_ids) - - project_id = request.GET.get("project") - status_id = request.GET.get("status") - priority_id = request.GET.get("priority") - - if project_id: - tickets = tickets.filter(project_id=project_id) - if status_id: - tickets = tickets.filter(status_id=status_id) - if priority_id: - tickets = tickets.filter(priority_id=priority_id) - - if not request.user.is_superuser: - user_project_ids = request.user.projects.values_list("id", flat=True) - projects = Project.objects.filter(id__in=user_project_ids) - statuses = TicketStatus.objects.filter(project_id__in=user_project_ids) - epics = Epic.objects.filter(project_id__in=user_project_ids) - else: - projects = Project.objects.all() - statuses = TicketStatus.objects.all() - epics = Epic.objects.all() - - priorities = TicketPriority.objects.all() - users = User.objects.filter(is_active=True).order_by("username") - - today = timezone.now().date() - - # JSON serialized data for Alpine.js bulk operations - statuses_json = json.dumps( - [{"id": s.id, "name": s.name, "project_id": s.project_id} for s in statuses] - ) - epics_json = json.dumps( - [{"id": e.id, "name": e.name, "project_id": e.project_id} for e in epics] - ) - users_json = json.dumps( - [{"id": u.id, "name": u.get_full_name() or u.username} for u in users] - ) - priorities_json = json.dumps( - [{"id": p.id, "name": p.name, "color": p.color} for p in priorities] - ) - ticket_ids_json = json.dumps([t.id for t in tickets]) - ticket_project_map_json = json.dumps({t.id: t.project_id for t in tickets}) - - return render( - request, - "tickets/list.html", - { - "tickets": tickets, - "projects": projects, - "statuses": statuses, - "priorities": priorities, - "users": users, - "epics": epics, - "today": today, - "statuses_json": statuses_json, - "epics_json": epics_json, - "users_json": users_json, - "priorities_json": priorities_json, - "ticket_ids_json": ticket_ids_json, - "ticket_project_map_json": ticket_project_map_json, - }, - ) - - -def notifications_unread_count(request): - count = Notification.objects.filter(user=request.user, read_at__isnull=True).count() - return render( - request, - "dashboard/partials/notif_badge.html", - { - "count": count, - }, - ) diff --git a/src/config/page_views.py b/src/config/page_views.py deleted file mode 100644 index 1fe5735..0000000 --- a/src/config/page_views.py +++ /dev/null @@ -1,1068 +0,0 @@ -import secrets -import string -from datetime import date, timedelta - -import openpyxl -from django.contrib import messages -from django.contrib.auth import get_user_model -from django.core.paginator import Paginator -from django.db.models import Count, Prefetch, Q -from django.http import HttpResponse, JsonResponse -from django.shortcuts import get_object_or_404, redirect, render -from django.utils import timezone -from django.views.decorators.http import require_http_methods - -from src.accounts.models import Setting -from src.epics.models import Epic -from src.external.models import ExternalAccess -from src.notifications.models import Notification -from src.projects.models import Project -from src.tickets.models import ( - Ticket, - TicketComment, - TicketHistory, - TicketPriority, - TicketStatus, - TicketUser, -) - -User = get_user_model() - - -# ── Project Timeline (Gantt) ────────────────────────────────────────────── - - -def project_timeline(request): - projects = Project.objects.filter( - start_date__isnull=False, end_date__isnull=False - ).order_by("start_date") - - today = date.today() - for p in projects: - total = (p.end_date - p.start_date).days or 1 - elapsed = (today - p.start_date).days - p.progress = max(0, min(100, int(elapsed / total * 100))) - remaining = (p.end_date - today).days - if remaining < 0: - p.status_label = "Overdue" - p.status_color = "text-red-500" - p.bar_color = "#ef4444" - elif remaining <= 7: - p.status_label = "Approaching Deadline" - p.status_color = "text-yellow-500" - p.bar_color = "#f59e0b" - elif p.progress >= 100: - p.status_label = "Completed" - p.status_color = "text-emerald-500" - p.bar_color = "#10b981" - elif p.progress > 0: - p.status_label = "In Progress" - p.status_color = "text-blue-500" - p.bar_color = "#3b82f6" - else: - p.status_label = "Not Started" - p.status_color = "text-gray-400" - p.bar_color = "#9ca3af" - - return render( - request, - "projects/timeline.html", - { - "projects": projects, - }, - ) - - -# ── Ticket Timeline (Gantt) ─────────────────────────────────────────────── - - -def ticket_timeline(request, project_id=None): - projects = Project.objects.annotate( - ticket_count=Count("tickets"), - member_count=Count("members", distinct=True), - ) - selected_project = None - tickets = [] - if project_id: - selected_project = get_object_or_404(Project, pk=project_id) - tickets = ( - Ticket.objects.filter(project=selected_project) - .select_related("status", "priority") - .filter(due_date__isnull=False) - .order_by("start_date", "due_date") - ) - - return render( - request, - "tickets/timeline.html", - { - "projects": projects, - "selected_project": selected_project, - "selected_project_id": project_id, - "tickets": tickets, - }, - ) - - -# ── Epics Overview ──────────────────────────────────────────────────────── - - -def epics_overview(request, project_id=None): - projects = Project.objects.annotate( - ticket_count=Count("tickets"), - member_count=Count("members", distinct=True), - ) - selected_project = None - epics = [] - if project_id: - selected_project = get_object_or_404(Project, pk=project_id) - epics = ( - Epic.objects.filter(project=selected_project) - .prefetch_related( - "tickets__status", "tickets__priority", "tickets__assignees" - ) - .order_by("sort_order") - ) - - return render( - request, - "epics/overview.html", - { - "projects": projects, - "selected_project": selected_project, - "selected_project_id": project_id, - "epics": epics, - }, - ) - - -# ── Leaderboard ─────────────────────────────────────────────────────────── - - -def leaderboard(request): - days = int(request.GET.get("days", 7)) - top_n = int(request.GET.get("top", 10)) - since = timezone.now() - timedelta(days=days) - - users = User.objects.filter(is_active=True) - leaderboard_data = [] - - for user in users: - tickets_created = Ticket.objects.filter( - created_by=user, created_at__gte=since - ).count() - status_changes = TicketHistory.objects.filter( - user=user, created_at__gte=since - ).count() - comments_made = TicketComment.objects.filter( - user=user, created_at__gte=since - ).count() - active_days = ( - TicketHistory.objects.filter(user=user, created_at__gte=since) - .dates("created_at", "day") - .count() - ) - - score = ( - (status_changes * 5) - + (tickets_created * 2) - + (comments_made * 2) - + (active_days * 1) - ) - - if score > 0 or tickets_created > 0 or comments_made > 0: - leaderboard_data.append( - { - "user": user, - "score": score, - "tickets_created": tickets_created, - "status_changes": status_changes, - "comments_made": comments_made, - "active_days": active_days, - } - ) - - leaderboard_data.sort(key=lambda x: x["score"], reverse=True) - leaderboard_data = leaderboard_data[:top_n] - - ranks = ["🥇", "🥈", "🥉", "🏅"] - - return render( - request, - "leaderboard/index.html", - { - "leaderboard": leaderboard_data, - "ranks": ranks, - "days": days, - "top_n": top_n, - }, - ) - - -# ── User Contributions (Heatmap) ────────────────────────────────────────── - - -def user_contributions(request): - range_days = int(request.GET.get("range", 30)) - since = timezone.now() - timedelta(days=range_days) - - users = User.objects.filter(is_active=True)[:10] - contributions = [] - - for user in users: - tickets_created = Ticket.objects.filter( - created_by=user, created_at__gte=since - ).count() - status_changes = TicketHistory.objects.filter( - user=user, created_at__gte=since - ).count() - comments_made = TicketComment.objects.filter( - user=user, created_at__gte=since - ).count() - active_days = ( - TicketHistory.objects.filter(user=user, created_at__gte=since) - .dates("created_at", "day") - .count() - ) - - # Build heatmap data - heatmap_values = {} - current = since.date() - end = timezone.now().date() - while current <= end: - heatmap_values[current.isoformat()] = 0 - current += timedelta(days=1) - - for h in TicketHistory.objects.filter(user=user, created_at__gte=since): - day = h.created_at.date().isoformat() - if day in heatmap_values: - heatmap_values[day] += 1 - - max_val = max(heatmap_values.values()) if heatmap_values else 1 - - # Group into weeks (list of lists of day objects) - weeks = [] - week = [] - sorted_days = sorted(heatmap_values.keys()) - for i, day_str in enumerate(sorted_days): - if i > 0 and sorted_days[i - 1] < day_str: - pass - day_date = date.fromisoformat(day_str) - count = heatmap_values[day_str] - if count == 0: - level = 0 - elif count <= max_val // 4: - level = 1 - elif count <= max_val // 2: - level = 2 - elif count <= int(max_val / 1.5): - level = 3 - else: - level = 4 - week.append( - { - "date": day_str, - "count": count, - "level": level, - "day": day_date.weekday(), - } - ) - if day_date.weekday() == 6: # Sunday - weeks.append(week) - week = [] - if week: - weeks.append(week) - - contributions.append( - { - "user": user, - "tickets_created": tickets_created, - "status_changes": status_changes, - "comments_made": comments_made, - "active_days": active_days, - "weeks": weeks, - "max_val": max_val, - } - ) - - return render( - request, - "contributions/index.html", - { - "contributions": contributions, - "range_days": range_days, - }, - ) - - -# ── Ticket Detail ────────────────────────────────────────────────────────── - - -@require_http_methods(["GET"]) -def ticket_detail(request, ticket_id): - comments_qs = TicketComment.objects.select_related("user").order_by("created_at") - histories_qs = TicketHistory.objects.select_related("user", "status").order_by( - "-created_at" - ) - - ticket = get_object_or_404( - Ticket.objects.select_related( - "status", "priority", "project", "created_by", "epic" - ).prefetch_related( - Prefetch("comments", queryset=comments_qs), - Prefetch("histories", queryset=histories_qs), - "assignees", - ), - pk=ticket_id, - ) - return render(request, "tickets/detail.html", {"ticket": ticket}) - - -# ── Ticket Comment Add ──────────────────────────────────────────────────── - - -@require_http_methods(["POST"]) -def ticket_comment_add(request, ticket_id): - ticket = get_object_or_404(Ticket, pk=ticket_id) - comment_text = request.POST.get("comment", "").strip() - if comment_text: - TicketComment.objects.create( - ticket=ticket, - user=request.user, - comment=comment_text, - ) - comments = ( - TicketComment.objects.select_related("user") - .filter(ticket=ticket) - .order_by("created_at") - ) - return render(request, "tickets/partials/comments.html", {"comments": comments}) - - -# ── Ticket Update Description ──────────────────────────────────────────── - - -@require_http_methods(["POST"]) -def ticket_update_description(request, ticket_id): - ticket = get_object_or_404(Ticket, pk=ticket_id) - description = request.POST.get("description", "").strip() - ticket.description = description - ticket.save(update_fields=["description", "updated_at"]) - if description: - return HttpResponse(f'
{description}
') - return HttpResponse( - '

No description set.

' - ) - - -# ── Ticket Update Field ────────────────────────────────────────────────── - - -@require_http_methods(["POST"]) -def ticket_update_field(request, ticket_id): - ticket = get_object_or_404(Ticket, pk=ticket_id) - field = request.POST.get("field") - value = request.POST.get("value", "").strip() - - if field in ("name",): - ticket.name = value - ticket.save(update_fields=["name", "updated_at"]) - return HttpResponse(value) - elif field in ("start_date", "due_date"): - if value: - setattr(ticket, field, value) - else: - setattr(ticket, field, None) - ticket.save(update_fields=[field, "updated_at"]) - return HttpResponse(value or "-") - return JsonResponse({"error": "Invalid field"}, status=400) - - -# ── Tickets Export ────────────────────────────────────────────────────── - - -@require_http_methods(["GET"]) -def tickets_export(request): - tickets = ( - Ticket.objects.select_related( - "status", "priority", "project", "created_by", "epic" - ) - .prefetch_related("assignees") - .order_by("-created_at") - ) - - if not request.user.is_superuser: - user_project_ids = request.user.projects.values_list("id", flat=True) - tickets = tickets.filter(project_id__in=user_project_ids) - - project_id = request.GET.get("project") - status_id = request.GET.get("status") - priority_id = request.GET.get("priority") - - if project_id: - tickets = tickets.filter(project_id=project_id) - if status_id: - tickets = tickets.filter(status_id=status_id) - if priority_id: - tickets = tickets.filter(priority_id=priority_id) - - wb = openpyxl.Workbook() - ws = wb.active - ws.title = "Tickets" - - headers = [ - "UUID", - "Name", - "Description", - "Project", - "Status", - "Priority", - "Epic", - "Assignees", - "Start Date", - "Due Date", - "Created By", - "Created At", - "Updated At", - ] - ws.append(headers) - - for t in tickets: - ws.append( - [ - t.uuid, - t.name, - t.description or "", - t.project.name, - t.status.name, - t.priority.name if t.priority else "", - t.epic.name if t.epic else "", - ", ".join(a.get_full_name() or a.username for a in t.assignees.all()), - t.start_date or "", - t.due_date or "", - t.created_by.get_full_name() or t.created_by.username - if t.created_by - else "", - t.created_at.replace(tzinfo=None) if t.created_at else "", - t.updated_at.replace(tzinfo=None) if t.updated_at else "", - ] - ) - - response = HttpResponse( - content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" - ) - response["Content-Disposition"] = ( - f'attachment; filename="tickets-export-{date.today().isoformat()}.xlsx"' - ) - wb.save(response) - return response - - -# ── Tickets Import ────────────────────────────────────────────────────── - - -@require_http_methods(["GET", "POST"]) -def tickets_import(request): - result = None - - if request.method == "POST" and request.FILES.get("file"): - excel_file = request.FILES["file"] - created_count = 0 - errors = [] - - try: - wb = openpyxl.load_workbook(excel_file, read_only=True) - ws = wb.active - rows = list(ws.iter_rows(values_only=True)) - if not rows: - errors.append("Empty file.") - else: - headers = [str(h).strip().lower() if h else "" for h in rows[0]] - expected = [ - "name", - "project", - "status", - "priority", - "epic", - "start date", - "due date", - "description", - "assignees", - ] - col_map = {} - for i, h in enumerate(headers): - for e in expected: - if h == e: - col_map[e] = i - - if "name" not in col_map: - errors.append("Missing required column: Name") - if "project" not in col_map: - errors.append("Missing required column: Project") - - for row_idx, row in enumerate(rows[1:], start=2): - if all(v is None or str(v).strip() == "" for v in row): - continue - row_errors = [] - try: - name = ( - str(row[col_map["name"]]).strip() - if col_map.get("name") is not None and row[col_map["name"]] - else "" - ) - if not name: - row_errors.append("Name is required") - - project_str = ( - str(row[col_map["project"]]).strip() - if col_map.get("project") is not None - and row[col_map["project"]] - else "" - ) - if not project_str: - row_errors.append("Project is required") - - if row_errors: - errors.append(f"Row {row_idx}: {'; '.join(row_errors)}") - continue - - # Find project by name or prefix - project = Project.objects.filter(name=project_str).first() - if not project: - project = Project.objects.filter( - ticket_prefix=project_str - ).first() - if not project: - errors.append( - f"Row {row_idx}: Project '{project_str}' not found" - ) - continue - - # Find status by name within project - status = None - if col_map.get("status") is not None and row[col_map["status"]]: - status_str = str(row[col_map["status"]]).strip() - status = TicketStatus.objects.filter( - project=project, name__iexact=status_str - ).first() - if not status: - status = TicketStatus.objects.filter( - project=project - ).first() - if not status: - errors.append( - f"Row {row_idx}: No status found for project '{project.name}'" - ) - continue - - # Find priority by name - priority = None - if ( - col_map.get("priority") is not None - and row[col_map["priority"]] - ): - priority_str = str(row[col_map["priority"]]).strip() - priority = TicketPriority.objects.filter( - name__iexact=priority_str - ).first() - - # Find epic by name within project - epic = None - if col_map.get("epic") is not None and row[col_map["epic"]]: - epic_str = str(row[col_map["epic"]]).strip() - epic = Epic.objects.filter( - project=project, name__iexact=epic_str - ).first() - - # Parse dates - start_date = None - if ( - col_map.get("start date") is not None - and row[col_map["start date"]] - ): - try: - start_date = row[col_map["start date"]] - if hasattr(start_date, "date"): - start_date = start_date.date() - except (ValueError, TypeError): - pass - - due_date = None - if ( - col_map.get("due date") is not None - and row[col_map["due date"]] - ): - try: - due_date = row[col_map["due date"]] - if hasattr(due_date, "date"): - due_date = due_date.date() - except (ValueError, TypeError): - pass - - description = "" - if ( - col_map.get("description") is not None - and row[col_map["description"]] - ): - description = str(row[col_map["description"]]).strip() - - # Create ticket - ticket = Ticket.objects.create( - project=project, - status=status, - priority=priority, - epic=epic, - name=name, - description=description or None, - start_date=start_date, - due_date=due_date, - created_by=request.user, - ) - - # Add assignees - if ( - col_map.get("assignees") is not None - and row[col_map["assignees"]] - ): - assignees_str = str(row[col_map["assignees"]]).strip() - if assignees_str: - for part in assignees_str.split(","): - part = part.strip() - if not part: - continue - user = User.objects.filter( - email__iexact=part - ).first() - if not user: - user = User.objects.filter( - username__iexact=part - ).first() - if user: - TicketUser.objects.get_or_create( - ticket=ticket, user=user - ) - else: - errors.append( - f"Row {row_idx}: Assignee '{part}' not found" - ) - - created_count += 1 - except Exception as e: - errors.append(f"Row {row_idx}: {str(e)}") - - wb.close() - except Exception as e: - errors.append(f"File error: {str(e)}") - - result = {"created": created_count, "errors": errors} - - return render(request, "tickets/import.html", {"result": result}) - - -# ── Notifications ────────────────────────────────────────────────────────── - - -@require_http_methods(["GET"]) -def notifications_list(request): - qs = Notification.objects.filter(user=request.user) - filter_param = request.GET.get("filter", "all") - if filter_param == "unread": - qs = qs.filter(read_at__isnull=True) - elif filter_param == "read": - qs = qs.filter(read_at__isnull=False) - paginator = Paginator(qs, 20) - page_number = request.GET.get("page", 1) - page_obj = paginator.get_page(page_number) - unread_count = Notification.objects.filter( - user=request.user, read_at__isnull=True - ).count() - return render( - request, - "notifications/list.html", - { - "notifications": page_obj, - "page_obj": page_obj, - "unread_count": unread_count, - "filter": filter_param, - }, - ) - - -@require_http_methods(["GET"]) -def notifications_dropdown(request): - notifications = Notification.objects.filter( - user=request.user, read_at__isnull=True - )[:10] - return render( - request, - "dashboard/partials/notif_dropdown.html", - { - "notifications": notifications, - }, - ) - - -@require_http_methods(["GET"]) -def notification_mark_read(request, notification_id): - notif = get_object_or_404(Notification, pk=notification_id, user=request.user) - notif.read_at = timezone.now() - notif.save(update_fields=["read_at"]) - next_url = request.GET.get("next") - if next_url: - return redirect(next_url) - return JsonResponse({"status": "ok"}) - - -@require_http_methods(["GET"]) -def notification_mark_all_read(request): - Notification.objects.filter(user=request.user, read_at__isnull=True).update( - read_at=timezone.now() - ) - return redirect("notifications-list") - - -# ── Ticket Priorities ───────────────────────────────────────────────────── - - -@require_http_methods(["GET"]) -def ticket_priorities(request): - priorities = TicketPriority.objects.all().order_by("name") - return render(request, "tickets/priorities.html", {"priorities": priorities}) - - -@require_http_methods(["POST"]) -def ticket_priority_add(request): - name = request.POST.get("name", "").strip() - color = request.POST.get("color", "#6B7280") - if name: - TicketPriority.objects.create(name=name, color=color) - messages.success(request, "Priority added successfully.") - else: - messages.error(request, "Priority name is required.") - return redirect("ticket-priorities") - - -@require_http_methods(["POST"]) -def ticket_priority_edit(request, priority_id): - priority = get_object_or_404(TicketPriority, pk=priority_id) - name = request.POST.get("name", "").strip() - color = request.POST.get("color", priority.color) - if name: - priority.name = name - priority.color = color - priority.save(update_fields=["name", "color"]) - messages.success(request, "Priority updated successfully.") - else: - messages.error(request, "Priority name is required.") - return redirect("ticket-priorities") - - -@require_http_methods(["POST"]) -def ticket_priority_delete(request, priority_id): - priority = get_object_or_404(TicketPriority, pk=priority_id) - priority.delete() - messages.success(request, "Priority deleted successfully.") - return redirect("ticket-priorities") - - -# ── Users List ──────────────────────────────────────────────────────────── - - -@require_http_methods(["GET"]) -def users_list(request): - qs = User.objects.filter(is_active=True).annotate( - project_count=Count("projects", distinct=True), - ticket_count=Count("created_tickets", distinct=True), - comment_count=Count("ticket_comments", distinct=True), - ) - search = request.GET.get("search", "").strip() - if search: - qs = qs.filter( - Q(first_name__icontains=search) - | Q(last_name__icontains=search) - | Q(email__icontains=search) - | Q(username__icontains=search) - ) - return render(request, "users/list.html", {"users": qs, "search": search}) - - -# ── UI Settings ──────────────────────────────────────────────────────────── - - -def ui_settings(request): - user_settings = {} - if request.user.is_authenticated: - for s in Setting.objects.filter(user=request.user, group="ui"): - user_settings[s.key] = s.value - - if request.method == "POST": - navigation_style = request.POST.get("navigation_style", "sidebar") - color_theme = request.POST.get("color_theme", "blue") - if navigation_style not in ("sidebar", "top"): - navigation_style = "sidebar" - if color_theme not in ("blue", "green", "purple", "orange", "red"): - color_theme = "blue" - Setting.objects.update_or_create( - user=request.user, - key="navigation_style", - defaults={"value": navigation_style, "group": "ui"}, - ) - Setting.objects.update_or_create( - user=request.user, - key="color_theme", - defaults={"value": color_theme, "group": "ui"}, - ) - messages.success(request, "UI settings saved successfully.") - - themes = [ - ("blue", "#3b82f6", "Blue"), - ("green", "#10b981", "Green"), - ("purple", "#8b5cf6", "Purple"), - ("orange", "#f59e0b", "Orange"), - ("red", "#ef4444", "Red"), - ] - return render( - request, - "settings/ui.html", - { - "navigation_style": user_settings.get("navigation_style", "sidebar"), - "color_theme": user_settings.get("color_theme", "blue"), - "themes": themes, - }, - ) - - -# ── External Access Management ────────────────────────────────────────── - - -@require_http_methods(["GET"]) -def external_access_list(request): - accesses = ExternalAccess.objects.select_related("project").all() - return render(request, "external/list.html", {"accesses": accesses}) - - -@require_http_methods(["POST"]) -def external_access_create(request, project_id): - project = get_object_or_404(Project, pk=project_id) - ExternalAccess.objects.create(project=project) - messages.success(request, "External access created successfully.") - return redirect("external-access-list") - - -@require_http_methods(["POST"]) -def external_access_regenerate(request, access_id): - access = get_object_or_404(ExternalAccess, pk=access_id) - access.password = "".join( - secrets.choice(string.ascii_letters + string.digits) for _ in range(8) - ) - access.save(update_fields=["password"]) - messages.success(request, "Password regenerated successfully.") - return redirect("external-access-list") - - -@require_http_methods(["POST"]) -def external_access_toggle(request, access_id): - access = get_object_or_404(ExternalAccess, pk=access_id) - access.is_active = not access.is_active - access.save(update_fields=["is_active"]) - status = "activated" if access.is_active else "deactivated" - messages.success(request, f"External access {status} successfully.") - return redirect("external-access-list") - - -# ── External Client Access (public, no auth) ──────────────────────────── - - -def external_access_view(request, token): - access = get_object_or_404(ExternalAccess, access_token=token) - authenticated = request.session.get("ext_token") == token - - if request.method == "POST": - password = request.POST.get("password", "") - if password == access.password and access.is_active: - request.session["ext_token"] = token - access.last_accessed_at = timezone.now() - access.save(update_fields=["last_accessed_at"]) - return redirect("external-dashboard", token=token) - return render( - request, - "external/access.html", - { - "access": access, - "error": "Invalid password or access is disabled.", - "authenticated": False, - }, - ) - - if authenticated and access.is_active: - tickets = ( - Ticket.objects.filter(project=access.project) - .select_related("status", "priority") - .order_by("-created_at") - ) - return render( - request, - "external/access.html", - { - "access": access, - "authenticated": True, - "tickets": tickets, - }, - ) - - return render( - request, - "external/access.html", - { - "access": access, - "authenticated": False, - }, - ) - - -@require_http_methods(["POST"]) -def external_access_logout(request, token): - if request.session.get("ext_token") == token: - del request.session["ext_token"] - return redirect("external-dashboard", token=token) - - -# ── Bulk Operations ──────────────────────────────────────────────────────── - - -@require_http_methods(["POST"]) -def tickets_bulk_status(request): - import json - - data = json.loads(request.body) - ticket_ids = data.get("ticket_ids", []) - status_id = data.get("status_id") - - if not ticket_ids or not status_id: - return JsonResponse( - {"success": False, "error": "Missing ticket_ids or status_id"}, status=400 - ) - - status = get_object_or_404(TicketStatus, pk=status_id) - tickets = Ticket.objects.filter(pk__in=ticket_ids).select_related("status") - - if not request.user.is_superuser: - user_project_ids = request.user.projects.values_list("id", flat=True) - tickets = tickets.filter(project_id__in=user_project_ids) - - count = 0 - for ticket in tickets: - old_name = ticket.status.name - ticket.status = status - ticket.save(update_fields=["status", "updated_at"]) - - TicketHistory.objects.create( - ticket=ticket, - user=request.user, - status=status, - ) - - notify_users = list(ticket.assignees.all()) - if ticket.created_by and ticket.created_by not in notify_users: - notify_users.append(ticket.created_by) - - for u in notify_users: - Notification.objects.create( - user=u, - type="status_changed", - title=f"Ticket {ticket.uuid} status changed", - message=f"Status changed from {old_name} to {status.name} by {request.user.get_full_name() or request.user.username}", - data={"ticket_id": ticket.id, "ticket_uuid": ticket.uuid}, - ) - - count += 1 - - return JsonResponse({"success": True, "count": count}) - - -@require_http_methods(["POST"]) -def tickets_bulk_priority(request): - import json - - data = json.loads(request.body) - ticket_ids = data.get("ticket_ids", []) - priority_id = data.get("priority_id") - - if not ticket_ids or not priority_id: - return JsonResponse( - {"success": False, "error": "Missing ticket_ids or priority_id"}, status=400 - ) - - priority = get_object_or_404(TicketPriority, pk=priority_id) - tickets = Ticket.objects.filter(pk__in=ticket_ids) - - if not request.user.is_superuser: - user_project_ids = request.user.projects.values_list("id", flat=True) - tickets = tickets.filter(project_id__in=user_project_ids) - - count = tickets.update(priority=priority, updated_at=timezone.now()) - - return JsonResponse({"success": True, "count": count}) - - -@require_http_methods(["POST"]) -def tickets_bulk_assign(request): - import json - - data = json.loads(request.body) - ticket_ids = data.get("ticket_ids", []) - user_id = data.get("user_id") - - if not ticket_ids or not user_id: - return JsonResponse( - {"success": False, "error": "Missing ticket_ids or user_id"}, status=400 - ) - - user_to_assign = get_object_or_404(User, pk=user_id, is_active=True) - tickets = Ticket.objects.filter(pk__in=ticket_ids) - - if not request.user.is_superuser: - user_project_ids = request.user.projects.values_list("id", flat=True) - tickets = tickets.filter(project_id__in=user_project_ids) - - count = 0 - for ticket in tickets: - _, created = TicketUser.objects.get_or_create( - ticket=ticket, user=user_to_assign - ) - if created: - count += 1 - - return JsonResponse({"success": True, "count": count}) - - -@require_http_methods(["POST"]) -def tickets_bulk_epic(request): - import json - - data = json.loads(request.body) - ticket_ids = data.get("ticket_ids", []) - epic_id = data.get("epic_id") - - if not ticket_ids: - return JsonResponse( - {"success": False, "error": "Missing ticket_ids"}, status=400 - ) - - epic = None - if epic_id is not None: - epic = get_object_or_404(Epic, pk=epic_id) - - tickets = Ticket.objects.filter(pk__in=ticket_ids) - - if not request.user.is_superuser: - user_project_ids = request.user.projects.values_list("id", flat=True) - tickets = tickets.filter(project_id__in=user_project_ids) - - count = 0 - for ticket in tickets: - ticket.epic = epic - ticket.save(update_fields=["epic", "updated_at"]) - count += 1 - - return JsonResponse({"success": True, "count": count}) diff --git a/src/config/views.py b/src/config/views.py deleted file mode 100644 index c936080..0000000 --- a/src/config/views.py +++ /dev/null @@ -1,214 +0,0 @@ -import json -from datetime import date, timedelta - -from django.contrib.auth import get_user_model -from django.db.models import Count, Q -from django.http import JsonResponse -from django.shortcuts import render -from django.utils import timezone -from django.views.decorators.http import require_http_methods - -from src.projects.models import Project -from src.tickets.models import Ticket, TicketHistory - -User = get_user_model() - - -def dashboard(request): - return render( - request, - "dashboard/index.html", - { - "project_count": Project.objects.count(), - "ticket_count": Ticket.objects.count(), - "user_count": User.objects.count(), - "my_assigned": Ticket.objects.filter(assignees=request.user).count(), - }, - ) - - -@require_http_methods(["GET"]) -def stats_cards(request): - user = request.user - now = timezone.now() - today = now.date() - week_start = today - timedelta(days=today.weekday()) - - if user.is_superuser: - total_projects = Project.objects.count() - total_tickets = Ticket.objects.count() - team_members = User.objects.count() - else: - total_projects = Project.objects.filter(members=user).count() - total_tickets = ( - Ticket.objects.filter(Q(project__members=user) | Q(assignees=user)) - .distinct() - .count() - ) - team_members = ( - User.objects.filter(project_memberships__project__members=user) - .distinct() - .count() - ) - - my_assigned = Ticket.objects.filter(assignees=user).count() - my_created = Ticket.objects.filter(created_by=user).count() - my_overdue = Ticket.objects.filter( - assignees=user, due_date__lt=today, status__is_completed=False - ).count() - completed_this_week = Ticket.objects.filter( - Q(assignees=user) | Q(created_by=user), - status__is_completed=True, - updated_at__gte=week_start, - ).count() - new_this_week = Ticket.objects.filter( - Q(assignees=user) | Q(created_by=user), - created_at__gte=week_start, - ).count() - - return render( - request, - "dashboard/partials/stats_cards.html", - { - "total_projects": total_projects, - "total_tickets": total_tickets, - "team_members": team_members, - "my_assigned": my_assigned, - "my_created": my_created, - "my_overdue": my_overdue, - "completed_this_week": completed_this_week, - "new_this_week": new_this_week, - "is_superuser": user.is_superuser, - }, - ) - - -@require_http_methods(["GET"]) -def tickets_per_project_chart(request): - data = list( - Project.objects.annotate(count=Count("tickets")).values( - "name", "count", "color" - ) - ) - max_count = max((d["count"] for d in data), default=1) - for d in data: - d["pct"] = round(d["count"] / max_count * 100, 1) if max_count else 0 - return render( - request, - "dashboard/partials/tickets_per_project_chart.html", - { - "chart_data": data, - }, - ) - - -@require_http_methods(["GET"]) -def user_statistics_data(request): - users = User.objects.annotate( - project_count=Count("projects", distinct=True), - assigned_count=Count("assigned_tickets", distinct=True), - ).values("username", "project_count", "assigned_count")[:10] - - return JsonResponse( - { - "labels": [u["username"] for u in users], - "projects": [u["project_count"] for u in users], - "assigned": [u["assigned_count"] for u in users], - } - ) - - -@require_http_methods(["GET"]) -def monthly_trend_chart(request): - now = timezone.now() - months = [] - for i in range(5, -1, -1): - m = now.month - i - y = now.year - while m < 1: - m += 12 - y -= 1 - months.append({"year": y, "month": m, "label": date(y, m, 1).strftime("%b %Y")}) - - data = [] - for m in months: - count = Ticket.objects.filter( - created_at__year=m["year"], - created_at__month=m["month"], - ).count() - data.append({"label": m["label"], "count": count}) - - return render( - request, - "dashboard/partials/monthly_trend_chart.html", - { - "chart_data": json.dumps(data), - }, - ) - - -@require_http_methods(["GET"]) -def recent_activity(request): - histories = TicketHistory.objects.select_related( - "ticket", "user", "status", "ticket__project" - ).order_by("-created_at")[:20] - - return render( - request, - "dashboard/partials/recent_activity.html", - { - "histories": histories, - }, - ) - - -@require_http_methods(["GET"]) -def project_timeline_widget(request): - filter_type = request.GET.get("filter", "pinned") - qs = Project.objects.filter(start_date__isnull=False, end_date__isnull=False) - - pinned_count = qs.filter(pinned_date__isnull=False).count() - all_count = qs.count() - - if filter_type == "pinned": - qs = qs.filter(pinned_date__isnull=False).order_by("-pinned_date") - else: - qs = qs.order_by("start_date") - - today = date.today() - for p in qs: - total = (p.end_date - p.start_date).days or 1 - elapsed = (today - p.start_date).days - p.progress = max(0, min(100, int(elapsed / total * 100))) - remaining = (p.end_date - today).days - if remaining < 0: - p.status_label = "Overdue" - p.status_color = "badge-error" - p.bar_color = "bg-red-500" - elif remaining <= 7: - p.status_label = "Approaching Deadline" - p.status_color = "badge-warning" - p.bar_color = "bg-yellow-500" - elif p.progress >= 100: - p.status_label = "Completed" - p.status_color = "badge-success" - p.bar_color = "bg-emerald-500" - elif p.progress > 0: - p.status_label = "In Progress" - p.status_color = "badge-info" - p.bar_color = "bg-blue-500" - else: - p.status_label = "Not Started" - p.status_color = "badge-ghost" - p.bar_color = "bg-gray-400" - - return render( - request, - "dashboard/partials/project_timeline.html", - { - "projects": qs, - "current_filter": filter_type, - "pinned_count": pinned_count, - "all_count": all_count, - }, - ) diff --git a/src/epics/admin.py b/src/epics/admin.py index 0a35ec4..0dc5f91 100644 --- a/src/epics/admin.py +++ b/src/epics/admin.py @@ -1,11 +1,5 @@ +"""generated with djinit""" + from django.contrib import admin -from src.epics.models import Epic - - -@admin.register(Epic) -class EpicAdmin(admin.ModelAdmin): - list_display = ("id", "name", "project", "start_date", "end_date", "sort_order") - list_filter = ("project",) - search_fields = ("name", "project__name") - +# Register your models here. \ No newline at end of file diff --git a/src/epics/models.py b/src/epics/models.py index 641ab6b..d7b9f75 100644 --- a/src/epics/models.py +++ b/src/epics/models.py @@ -1,24 +1,5 @@ +"""generated with djinit""" + from django.db import models - -class Epic(models.Model): - project = models.ForeignKey( - "projects.Project", on_delete=models.CASCADE, related_name="epics" - ) - name = models.CharField(max_length=255) - description = models.TextField(blank=True, null=True) - start_date = models.DateField(blank=True, null=True) - end_date = models.DateField(blank=True, null=True) - sort_order = models.IntegerField(default=0) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - db_table = "epics" - verbose_name = "Epic" - verbose_name_plural = "Epics" - ordering = ("sort_order",) - - def __str__(self): - return f"[{self.project.ticket_prefix}] {self.name}" - +# Create your models here. \ No newline at end of file diff --git a/src/epics/serializers.py b/src/epics/serializers.py index b82e435..fbcbed8 100644 --- a/src/epics/serializers.py +++ b/src/epics/serializers.py @@ -1,27 +1,5 @@ +"""generated with djinit""" + from rest_framework import serializers -from src.epics.models import Epic - - -class EpicSerializer(serializers.ModelSerializer): - ticket_count = serializers.SerializerMethodField() - - class Meta: - model = Epic - fields = ( - "id", - "project", - "name", - "description", - "start_date", - "end_date", - "sort_order", - "ticket_count", - "created_at", - "updated_at", - ) - read_only_fields = ("id", "created_at", "updated_at") - - def get_ticket_count(self, obj): - return obj.tickets.count() - +# Create your serializers here. \ No newline at end of file diff --git a/src/epics/urls.py b/src/epics/urls.py index fe9b3f2..8020533 100644 --- a/src/epics/urls.py +++ b/src/epics/urls.py @@ -1,9 +1,7 @@ +"""generated with djinit""" + from django.urls import path -from src.epics import views - urlpatterns = [ - path("", views.EpicListCreateView.as_view(), name="epic-list"), - path("/", views.EpicDetailView.as_view(), name="epic-detail"), -] - + # Add your URL patterns here +] \ No newline at end of file diff --git a/src/epics/views.py b/src/epics/views.py index 03641ab..6630beb 100644 --- a/src/epics/views.py +++ b/src/epics/views.py @@ -1,23 +1,5 @@ -from rest_framework import generics, permissions +"""generated with djinit""" -from src.epics.models import Epic -from src.epics.serializers import EpicSerializer - - -class EpicListCreateView(generics.ListCreateAPIView): - serializer_class = EpicSerializer - permission_classes = (permissions.IsAuthenticated,) - - def get_queryset(self): - qs = Epic.objects.all() - project_id = self.request.query_params.get("project") - if project_id: - qs = qs.filter(project_id=project_id) - return qs - - -class EpicDetailView(generics.RetrieveUpdateDestroyAPIView): - queryset = Epic.objects.all() - serializer_class = EpicSerializer - permission_classes = (permissions.IsAuthenticated,) +from rest_framework import viewsets +# Create your views here. \ No newline at end of file diff --git a/src/external/admin.py b/src/external/admin.py index f7d6568..0dc5f91 100644 --- a/src/external/admin.py +++ b/src/external/admin.py @@ -1,10 +1,5 @@ +"""generated with djinit""" + from django.contrib import admin -from src.external.models import ExternalAccess - - -@admin.register(ExternalAccess) -class ExternalAccessAdmin(admin.ModelAdmin): - list_display = ("id", "project", "access_token", "is_active", "last_accessed_at") - list_filter = ("is_active", "project") - +# Register your models here. \ No newline at end of file diff --git a/src/external/models.py b/src/external/models.py index 33b729d..d7b9f75 100644 --- a/src/external/models.py +++ b/src/external/models.py @@ -1,37 +1,5 @@ -import secrets -import string +"""generated with djinit""" from django.db import models - -def generate_access_token(): - return secrets.token_hex(16) - - -def generate_access_password(): - return "".join( - secrets.choice(string.ascii_letters + string.digits) for _ in range(8) - ) - - -class ExternalAccess(models.Model): - project = models.ForeignKey( - "projects.Project", on_delete=models.CASCADE, related_name="external_accesses" - ) - access_token = models.CharField( - max_length=255, unique=True, default=generate_access_token - ) - password = models.CharField(max_length=255, default=generate_access_password) - is_active = models.BooleanField(default=True) - last_accessed_at = models.DateTimeField(blank=True, null=True) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - db_table = "external_access" - verbose_name = "External Access" - verbose_name_plural = "External Accesses" - - def __str__(self): - return f"ExternalAccess for {self.project.name}" - +# Create your models here. \ No newline at end of file diff --git a/src/external/serializers.py b/src/external/serializers.py index 52ad073..fbcbed8 100644 --- a/src/external/serializers.py +++ b/src/external/serializers.py @@ -1,38 +1,5 @@ +"""generated with djinit""" + from rest_framework import serializers -from src.external.models import ExternalAccess - - -class ExternalAccessSerializer(serializers.ModelSerializer): - class Meta: - model = ExternalAccess - fields = ( - "id", - "project", - "access_token", - "password", - "is_active", - "last_accessed_at", - "created_at", - "updated_at", - ) - read_only_fields = ( - "id", - "access_token", - "password", - "last_accessed_at", - "created_at", - "updated_at", - ) - - -class ExternalLoginSerializer(serializers.Serializer): - token = serializers.CharField() - password = serializers.CharField() - - -class ExternalAccessCreateSerializer(serializers.ModelSerializer): - class Meta: - model = ExternalAccess - fields = ("project", "is_active") - +# Create your serializers here. \ No newline at end of file diff --git a/src/external/urls.py b/src/external/urls.py index d3d410b..8020533 100644 --- a/src/external/urls.py +++ b/src/external/urls.py @@ -1,10 +1,7 @@ +"""generated with djinit""" + from django.urls import path -from src.external import views - urlpatterns = [ - path("", views.ExternalAccessListCreateView.as_view(), name="external-list"), - path("login/", views.external_login_view, name="external-login"), - path("/", views.ExternalAccessDetailView.as_view(), name="external-detail"), -] - + # Add your URL patterns here +] \ No newline at end of file diff --git a/src/external/views.py b/src/external/views.py index 81803d8..6630beb 100644 --- a/src/external/views.py +++ b/src/external/views.py @@ -1,68 +1,5 @@ -from django.utils import timezone -from rest_framework import generics, permissions, status -from rest_framework.decorators import api_view, permission_classes -from rest_framework.response import Response +"""generated with djinit""" -from src.external.models import ExternalAccess -from src.external.serializers import ( - ExternalAccessCreateSerializer, - ExternalAccessSerializer, - ExternalLoginSerializer, -) - - -class ExternalAccessListCreateView(generics.ListCreateAPIView): - permission_classes = (permissions.IsAuthenticated,) - - def get_serializer_class(self): - if self.request.method == "POST": - return ExternalAccessCreateSerializer - return ExternalAccessSerializer - - def get_queryset(self): - return ExternalAccess.objects.select_related("project").all() - - -class ExternalAccessDetailView(generics.RetrieveUpdateDestroyAPIView): - queryset = ExternalAccess.objects.all() - serializer_class = ExternalAccessSerializer - permission_classes = (permissions.IsAuthenticated,) - - -@api_view(["POST"]) -@permission_classes([permissions.AllowAny]) -def external_login_view(request): - serializer = ExternalLoginSerializer(data=request.data) - serializer.is_valid(raise_exception=True) - - try: - access = ExternalAccess.objects.get( - access_token=serializer.validated_data["token"], - is_active=True, - ) - except ExternalAccess.DoesNotExist: - return Response( - {"error": "Invalid access token"}, - status=status.HTTP_401_UNAUTHORIZED, - ) - - if serializer.validated_data["password"] != access.password: - return Response( - {"error": "Invalid password"}, - status=status.HTTP_401_UNAUTHORIZED, - ) - - access.last_accessed_at = timezone.now() - access.save(update_fields=["last_accessed_at"]) - - return Response( - { - "access_token": access.access_token, - "project": { - "id": access.project_id, - "name": access.project.name, - "ticket_prefix": access.project.ticket_prefix, - }, - } - ) +from rest_framework import viewsets +# Create your views here. \ No newline at end of file diff --git a/src/notes/admin.py b/src/notes/admin.py index b226321..0dc5f91 100644 --- a/src/notes/admin.py +++ b/src/notes/admin.py @@ -1,11 +1,5 @@ +"""generated with djinit""" + from django.contrib import admin -from src.notes.models import ProjectNote - - -@admin.register(ProjectNote) -class ProjectNoteAdmin(admin.ModelAdmin): - list_display = ("id", "title", "project", "created_by", "note_date") - list_filter = ("project", "note_date") - search_fields = ("title", "project__name") - +# Register your models here. \ No newline at end of file diff --git a/src/notes/models.py b/src/notes/models.py index 494fcf5..d7b9f75 100644 --- a/src/notes/models.py +++ b/src/notes/models.py @@ -1,28 +1,5 @@ -from django.conf import settings +"""generated with djinit""" + from django.db import models - -class ProjectNote(models.Model): - project = models.ForeignKey( - "projects.Project", on_delete=models.CASCADE, related_name="notes" - ) - created_by = models.ForeignKey( - settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="project_notes" - ) - title = models.CharField(max_length=255) - content = models.TextField(blank=True, null=True) - note_date = models.DateField() - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - db_table = "project_notes" - verbose_name = "Project Note" - verbose_name_plural = "Project Notes" - indexes = [ - models.Index(fields=["project", "note_date"]), - ] - - def __str__(self): - return f"{self.title} - {self.project.name}" - +# Create your models here. \ No newline at end of file diff --git a/src/notes/serializers.py b/src/notes/serializers.py index b70c454..fbcbed8 100644 --- a/src/notes/serializers.py +++ b/src/notes/serializers.py @@ -1,23 +1,5 @@ -from src.accounts.serializers import UserSerializer +"""generated with djinit""" + from rest_framework import serializers -from src.notes.models import ProjectNote - - -class ProjectNoteSerializer(serializers.ModelSerializer): - created_by = UserSerializer(read_only=True) - - class Meta: - model = ProjectNote - fields = ( - "id", - "project", - "created_by", - "title", - "content", - "note_date", - "created_at", - "updated_at", - ) - read_only_fields = ("id", "created_by", "created_at", "updated_at") - +# Create your serializers here. \ No newline at end of file diff --git a/src/notes/urls.py b/src/notes/urls.py index 7c18d76..8020533 100644 --- a/src/notes/urls.py +++ b/src/notes/urls.py @@ -1,9 +1,7 @@ +"""generated with djinit""" + from django.urls import path -from src.notes import views - urlpatterns = [ - path("", views.ProjectNoteListCreateView.as_view(), name="note-list"), - path("/", views.ProjectNoteDetailView.as_view(), name="note-detail"), -] - + # Add your URL patterns here +] \ No newline at end of file diff --git a/src/notes/views.py b/src/notes/views.py index f06968d..6630beb 100644 --- a/src/notes/views.py +++ b/src/notes/views.py @@ -1,26 +1,5 @@ -from rest_framework import generics, permissions +"""generated with djinit""" -from src.notes.models import ProjectNote -from src.notes.serializers import ProjectNoteSerializer - - -class ProjectNoteListCreateView(generics.ListCreateAPIView): - serializer_class = ProjectNoteSerializer - permission_classes = (permissions.IsAuthenticated,) - - def get_queryset(self): - qs = ProjectNote.objects.select_related("created_by") - project_id = self.request.query_params.get("project") - if project_id: - qs = qs.filter(project_id=project_id) - return qs.order_by("-note_date", "-created_at") - - def perform_create(self, serializer): - serializer.save(created_by=self.request.user) - - -class ProjectNoteDetailView(generics.RetrieveUpdateDestroyAPIView): - queryset = ProjectNote.objects.all() - serializer_class = ProjectNoteSerializer - permission_classes = (permissions.IsAuthenticated,) +from rest_framework import viewsets +# Create your views here. \ No newline at end of file diff --git a/src/notifications/admin.py b/src/notifications/admin.py index 9f49944..0dc5f91 100644 --- a/src/notifications/admin.py +++ b/src/notifications/admin.py @@ -1,11 +1,5 @@ +"""generated with djinit""" + from django.contrib import admin -from src.notifications.models import Notification - - -@admin.register(Notification) -class NotificationAdmin(admin.ModelAdmin): - list_display = ("id", "user", "type", "title", "read_at", "created_at") - list_filter = ("type", "read_at", "user") - search_fields = ("title", "message", "user__email") - +# Register your models here. \ No newline at end of file diff --git a/src/notifications/models.py b/src/notifications/models.py index 612ac47..d7b9f75 100644 --- a/src/notifications/models.py +++ b/src/notifications/models.py @@ -1,32 +1,5 @@ -from django.conf import settings +"""generated with djinit""" + from django.db import models - -class Notification(models.Model): - user = models.ForeignKey( - settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="notifications" - ) - type = models.CharField( - max_length=255, help_text="e.g. comment_added, project_assigned" - ) - title = models.CharField(max_length=255) - message = models.TextField(blank=True, null=True) - data = models.JSONField( - blank=True, null=True, help_text="Arbitrary payload e.g. ticket_id, comment_id" - ) - read_at = models.DateTimeField(blank=True, null=True) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - db_table = "notifications" - verbose_name = "Notification" - verbose_name_plural = "Notifications" - indexes = [ - models.Index(fields=["user", "read_at"]), - ] - ordering = ("-created_at",) - - def __str__(self): - return f"[{self.type}] {self.title}" - +# Create your models here. \ No newline at end of file diff --git a/src/notifications/serializers.py b/src/notifications/serializers.py index 4c23082..fbcbed8 100644 --- a/src/notifications/serializers.py +++ b/src/notifications/serializers.py @@ -1,21 +1,5 @@ +"""generated with djinit""" + from rest_framework import serializers -from src.notifications.models import Notification - - -class NotificationSerializer(serializers.ModelSerializer): - class Meta: - model = Notification - fields = ( - "id", - "user", - "type", - "title", - "message", - "data", - "read_at", - "created_at", - "updated_at", - ) - read_only_fields = ("id", "user", "created_at", "updated_at") - +# Create your serializers here. \ No newline at end of file diff --git a/src/notifications/urls.py b/src/notifications/urls.py index ffe9ef1..8020533 100644 --- a/src/notifications/urls.py +++ b/src/notifications/urls.py @@ -1,13 +1,7 @@ +"""generated with djinit""" + from django.urls import path -from src.notifications import views - urlpatterns = [ - path("", views.NotificationListView.as_view(), name="notification-list"), - path("unread-count/", views.unread_count_view, name="notification-unread-count"), - path("mark-all-read/", views.mark_all_read_view, name="notification-mark-all-read"), - path( - "/", views.NotificationDetailView.as_view(), name="notification-detail" - ), -] - + # Add your URL patterns here +] \ No newline at end of file diff --git a/src/notifications/views.py b/src/notifications/views.py index de6b273..6630beb 100644 --- a/src/notifications/views.py +++ b/src/notifications/views.py @@ -1,40 +1,5 @@ -from django.utils import timezone -from rest_framework import generics, permissions -from rest_framework.decorators import api_view, permission_classes -from rest_framework.response import Response +"""generated with djinit""" -from src.notifications.models import Notification -from src.notifications.serializers import NotificationSerializer - - -class NotificationListView(generics.ListAPIView): - serializer_class = NotificationSerializer - permission_classes = (permissions.IsAuthenticated,) - - def get_queryset(self): - return Notification.objects.filter(user=self.request.user) - - -class NotificationDetailView(generics.RetrieveUpdateDestroyAPIView): - serializer_class = NotificationSerializer - permission_classes = (permissions.IsAuthenticated,) - - def get_queryset(self): - return Notification.objects.filter(user=self.request.user) - - -@api_view(["POST"]) -@permission_classes([permissions.IsAuthenticated]) -def mark_all_read_view(request): - Notification.objects.filter(user=request.user, read_at__isnull=True).update( - read_at=timezone.now() - ) - return Response({"status": "ok"}) - - -@api_view(["GET"]) -@permission_classes([permissions.IsAuthenticated]) -def unread_count_view(request): - count = Notification.objects.filter(user=request.user, read_at__isnull=True).count() - return Response({"unread_count": count}) +from rest_framework import viewsets +# Create your views here. \ No newline at end of file diff --git a/src/projects/admin.py b/src/projects/admin.py index b6e71be..0dc5f91 100644 --- a/src/projects/admin.py +++ b/src/projects/admin.py @@ -1,30 +1,5 @@ +"""generated with djinit""" + from django.contrib import admin -from src.projects.models import Project, ProjectMember - - -class ProjectMemberInline(admin.TabularInline): - model = ProjectMember - extra = 1 - - -@admin.register(Project) -class ProjectAdmin(admin.ModelAdmin): - list_display = ( - "id", - "name", - "ticket_prefix", - "color", - "start_date", - "end_date", - "pinned_date", - ) - search_fields = ("name", "ticket_prefix") - inlines = (ProjectMemberInline,) - - -@admin.register(ProjectMember) -class ProjectMemberAdmin(admin.ModelAdmin): - list_display = ("id", "project", "user") - search_fields = ("project__name", "user__email") - +# Register your models here. \ No newline at end of file diff --git a/src/projects/models.py b/src/projects/models.py index f385f02..d7b9f75 100644 --- a/src/projects/models.py +++ b/src/projects/models.py @@ -1,61 +1,5 @@ -from django.conf import settings +"""generated with djinit""" + from django.db import models - -class Project(models.Model): - name = models.CharField(max_length=255) - description = models.TextField(blank=True, null=True) - ticket_prefix = models.CharField(max_length=255) - color = models.CharField( - max_length=7, blank=True, null=True, help_text="Hex color e.g. #3B82F6" - ) - start_date = models.DateField(blank=True, null=True) - end_date = models.DateField(blank=True, null=True) - pinned_date = models.DateTimeField(blank=True, null=True) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - members = models.ManyToManyField( - settings.AUTH_USER_MODEL, - through="ProjectMember", - related_name="projects", - ) - - class Meta: - db_table = "projects" - verbose_name = "Project" - verbose_name_plural = "Projects" - indexes = [ - models.Index(fields=["pinned_date"]), - models.Index(fields=["start_date", "end_date"]), - ] - - def __str__(self): - return self.name - - -class ProjectMember(models.Model): - project = models.ForeignKey( - Project, on_delete=models.CASCADE, related_name="project_members" - ) - user = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE, - related_name="project_memberships", - ) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - db_table = "project_members" - verbose_name = "Project Member" - verbose_name_plural = "Project Members" - constraints = [ - models.UniqueConstraint( - fields=["project", "user"], name="unique_project_member" - ) - ] - - def __str__(self): - return f"{self.user} -> {self.project}" - +# Create your models here. \ No newline at end of file diff --git a/src/projects/serializers.py b/src/projects/serializers.py index a913beb..fbcbed8 100644 --- a/src/projects/serializers.py +++ b/src/projects/serializers.py @@ -1,67 +1,5 @@ -from src.accounts.serializers import UserSerializer +"""generated with djinit""" + from rest_framework import serializers -from src.projects.models import Project, ProjectMember - - -class ProjectMemberSerializer(serializers.ModelSerializer): - user = UserSerializer(read_only=True) - user_id = serializers.IntegerField(write_only=True) - - class Meta: - model = ProjectMember - fields = ("id", "user", "user_id", "created_at") - read_only_fields = ("id", "created_at") - - -class ProjectListSerializer(serializers.ModelSerializer): - member_count = serializers.SerializerMethodField() - ticket_count = serializers.SerializerMethodField() - - class Meta: - model = Project - fields = ( - "id", - "name", - "description", - "ticket_prefix", - "color", - "start_date", - "end_date", - "pinned_date", - "member_count", - "ticket_count", - "created_at", - "updated_at", - ) - read_only_fields = ("id", "created_at", "updated_at") - - def get_member_count(self, obj): - return obj.members.count() - - def get_ticket_count(self, obj): - return obj.tickets.count() - - -class ProjectDetailSerializer(serializers.ModelSerializer): - members = ProjectMemberSerializer( - source="project_members", many=True, read_only=True - ) - - class Meta: - model = Project - fields = ( - "id", - "name", - "description", - "ticket_prefix", - "color", - "start_date", - "end_date", - "pinned_date", - "members", - "created_at", - "updated_at", - ) - read_only_fields = ("id", "created_at", "updated_at") - +# Create your serializers here. \ No newline at end of file diff --git a/src/projects/urls.py b/src/projects/urls.py index 687911f..8020533 100644 --- a/src/projects/urls.py +++ b/src/projects/urls.py @@ -1,26 +1,7 @@ +"""generated with djinit""" + from django.urls import path -from src.projects import views - urlpatterns = [ - path("", views.ProjectListCreateView.as_view(), name="project-list"), - path("dashboard/", views.dashboard_stats_view, name="project-dashboard"), - path("/", views.ProjectDetailView.as_view(), name="project-detail"), - path( - "/members/", - views.ProjectMemberListCreateView.as_view(), - name="project-member-list", - ), - path( - "/members//", - views.ProjectMemberDestroyView.as_view(), - name="project-member-destroy", - ), - path( - "/statuses/", - views.project_statuses_view, - name="project-statuses", - ), - path("/stats/", views.project_stats_view, name="project-stats"), -] - + # Add your URL patterns here +] \ No newline at end of file diff --git a/src/projects/views.py b/src/projects/views.py index da4f797..6630beb 100644 --- a/src/projects/views.py +++ b/src/projects/views.py @@ -1,140 +1,5 @@ -from src.tickets.models import Ticket, TicketStatus -from django.db import models -from django.utils import timezone -from rest_framework import generics, permissions -from rest_framework.decorators import api_view, permission_classes -from rest_framework.response import Response +"""generated with djinit""" -from src.projects.models import Project, ProjectMember -from src.projects.serializers import ( - ProjectDetailSerializer, - ProjectListSerializer, - ProjectMemberSerializer, -) - - -class ProjectListCreateView(generics.ListCreateAPIView): - serializer_class = ProjectListSerializer - permission_classes = (permissions.IsAuthenticated,) - - def get_queryset(self): - user = self.request.user - if user.is_superuser: - return Project.objects.all() - return Project.objects.filter(members=user).distinct() - - def perform_create(self, serializer): - project = serializer.save() - ProjectMember.objects.get_or_create(project=project, user=self.request.user) - - -class ProjectDetailView(generics.RetrieveUpdateDestroyAPIView): - serializer_class = ProjectDetailSerializer - permission_classes = (permissions.IsAuthenticated,) - - def get_queryset(self): - user = self.request.user - if user.is_superuser: - return Project.objects.all() - return Project.objects.filter(members=user).distinct() - - -class ProjectMemberListCreateView(generics.ListCreateAPIView): - serializer_class = ProjectMemberSerializer - permission_classes = (permissions.IsAuthenticated,) - - def get_queryset(self): - return ProjectMember.objects.filter(project_id=self.kwargs["project_pk"]) - - def perform_create(self, serializer): - serializer.save(project_id=self.kwargs["project_pk"]) - - -class ProjectMemberDestroyView(generics.DestroyAPIView): - permission_classes = (permissions.IsAuthenticated,) - - def get_queryset(self): - return ProjectMember.objects.filter(project_id=self.kwargs["project_pk"]) - - def get_object(self): - return self.get_queryset().get(user_id=self.kwargs["user_id"]) - - -@api_view(["GET"]) -@permission_classes([permissions.IsAuthenticated]) -def project_statuses_view(request, project_pk): - statuses = TicketStatus.objects.filter(project_id=project_pk).order_by("sort_order") - from src.tickets.serializers import TicketStatusSerializer - - serializer = TicketStatusSerializer(statuses, many=True) - return Response(serializer.data) - - -@api_view(["GET"]) -@permission_classes([permissions.IsAuthenticated]) -def project_stats_view(request, project_pk): - project = generics.get_object_or_404( - Project.objects.filter(members=request.user) - if not request.user.is_superuser - else Project.objects.all(), - pk=project_pk, - ) - tickets = project.tickets.all() - stats = { - "total_tickets": tickets.count(), - "open_tickets": tickets.exclude(status__is_completed=True).count(), - "completed_tickets": tickets.filter(status__is_completed=True).count(), - "total_members": project.members.count(), - "total_epics": project.epics.count(), - } - return Response(stats) - - -@api_view(["GET"]) -@permission_classes([permissions.IsAuthenticated]) -def dashboard_stats_view(request): - user = request.user - if user.is_superuser: - total_projects = Project.objects.count() - total_tickets = Ticket.objects.count() - total_members = user.__class__.objects.count() - else: - total_projects = Project.objects.filter(members=user).count() - total_tickets = ( - Ticket.objects.filter( - models.Q(project__members=user) | models.Q(assignees=user) - ) - .distinct() - .count() - ) - total_members = ( - user.__class__.objects.filter(project_memberships__project__members=user) - .distinct() - .count() - ) - - my_assigned = Ticket.objects.filter(assignees=user).count() - my_overdue = Ticket.objects.filter( - assignees=user, due_date__lt=timezone.now().date(), status__is_completed=False - ).count() - completed_this_week = ( - Ticket.objects.filter( - models.Q(assignees=user) | models.Q(created_by=user), - histories__created_at__week=timezone.now().isocalendar()[1], - histories__status__is_completed=True, - ) - .distinct() - .count() - ) - - return Response( - { - "total_projects": total_projects, - "total_tickets": total_tickets, - "total_members": total_members, - "my_assigned": my_assigned, - "my_overdue": my_overdue, - "completed_this_week": completed_this_week, - } - ) +from rest_framework import viewsets +# Create your views here. \ No newline at end of file diff --git a/src/tickets/admin.py b/src/tickets/admin.py index 76be9bc..0dc5f91 100644 --- a/src/tickets/admin.py +++ b/src/tickets/admin.py @@ -1,64 +1,5 @@ +"""generated with djinit""" + from django.contrib import admin -from src.tickets.models import ( - Ticket, - TicketComment, - TicketHistory, - TicketPriority, - TicketStatus, - TicketUser, -) - - -class TicketUserInline(admin.TabularInline): - model = TicketUser - extra = 1 - - -class TicketCommentInline(admin.TabularInline): - model = TicketComment - extra = 0 - readonly_fields = ("user", "comment", "created_at") - - -class TicketHistoryInline(admin.TabularInline): - model = TicketHistory - extra = 0 - readonly_fields = ("user", "status", "created_at") - - -@admin.register(Ticket) -class TicketAdmin(admin.ModelAdmin): - list_display = ("id", "uuid", "name", "project", "status", "priority", "due_date") - list_filter = ("project", "status", "priority") - search_fields = ("name", "uuid") - inlines = (TicketUserInline, TicketCommentInline, TicketHistoryInline) - - -@admin.register(TicketStatus) -class TicketStatusAdmin(admin.ModelAdmin): - list_display = ("id", "name", "project", "color", "sort_order", "is_completed") - list_filter = ("project",) - - -@admin.register(TicketPriority) -class TicketPriorityAdmin(admin.ModelAdmin): - list_display = ("id", "name", "color") - - -@admin.register(TicketComment) -class TicketCommentAdmin(admin.ModelAdmin): - list_display = ("id", "ticket", "user", "created_at") - readonly_fields = ("ticket", "user", "comment", "created_at") - - -@admin.register(TicketHistory) -class TicketHistoryAdmin(admin.ModelAdmin): - list_display = ("id", "ticket", "user", "status", "created_at") - readonly_fields = ("ticket", "user", "status", "created_at") - - -@admin.register(TicketUser) -class TicketUserAdmin(admin.ModelAdmin): - list_display = ("id", "ticket", "user") - +# Register your models here. \ No newline at end of file diff --git a/src/tickets/models.py b/src/tickets/models.py index 8bbbff7..d7b9f75 100644 --- a/src/tickets/models.py +++ b/src/tickets/models.py @@ -1,186 +1,5 @@ -import random -import string +"""generated with djinit""" -from django.conf import settings from django.db import models - -def generate_ticket_uuid(prefix): - rand = "".join(random.choices(string.ascii_uppercase + string.digits, k=6)) - return f"{prefix}-{rand}" - - -class TicketPriority(models.Model): - name = models.CharField(max_length=255, unique=True) - color = models.CharField(max_length=7, default="#6B7280") - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - db_table = "ticket_priorities" - verbose_name = "Ticket Priority" - verbose_name_plural = "Ticket Priorities" - - def __str__(self): - return self.name - - -class TicketStatus(models.Model): - project = models.ForeignKey( - "projects.Project", on_delete=models.CASCADE, related_name="ticket_statuses" - ) - name = models.CharField(max_length=255) - color = models.CharField(max_length=7, default="#10B981") - sort_order = models.IntegerField(default=0) - is_completed = models.BooleanField(default=False) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - db_table = "ticket_statuses" - verbose_name = "Ticket Status" - verbose_name_plural = "Ticket Statuses" - indexes = [ - models.Index(fields=["project", "sort_order"]), - models.Index(fields=["project", "is_completed"]), - ] - - def __str__(self): - return f"[{self.project.ticket_prefix}] {self.name}" - - -class Ticket(models.Model): - project = models.ForeignKey( - "projects.Project", on_delete=models.CASCADE, related_name="tickets" - ) - status = models.ForeignKey( - TicketStatus, on_delete=models.CASCADE, related_name="tickets" - ) - epic = models.ForeignKey( - "epics.Epic", - on_delete=models.SET_NULL, - blank=True, - null=True, - related_name="tickets", - ) - priority = models.ForeignKey( - TicketPriority, - on_delete=models.SET_NULL, - blank=True, - null=True, - related_name="tickets", - ) - name = models.CharField(max_length=255) - description = models.TextField(blank=True, null=True) - uuid = models.CharField(max_length=255, unique=True, editable=False) - start_date = models.DateField(blank=True, null=True) - due_date = models.DateField(blank=True, null=True) - created_by = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.SET_NULL, - blank=True, - null=True, - related_name="created_tickets", - ) - assignees = models.ManyToManyField( - settings.AUTH_USER_MODEL, - through="TicketUser", - related_name="assigned_tickets", - ) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - db_table = "tickets" - verbose_name = "Ticket" - verbose_name_plural = "Tickets" - indexes = [ - models.Index(fields=["project", "status"]), - models.Index(fields=["status", "created_at"]), - models.Index(fields=["project", "created_at"]), - models.Index(fields=["project", "updated_at"]), - models.Index(fields=["due_date"]), - models.Index(fields=["priority"]), - models.Index(fields=["created_by"]), - ] - - def __str__(self): - return f"{self.uuid} - {self.name}" - - def save(self, *args, **kwargs): - if not self.uuid: - self.uuid = generate_ticket_uuid(self.project.ticket_prefix) - super().save(*args, **kwargs) - - -class TicketUser(models.Model): - ticket = models.ForeignKey( - Ticket, on_delete=models.CASCADE, related_name="ticket_assignees" - ) - user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - db_table = "ticket_users" - verbose_name = "Ticket Assignee" - verbose_name_plural = "Ticket Assignees" - constraints = [ - models.UniqueConstraint( - fields=["ticket", "user"], name="unique_ticket_user" - ) - ] - indexes = [ - models.Index(fields=["ticket", "user"]), - models.Index(fields=["user"]), - ] - - def __str__(self): - return f"{self.user} -> {self.ticket}" - - -class TicketHistory(models.Model): - ticket = models.ForeignKey( - Ticket, on_delete=models.CASCADE, related_name="histories" - ) - user = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE, - related_name="ticket_histories", - ) - status = models.ForeignKey( - TicketStatus, on_delete=models.CASCADE, related_name="histories" - ) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - db_table = "ticket_histories" - verbose_name = "Ticket History" - verbose_name_plural = "Ticket Histories" - - def __str__(self): - return f"{self.ticket.uuid} -> {self.status.name} by {self.user}" - - -class TicketComment(models.Model): - ticket = models.ForeignKey( - Ticket, on_delete=models.CASCADE, related_name="comments" - ) - user = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE, - related_name="ticket_comments", - ) - comment = models.TextField(help_text="Rich text HTML content") - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - db_table = "ticket_comments" - verbose_name = "Ticket Comment" - verbose_name_plural = "Ticket Comments" - - def __str__(self): - return f"Comment by {self.user} on {self.ticket.uuid}" - +# Create your models here. \ No newline at end of file diff --git a/src/tickets/serializers.py b/src/tickets/serializers.py index ea5c430..fbcbed8 100644 --- a/src/tickets/serializers.py +++ b/src/tickets/serializers.py @@ -1,174 +1,5 @@ -from src.accounts.serializers import UserSerializer -from src.epics.serializers import EpicSerializer +"""generated with djinit""" + from rest_framework import serializers -from src.tickets.models import ( - Ticket, - TicketComment, - TicketHistory, - TicketPriority, - TicketStatus, - TicketUser, -) - - -class TicketPrioritySerializer(serializers.ModelSerializer): - class Meta: - model = TicketPriority - fields = ("id", "name", "color", "created_at", "updated_at") - read_only_fields = ("id", "created_at", "updated_at") - - -class TicketStatusSerializer(serializers.ModelSerializer): - class Meta: - model = TicketStatus - fields = ( - "id", - "project", - "name", - "color", - "sort_order", - "is_completed", - "created_at", - "updated_at", - ) - read_only_fields = ("id", "created_at", "updated_at") - - -class TicketUserSerializer(serializers.ModelSerializer): - user = UserSerializer(read_only=True) - user_id = serializers.IntegerField(write_only=True) - - class Meta: - model = TicketUser - fields = ("id", "user", "user_id", "created_at") - read_only_fields = ("id", "created_at") - - -class TicketCommentSerializer(serializers.ModelSerializer): - user = UserSerializer(read_only=True) - - class Meta: - model = TicketComment - fields = ("id", "ticket", "user", "comment", "created_at", "updated_at") - read_only_fields = ("id", "user", "created_at", "updated_at") - - -class TicketHistorySerializer(serializers.ModelSerializer): - user = UserSerializer(read_only=True) - status = TicketStatusSerializer(read_only=True) - - class Meta: - model = TicketHistory - fields = ("id", "ticket", "user", "status", "created_at") - read_only_fields = ("id", "created_at") - - -class TicketListSerializer(serializers.ModelSerializer): - status = TicketStatusSerializer(read_only=True) - priority = TicketPrioritySerializer(read_only=True) - epic = EpicSerializer(read_only=True) - assignees = UserSerializer(many=True, read_only=True) - creator = UserSerializer(source="created_by", read_only=True) - - class Meta: - model = Ticket - fields = ( - "id", - "uuid", - "project", - "status", - "epic", - "priority", - "name", - "description", - "start_date", - "due_date", - "creator", - "assignees", - "created_at", - "updated_at", - ) - read_only_fields = ("id", "uuid", "created_at", "updated_at") - - -class TicketDetailSerializer(serializers.ModelSerializer): - status = TicketStatusSerializer(read_only=True) - priority = TicketPrioritySerializer(read_only=True) - epic = EpicSerializer(read_only=True) - assignees = UserSerializer(many=True, read_only=True) - creator = UserSerializer(source="created_by", read_only=True) - comments = TicketCommentSerializer(many=True, read_only=True) - histories = TicketHistorySerializer(many=True, read_only=True) - - class Meta: - model = Ticket - fields = ( - "id", - "uuid", - "project", - "status", - "epic", - "priority", - "name", - "description", - "start_date", - "due_date", - "creator", - "assignees", - "comments", - "histories", - "created_at", - "updated_at", - ) - read_only_fields = ("id", "uuid", "created_at", "updated_at") - - -class TicketWriteSerializer(serializers.ModelSerializer): - assignee_ids = serializers.ListField( - child=serializers.IntegerField(), write_only=True, required=False - ) - - class Meta: - model = Ticket - fields = ( - "project", - "status", - "epic", - "priority", - "name", - "description", - "start_date", - "due_date", - "assignee_ids", - ) - - def create(self, validated_data): - assignee_ids = validated_data.pop("assignee_ids", []) - user = self.context["request"].user - ticket = Ticket.objects.create(**validated_data, created_by=user) - for uid in assignee_ids: - TicketUser.objects.get_or_create(ticket=ticket, user_id=uid) - return ticket - - def update(self, instance, validated_data): - assignee_ids = validated_data.pop("assignee_ids", None) - for attr, value in validated_data.items(): - setattr(instance, attr, value) - old_status_id = instance.status_id - instance.save() - - if assignee_ids is not None: - instance.ticket_assignees.all().delete() - for uid in assignee_ids: - TicketUser.objects.get_or_create(ticket=instance, user_id=uid) - - if "status" in validated_data and old_status_id != instance.status_id: - TicketHistory.objects.create( - ticket=instance, - user=self.context["request"].user, - status=instance.status, - ) - - return instance - +# Create your serializers here. \ No newline at end of file diff --git a/src/tickets/urls.py b/src/tickets/urls.py index a9d7410..8020533 100644 --- a/src/tickets/urls.py +++ b/src/tickets/urls.py @@ -1,26 +1,7 @@ +"""generated with djinit""" + from django.urls import path -from src.tickets import views - urlpatterns = [ - path("", views.TicketListCreateView.as_view(), name="ticket-list"), - path("priorities/", views.PriorityListView.as_view(), name="priority-list"), - path( - "priorities//", - views.PriorityDetailView.as_view(), - name="priority-detail", - ), - path("/", views.TicketDetailView.as_view(), name="ticket-detail"), - path("/move/", views.move_ticket_view, name="ticket-move"), - path( - "/comments/", - views.TicketCommentListCreateView.as_view(), - name="ticket-comment-list", - ), - path( - "/histories/", - views.TicketHistoryListView.as_view(), - name="ticket-history-list", - ), -] - + # Add your URL patterns here +] \ No newline at end of file diff --git a/src/tickets/views.py b/src/tickets/views.py index 5062b24..6630beb 100644 --- a/src/tickets/views.py +++ b/src/tickets/views.py @@ -1,136 +1,5 @@ -from django.db import models -from rest_framework import generics, permissions -from rest_framework.decorators import api_view, permission_classes -from rest_framework.response import Response +"""generated with djinit""" -from src.tickets.models import ( - Ticket, - TicketComment, - TicketHistory, - TicketPriority, -) -from src.tickets.serializers import ( - TicketCommentSerializer, - TicketDetailSerializer, - TicketHistorySerializer, - TicketListSerializer, - TicketPrioritySerializer, - TicketWriteSerializer, -) - - -class TicketListCreateView(generics.ListCreateAPIView): - permission_classes = (permissions.IsAuthenticated,) - - def get_serializer_class(self): - if self.request.method == "POST": - return TicketWriteSerializer - return TicketListSerializer - - def get_queryset(self): - qs = Ticket.objects.select_related( - "status", "priority", "epic", "created_by" - ).prefetch_related("assignees") - project_id = self.request.query_params.get("project") - status_id = self.request.query_params.get("status") - epic_id = self.request.query_params.get("epic") - priority_id = self.request.query_params.get("priority") - assignee_id = self.request.query_params.get("assignee") - - if project_id: - qs = qs.filter(project_id=project_id) - if status_id: - qs = qs.filter(status_id=status_id) - if epic_id: - qs = qs.filter(epic_id=epic_id) - if priority_id: - qs = qs.filter(priority_id=priority_id) - if assignee_id: - qs = qs.filter(ticket_assignees__user_id=assignee_id) - - user = self.request.user - if not user.is_superuser: - qs = qs.filter( - models.Q(project__members=user) | models.Q(assignees=user) - ).distinct() - - return qs.order_by("-created_at") - - -class TicketDetailView(generics.RetrieveUpdateDestroyAPIView): - permission_classes = (permissions.IsAuthenticated,) - - def get_serializer_class(self): - if self.request.method in ("PUT", "PATCH"): - return TicketWriteSerializer - return TicketDetailSerializer - - def get_queryset(self): - return Ticket.objects.select_related( - "status", "priority", "epic", "created_by" - ).prefetch_related("assignees", "comments__user", "histories__user__status") - - -class TicketCommentListCreateView(generics.ListCreateAPIView): - serializer_class = TicketCommentSerializer - permission_classes = (permissions.IsAuthenticated,) - - def get_queryset(self): - return ( - TicketComment.objects.filter(ticket_id=self.kwargs["ticket_pk"]) - .select_related("user") - .order_by("created_at") - ) - - def perform_create(self, serializer): - serializer.save( - ticket_id=self.kwargs["ticket_pk"], - user=self.request.user, - ) - - -class TicketHistoryListView(generics.ListAPIView): - serializer_class = TicketHistorySerializer - permission_classes = (permissions.IsAuthenticated,) - - def get_queryset(self): - return ( - TicketHistory.objects.filter(ticket_id=self.kwargs["ticket_pk"]) - .select_related("user", "status") - .order_by("-created_at") - ) - - -@api_view(["PATCH"]) -@permission_classes([permissions.IsAuthenticated]) -def move_ticket_view(request, pk): - ticket = generics.get_object_or_404(Ticket, pk=pk) - new_status_id = request.data.get("status_id") - new_sort = request.data.get("sort_order") - - if new_status_id: - old_status_id = ticket.status_id - ticket.status_id = new_status_id - ticket.save() - if old_status_id != int(new_status_id): - TicketHistory.objects.create( - ticket=ticket, - user=request.user, - status_id=new_status_id, - ) - - return Response(TicketListSerializer(ticket).data) - - -# Priority and Status list views (global) -class PriorityListView(generics.ListCreateAPIView): - queryset = TicketPriority.objects.all() - serializer_class = TicketPrioritySerializer - permission_classes = (permissions.IsAuthenticated,) - - -class PriorityDetailView(generics.RetrieveUpdateDestroyAPIView): - queryset = TicketPriority.objects.all() - serializer_class = TicketPrioritySerializer - permission_classes = (permissions.IsAuthenticated,) +from rest_framework import viewsets +# Create your views here. \ No newline at end of file diff --git a/uv.lock b/uv.lock index 7f7440a..fcf8e22 100644 --- a/uv.lock +++ b/uv.lock @@ -20,15 +20,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" }, ] -[[package]] -name = "attrs" -version = "26.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, -] - [[package]] name = "backend" version = "0.1.0" @@ -37,18 +28,14 @@ dependencies = [ { name = "django" }, { name = "django-cors-headers" }, { name = "django-environ" }, - { name = "django-filter" }, { name = "django-htmx" }, - { name = "django-import-export" }, { name = "django-jazzmin" }, { name = "django-tailwind-cli" }, { name = "djangorestframework" }, { name = "djangorestframework-simplejwt" }, - { name = "drf-spectacular" }, + { name = "drf-yasg" }, { name = "gunicorn" }, { name = "mysqlclient" }, - { name = "openpyxl" }, - { name = "requests" }, { name = "whitenoise" }, ] @@ -64,18 +51,14 @@ requires-dist = [ { name = "django", specifier = ">=6.0.6" }, { name = "django-cors-headers", specifier = ">=4.9.0" }, { name = "django-environ", specifier = ">=0.14.0" }, - { name = "django-filter", specifier = ">=25.2" }, { name = "django-htmx", specifier = ">=1.27.0" }, - { name = "django-import-export", specifier = ">=4.4.1" }, - { name = "django-jazzmin", specifier = ">=3.0.4" }, + { name = "django-jazzmin", specifier = ">=3.0.5" }, { name = "django-tailwind-cli", specifier = ">=4.6.2" }, { name = "djangorestframework", specifier = ">=3.17.1" }, { name = "djangorestframework-simplejwt", specifier = ">=5.5.1" }, - { name = "drf-spectacular", specifier = ">=0.29.0" }, + { name = "drf-yasg", specifier = ">=1.21.15" }, { name = "gunicorn", specifier = ">=26.0.0" }, { name = "mysqlclient", specifier = ">=2.2.8" }, - { name = "openpyxl", specifier = ">=3.1.5" }, - { name = "requests", specifier = ">=2.34.2" }, { name = "whitenoise", specifier = ">=6.12.0" }, ] @@ -83,73 +66,7 @@ requires-dist = [ dev = [ { name = "pytest", specifier = ">=9.1.1" }, { name = "pytest-django", specifier = ">=4.12.0" }, - { name = "ruff", specifier = ">=0.15.19" }, -] - -[[package]] -name = "certifi" -version = "2026.6.17" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, - { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, - { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, - { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, - { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, - { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, - { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, - { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, - { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, - { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, - { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, - { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, - { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, - { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, - { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, - { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, - { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, - { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, - { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, - { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, - { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, - { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, - { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, - { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, - { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, - { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, - { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, - { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, - { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, - { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, - { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, - { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, - { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, - { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, - { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, - { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, - { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, - { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, - { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, - { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, - { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, - { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, + { name = "ruff", specifier = ">=0.15.20" }, ] [[package]] @@ -173,15 +90,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] -[[package]] -name = "diff-match-patch" -version = "20241021" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0e/ad/32e1777dd57d8e85fa31e3a243af66c538245b8d64b7265bec9a61f2ca33/diff_match_patch-20241021.tar.gz", hash = "sha256:beae57a99fa48084532935ee2968b8661db861862ec82c6f21f4acdd6d835073", size = 39962, upload-time = "2024-10-21T19:41:21.094Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/bb/2aa9b46a01197398b901e458974c20ed107935c26e44e37ad5b0e5511e44/diff_match_patch-20241021-py3-none-any.whl", hash = "sha256:93cea333fb8b2bc0d181b0de5e16df50dd344ce64828226bda07728818936782", size = 43252, upload-time = "2024-10-21T19:41:19.914Z" }, -] - [[package]] name = "django" version = "6.0.6" @@ -218,18 +126,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/fe/67b423fc30f16d10259e901320fbc121746bf168aaaad9043e6ebb0110c1/django_environ-0.14.0-py3-none-any.whl", hash = "sha256:8dbe8a57f0a540ab8abd6f54f230de5e99e3a2c9d797cb9caecb037bca3d47d8", size = 20934, upload-time = "2026-06-18T22:49:54.355Z" }, ] -[[package]] -name = "django-filter" -version = "25.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "django" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2c/e4/465d2699cd388c0005fb8d6ae6709f239917c6d8790ac35719676fffdcf3/django_filter-25.2.tar.gz", hash = "sha256:760e984a931f4468d096f5541787efb8998c61217b73006163bf2f9523fe8f23", size = 143818, upload-time = "2025-10-05T09:51:31.521Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/40/6a02495c5658beb1f31eb09952d8aa12ef3c2a66342331ce3a35f7132439/django_filter-25.2-py3-none-any.whl", hash = "sha256:9c0f8609057309bba611062fe1b720b4a873652541192d232dd28970383633e3", size = 94145, upload-time = "2025-10-05T09:51:29.728Z" }, -] - [[package]] name = "django-htmx" version = "1.27.0" @@ -243,30 +139,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/23/ac/25d28489dc43224e260f4ebee7565f7ef1efe12af0f284a89500c19f75e2/django_htmx-1.27.0-py3-none-any.whl", hash = "sha256:13e1e13b87d39b57f95aae6e4987cb3df056d0b1373a41f4a94504a00298ffd8", size = 62126, upload-time = "2025-11-28T23:18:53.57Z" }, ] -[[package]] -name = "django-import-export" -version = "4.4.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "diff-match-patch" }, - { name = "django" }, - { name = "tablib" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/92/32/5b83cc46a70ac94c1d17144097bf8aee7d9bb401d0863652c8d5ce1d2e50/django_import_export-4.4.1.tar.gz", hash = "sha256:165dc7fdcd56a6356342eef3f88f3f3682b64b6f67e9a259a137e8b57aeff10c", size = 2238690, upload-time = "2026-05-05T12:42:44.295Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/91/d1/110aeb2acffea56d4222861b5678c2643f0bda00081e40d687077348bb7c/django_import_export-4.4.1-py3-none-any.whl", hash = "sha256:8be2782e505ae303ccb02070a1b4c528995922126fca9ee449eb28666835dd4b", size = 157691, upload-time = "2026-05-05T12:42:42.554Z" }, -] - [[package]] name = "django-jazzmin" -version = "3.0.4" +version = "3.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e0/70/a652284cd612bcd45da3cc18715db391c6cdeafd1ec9af60f4a3a243c74e/django_jazzmin-3.0.4.tar.gz", hash = "sha256:e8aef8b75edd8792408a69407c22ece924ab7a3d5c9d6f84d7a4eb57e06e7a44", size = 2423027, upload-time = "2026-03-13T11:29:18.821Z" } +sdist = { url = "https://files.pythonhosted.org/packages/15/e4/17cd62e80c4876088bc5f8bca24446586a62eca386c82755894408751a44/django_jazzmin-3.0.5.tar.gz", hash = "sha256:7e39b029bddbb60f3aaf9897bd009f39f8f2bdc8a7d86beed1e943706dd79692", size = 2509734, upload-time = "2026-06-25T21:05:52.699Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/1c/42d37c90bec6f6763206b966252dabe019f0c0197a6aaebafa5b213b78f1/django_jazzmin-3.0.4-py3-none-any.whl", hash = "sha256:70935a17604cc530e99be68d3d2b8c576095e1dac4af50464c827d66646f40fa", size = 2495119, upload-time = "2026-03-13T11:29:20.447Z" }, + { url = "https://files.pythonhosted.org/packages/23/40/892f4c922c92d0d2b8ca5f96f556c5dc04d19ea2ea717910d8bbf2dc23fc/django_jazzmin-3.0.5-py3-none-any.whl", hash = "sha256:578fb1fc0a1b7e3b8472d2692ace60b1584ccd937c3828e2c9a89bdf94067f94", size = 2581722, upload-time = "2026-06-25T21:05:50.948Z" }, ] [[package]] @@ -324,29 +206,21 @@ wheels = [ ] [[package]] -name = "drf-spectacular" -version = "0.29.0" +name = "drf-yasg" +version = "1.21.15" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django" }, { name = "djangorestframework" }, { name = "inflection" }, - { name = "jsonschema" }, + { name = "packaging" }, + { name = "pytz" }, { name = "pyyaml" }, { name = "uritemplate" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/0e/a4f50d83e76cbe797eda88fc0083c8ca970cfa362b5586359ef06ec6f70a/drf_spectacular-0.29.0.tar.gz", hash = "sha256:0a069339ea390ce7f14a75e8b5af4a0860a46e833fd4af027411a3e94fc1a0cc", size = 241722, upload-time = "2025-11-02T03:40:26.348Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/88/345135459b9cbaff0e8ee3270819e89ca92064a35a0a94a1cfce41c084db/drf_yasg-1.21.15.tar.gz", hash = "sha256:ef86838c4ef10dcd3ac1ebf2be601cbe02978b999671caa43667f7c9db961468", size = 5153419, upload-time = "2026-02-24T18:09:21.072Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/d9/502c56fc3ca960075d00956283f1c44e8cafe433dada03f9ed2821f3073b/drf_spectacular-0.29.0-py3-none-any.whl", hash = "sha256:d1ee7c9535d89848affb4427347f7c4a22c5d22530b8842ef133d7b72e19b41a", size = 105433, upload-time = "2025-11-02T03:40:24.823Z" }, -] - -[[package]] -name = "et-xmlfile" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a4/400b0565cf25395f1d5e1a24e5d18dab8f4199e4212174341fea6f05747c/drf_yasg-1.21.15-py3-none-any.whl", hash = "sha256:7c7a7ab9feb0e13cdd6e25147d99adb0500a68bd96509ffd8f8cf7efd4bdc77e", size = 4856209, upload-time = "2026-02-24T18:09:18.982Z" }, ] [[package]] @@ -361,15 +235,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/40/9c2384fc2be4ad25dd4a49decd5ad9ea5a3639814c11bd40ab77cb9f0a14/gunicorn-26.0.0-py3-none-any.whl", hash = "sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc", size = 212009, upload-time = "2026-05-05T06:38:23.007Z" }, ] -[[package]] -name = "idna" -version = "3.18" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, -] - [[package]] name = "inflection" version = "0.5.1" @@ -388,33 +253,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] -[[package]] -name = "jsonschema" -version = "4.26.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "jsonschema-specifications" }, - { name = "referencing" }, - { name = "rpds-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, -] - -[[package]] -name = "jsonschema-specifications" -version = "2025.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "referencing" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, -] - [[package]] name = "markdown-it-py" version = "4.2.0" @@ -447,18 +285,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/66/e5/037d55623be9f681236e04abe12e1290847c06bd48270c3f19ac33493cbf/mysqlclient-2.2.8-cp314-cp314t-win_amd64.whl", hash = "sha256:260cce0e81446c83bf0a389e0fae38d68547d9f8fc0833bc733014e10ce28a99", size = 213067, upload-time = "2026-02-10T10:58:43.389Z" }, ] -[[package]] -name = "openpyxl" -version = "3.1.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "et-xmlfile" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, -] - [[package]] name = "packaging" version = "26.2" @@ -523,6 +349,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/83/a5/41d091f697c09609e7ef1d5d61925494e0454ebf51de7de05f0f0a728f1d/pytest_django-4.12.0-py3-none-any.whl", hash = "sha256:3ff300c49f8350ba2953b90297d23bf5f589db69545f56f1ec5f8cff5da83e85", size = 26123, upload-time = "2026-02-14T18:40:47.381Z" }, ] +[[package]] +name = "pytz" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/46/dd499ec9038423421951e4fad73051febaa13d2df82b4064f87af8b8c0c3/pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a", size = 320861, upload-time = "2026-05-04T01:35:29.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126", size = 510141, upload-time = "2026-05-04T01:35:27.408Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -559,34 +394,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] -[[package]] -name = "referencing" -version = "0.37.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "rpds-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, -] - -[[package]] -name = "requests" -version = "2.34.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, -] - [[package]] name = "rich" version = "15.0.0" @@ -600,124 +407,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, ] -[[package]] -name = "rpds-py" -version = "2026.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2e/43/25a8dcd3feedd735039a8f0b5b7e3b118232b5eae288c4fd9ab200d41094/rpds_py-2026.5.1.tar.gz", hash = "sha256:07b24fea40541e28570e5b795a4a38fbdcd12550c06bd0748005ecc8116ca256", size = 64459, upload-time = "2026-05-28T12:02:13.232Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/32/14c961ad295f490eb0849ada8b79683e93a59b9de3afdd983eaf55fa6867/rpds_py-2026.5.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:efef4ac29c6ff495531eb17ee705b62841ecaa291b7c7077e848ea03e237164d", size = 352787, upload-time = "2026-05-28T11:59:33.655Z" }, - { url = "https://files.pythonhosted.org/packages/ca/bb/d1b85117967c11191441a7274ae616c65d93901d082c588f89a50a8da5ae/rpds_py-2026.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c39f5b67a8a2e67179ada2a954227d670fe65fa9098457f698f56ddf248709b3", size = 345179, upload-time = "2026-05-28T11:59:35Z" }, - { url = "https://files.pythonhosted.org/packages/7c/46/d84105f062e626a1b233f863907288a4708c2d833b8b4c6fb2764bc080c0/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5c30f3f04eef4fbd362226a6f31d7c8895ca4fbb6e0b790f6890a98d8da8559", size = 376173, upload-time = "2026-05-28T11:59:36.43Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ae/469d7959ce5b1201e1de135dc735b86db3b35dd0d1734f6a44246d5f061c/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:277f6c82f0580848796c7ecc8a7173aa3bfb928e4ff831261c2f60a81dc270db", size = 383162, upload-time = "2026-05-28T11:59:37.995Z" }, - { url = "https://files.pythonhosted.org/packages/dc/a2/57853d31a1116a561aa072794602ad3f6341e18d70a8523f1bd5b9fc1e5a/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63c2c4c213f1a4e3f3de28ecab029dbdee976324e729c0d7a55211be72576b02", size = 495093, upload-time = "2026-05-28T11:59:39.453Z" }, - { url = "https://files.pythonhosted.org/packages/99/63/3a8eabcad9314b7daf5c65f451d2c33d989235cd8a5762186cf2c3f5a4f8/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3350ec808fb538fe71a1f94dfaa0e29c598dfad805ce49f0caec5ae3183c652b", size = 389829, upload-time = "2026-05-28T11:59:40.896Z" }, - { url = "https://files.pythonhosted.org/packages/4b/25/05678d97fc25e2622df14dc530fb82023174ecfff6733991ed0d78f167bd/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b964e3ab599e718dc46c018d104b1ebc007cbc6567d827c94a687fca56d77e", size = 374786, upload-time = "2026-05-28T11:59:42.626Z" }, - { url = "https://files.pythonhosted.org/packages/88/d1/8c90b6431e80a3b91b284a5c7c8c0c4f9c006444d90477a740d6e0f9c694/rpds_py-2026.5.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:19cb09fab7b7fc96b2a6e28f2e34b72a3705ff27b37edb77455316e5d3f3dc9b", size = 386920, upload-time = "2026-05-28T11:59:44.124Z" }, - { url = "https://files.pythonhosted.org/packages/ff/99/4638f672ab356682d633ee0da9255f5b67ce6efd0b85eb94ad3e255e65a5/rpds_py-2026.5.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abe76bcdba31e576cb83eeb8797aa0d882b738fef6dc65d0601fc753806a5b46", size = 405059, upload-time = "2026-05-28T11:59:47.177Z" }, - { url = "https://files.pythonhosted.org/packages/66/3f/3546524b6eb4cc2e1f363a3d638fa52f6c24faae3500c25fb488b02f1740/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bff7073db3899158fff55ebf57b113a67030af26f80a18978f9f0aa60250ddf", size = 553030, upload-time = "2026-05-28T11:59:48.603Z" }, - { url = "https://files.pythonhosted.org/packages/c6/c3/7b3388c796fcf471bd17194242d4dc1a7608567c0fa422bcc1c5e79f9c1e/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8ba264fa49be666cd9cc56bf34ec7002fb3d27a4aee5bcb4d43d0d18feb1bb6f", size = 618975, upload-time = "2026-05-28T11:59:50.314Z" }, - { url = "https://files.pythonhosted.org/packages/61/1e/a3cb07f2795075d1d88efddae2f541359fde5f08c81ee114c29c2949c90a/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4860b603ddda0475a8885499b3729e90229d480105b42651962a5397d995fa89", size = 581178, upload-time = "2026-05-28T11:59:51.673Z" }, - { url = "https://files.pythonhosted.org/packages/a1/74/e758c03a5ef46f04c37f2651a2893db846d569ba8a7bca469d4b58939bcd/rpds_py-2026.5.1-cp313-cp313-win32.whl", hash = "sha256:7944270ae71383f6e2657dd7d5ce4eeb4ac2d0059a6738f0510583d462ab4842", size = 212481, upload-time = "2026-05-28T11:59:53.148Z" }, - { url = "https://files.pythonhosted.org/packages/70/ec/a2aca432db9c7359b40fa393eeeaa0d166c2f70175be956e75fa24197c44/rpds_py-2026.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:88647f43a73c4e01be19b04ceef0c8d3a1958153604d13c773becd8016f2a0cf", size = 228519, upload-time = "2026-05-28T11:59:54.505Z" }, - { url = "https://files.pythonhosted.org/packages/29/60/a73bfdd45b096574556acf303bbd9fa9eed36ca8a818b514e2a5d5fe2b9d/rpds_py-2026.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:453895624ecf7db7063b1004e44037522bbaef9ff6a945e59bc71662d7a03abd", size = 223446, upload-time = "2026-05-28T11:59:56.081Z" }, - { url = "https://files.pythonhosted.org/packages/18/e2/408105fd611823f00882aea810f3989a30d26b1bab8b6beb20f98c724e0e/rpds_py-2026.5.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:b4e4bc98639ec915f512fde3aa7a95e0041d95d9c3cc86eea841fa63cb1e8600", size = 355287, upload-time = "2026-05-28T11:59:57.448Z" }, - { url = "https://files.pythonhosted.org/packages/8d/58/5c4a43436843c90d0f6d19f82c200c80e3843ca9fa07b237623327f6d384/rpds_py-2026.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cacedb7a6e167680acba45ad5716e89067d225dc80da0d7040cae8c81d4572fa", size = 347033, upload-time = "2026-05-28T11:59:58.881Z" }, - { url = "https://files.pythonhosted.org/packages/fb/c2/1a71acdacaf4e259b10278fb87b039ded3cf80041bcd89dd8a3ea702ded6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68700371c5d7ae1412862ddfa719090925c93ecf351c566d66f09d04b136ea00", size = 376891, upload-time = "2026-05-28T12:00:00.516Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c8/535f3d9b65addd8e28aa87b83c6e526799c3717a88273db8ea795beeef7a/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:296c799becfa849c779c8725494fe9ed94959ed886787df4364b058465bad7f0", size = 385646, upload-time = "2026-05-28T12:00:02.394Z" }, - { url = "https://files.pythonhosted.org/packages/1c/91/dc033f313345c354ade914dbe73cdb90b615a4409ea02430d5356794f3d8/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3858b908218ee108d0bbfb2095ccc237648053c9bf98affad7cb079acaf1d97", size = 498830, upload-time = "2026-05-28T12:00:04.189Z" }, - { url = "https://files.pythonhosted.org/packages/27/fc/90fcbea459dbb8ddc18a2e0fd1de9412b48bc84ffff2db771cf714bacfd6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4fb8d2e7cb2f850b169806d61d1b991738acec96500a75c30f49caf064ce7cef", size = 392830, upload-time = "2026-05-28T12:00:05.797Z" }, - { url = "https://files.pythonhosted.org/packages/b2/1d/46cd11a228c9750684a798d98f878be6f614aa762438da7378f035e79e35/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27b74c10ed6a8f190f4287f53bcfea348b92a84a9c9f70d30183d1e6172d580d", size = 379613, upload-time = "2026-05-28T12:00:07.433Z" }, - { url = "https://files.pythonhosted.org/packages/24/4a/d9b0c6af3a1de03eb93741bbe8be2bdce84d8fda8224f3005451d86df389/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b9a6528956191c48c52294a592dbd4a8386d7048bdb25c0efcb6b966466c6d83", size = 388183, upload-time = "2026-05-28T12:00:09.227Z" }, - { url = "https://files.pythonhosted.org/packages/c5/b4/db7aaabdda6d020afc87d981bcc2f57a434c7dec60ecfc2ab3dd50b20351/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:af03e34e860047bc7a352b842856fcf78798fbb81132cc98bd2f907ab4eb9cd2", size = 408578, upload-time = "2026-05-28T12:00:10.779Z" }, - { url = "https://files.pythonhosted.org/packages/08/d6/070f6a41cbb343e2ac4171859bf3f3623e0ab002f72619d6d505313ec2de/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fea6e836d10abbe191d557d33bd58bd5987725fe63aa1eefe557d230209855bd", size = 553573, upload-time = "2026-05-28T12:00:12.443Z" }, - { url = "https://files.pythonhosted.org/packages/75/ab/1a71ea3589c4345dac0a0518f0e6a031cb42689277851b683c46d27463a5/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:fc0c0f878ea770a0a8a462456c5ad36fc9fe6358e6b76fdadc7f17575e0b8bf1", size = 620861, upload-time = "2026-05-28T12:00:14.09Z" }, - { url = "https://files.pythonhosted.org/packages/8a/22/9bf80a56069c0c443fcfefac639a86a744550a2898817a6dfd3e26654924/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e0b360f316d966b048b085857630b3cc51f3db2f07b06f440eac8f695374d1e3", size = 585633, upload-time = "2026-05-28T12:00:15.66Z" }, - { url = "https://files.pythonhosted.org/packages/da/68/3b2c0a75c9e04125696f84ebdbbf304acf5a40b58ba4481cdb98a922c3ba/rpds_py-2026.5.1-cp313-cp313t-win32.whl", hash = "sha256:a2999883eedf72fdfb7520b92c7d4ec2572a71ff40239377aa604cc529eecafc", size = 210074, upload-time = "2026-05-28T12:00:17.291Z" }, - { url = "https://files.pythonhosted.org/packages/e7/8b/609157d5a25d37d4f29f92840ba531f416907c34ae5c5739dd21fc2bef98/rpds_py-2026.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e07be2a9d7122bd6e82dea89814ef8dc893feb1aae97fec1630f3263bbb30e55", size = 228635, upload-time = "2026-05-28T12:00:18.73Z" }, - { url = "https://files.pythonhosted.org/packages/d4/6f/19c1918a4b590d8de87e712e4abe4b3875771eff60216fb6153cf6665c68/rpds_py-2026.5.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1f2c391c3059798093b65df23aca2cac150460ae9c630d99dec83d703d9485b9", size = 349756, upload-time = "2026-05-28T12:00:20.217Z" }, - { url = "https://files.pythonhosted.org/packages/e5/60/a06fe7da34eca79dacbf958a2ba0c6eea85bc2b29de20080bf40f72f66fa/rpds_py-2026.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:413b424f7c4ee65ab5e5be91f5731be0f8b41a1ee2b12dfe810d716312e95a78", size = 343831, upload-time = "2026-05-28T12:00:21.711Z" }, - { url = "https://files.pythonhosted.org/packages/bf/ec/b2333b97b90e2a6ef6ca8ad386ee284968e74bcfe113b3f1a8d9036429a9/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c595a1d9255dce0599e13130d1440ab2506654f2b50294226ee06402f8fef63", size = 375127, upload-time = "2026-05-28T12:00:23.326Z" }, - { url = "https://files.pythonhosted.org/packages/14/7f/e00aae54067f2b488c4637961d5f58204d470795fc791085fa3f15060d2e/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c27c5f6102eac8c03e7595a00827a53b271ba40a53b59ff8709170e0855ea4a", size = 379034, upload-time = "2026-05-28T12:00:24.89Z" }, - { url = "https://files.pythonhosted.org/packages/be/cc/423999bbb8ae8dc93c77fc1d5e984ade5eb89d237d3bb884ccfa72ae2890/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c7fcf61d44cacecaf3aea542b0e053db77972a4573e7ceda16fb2b399161195", size = 490823, upload-time = "2026-05-28T12:00:26.676Z" }, - { url = "https://files.pythonhosted.org/packages/0f/aa/c671bf660f12e68d3c52ff86c7066ed1372df5a0f4f2ff584e419b8207e7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c817a189d4ee14290420e5ff051e4dd6baa13f3edf84685071dee07a6d538ee", size = 388144, upload-time = "2026-05-28T12:00:28.577Z" }, - { url = "https://files.pythonhosted.org/packages/19/c8/d63bb75b68afe77b229e3021c6031bcaf01da5db5b0e69d0d10f9ba679a7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21846aac0ed2e0589f38c12dc44e77bb64e494b771eadbcf169cba00566ba7ba", size = 371959, upload-time = "2026-05-28T12:00:30.304Z" }, - { url = "https://files.pythonhosted.org/packages/82/35/c51122014d8274ff37dc606d60049c3db7d83da02b5b282511e5a906a9a6/rpds_py-2026.5.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b317c87a13f769a4e787819bd508aaa5d69aa09b0880de9af6d3a8a54571cdec", size = 383558, upload-time = "2026-05-28T12:00:31.764Z" }, - { url = "https://files.pythonhosted.org/packages/e3/f9/2790cb99c136a5363acdeacf5c27c56f3de0d4118a1f48fca83404c99c89/rpds_py-2026.5.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce87129d9f2c14fa6c4a8601fb80eb4488c80d38a20cd13758ef11123e14995d", size = 402789, upload-time = "2026-05-28T12:00:33.247Z" }, - { url = "https://files.pythonhosted.org/packages/e5/1b/e4fb584f8c75d35c38150ff6a332cda949e6f97acba1f4fd123b14ab56fe/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9cdddb6c1207d284d94fd1530adf57fbd797fe7c4b8704ba85f49414f2557e7d", size = 551405, upload-time = "2026-05-28T12:00:34.819Z" }, - { url = "https://files.pythonhosted.org/packages/d8/f7/a6731b4216cb3793ea1af5391da240f5683dacc0d13e034fe5fc3503f240/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4e237e139f94d3c036fd28eb9f564c99055476ff4ff05cd42be55ce349b5aa02", size = 616975, upload-time = "2026-05-28T12:00:36.268Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/2e051a81d95d8e63f4b35a1c463a87e8766bc3d083c067c5dfb6bf220747/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ed0954b524873214369184a9c82b0eaa45a3fbb9a798cd95b17e0d98499e7ea0", size = 578701, upload-time = "2026-05-28T12:00:37.82Z" }, - { url = "https://files.pythonhosted.org/packages/65/56/b5f6fdb2083e32bca8a8993d89e70db114b4756c9e2c38421328126689d2/rpds_py-2026.5.1-cp314-cp314-win32.whl", hash = "sha256:2d88621d6a7d4dfa633d21abe90f280bb205274e16b1d1e61c6ad4640b2453b7", size = 209806, upload-time = "2026-05-28T12:00:39.492Z" }, - { url = "https://files.pythonhosted.org/packages/fb/80/65a5aa96c155e611d1ed844e4e1f57f3e36b021f396d9f8585d756e6b90d/rpds_py-2026.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:cef8ac28d26f4dda3533060c20fbf80a325458fa9fd23ea72a73cdfa8e978838", size = 225985, upload-time = "2026-05-28T12:00:40.94Z" }, - { url = "https://files.pythonhosted.org/packages/27/7c/ad185212e87b05f196daef92bc5f3caf07298eb47c295b5585c3dd3093ac/rpds_py-2026.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:eaaea962c68cdc68d4a533ba985ab8e9484277910bbfaa2ab3ef7732667bfed8", size = 221219, upload-time = "2026-05-28T12:00:43.15Z" }, - { url = "https://files.pythonhosted.org/packages/23/58/e14ae18759020334646b031e708ab4158d653a938822bfb7b95ef2e93aa3/rpds_py-2026.5.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:21942f52dbbd5f8758bf021213d28bd45c39e873e65e2407faf5f1846f5761ad", size = 352148, upload-time = "2026-05-28T12:00:44.638Z" }, - { url = "https://files.pythonhosted.org/packages/31/9b/5f4a1e2f960bca3ac5d052b139dd31eed97b259f9d909173821760d542e8/rpds_py-2026.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f414556f6e3958300ff941e40c9f97e3dc9774ddd1b3434c475d73dd354bbed3", size = 345196, upload-time = "2026-05-28T12:00:46.14Z" }, - { url = "https://files.pythonhosted.org/packages/1a/71/1d9574d6a2fa20ab60eaa55c7467f5aa20cbc770f341a05f09c0876f59e2/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef1013a8625c74043210190b246f5b1551e09757c1f356c6e4160ef96c5bc081", size = 374981, upload-time = "2026-05-28T12:00:47.531Z" }, - { url = "https://files.pythonhosted.org/packages/0c/9a/37e99f4915a80aa71670263c1267f7ae0af95f53a3f61e6c3bdc016d4515/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cc68e231a77a5f0d774ae278a1f8e55c0456501820847c1e4efb3829f3441df6", size = 379961, upload-time = "2026-05-28T12:00:49.216Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ff/6e73f74b89d2e0715e0fc86b7dde893f9a61ae2f9b256ff3bdfe41ac4e94/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9baffb505aff33acc69b422a19f77806680f3c8632227d79f48de8a810d1c2c5", size = 495965, upload-time = "2026-05-28T12:00:51.111Z" }, - { url = "https://files.pythonhosted.org/packages/ea/e0/425faba25f59d74d4638b267f7c7a80e8649d2ef4db10a19b0c4a71e6e6f/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8d2f912928d426e8cfa396f7f3f8d29a59e6689c86dcca3c420730c1096322b", size = 389526, upload-time = "2026-05-28T12:00:52.77Z" }, - { url = "https://files.pythonhosted.org/packages/c6/76/7a41960e3fddae47fab43a28684d5da981401dffd88253de0944148654cb/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90f628283be835db980c941767d41c9a27b5239e54ba0a9c1335247e82406964", size = 376190, upload-time = "2026-05-28T12:00:54.215Z" }, - { url = "https://files.pythonhosted.org/packages/27/60/5f38dc70824fc6951b51d35377e577a3a3a4c81a6769cc5a2de25ebe0ad1/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:1ebb2f0ab7e16132995a72de805170e0203df0c3dd22e1ef1cd1fdd90bd7a131", size = 383921, upload-time = "2026-05-28T12:00:55.673Z" }, - { url = "https://files.pythonhosted.org/packages/60/1a/d60a38caa1505f4b9483c3fbbde12c94e1079154f4f401a6da96f7e77621/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f3df3d16ded76f1f8c9cdebd0e1ea55fdf4c23b812de189814da7cf229c22a81", size = 404766, upload-time = "2026-05-28T12:00:57.518Z" }, - { url = "https://files.pythonhosted.org/packages/87/ff/602fd3f174d6425f0bce05ad0dfbec0e96b38d0f7d08a79af5aa20083885/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9af8905b8f854990e40d5206aa5ac58d9b0fe0b7f351ff2bb086c20f6c8c6a47", size = 551343, upload-time = "2026-05-28T12:00:58.978Z" }, - { url = "https://files.pythonhosted.org/packages/b8/c1/1be13327acdbead3eca1fde03b6a34dbb011f1e864e217f0d32cc1779a7f/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:036a36a87fb1cd3b214d11c4b3c4f7d2ddad933625dca1c900b56a057c07740a", size = 618502, upload-time = "2026-05-28T12:01:00.656Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d7/afb49b49d7f2be8b7ba1a9f0977fa5168003437b93086726f066544e8351/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ae3853454fe9ef283a03c96c2d835d39e84b14643a9d62c82ef0fb87d702ca", size = 581916, upload-time = "2026-05-28T12:01:02.22Z" }, - { url = "https://files.pythonhosted.org/packages/25/d1/dbef8c1f8a10f07beb62b5f054e20099fd9924b3ec001b8f0b6ac7813a85/rpds_py-2026.5.1-cp314-cp314t-win32.whl", hash = "sha256:6c3d771a46ec18b12af06ce36243a9a80b07a5d0515236332d90863ca8bb326a", size = 207855, upload-time = "2026-05-28T12:01:03.821Z" }, - { url = "https://files.pythonhosted.org/packages/2a/72/bfa4e61ab8e7dc1c8adf397e05e6cbdd4239357bd72b248d3de662f23915/rpds_py-2026.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c93c629be4636cf54337bd5f06c104d55e42ced54d681f6fe21ae510a65116f6", size = 225422, upload-time = "2026-05-28T12:01:05.194Z" }, - { url = "https://files.pythonhosted.org/packages/27/3a/7b5da92b640f67b6717ccafc83cdd06bfa7ff2395c3685c68922bb54d703/rpds_py-2026.5.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:3574b55c604b8f75dacb007136508bbc0db406e626301778096a133327e7f2fb", size = 349576, upload-time = "2026-05-28T12:01:06.722Z" }, - { url = "https://files.pythonhosted.org/packages/d7/8a/2aafd7ad355a1bd48ca76e2262b74b15e6432b5a1efe150efd4d779cd55d/rpds_py-2026.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:94068eb3ae6d43f5a786b7db96a406a34e6d5c24489feef32fd6e8946ea7b291", size = 343640, upload-time = "2026-05-28T12:01:08.441Z" }, - { url = "https://files.pythonhosted.org/packages/f7/7d/6c9523c1abbe840a1b7fba3c516d48e1d3487cc80fea4366c4071cf56784/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a5b10e8ce894825f380a8f1b6444cf73c294dfea62afbb2d13e3a9e630cec1", size = 375322, upload-time = "2026-05-28T12:01:09.934Z" }, - { url = "https://files.pythonhosted.org/packages/5a/5d/0b7b03fb1dc509321f01de3149784ab773e34c8573022029af8076afcb9c/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc09f82e63d4bcd58149572f857a431bae851dc747e313c3b5bdf7abb907fda8", size = 379066, upload-time = "2026-05-28T12:01:11.48Z" }, - { url = "https://files.pythonhosted.org/packages/d7/e2/8ef6012999ebf1cb1c22f876d9ce5e63d960fd4631d2af3202d3f480aa25/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e10464d17df3b582745c25cec695cb9558bca2cb6ddb631aee1787fc72c767b2", size = 494586, upload-time = "2026-05-28T12:01:13.051Z" }, - { url = "https://files.pythonhosted.org/packages/80/af/1eeb029bec67582c226b7809172207cd005073af4ebd906e65ff494f4983/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba05adbf15d994c38ec0b7ab32e858e5110c21e9009a00a86545fd220f84e038", size = 388415, upload-time = "2026-05-28T12:01:14.631Z" }, - { url = "https://files.pythonhosted.org/packages/18/23/ffbe10711c4d766c1cab0557d6906c074f795814863c67b351355d29354a/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77c004fdc7b891967106f78ddfd7b076bfe6813c6139c6fff6aed3bcaa960b26", size = 372427, upload-time = "2026-05-28T12:01:16.153Z" }, - { url = "https://files.pythonhosted.org/packages/bd/3a/30ba4a6ad457e5b070c18d742a33fb77d8d922b565cc881f8a5313d63bfe/rpds_py-2026.5.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:83bcf894486c9d78dd290d3c0124ff6dd8875d3025e2090a8ec49fcc37c55fdd", size = 383615, upload-time = "2026-05-28T12:01:17.809Z" }, - { url = "https://files.pythonhosted.org/packages/d3/69/62e242b53ce39c0814bd24e1a6e6eba6c92be716277745f317f9540a2e7b/rpds_py-2026.5.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3df104083952a0e0c6f10de33e440eabe98fb6317d23e1a58c68f6df08d01b9", size = 402786, upload-time = "2026-05-28T12:01:19.419Z" }, - { url = "https://files.pythonhosted.org/packages/38/c1/a770b9c186928a1ed0f7e6d7ae50e7f3950ed23e3f9e366dbc8e38cb55de/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:980450826cf22e133c57e0835070bdd0dd3f73b9b708c3ce223def2cb9469e14", size = 551583, upload-time = "2026-05-28T12:01:21.013Z" }, - { url = "https://files.pythonhosted.org/packages/21/7c/68e8579b95375b70d2a963103c42e705856cdb98569258bd807f4423891c/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:205dde846f24332ab0c1188699a043b8d165b79bb84529ce272c45048ff6be01", size = 616941, upload-time = "2026-05-28T12:01:22.548Z" }, - { url = "https://files.pythonhosted.org/packages/70/a1/a6135aed5730ff03ab957182259987ac11e55fb392a28dc6f0592048a280/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:3966b82dd563176396df030f3dd52a6e54cb69b718e95e78bd555ed3d1e0185d", size = 578349, upload-time = "2026-05-28T12:01:24.118Z" }, - { url = "https://files.pythonhosted.org/packages/09/6e/f24201a76a84e6c49d0bdfdfcb735210e21701e9b21c5bfc0ba497dd62f6/rpds_py-2026.5.1-cp315-cp315-win32.whl", hash = "sha256:7818f8d0a415be74d2be3590b0a1c1f463a642f4d0217e7d10602dceef5b79aa", size = 209922, upload-time = "2026-05-28T12:01:25.522Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e4/966bc240bb0485fc265278f6de44d05834bf0b3618886e0b22e33d54c49a/rpds_py-2026.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:b3cc20c0d800af78fd0fac68086e28c1856cec51ea528bb81ea851aa40d39325", size = 226003, upload-time = "2026-05-28T12:01:27.062Z" }, - { url = "https://files.pythonhosted.org/packages/5c/5c/a15a59269cd5e74472734516c73795c15eccfc841b3d4b0228c3f53f19d0/rpds_py-2026.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:3609e9939a8a76cd904cf98a3f1f13b5dc7e150adeaee89e0ea09652ea213e16", size = 221245, upload-time = "2026-05-28T12:01:28.51Z" }, - { url = "https://files.pythonhosted.org/packages/e0/22/135ce03804e179a71ceb13be095deda4a279bc88f7a6b8fa161c5ad44e12/rpds_py-2026.5.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:5d333a7127d4b307601ac37792bee01bb95c867cbfacf21b6375b804d6bbd723", size = 352015, upload-time = "2026-05-28T12:01:30.214Z" }, - { url = "https://files.pythonhosted.org/packages/3b/5f/f1f6d2652eb9d848f6eb369d8db83a2da6249bb49ad2c2a48f45d54538d3/rpds_py-2026.5.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:b5f077b44a4f7808520f66dae234988d867deb9aed9be5da057ce9ba831b2a41", size = 345016, upload-time = "2026-05-28T12:01:31.656Z" }, - { url = "https://files.pythonhosted.org/packages/88/66/b74182775691ea2290c99e52ac8d5db844e56fbec90ce421f107658c8314/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d8f9b7b78c9538fc9e04e82ec0e888ff0c3cffcfad152c77e57cd09351a98a", size = 374775, upload-time = "2026-05-28T12:01:33.136Z" }, - { url = "https://files.pythonhosted.org/packages/ff/8f/15e5a61d9f0a43902d36561d4f07cae6ae9f4716be825159fd72717f33af/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e3a8ae58895ac107ed934a6bf51e5846f95c53b9b940c2c6d310838fd5846358", size = 380270, upload-time = "2026-05-28T12:01:34.574Z" }, - { url = "https://files.pythonhosted.org/packages/02/c3/f859b12763a80540cdf2af0f15b19904cf756a71d7bdd3f82ff3e5b1bbf9/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0957cf3c2b8632ec7aaebffebea8005b353cc2a237b6e2ae3c2cac0820704cfb", size = 495285, upload-time = "2026-05-28T12:01:36.127Z" }, - { url = "https://files.pythonhosted.org/packages/1c/c7/ff27c2ac8411d30b03b1829fd88cae8dad1a4d0da48dd25e57c4038042e6/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c396c1304de421050b3681ea70f371874b54d41b0151e96109758144c231e30b", size = 389581, upload-time = "2026-05-28T12:01:37.635Z" }, - { url = "https://files.pythonhosted.org/packages/6e/67/fe92ee32a6cc05c77228a2f8b1762e7124f386ec20ff83d0757b762d58d0/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad1bff7f666b9598e573815affd666aac6a13a585dde336f843e33350c7fadc", size = 376041, upload-time = "2026-05-28T12:01:39.307Z" }, - { url = "https://files.pythonhosted.org/packages/f8/91/b4d6685c27aba55bd82f25b278be8237038117d05f9659a6213ad3408130/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:656a042550878f12d45752452d47094b7cfe5ad1e9d7b87b5a22ad3ae5ff8015", size = 383946, upload-time = "2026-05-28T12:01:41.043Z" }, - { url = "https://files.pythonhosted.org/packages/bd/79/2c1d832a53c8e0f8e98fc970ec257b950fecd4f62be2ab7182b500a0cbc8/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c4bd4f70294737b5206a3e8e30ccadbf8a60301831c8ea23eec5dbeea1ecfa", size = 405526, upload-time = "2026-05-28T12:01:43.032Z" }, - { url = "https://files.pythonhosted.org/packages/78/c4/c98117b03c6a8581ab2c2dfccfe9a5ad82bd8128a3c28b46a6ad2d97c393/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:43bca78665423cabae77146f2fe7ce55272b6c8d55d82cca83effd42c7e13972", size = 551165, upload-time = "2026-05-28T12:01:44.648Z" }, - { url = "https://files.pythonhosted.org/packages/3b/c1/bc479ca069200af730881b1bd525e3114b2b391a351509fcb1b772f28086/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:42d0f20e85e549c870749d0e247f0c10d318a45b7e9676d575d2dcb04a1b2e66", size = 618778, upload-time = "2026-05-28T12:01:46.337Z" }, - { url = "https://files.pythonhosted.org/packages/77/65/38ab2f90df44c2febfb63cc10ced40763d9b4bc94d173e734528663fe7f5/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b1be5c35683684d5331b93600c210e8367c254683d8a6df6bd21bd2da3a334fb", size = 581839, upload-time = "2026-05-28T12:01:48.109Z" }, - { url = "https://files.pythonhosted.org/packages/15/2d/ce1f605fe036aadd460e5822e578c6c7ec3a860936cca37d6e0f299daa77/rpds_py-2026.5.1-cp315-cp315t-win32.whl", hash = "sha256:75808f6c38ce7749bb68cc2770161aae5045e6c6f6781a9782e74b93304399df", size = 207866, upload-time = "2026-05-28T12:01:49.648Z" }, - { url = "https://files.pythonhosted.org/packages/79/cb/966040123eb102371559746908ef2c9471f4d43e17ec9a645a2258dab64b/rpds_py-2026.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:90bd6630002a1c7f09e7843dd79f0d24f3d2897cc25a753480917865d14f15b3", size = 225441, upload-time = "2026-05-28T12:01:51.408Z" }, -] - [[package]] name = "ruff" -version = "0.15.19" +version = "0.15.20" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d5/e6/15800dfde183a1a106594016c912b4c12d050a301989d1aca6cb63759fe8/ruff-0.15.19.tar.gz", hash = "sha256:edc27f7172a93b32b102687009d6a588508815072141543ae603a8b9b0823063", size = 4772071, upload-time = "2026-06-24T01:10:46.942Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/dc/35b341fc554ba02f217fc10da57d1a75168cfbcf75b0ef2202176d4c4f2d/ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566", size = 4755489, upload-time = "2026-06-25T17:20:37.578Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/4c/9ded7626c39a0440c575bf69e2bf500d443388272c842662c59852ee7fcd/ruff-0.15.19-py3-none-linux_armv6l.whl", hash = "sha256:922d1eb283161564759bd49f507e91dc6112c15da8bd5b84ed714e086243cf86", size = 10950859, upload-time = "2026-06-24T01:10:38.491Z" }, - { url = "https://files.pythonhosted.org/packages/fb/ef/c211505ece1d00ef493d58e54e3b6383c946a21e9874774eb531f2512cf3/ruff-0.15.19-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4d190d8f62a0b94aba8f721116538a9ee29b1e74d26650846ba9b99f0ae21c40", size = 11294529, upload-time = "2026-06-24T01:10:36.481Z" }, - { url = "https://files.pythonhosted.org/packages/fe/93/78d462e7d39968e58094dc57be7d09ffb14ce37da5b68ed70338a35a1f21/ruff-0.15.19-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5a2c86ba6870dd415a9d9eb8be94d7924ebec6a26ffc7958ec7ca29d4bff967d", size = 10641416, upload-time = "2026-06-24T01:10:48.923Z" }, - { url = "https://files.pythonhosted.org/packages/76/c4/5cb66cfd1f865d5cca908b86c93ac785e7f572193d3c7426079ca6643e24/ruff-0.15.19-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b432bc087264aea70fd25ac198918b70bd9e2aa0db4297b0bb91bbfbbc63ce", size = 11015582, upload-time = "2026-06-24T01:10:30.089Z" }, - { url = "https://files.pythonhosted.org/packages/51/9f/8ecfaec10cf5eecd28fbc00ff4fb867db90a1be54bf3d39ebf93f893cd52/ruff-0.15.19-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8530a09d03b3a8c994f8b559a7dcdabc690bcd3f78ef276c38c83166798ebf56", size = 10744059, upload-time = "2026-06-24T01:10:32.48Z" }, - { url = "https://files.pythonhosted.org/packages/35/6b/983249d04562bc2d590edd75f32455cdb473affb3ba4bc8d883e939c697d/ruff-0.15.19-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87bf21fb3875fe69f0eacc825411657e2e85589cce633c35c0adf1113649c62b", size = 11568461, upload-time = "2026-06-24T01:10:17.435Z" }, - { url = "https://files.pythonhosted.org/packages/eb/39/bc7794f127b18f492a3b4ee82bba5a900c985ff13b72b46f46e3c171ba34/ruff-0.15.19-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9b229cb3ef56ecc2c1c8ebeca64b7a7740ccaef40a9eb097e78dde5a8560b83", size = 12429690, upload-time = "2026-06-24T01:10:40.638Z" }, - { url = "https://files.pythonhosted.org/packages/0a/3b/0de6859e698ed11c8a49e765196c8d333599b6a546c0715df39b6ba1aa2e/ruff-0.15.19-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c754515be7b76afe6e7e62df7776709571bcfc1631183828afcf3bafa869e3", size = 11693067, upload-time = "2026-06-24T01:10:25.681Z" }, - { url = "https://files.pythonhosted.org/packages/89/3d/0b1f30f84bee9ae6ae8d349c2ba8b6f4b040966744efdd3acc804ae7c024/ruff-0.15.19-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a498f82e0f4d8904c4e0aea5139cdfac1f39d19a3c51d491292f63a36e83b2e", size = 11616911, upload-time = "2026-06-24T01:10:44.809Z" }, - { url = "https://files.pythonhosted.org/packages/4d/eb/c90bd3dfc12eed9032c2c1bfe05105b93a1b2c8bce555db6308315b853ce/ruff-0.15.19-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:d48caa34488fb521fd0ef4aea2b0e8fe758298df044138f0d67b687a6a0d07ed", size = 11649343, upload-time = "2026-06-24T01:10:23.472Z" }, - { url = "https://files.pythonhosted.org/packages/82/91/01caa13602a2f12fae5edbe8caf78b3c1e6db1293132aee6959eecce095c/ruff-0.15.19-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4171b6613effa9363cd46dd4f75bd1827b6d1b946b5e278ed0c600d305379445", size = 10977610, upload-time = "2026-06-24T01:10:50.892Z" }, - { url = "https://files.pythonhosted.org/packages/3c/51/acb817922feab9ecbb3201377d4dbe7a25f1395e46545820061973f03468/ruff-0.15.19-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:27c15b2a241dd4d995557949a094fe78b8ad99122a38ccae1595849bcc947b3f", size = 10744900, upload-time = "2026-06-24T01:10:42.726Z" }, - { url = "https://files.pythonhosted.org/packages/84/bc/5c8ca46b8a7a3f2b16cfbec88721d772b1c93912904e8f8c2e49470fea63/ruff-0.15.19-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ed03b7862d68f0a8771d50ee129980cbf1b113f96e250b73954bc292f689e0bb", size = 11293560, upload-time = "2026-06-24T01:10:21.262Z" }, - { url = "https://files.pythonhosted.org/packages/81/e0/4a888cbe4d5523b3f77a2b1fa043f46cfeba1b32eac35dcfadee0578fa8a/ruff-0.15.19-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:08143f0685ae278b30727ea72e90c61e5bd9c31b91aac4f5bb989538f73d24b8", size = 11696533, upload-time = "2026-06-24T01:10:53.046Z" }, - { url = "https://files.pythonhosted.org/packages/98/43/c34b2fcd79262a85161764a97aaca89c3e4f574340ab61430cefa2bdd2c1/ruff-0.15.19-py3-none-win32.whl", hash = "sha256:8f47f0f92952af2557212bb10cf3e695cd4cf28b2c6e42cdb18ec6c9ebfa19da", size = 10986299, upload-time = "2026-06-24T01:10:55.185Z" }, - { url = "https://files.pythonhosted.org/packages/22/e8/15fd23e02b2442b56b2026b455977bc3057aa34b26e6323d1e99e8531a9f/ruff-0.15.19-py3-none-win_amd64.whl", hash = "sha256:efeca47ee3f9d4a7162655a3b8e6ee4a878646044233978d4d2c1ff8cdd914f0", size = 12123473, upload-time = "2026-06-24T01:10:27.74Z" }, - { url = "https://files.pythonhosted.org/packages/30/66/9a73695e31eaee04f35d8475998bf8ab354465f9c638936d76111603dcc5/ruff-0.15.19-py3-none-win_arm64.whl", hash = "sha256:6c6b607466e47349332eb1d9be52fb1467423fc07c217341af41cd0f3f0573be", size = 11376779, upload-time = "2026-06-24T01:10:34.465Z" }, + { url = "https://files.pythonhosted.org/packages/94/d9/2d5014f0253ba541d2061d9fa7193f48e941c8b21bb88a7ff9bbe0bd0596/ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078", size = 10839665, upload-time = "2026-06-25T17:19:44.702Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d3/ac1798ba64f670698867fcfc591d50e7e421bef137db564858f619a30fcf/ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b", size = 11208649, upload-time = "2026-06-25T17:19:48.787Z" }, + { url = "https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632", size = 10622638, upload-time = "2026-06-25T17:19:51.354Z" }, + { url = "https://files.pythonhosted.org/packages/33/13/4e043fe30aa94d4ff5213a9881fc296d12960f5971b234a5263fdc225312/ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd", size = 10984227, upload-time = "2026-06-25T17:19:54.044Z" }, + { url = "https://files.pythonhosted.org/packages/76/e6/92e7bf40388bc5800073b96564f56264f7e48bfd1a498f5ced6ae6d5a769/ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b", size = 10622882, upload-time = "2026-06-25T17:19:57.037Z" }, + { url = "https://files.pythonhosted.org/packages/13/7a/43460be3f24495a3aa46d4b16873e2c4941b3b5f0b00cf88c03b7b94b339/ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267", size = 11474808, upload-time = "2026-06-25T17:20:00.357Z" }, + { url = "https://files.pythonhosted.org/packages/27/a0/f37077884873221c6b33b4ab49eb18f9f88e54a16a25a5bca59bef46dd66/ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c", size = 12293094, upload-time = "2026-06-25T17:20:03.446Z" }, + { url = "https://files.pythonhosted.org/packages/a6/74/165545b60256a9704c21ac0ec4a0d07933b320812f9584836c9f4aca4292/ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae", size = 11526176, upload-time = "2026-06-25T17:20:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b", size = 11520767, upload-time = "2026-06-25T17:20:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/f032696cb01c9b54c0263fa393474d7758f1cdc021a01b04e3cbc2500999/ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487", size = 11500132, upload-time = "2026-06-25T17:20:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f4/51b1a14bc69e8c224b15dab9cce8e99b425e0455d462caa2b3c9be2b6a8e/ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3", size = 10943828, upload-time = "2026-06-25T17:20:16.635Z" }, + { url = "https://files.pythonhosted.org/packages/71/4b/fe267640783cd02bf6c5cc290b1df1051be2ec294c678b5c15fe19e52343/ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053", size = 10645418, upload-time = "2026-06-25T17:20:19.4Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c0/a65aa4ec2f5e87a1df32dc3ec1fede434fe3dfd5cbcf3b503cafc676ab54/ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4", size = 11211770, upload-time = "2026-06-25T17:20:22.033Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/0caa331d954ae2723d729d351c989cb4ca8b6077d5c6c2cb6de75e98c041/ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460", size = 11618698, upload-time = "2026-06-25T17:20:25.259Z" }, + { url = "https://files.pythonhosted.org/packages/10/9b/5f14927848d2fd4aa891fd88d883788c5a7baba561c7874732364045708c/ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21", size = 10857322, upload-time = "2026-06-25T17:20:28.612Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/fe47c501f9dea92a26d788ff98bb5d92ed4cb4c88792c5c88af6b697dc8e/ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415", size = 11993274, upload-time = "2026-06-25T17:20:31.871Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" }, ] [[package]] @@ -747,15 +459,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" }, ] -[[package]] -name = "tablib" -version = "3.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/00/416d2ba54d7d58a7f7c61bf62dfeb48fd553cf49614daf83312f2d2c156e/tablib-3.9.0.tar.gz", hash = "sha256:1b6abd8edb0f35601e04c6161d79660fdcde4abb4a54f66cc9f9054bd55d5fe2", size = 125565, upload-time = "2025-10-15T18:21:56.263Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/66/6b/32e51d847148b299088fc42d3d896845fd09c5247190133ea69dbe71ba51/tablib-3.9.0-py3-none-any.whl", hash = "sha256:eda17cd0d4dda614efc0e710227654c60ddbeb1ca92cdcfc5c3bd1fc5f5a6e4a", size = 49580, upload-time = "2025-10-15T18:21:44.185Z" }, -] - [[package]] name = "typer" version = "0.25.1" @@ -789,15 +492,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488, upload-time = "2025-06-02T15:12:03.405Z" }, ] -[[package]] -name = "urllib3" -version = "2.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, -] - [[package]] name = "whitenoise" version = "6.12.0"