diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 49e8e4a..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b16f96a..7478a4e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,9 +42,9 @@ jobs: - name: Run Tests env: - SECRET_KEY: "j+(#!sag4%^ay+oanu&t-&3x@2$!+s%x4u!4%pser4o9)2!ua1" - ENVIRONMENT: "local" - STRIPE_SECRET_KEY: "fail" - STRIPE_PUBLIC_KEY: "fail" + SECRET_KEY: ${{ secrets.SECRET_KEY }} + ENVIRONMENT: ${{ secrets.ENVIRONMENT }} + STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }} + STRIPE_PUBLIC_KEY: ${{ secrets.STRIPE_PUBLIC_KEY }} timeout-minutes: 5 run: poetry run python manage.py test diff --git a/.gitignore b/.gitignore index 2cbc65e..b4a2009 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ local_settings.py db.sqlite3 db.sqlite3-journal media +.DS_Store # If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ # in your Git repository. Update and uncomment the following line accordingly. diff --git a/Dockerfile b/Dockerfile index 73e49aa..a65bc0d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,9 +11,6 @@ RUN adduser \ --disabled-password \ --gecos "" \ --home "/nonexistent" \ - --shell "/sbin/nologin" \ - --no-create-home \ - --uid "${UID}" \ appuser RUN pip install --no-cache-dir poetry diff --git a/README.md b/README.md index 79482d5..c6d4428 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ git clone https://github.com/dmytrik/LibraryServiceAPI.git cd LibraryServiceAPI ``` -### 2. 📄 Create a `.env` File +### 2. 📄 Environment Create a `.env` file based on the `.env.sample` file. You can do this by copying the `.env.sample` to `.env`: ```bash diff --git a/book/management/commands/wait_for_db.py b/book/management/commands/wait_for_db.py index 51a4782..b5cd964 100644 --- a/book/management/commands/wait_for_db.py +++ b/book/management/commands/wait_for_db.py @@ -14,8 +14,6 @@ def handle(self, *args, **options): db_conn.cursor() except OperationalError: self.stdout.write( - self.style.WARNING( - "waiting for connection with database..." - ) + self.style.WARNING("waiting for connection with database...") ) time.sleep(1) diff --git a/book/tests/test_book_model.py b/book/tests/test_book_model.py index d68cefd..945a138 100644 --- a/book/tests/test_book_model.py +++ b/book/tests/test_book_model.py @@ -1,4 +1,5 @@ from django.test import TestCase + from book.models import Book diff --git a/book/tests/test_book_serializer.py b/book/tests/test_book_serializer.py index a905bec..3e1c2d9 100644 --- a/book/tests/test_book_serializer.py +++ b/book/tests/test_book_serializer.py @@ -1,5 +1,6 @@ from rest_framework.exceptions import ValidationError from rest_framework.test import APITestCase + from book.models import Book from book.serializers import BookSerializer diff --git a/book/tests/test_book_view_set.py b/book/tests/test_book_view_set.py index 5a78f93..f724173 100644 --- a/book/tests/test_book_view_set.py +++ b/book/tests/test_book_view_set.py @@ -2,6 +2,7 @@ from django.core.cache import cache from rest_framework import status from rest_framework.test import APITestCase + from book.models import Book User = get_user_model() @@ -18,12 +19,10 @@ def setUp(self): daily_fee=10.00, ) - # Create a superuser using the custom user model self.superuser = User.objects.create_superuser( email="admin@example.com", password="admin123" ) - # Create a regular user using the custom user model self.user = User.objects.create_user( email="user@example.com", password="user123" ) @@ -79,9 +78,7 @@ def test_update_book_as_superuser(self): "cover": self.book.cover, "daily_fee": self.book.daily_fee, } - response = self.client.put( - f"/api/books/{self.book.id}/", data, format="json" - ) + response = self.client.put(f"/api/books/{self.book.id}/", data, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) self.book.refresh_from_db() self.assertEqual(self.book.inventory, 10) @@ -96,9 +93,7 @@ def test_update_book_as_regular_user(self): "cover": self.book.cover, "daily_fee": self.book.daily_fee, } - response = self.client.put( - f"/api/books/{self.book.id}/", data, format="json" - ) + response = self.client.put(f"/api/books/{self.book.id}/", data, format="json") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_delete_book_as_superuser(self): diff --git a/book/urls.py b/book/urls.py index bc8dbb4..6ab3754 100644 --- a/book/urls.py +++ b/book/urls.py @@ -1,4 +1,5 @@ from rest_framework import routers + from book.views import BookViewSet app_name = "book" diff --git a/borrowing/filters.py b/borrowing/filters.py index ef875ad..c506dce 100644 --- a/borrowing/filters.py +++ b/borrowing/filters.py @@ -11,23 +11,23 @@ class CustomFilter(filters.FilterSet): is_active = filters.BooleanFilter( method="filter_is_active", label="Active" ) - user_id = filters.NumberFilter(method="filter_user_id") + user_id = filters.NumberFilter(method="filter_by_user_id") class Meta: model = Borrowing fields = ["is_active", "user_id"] - def filter_is_active(self, queryset, name, value): - if value: + def filter_is_active(self, queryset, value): + if value is True: return queryset.filter(actual_return_date__isnull=True) - else: + elif value is False: return queryset.filter(actual_return_date__isnull=False) + return queryset - def filter_user_id(self, queryset, name, value): - request = self.request - if request.user.is_staff: + def filter_by_user_id(self, queryset, value): + if self.request.user.is_staff: if value: return queryset.filter(user_id=value) return queryset - else: - return queryset.filter(user=request.user) + + return queryset.filter(user=self.request.user) diff --git a/borrowing/models.py b/borrowing/models.py index 9e79c0f..f62ad6f 100644 --- a/borrowing/models.py +++ b/borrowing/models.py @@ -1,7 +1,7 @@ from django.db import models +from django.conf import settings from book.models import Book -from django.conf import settings class Borrowing(models.Model): diff --git a/borrowing/serializers.py b/borrowing/serializers.py index 1934ea9..0fc60c9 100644 --- a/borrowing/serializers.py +++ b/borrowing/serializers.py @@ -27,8 +27,9 @@ def validate(self, attrs): if active_borrowings.exists(): raise serializers.ValidationError( - "You already have an active borrowing. " - "Please return the current book before borrowing another." + "It looks like you already have a book borrowed." + " Please return it before borrowing another." + " Thank you for your understanding!" ) return attrs diff --git a/borrowing/signals.py b/borrowing/signals.py index c6234e0..768824b 100644 --- a/borrowing/signals.py +++ b/borrowing/signals.py @@ -1,7 +1,8 @@ from django.core.cache import cache from django.db.models.signals import post_save, post_delete from django.dispatch import receiver -from .models import Borrowing + +from borrowing.models import Borrowing from tg_bot.utils import send_telegram_notification diff --git a/borrowing/tasks.py b/borrowing/tasks.py index 8dcca31..ac90eb6 100644 --- a/borrowing/tasks.py +++ b/borrowing/tasks.py @@ -1,8 +1,8 @@ import datetime from celery import shared_task -from tg_bot.utils import send_telegram_notification +from tg_bot.utils import send_telegram_notification from borrowing.models import Borrowing diff --git a/borrowing/tests/test_borrowing_api.py b/borrowing/tests/test_borrowing_api.py index 231ed9d..b00a555 100644 --- a/borrowing/tests/test_borrowing_api.py +++ b/borrowing/tests/test_borrowing_api.py @@ -1,8 +1,9 @@ +from datetime import timedelta + from rest_framework.test import APITestCase from rest_framework import status from django.urls import reverse from django.core.cache import cache -from datetime import timedelta from django.utils.timezone import now from django.contrib.auth import get_user_model @@ -17,8 +18,8 @@ def setUp(self): self.user = User.objects.create_user( email="test@user.com", password="password123" ) - self.staff_user = User.objects.create_user( - email="test@admin.com", password="adminpassword", is_staff=True + self.staff_user = User.objects.create_superuser( + email="test@admin.com", password="adminpassword" ) self.book = Book.objects.create( title="Test Book", author="Author", inventory=5, daily_fee=1 diff --git a/borrowing/tests/test_borrowing_models.py b/borrowing/tests/test_borrowing_models.py index bd019d3..586e191 100644 --- a/borrowing/tests/test_borrowing_models.py +++ b/borrowing/tests/test_borrowing_models.py @@ -1,9 +1,12 @@ +from datetime import timedelta + from django.test import TestCase from django.contrib.auth import get_user_model from django.utils.timezone import now -from datetime import timedelta + from borrowing.models import Borrowing, Book + User = get_user_model() diff --git a/borrowing/tests/test_borrowing_serializers.py b/borrowing/tests/test_borrowing_serializers.py index d7c2e43..341706b 100644 --- a/borrowing/tests/test_borrowing_serializers.py +++ b/borrowing/tests/test_borrowing_serializers.py @@ -1,8 +1,8 @@ +from datetime import timedelta from unittest.mock import MagicMock from rest_framework.test import APITestCase from rest_framework.exceptions import ValidationError -from datetime import timedelta from django.utils.timezone import now from django.contrib.auth import get_user_model @@ -15,6 +15,7 @@ BorrowingDetailSerializer, ) + User = get_user_model() diff --git a/borrowing/urls.py b/borrowing/urls.py index 6d73855..7bcb355 100644 --- a/borrowing/urls.py +++ b/borrowing/urls.py @@ -2,6 +2,7 @@ from borrowing.views import BorrowingViewSet + router = routers.DefaultRouter() router.register("", BorrowingViewSet, basename="borrowings") diff --git a/borrowing/views.py b/borrowing/views.py index d46a5cc..34f0df7 100644 --- a/borrowing/views.py +++ b/borrowing/views.py @@ -1,5 +1,6 @@ import datetime +from django.utils import timezone from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_page from django.http import HttpResponseRedirect @@ -38,7 +39,10 @@ class BorrowingViewSet( filterset_class = CustomFilter def get_queryset(self): - queryset = Borrowing.objects.select_related("book", "user") + queryset = ( + Borrowing.objects.select_related("book", "user"). + prefetch_related("payments") + ) if self.request.user.is_staff: return queryset.order_by("actual_return_date") return queryset.filter(user=self.request.user).order_by( @@ -58,30 +62,31 @@ def get_serializer_class(self): def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - book = serializer.validated_data["book"] + with transaction.atomic(): + book = serializer.validated_data["book"] - expected_return_date = serializer.validated_data[ - "expected_return_date" - ] + expected_return_date = serializer.validated_data[ + "expected_return_date" + ] - if datetime.date.today() > expected_return_date: - raise ValidationError("No valid expected return date") + if datetime.date.today() > expected_return_date: + raise ValidationError("No valid expected return date") - if book.inventory <= 0: - raise ValidationError("No copies available in inventory.") + if book.inventory <= 0: + raise ValidationError("No copies available in inventory.") - book.inventory -= 1 - book.save() + book.inventory -= 1 + book.save() - borrowing = serializer.save(user=self.request.user) + borrowing = serializer.save(user=self.request.user) - create_stripe_session(borrowing, request) + create_stripe_session(borrowing, request) - payment = Payment.objects.get(borrowing=borrowing) + payment = Payment.objects.get(borrowing=borrowing) - return HttpResponseRedirect( - payment.session_url, status=status.HTTP_302_FOUND - ) + return HttpResponseRedirect( + payment.session_url, status=status.HTTP_302_FOUND + ) @action( methods=["POST"], @@ -101,7 +106,7 @@ def return_book(self, request, pk=None): "This borrowing has already been returned." ) - borrowing.actual_return_date = datetime.date.today() + borrowing.actual_return_date = timezone.now().date() borrowing.save() book = borrowing.book diff --git a/core/__init__.py b/core/__init__.py index 8a891ca..0118d0c 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,5 +1,6 @@ from __future__ import absolute_import, unicode_literals -from .celery import app as celery_app +from core.celery import app as celery_app + __all__ = ("celery_app",) diff --git a/core/asgi.py b/core/asgi.py index 788f33e..d863d63 100644 --- a/core/asgi.py +++ b/core/asgi.py @@ -11,6 +11,7 @@ from django.core.asgi import get_asgi_application + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings") application = get_asgi_application() diff --git a/core/celery.py b/core/celery.py index 279dc67..2c8a3e1 100644 --- a/core/celery.py +++ b/core/celery.py @@ -1,22 +1,17 @@ from __future__ import absolute_import, unicode_literals import os + from celery import Celery -# set the default Django settings module for the 'celery' program. + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings") app = Celery("core") -# Using a string here means the worker doesn't have to serialize -# the configuration object to child processes. -# - namespace='CELERY' means all celery-related configuration keys -# should have a `CELERY_` prefix. app.config_from_object("django.conf:settings", namespace="CELERY") -# Load task modules from all registered Django app configs. app.autodiscover_tasks() - @app.task(bind=True) def debug_task(self): print("Request: {0!r}".format(self.request)) diff --git a/core/urls.py b/core/urls.py index a2c46f6..6e3f0e8 100644 --- a/core/urls.py +++ b/core/urls.py @@ -24,6 +24,7 @@ ) from debug_toolbar.toolbar import debug_toolbar_urls + urlpatterns = [ path("admin/", admin.site.urls), path("api/users/", include("user.urls", namespace="user")), diff --git a/core/wsgi.py b/core/wsgi.py index 3a0e1d4..15d1bc8 100644 --- a/core/wsgi.py +++ b/core/wsgi.py @@ -11,6 +11,7 @@ from django.core.wsgi import get_wsgi_application + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings") application = get_wsgi_application() diff --git a/payment/admin.py b/payment/admin.py index 2d3cff2..7a5f1b0 100644 --- a/payment/admin.py +++ b/payment/admin.py @@ -2,4 +2,5 @@ from payment.models import Payment + admin.site.register(Payment) diff --git a/payment/service.py b/payment/service.py index dee3ba0..b884674 100644 --- a/payment/service.py +++ b/payment/service.py @@ -52,7 +52,7 @@ def calculate_money_to_pay(borrowing: Borrowing): borrowing.actual_return_date - borrowing.expected_return_date ).days - count_of_money = overdue_days * borrowing.book.daily_fee * 2 + count_of_money = overdue_days * borrowing.book.daily_fee * OVERDUE_COEFFICIENT result = (count_of_money, "FINE") return result diff --git a/payment/views.py b/payment/views.py index cb6b5be..9b21786 100644 --- a/payment/views.py +++ b/payment/views.py @@ -1,3 +1,4 @@ +import stripe from django.conf import settings from django.shortcuts import get_object_or_404 from rest_framework import viewsets, mixins @@ -5,8 +6,14 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework import status - -import stripe +from stripe import ( + CardError, + RateLimitError, + InvalidRequestError, + AuthenticationError, + APIConnectionError, + StripeError +) from payment.models import Payment from payment.serializers import ( @@ -99,11 +106,19 @@ class PaymentSuccessView(APIView): get: Handles the GET request for successful payment. Updates the payment status and returns the payment details. """ + permission_classes = [IsAuthenticated] def get(self, request, *args, **kwargs): try: payment_id = request.query_params.get("payment_id") - payment = get_object_or_404(Payment, id=int(payment_id)) + if not payment_id: + return Response( + {"error": "Payment ID is required."}, + status=status.HTTP_400_BAD_REQUEST, + ) + payment = get_object_or_404( + Payment.objects.filter(borrowing__user=request.user), id=int(payment_id) + ) session = stripe.checkout.Session.retrieve(payment.session_id) if payment.status != "PAID": @@ -118,10 +133,64 @@ def get(self, request, *args, **kwargs): }, status=status.HTTP_200_OK, ) + except CardError: + return Response( + { + "error": "Your card has been declined. " + "Please check your card details or use a different card." + }, + status=status.HTTP_400_BAD_REQUEST, + ) + except RateLimitError: + return Response( - except stripe.error.StripeError as e: + { + "error": "Payment failed due to high request traffic. " + "Please try again later." + }, + status=status.HTTP_429_TOO_MANY_REQUESTS, + ) + except InvalidRequestError: + return Response( + + { + "error": "There was an issue with the payment details. " + "Please verify the information and try again." + }, + + status=status.HTTP_400_BAD_REQUEST, + ) + except AuthenticationError: + return Response( + { + "error": "Authentication with the payment gateway failed. " + "Please contact support." + }, + status=status.HTTP_401_UNAUTHORIZED, + ) + except APIConnectionError: + return Response( + { + "error": "A network error occurred. " + "Please check your internet connection and try again." + }, + status=status.HTTP_503_SERVICE_UNAVAILABLE, + ) + except StripeError: + return Response( + { + "error": "An error occurred with the payment process. " + "Please try again or contact support." + }, + status=status.HTTP_400_BAD_REQUEST, + ) + except Exception: return Response( - {"error": str(e)}, status=status.HTTP_400_BAD_REQUEST + { + "error": "An unexpected error occurred. " + "Please try again or contact support." + }, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) @@ -138,11 +207,19 @@ class PaymentCancelView(APIView): status and returns a message indicating cancellation and a link for retrying the payment. """ + permission_classes = [IsAuthenticated] def get(self, request, *args, **kwargs): try: payment_id = request.query_params.get("payment_id") - payment = get_object_or_404(Payment, id=int(payment_id)) + if not payment_id: + return Response( + {"error": "Payment ID is required."}, + status=status.HTTP_400_BAD_REQUEST, + ) + payment = get_object_or_404( + Payment.objects.filter(borrowing__user=request.user), id=int(payment_id) + ) session = stripe.checkout.Session.retrieve(payment.session_id) if payment.status != "PENDING": @@ -158,8 +235,62 @@ def get(self, request, *args, **kwargs): }, status=status.HTTP_400_BAD_REQUEST, ) + except CardError: + return Response( + { + "error": "Your card has been declined. " + "Please check your card details or use a different card." + }, + status=status.HTTP_400_BAD_REQUEST, + ) + except RateLimitError: + return Response( - except stripe.error.StripeError as e: + { + "error": "Payment failed due to high request traffic. " + "Please try again later." + }, + status=status.HTTP_429_TOO_MANY_REQUESTS, + ) + except InvalidRequestError: return Response( - {"error": str(e)}, status=status.HTTP_400_BAD_REQUEST + + { + "error": "There was an issue with the payment details. " + "Please verify the information and try again." + }, + + status=status.HTTP_400_BAD_REQUEST, + ) + except AuthenticationError: + return Response( + { + "error": "Authentication with the payment gateway failed. " + "Please contact support." + }, + status=status.HTTP_401_UNAUTHORIZED, + ) + except APIConnectionError: + return Response( + { + "error": "A network error occurred. " + "Please check your internet connection and try again." + }, + status=status.HTTP_503_SERVICE_UNAVAILABLE, + ) + except StripeError: + return Response( + { + "error": "An error occurred with the payment process. " + "Please try again or contact support." + }, + status=status.HTTP_400_BAD_REQUEST, + ) + except Exception: + return Response( + { + "error": "An unexpected error occurred. " + "Please try again or contact support." + }, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) diff --git a/poetry.lock b/poetry.lock index 838bdc3..dfd78b5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -79,6 +79,50 @@ files = [ {file = "billiard-4.2.1.tar.gz", hash = "sha256:12b641b0c539073fc8d3f5b8b7be998956665c4233c7c1fcd66a7e677c4fb36f"}, ] +[[package]] +name = "black" +version = "24.10.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.9" +files = [ + {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, + {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, + {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, + {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, + {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, + {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, + {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, + {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, + {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, + {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, + {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, + {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, + {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, + {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, + {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, + {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, + {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, + {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, + {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, + {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, + {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, + {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + [[package]] name = "celery" version = "5.4.0" @@ -646,6 +690,55 @@ sqs = ["boto3 (>=1.26.143)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] yaml = ["PyYAML (>=3.10)"] zookeeper = ["kazoo (>=2.8.0)"] +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] + [[package]] name = "prompt-toolkit" version = "3.0.48" diff --git a/tg_bot/admin.py b/tg_bot/admin.py deleted file mode 100644 index 846f6b4..0000000 --- a/tg_bot/admin.py +++ /dev/null @@ -1 +0,0 @@ -# Register your models here. diff --git a/tg_bot/models.py b/tg_bot/models.py deleted file mode 100644 index 6b20219..0000000 --- a/tg_bot/models.py +++ /dev/null @@ -1 +0,0 @@ -# Create your models here. diff --git a/tg_bot/tests.py b/tg_bot/tests.py deleted file mode 100644 index a39b155..0000000 --- a/tg_bot/tests.py +++ /dev/null @@ -1 +0,0 @@ -# Create your tests here. diff --git a/tg_bot/utils.py b/tg_bot/utils.py index 0e1109f..fbb27dc 100644 --- a/tg_bot/utils.py +++ b/tg_bot/utils.py @@ -1,8 +1,9 @@ +import os import asyncio from celery import shared_task from telegram import Bot -import os + BOT_TOKEN = os.getenv("BOT_TOKEN") CHAT_ID = os.getenv("CHAT_ID") diff --git a/tg_bot/views.py b/tg_bot/views.py deleted file mode 100644 index 60f00ef..0000000 --- a/tg_bot/views.py +++ /dev/null @@ -1 +0,0 @@ -# Create your views here. diff --git a/user/urls.py b/user/urls.py index c09a362..da1ca22 100644 --- a/user/urls.py +++ b/user/urls.py @@ -7,6 +7,7 @@ from user.views import CreateUserView, ManageUserView + app_name = "user" urlpatterns = [