From 00ac38d126c2d5b412128965ee1d3615a63c2a99 Mon Sep 17 00:00:00 2001 From: Nell Date: Sat, 11 Apr 2026 22:07:59 +0200 Subject: [PATCH] vpn integration --- app/api/admin.py | 2 - app/api/apps.py | 4 +- app/api/models.py | 2 - app/api/routers.py | 14 +- app/api/tests.py | 20 +- app/api/urls.py | 8 +- app/api/views.py | 2 - app/app/asgi.py | 23 +- app/app/channels_middleware.py | 42 ++-- app/app/settings.py | 145 ++++++------ app/app/urls.py | 53 +++-- app/app/utils.py | 37 ++-- app/app/ws_urls.py | 11 +- app/app/wsgi.py | 2 +- app/manage.py | 5 +- app/run_uvicorn.py | 4 +- app/torrent/apps.py | 13 +- app/torrent/consumers.py | 68 +++--- .../management/commands/torrent_event.py | 45 ++-- app/torrent/migrations/0001_initial.py | 47 ++-- app/torrent/migrations/0002_initial.py | 54 +++-- .../migrations/0003_torrent_date_modified.py | 7 +- ...004_rename_date_shareduser_date_created.py | 9 +- ..._rename_date_added_torrent_date_created.py | 9 +- app/torrent/models.py | 33 ++- app/torrent/serializers.py | 7 +- app/torrent/signals.py | 43 +++- app/torrent/tasks.py | 18 +- app/torrent/tests.py | 192 +++++++--------- app/torrent/urls.py | 6 +- app/torrent/utils.py | 100 +++++---- app/torrent/views.py | 80 +++---- app/user/admin.py | 34 ++- app/user/apps.py | 4 +- app/user/forms.py | 2 +- .../management/commands/import_old_users.py | 16 +- app/user/migrations/0001_initial.py | 207 ++++++++++++++---- app/user/models.py | 25 ++- app/user/serializers.py | 2 +- app/user/tests.py | 195 +++++++---------- app/user/urls.py | 2 +- app/user/views.py | 88 ++++---- app/watch_party/admin.py | 2 - app/watch_party/apps.py | 4 +- app/watch_party/models.py | 4 +- app/watch_party/tests.py | 2 - app/watch_party/views.py | 2 - 47 files changed, 945 insertions(+), 749 deletions(-) diff --git a/app/api/admin.py b/app/api/admin.py index 8c38f3f..846f6b4 100644 --- a/app/api/admin.py +++ b/app/api/admin.py @@ -1,3 +1 @@ -from django.contrib import admin - # Register your models here. diff --git a/app/api/apps.py b/app/api/apps.py index 66656fd..878e7d5 100644 --- a/app/api/apps.py +++ b/app/api/apps.py @@ -2,5 +2,5 @@ from django.apps import AppConfig class ApiConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'api' + default_auto_field = "django.db.models.BigAutoField" + name = "api" diff --git a/app/api/models.py b/app/api/models.py index 71a8362..6b20219 100644 --- a/app/api/models.py +++ b/app/api/models.py @@ -1,3 +1 @@ -from django.db import models - # Create your models here. diff --git a/app/api/routers.py b/app/api/routers.py index c1ed5ff..ddfa0c8 100644 --- a/app/api/routers.py +++ b/app/api/routers.py @@ -1,10 +1,10 @@ from rest_framework.routers import DefaultRouter -from user.views import UserViewSet -from torrent.views import TorrentViewSet, FileViewSet -from user.views import FriendRequestViewSet + +from torrent.views import FileViewSet, TorrentViewSet +from user.views import FriendRequestViewSet, UserViewSet router = DefaultRouter() -router.register(r'users', UserViewSet, basename='user') -router.register(r'torrents', TorrentViewSet, basename='torrent') -router.register(r'torrent/files', FileViewSet, basename='file') -router.register(r'friend_requests', FriendRequestViewSet, basename='friend-request') +router.register(r"users", UserViewSet, basename="user") +router.register(r"torrents", TorrentViewSet, basename="torrent") +router.register(r"torrent/files", FileViewSet, basename="file") +router.register(r"friend_requests", FriendRequestViewSet, basename="friend-request") diff --git a/app/api/tests.py b/app/api/tests.py index 968a851..1ff8e15 100644 --- a/app/api/tests.py +++ b/app/api/tests.py @@ -1,7 +1,7 @@ from django.test import TestCase from django.urls import reverse -from rest_framework.test import APITestCase, APIClient from rest_framework import status +from rest_framework.test import APIClient, APITestCase from .routers import router @@ -12,8 +12,10 @@ class RouterTestCase(TestCase): url_patterns = router.urls # Check that all expected viewsets are registered - expected_basenames = ['user', 'torrent', 'file', 'friend-request'] - registered_basenames = [url.name.split('-')[0] for url in url_patterns if '-list' in url.name] + expected_basenames = ["user", "torrent", "file", "friend-request"] + registered_basenames = [ + url.name.split("-")[0] for url in url_patterns if "-list" in url.name + ] for basename in expected_basenames: self.assertIn(basename, registered_basenames) @@ -33,9 +35,7 @@ class APIEndpointsTestCase(APITestCase): # Create a test user self.user = User.objects.create_user( - username='testuser', - email='test@example.com', - password='testpassword' + username="testuser", email="test@example.com", password="testpassword" ) # Authenticate the client @@ -44,24 +44,24 @@ class APIEndpointsTestCase(APITestCase): def test_user_endpoint(self): """Test that the users endpoint is accessible""" - url = reverse('user-list') + url = reverse("user-list") response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_torrent_endpoint(self): """Test that the torrents endpoint is accessible""" - url = reverse('torrent-list') + url = reverse("torrent-list") response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_file_endpoint(self): """Test that the files endpoint is accessible""" - url = reverse('file-list') + url = reverse("file-list") response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_friend_request_endpoint(self): """Test that the friend requests endpoint is accessible""" - url = reverse('friendrequest-list') + url = reverse("friendrequest-list") response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/app/api/urls.py b/app/api/urls.py index 347bc16..06f2bce 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -1,5 +1,4 @@ - -from django.urls import path, include +from django.urls import include, path # Ajout du package manquant dans requirements.txt ou installez-le avec: # pip install djangorestframework-simplejwt @@ -8,12 +7,11 @@ from rest_framework_simplejwt.views import ( TokenRefreshView, ) - from .routers import router app_name = "api" urlpatterns = [ path("", include(router.urls)), - path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), - path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), + path("api/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"), + path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), ] diff --git a/app/api/views.py b/app/api/views.py index 91ea44a..60f00ef 100644 --- a/app/api/views.py +++ b/app/api/views.py @@ -1,3 +1 @@ -from django.shortcuts import render - # Create your views here. diff --git a/app/app/asgi.py b/app/app/asgi.py index 54792e0..9a3c0a2 100644 --- a/app/app/asgi.py +++ b/app/app/asgi.py @@ -1,21 +1,22 @@ import os -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings') + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") from django.core.asgi import get_asgi_application + django_asgi_app = get_asgi_application() -from channels.auth import AuthMiddlewareStack +from channels.routing import ProtocolTypeRouter from channels.sessions import SessionMiddlewareStack -from channels.routing import ProtocolTypeRouter, URLRouter from app.channels_middleware import JwtOrSessionAuthMiddleware - - - from .ws_urls import websocket_urlpatterns - -application = ProtocolTypeRouter({ - "http": django_asgi_app, - "websocket": SessionMiddlewareStack(JwtOrSessionAuthMiddleware(websocket_urlpatterns)) -}) +application = ProtocolTypeRouter( + { + "http": django_asgi_app, + "websocket": SessionMiddlewareStack( + JwtOrSessionAuthMiddleware(websocket_urlpatterns) + ), + } +) diff --git a/app/app/channels_middleware.py b/app/app/channels_middleware.py index f4a256d..56fe87d 100644 --- a/app/app/channels_middleware.py +++ b/app/app/channels_middleware.py @@ -1,16 +1,14 @@ -from django.contrib.auth.models import AnonymousUser -from django.db import close_old_connections +from urllib.parse import parse_qs from channels.db import database_sync_to_async from channels.middleware import BaseMiddleware -from channels.auth import AuthMiddlewareStack -from rest_framework_simplejwt.tokens import UntypedToken -from rest_framework_simplejwt.exceptions import InvalidToken, TokenError -from jwt import decode as jwt_decode from django.conf import settings -from urllib.parse import parse_qs from django.contrib.auth import get_user_model - +from django.contrib.auth.models import AnonymousUser +from django.db import close_old_connections +from jwt import decode as jwt_decode +from rest_framework_simplejwt.exceptions import InvalidToken, TokenError +from rest_framework_simplejwt.tokens import UntypedToken User = get_user_model() @@ -23,12 +21,12 @@ def get_user_from_token(token): # Décoder le token et obtenir l'ID de l'utilisateur decoded_data = jwt_decode(token, settings.SECRET_KEY, algorithms=["HS256"]) - user_id = decoded_data.get('user_id') + user_id = decoded_data.get("user_id") if user_id: return User.objects.get(id=user_id) return AnonymousUser() - except (InvalidToken, TokenError, User.DoesNotExist): + except InvalidToken, TokenError, User.DoesNotExist: return AnonymousUser() @@ -51,29 +49,29 @@ class JwtOrSessionAuthMiddleware(BaseMiddleware): close_old_connections() # Par défaut, définir un utilisateur anonyme - scope['user'] = AnonymousUser() + scope["user"] = AnonymousUser() # Essayer d'abord l'authentification par session if "session" in scope: - scope['user'] = await get_user(scope) - if not scope['user'].is_anonymous: + scope["user"] = await get_user(scope) + if not scope["user"].is_anonymous: return await super().__call__(scope, receive, send) # Si l'utilisateur est toujours anonyme, essayer JWT - if scope['user'].is_anonymous and 'query_string' in scope: + if scope["user"].is_anonymous and "query_string" in scope: # Extraire token des query parameters - query_params = parse_qs(scope['query_string'].decode('utf-8')) - token = query_params.get('token', [None])[0] + query_params = parse_qs(scope["query_string"].decode("utf-8")) + token = query_params.get("token", [None])[0] # Si aucun token dans les query params, chercher dans les headers - if not token and 'headers' in scope: - headers = dict(scope['headers']) - auth_header = headers.get(b'authorization', b'') - if auth_header.startswith(b'Bearer '): - token = auth_header.decode('utf-8')[7:] + if not token and "headers" in scope: + headers = dict(scope["headers"]) + auth_header = headers.get(b"authorization", b"") + if auth_header.startswith(b"Bearer "): + token = auth_header.decode("utf-8")[7:] # Authentifier avec le token si présent if token: - scope['user'] = await get_user_from_token(token) + scope["user"] = await get_user_from_token(token) return await super().__call__(scope, receive, send) diff --git a/app/app/settings.py b/app/app/settings.py index ab88eb4..2ff5757 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -10,10 +10,10 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/5.1/ref/settings/ """ -from pathlib import Path -from os import getenv import ast from datetime import timedelta +from os import getenv +from pathlib import Path # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -41,55 +41,53 @@ CORS_ALLOW_ALL_ORIGINS = True # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - - 'django_vite', - 'rest_framework', - 'corsheaders', - 'channels', - 'django_filters', - 'rest_framework_simplejwt', - - 'user', - 'api', - 'torrent', - 'watch_party' + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django_vite", + "rest_framework", + "corsheaders", + "channels", + "django_filters", + "rest_framework_simplejwt", + "user", + "api", + "torrent", + "watch_party", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = 'app.urls' +ROOT_URLCONF = "app.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [BASE_DIR / 'templates'], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [BASE_DIR / "templates"], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'app.wsgi.application' +WSGI_APPLICATION = "app.wsgi.application" ASGI_APPLICATION = "app.asgi.application" @@ -97,9 +95,9 @@ ASGI_APPLICATION = "app.asgi.application" # https://docs.djangoproject.com/en/5.1/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", } } @@ -109,16 +107,16 @@ DATABASES = { AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] @@ -126,9 +124,9 @@ AUTH_PASSWORD_VALIDATORS = [ # Internationalization # https://docs.djangoproject.com/en/5.1/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'Europe/Paris' +TIME_ZONE = "Europe/Paris" USE_I18N = True @@ -138,7 +136,7 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.1/howto/static-files/ -STATIC_URL = '/static/' +STATIC_URL = "/static/" STATICFILES_DIRS = [ BASE_DIR / "static", BASE_DIR / "frontend/dist", @@ -151,14 +149,14 @@ MEDIA_ROOT = BASE_DIR / "media" # Default primary key field type # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" # User related AUTH_USER_MODEL = "user.User" LOGIN_REDIRECT_URL = "/" LOGIN_URL = "/user/login/" LOGOUT_REDIRECT_URL = "/user/login/" -SESSION_COOKIE_AGE = 60*60*24*365*5 +SESSION_COOKIE_AGE = 60 * 60 * 24 * 365 * 5 # Django-vite related # https://github.com/MrBin99/django-vite/tree/master @@ -171,13 +169,12 @@ DJANGO_VITE = { } } -REDIS_HOST = { - "host": getenv("REDIS_HOST"), - "port": int(getenv("REDIS_PORT", 6379)) -} +REDIS_HOST = {"host": getenv("REDIS_HOST"), "port": int(getenv("REDIS_PORT", 6379))} # Email related -EMAIL_BACKEND = getenv("EMAIL_BACKEND", 'django.core.mail.backends.console.EmailBackend') +EMAIL_BACKEND = getenv( + "EMAIL_BACKEND", "django.core.mail.backends.console.EmailBackend" +) EMAIL_HOST = getenv("EMAIL_HOST", None) EMAIL_PORT = getenv("EMAIL_PORT", None) EMAIL_HOST_USER = getenv("EMAIL_USER", None) @@ -194,12 +191,12 @@ CELERY_TIMEZONE = "Europe/Paris" CELERY_TASK_TRACK_STARTED = True # CELERY_TASK_TIME_LIMIT = 30 * 60 CELERY_TASK_SERIALIZER = "json" -CELERY_ACCEPT_CONTENT = ['json'] +CELERY_ACCEPT_CONTENT = ["json"] CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True CELERY_BEAT_SCHEDULE = { "update_transmission_data": { "task": "torrent.tasks.update_transmission_data", - "schedule": int(getenv("UPDATE_TRANSMISSION_DELAY", 5)) + "schedule": int(getenv("UPDATE_TRANSMISSION_DELAY", 5)), } } @@ -215,31 +212,31 @@ CHANNEL_LAYERS = { REST_FRAMEWORK = { "DEFAULT_FILTER_BACKENDS": [ - 'django_filters.rest_framework.DjangoFilterBackend', - 'rest_framework.filters.OrderingFilter' + "django_filters.rest_framework.DjangoFilterBackend", + "rest_framework.filters.OrderingFilter", ], "DEFAULT_AUTHENTICATION_CLASSES": [ "rest_framework.authentication.SessionAuthentication", "rest_framework.authentication.BasicAuthentication", - 'rest_framework_simplejwt.authentication.JWTAuthentication', + "rest_framework_simplejwt.authentication.JWTAuthentication", ], "DEFAULT_RENDERER_CLASSES": [ "rest_framework.renderers.JSONRenderer", - "rest_framework.renderers.BrowsableAPIRenderer" - ] + "rest_framework.renderers.BrowsableAPIRenderer", + ], } SIMPLE_JWT = { - 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30), - 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), - 'ROTATE_REFRESH_TOKENS': False, - 'BLACKLIST_AFTER_ROTATION': True, - 'ALGORITHM': 'HS256', - 'SIGNING_KEY': SECRET_KEY, - 'VERIFYING_KEY': None, - 'AUTH_HEADER_TYPES': ('Bearer',), - 'USER_ID_FIELD': 'id', - 'USER_ID_CLAIM': 'user_id', + "ACCESS_TOKEN_LIFETIME": timedelta(minutes=30), + "REFRESH_TOKEN_LIFETIME": timedelta(days=1), + "ROTATE_REFRESH_TOKENS": False, + "BLACKLIST_AFTER_ROTATION": True, + "ALGORITHM": "HS256", + "SIGNING_KEY": SECRET_KEY, + "VERIFYING_KEY": None, + "AUTH_HEADER_TYPES": ("Bearer",), + "USER_ID_FIELD": "id", + "USER_ID_CLAIM": "user_id", } # Torrent related @@ -250,6 +247,6 @@ TRANSMISSION = { "host": getenv("TRANSMISSION_HOST", "127.0.0.1"), "port": getenv("TRANSMISSION_PORT", 9091), "username": getenv("TRANSMISSION_USERNAME"), - "password": getenv("TRANSMISSION_PASSWORD") + "password": getenv("TRANSMISSION_PASSWORD"), } -TORRENT_TTL = int(getenv("TORRENT_TTL", 90*24*60*60)) # 90 jours \ No newline at end of file +TORRENT_TTL = int(getenv("TORRENT_TTL", 90 * 24 * 60 * 60)) # 90 jours diff --git a/app/app/urls.py b/app/app/urls.py index 0b7f22c..4795da5 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -14,31 +14,54 @@ Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ + from django.contrib import admin -from django.urls import path, include # Added include for including app URLs -from django.http import HttpResponse -from django.views.generic import RedirectView from django.contrib.auth.views import ( - PasswordResetView, PasswordResetDoneView, PasswordResetConfirmView, PasswordResetCompleteView, PasswordChangeView, - PasswordChangeDoneView, LogoutView + LogoutView, + PasswordChangeDoneView, + PasswordChangeView, + PasswordResetCompleteView, + PasswordResetConfirmView, + PasswordResetDoneView, + PasswordResetView, ) +from django.http import HttpResponse +from django.urls import include, path # Added include for including app URLs +from django.views.generic import RedirectView urlpatterns = [ - path('admin/', admin.site.urls), + path("admin/", admin.site.urls), path("health/", lambda request: HttpResponse("OK")), path("", include("torrent.urls", "torrent")), path("user/", include("user.urls", "user")), path("api/", include("api.urls", "api")), path("home", RedirectView.as_view(url="/", permanent=False), name="home"), - -# reset password related + # reset password related path("password_reset/", PasswordResetView.as_view(), name="password_reset"), - path("password_reset_done/", PasswordResetDoneView.as_view(), name="password_reset_done"), - path("reset///", PasswordResetConfirmView.as_view(), name="password_reset_confirm"), - path("reset/done/", PasswordResetCompleteView.as_view(), name="password_reset_complete"), - path("password_change/", PasswordChangeView.as_view( - success_url="/" - ), name="password_change"), - path("password_change_done/", PasswordChangeDoneView.as_view(), name="password_change_done"), + path( + "password_reset_done/", + PasswordResetDoneView.as_view(), + name="password_reset_done", + ), + path( + "reset///", + PasswordResetConfirmView.as_view(), + name="password_reset_confirm", + ), + path( + "reset/done/", + PasswordResetCompleteView.as_view(), + name="password_reset_complete", + ), + path( + "password_change/", + PasswordChangeView.as_view(success_url="/"), + name="password_change", + ), + path( + "password_change_done/", + PasswordChangeDoneView.as_view(), + name="password_change_done", + ), path("logout/", LogoutView.as_view(), name="logout"), ] diff --git a/app/app/utils.py b/app/app/utils.py index 075bf1c..644c3ed 100644 --- a/app/app/utils.py +++ b/app/app/utils.py @@ -1,29 +1,30 @@ -from django.http import StreamingHttpResponse - -import zlib import datetime import os -import anyio +import zlib from stat import S_IFREG -from stream_zip import ZIP_64, stream_zip, async_stream_zip -from channels.layers import get_channel_layer + +import anyio from asgiref.sync import async_to_sync +from channels.layers import get_channel_layer +from django.http import StreamingHttpResponse +from stream_zip import ZIP_64, async_stream_zip, stream_zip def send_sync_channel_message(channel_name, context, data): - async_to_sync(get_channel_layer().group_send)(channel_name, { - "type": context, - "data": data - }) + async_to_sync(get_channel_layer().group_send)( + channel_name, {"type": context, "data": data} + ) class StreamingZipFileResponse(StreamingHttpResponse): # https://stream-zip.docs.trade.gov.uk/ # https://github.com/sandes/zipfly/tree/master - def __init__(self, filename, file_list, compression_level=0, is_async=False, *args, **kwargs): + def __init__( + self, filename, file_list, compression_level=0, is_async=False, *args, **kwargs + ): self.file_list = file_list - super().__init__(content_type='application/octet-stream', *args, **kwargs) - self['Content-Disposition'] = f'attachment; filename="{filename}"' + super().__init__(content_type="application/octet-stream", *args, **kwargs) + self["Content-Disposition"] = f'attachment; filename="{filename}"' # self['Cache-Control'] = "no-cache" # self['X-Accel-Buffering'] = "no" @@ -32,12 +33,12 @@ class StreamingZipFileResponse(StreamingHttpResponse): if is_async: self.zipped = async_stream_zip( self._async_local_files(), - get_compressobj=lambda: zlib.compressobj(wbits=-zlib.MAX_WBITS, level=compression_level) + get_compressobj=lambda: zlib.compressobj( + wbits=-zlib.MAX_WBITS, level=compression_level + ), ) else: - self.zipped = stream_zip( - self._sync_local_files() - ) + self.zipped = stream_zip(self._sync_local_files()) self.streaming_content = self.zipped def _get_total_length(self): @@ -69,4 +70,4 @@ class StreamingZipFileResponse(StreamingHttpResponse): raise e for pathname, dest in self.file_list: - yield dest, now, S_IFREG | 0o600, ZIP_64, contents(pathname) \ No newline at end of file + yield dest, now, S_IFREG | 0o600, ZIP_64, contents(pathname) diff --git a/app/app/ws_urls.py b/app/app/ws_urls.py index 07ebf1f..828a807 100644 --- a/app/app/ws_urls.py +++ b/app/app/ws_urls.py @@ -1,9 +1,10 @@ +from channels.routing import URLRouter from django.urls import path -from channels.routing import ProtocolTypeRouter, URLRouter from torrent.consumers import TorrentEventConsumer - -websocket_urlpatterns = URLRouter([ - path("ws/torrent_event/", TorrentEventConsumer.as_asgi()), -]) +websocket_urlpatterns = URLRouter( + [ + path("ws/torrent_event/", TorrentEventConsumer.as_asgi()), + ] +) diff --git a/app/app/wsgi.py b/app/app/wsgi.py index 829fcc7..3cba99e 100644 --- a/app/app/wsgi.py +++ b/app/app/wsgi.py @@ -11,6 +11,6 @@ import os from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") application = get_wsgi_application() diff --git a/app/manage.py b/app/manage.py index 4931389..923e331 100644 --- a/app/manage.py +++ b/app/manage.py @@ -1,12 +1,13 @@ #!/usr/bin/env python """Django's command-line utility for administrative tasks.""" + import os import sys def main(): """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -18,5 +19,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/app/run_uvicorn.py b/app/run_uvicorn.py index 2c10f39..04c3a96 100644 --- a/app/run_uvicorn.py +++ b/app/run_uvicorn.py @@ -14,7 +14,7 @@ def build_config() -> dict: "proxy_headers": True, "forwarded_allow_ips": "*", "log_level": "debug" if debug else "info", - "access_log": True + "access_log": True, } if debug: @@ -36,4 +36,4 @@ def build_config() -> dict: if __name__ == "__main__": - uvicorn.run("app.asgi:application", **build_config()) \ No newline at end of file + uvicorn.run("app.asgi:application", **build_config()) diff --git a/app/torrent/apps.py b/app/torrent/apps.py index 9a4a7f6..6e5108a 100644 --- a/app/torrent/apps.py +++ b/app/torrent/apps.py @@ -2,13 +2,18 @@ from django.apps import AppConfig class TorrentConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'torrent' + default_auto_field = "django.db.models.BigAutoField" + name = "torrent" def ready(self): - from django.db.models.signals import post_save, pre_delete, m2m_changed - from .signals import on_post_save_torrent, on_pre_delete_torrent, on_shared_user_changed + from django.db.models.signals import m2m_changed, post_save, pre_delete + from .models import Torrent + from .signals import ( + on_post_save_torrent, + on_pre_delete_torrent, + on_shared_user_changed, + ) post_save.connect(on_post_save_torrent, sender=Torrent) pre_delete.connect(on_pre_delete_torrent, sender=Torrent) diff --git a/app/torrent/consumers.py b/app/torrent/consumers.py index 9c54e5f..a6521db 100644 --- a/app/torrent/consumers.py +++ b/app/torrent/consumers.py @@ -1,11 +1,8 @@ -from django.db.models import Q -from django.db.models.functions import Coalesce - from channels.generic.websocket import AsyncJsonWebsocketConsumer -from typing import Optional, Union -import asyncio +from django.db.models import Q from user.models import User + from .models import Torrent @@ -14,17 +11,19 @@ class TorrentEventConsumer(AsyncJsonWebsocketConsumer): super().__init__(*args, **kwargs) self.channel_groups = set() - self.user: Optional[User] = None - self.follow_user: Optional[User] = None + self.user: User | None = None + self.follow_user: User | None = None async def connect(self): - self.user = self.scope['user'] + self.user = self.scope["user"] if not self.user.is_authenticated: await self.close() return self.follow_user = self.user - await self.channel_layer.group_add(f"user_{self.follow_user.id}", self.channel_name) + await self.channel_layer.group_add( + f"user_{self.follow_user.id}", self.channel_name + ) # user_id = int(self.scope['url_route']["kwargs"]["user_id"]) # if user_id == self.user.id: @@ -53,47 +52,52 @@ class TorrentEventConsumer(AsyncJsonWebsocketConsumer): print("call websocket not supported", content) async def change_follow_user(self, user_id): - await self.channel_layer.group_discard(f"user_{self.follow_user.id}", self.channel_name) + await self.channel_layer.group_discard( + f"user_{self.follow_user.id}", self.channel_name + ) if user_id == self.user.id: self.follow_user = self.user - await self.channel_layer.group_add(f"user_{self.follow_user.id}", self.channel_name) + await self.channel_layer.group_add( + f"user_{self.follow_user.id}", self.channel_name + ) return self.follow_user elif await self.user.friends.filter(id=user_id).aexists(): self.follow_user = await User.objects.filter(id=user_id).aget() - await self.channel_layer.group_add(f"user_{self.follow_user.id}", self.channel_name) + await self.channel_layer.group_add( + f"user_{self.follow_user.id}", self.channel_name + ) return self.follow_user else: return None async def transmission_data_updated(self, datas): torrent_stats = datas["data"] - qs = (Torrent.objects - .filter(Q(user_id=self.follow_user.id) | Q(shared_users=self.follow_user.id)) - .values_list("id", flat=True).distinct()) + qs = ( + Torrent.objects.filter( + Q(user_id=self.follow_user.id) | Q(shared_users=self.follow_user.id) + ) + .values_list("id", flat=True) + .distinct() + ) torrent_ids = [i async for i in qs] for hash_string, data in torrent_stats.items(): if hash_string in torrent_ids: - await self.send_json({ - "context": "transmission_data_updated", - "data": data - }) + await self.send_json( + {"context": "transmission_data_updated", "data": data} + ) async def add_torrent(self, data): - await self.send_json({ - "context": "add_torrent", - "torrent_id": data["data"] - }) + await self.send_json({"context": "add_torrent", "torrent_id": data["data"]}) async def remove_torrent(self, data): - await self.send_json({ - "context": "remove_torrent", - "torrent_id": data["data"] - }) + await self.send_json({"context": "remove_torrent", "torrent_id": data["data"]}) async def update_torrent(self, data): - await self.send_json({ - "context": "update_torrent", - "torrent_id": data["data"]["torrent_id"], - "updated_fields": data["data"]["updated_fields"] - }) + await self.send_json( + { + "context": "update_torrent", + "torrent_id": data["data"]["torrent_id"], + "updated_fields": data["data"]["updated_fields"], + } + ) diff --git a/app/torrent/management/commands/torrent_event.py b/app/torrent/management/commands/torrent_event.py index 53e802d..fd57d6d 100644 --- a/app/torrent/management/commands/torrent_event.py +++ b/app/torrent/management/commands/torrent_event.py @@ -1,17 +1,16 @@ -from django.conf import settings -from django.utils import timezone -from django.core.management.base import BaseCommand -from django.db import close_old_connections - -import time import signal -import sys +import time import traceback from datetime import timedelta +from django.conf import settings +from django.core.management.base import BaseCommand +from django.db import close_old_connections +from django.utils import timezone + +from app.utils import send_sync_channel_message from torrent.models import Torrent from torrent.utils import transmission_handler -from app.utils import send_sync_channel_message def update_transmission_data(): @@ -24,10 +23,11 @@ def update_transmission_data(): updated_torrents.append(torrent) if updated_torrents: Torrent.objects.bulk_update(updated_torrents, ["transmission_data"]) - send_sync_channel_message("torrent", "transmission_data_updated", { - torrent.id: torrent.transmission_data - for torrent in updated_torrents - }) + send_sync_channel_message( + "torrent", + "transmission_data_updated", + {torrent.id: torrent.transmission_data for torrent in updated_torrents}, + ) def clean_old_torrents(): @@ -37,24 +37,16 @@ def clean_old_torrents(): print(f"delete torrent {torrent.name}") torrent.delete() + def update_peer_port(): transmission_handler.update_vpn_port() class Command(BaseCommand): task_schedule = { - "update_transmission_data": { - "func": update_transmission_data, - "schedule": 5.0 - }, - "clean_old_torrents": { - "func": clean_old_torrents, - "schedule": 5.0 - }, - "update_peer_port": { - "func": update_peer_port, - "schedule": 10.0 - } + "update_transmission_data": {"func": update_transmission_data, "schedule": 5.0}, + "clean_old_torrents": {"func": clean_old_torrents, "schedule": 5.0}, + "update_peer_port": {"func": update_peer_port, "schedule": 10.0}, } histories = {} run = True @@ -66,7 +58,10 @@ class Command(BaseCommand): self.stdout.write(self.style.SUCCESS("start")) while self.run: for name, task in self.task_schedule.items(): - if name not in self.histories or time.time() - self.histories[name] > task["schedule"]: + if ( + name not in self.histories + or time.time() - self.histories[name] > task["schedule"] + ): self.call_func(name) time.sleep(1) diff --git a/app/torrent/migrations/0001_initial.py b/app/torrent/migrations/0001_initial.py index b4ae650..61332c6 100644 --- a/app/torrent/migrations/0001_initial.py +++ b/app/torrent/migrations/0001_initial.py @@ -1,40 +1,55 @@ # Generated by Django 5.1.6 on 2025-03-04 23:41 import uuid + from django.db import migrations, models class Migration(migrations.Migration): - initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='File', + name="File", fields=[ - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('rel_name', models.TextField()), - ('size', models.BigIntegerField()), + ( + "id", + models.UUIDField( + default=uuid.uuid4, primary_key=True, serialize=False + ), + ), + ("rel_name", models.TextField()), + ("size", models.BigIntegerField()), ], ), migrations.CreateModel( - name='SharedUser', + name="SharedUser", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('date', models.DateTimeField(auto_now_add=True)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("date", models.DateTimeField(auto_now_add=True)), ], ), migrations.CreateModel( - name='Torrent', + name="Torrent", fields=[ - ('id', models.CharField(max_length=40, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=255)), - ('date_added', models.DateTimeField(auto_now_add=True)), - ('size', models.PositiveBigIntegerField()), - ('transmission_data', models.JSONField(default=dict)), + ( + "id", + models.CharField(max_length=40, primary_key=True, serialize=False), + ), + ("name", models.CharField(max_length=255)), + ("date_added", models.DateTimeField(auto_now_add=True)), + ("size", models.PositiveBigIntegerField()), + ("transmission_data", models.JSONField(default=dict)), ], ), ] diff --git a/app/torrent/migrations/0002_initial.py b/app/torrent/migrations/0002_initial.py index ee20786..37e21f3 100644 --- a/app/torrent/migrations/0002_initial.py +++ b/app/torrent/migrations/0002_initial.py @@ -6,42 +6,58 @@ from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [ - ('torrent', '0001_initial'), + ("torrent", "0001_initial"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.AddField( - model_name='shareduser', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + model_name="shareduser", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), ), migrations.AddField( - model_name='torrent', - name='shared_users', - field=models.ManyToManyField(blank=True, related_name='torrents_shares', through='torrent.SharedUser', to=settings.AUTH_USER_MODEL), + model_name="torrent", + name="shared_users", + field=models.ManyToManyField( + blank=True, + related_name="torrents_shares", + through="torrent.SharedUser", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AddField( - model_name='torrent', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='torrents', to=settings.AUTH_USER_MODEL), + model_name="torrent", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="torrents", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AddField( - model_name='shareduser', - name='torrent', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='torrent.torrent'), + model_name="shareduser", + name="torrent", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="torrent.torrent" + ), ), migrations.AddField( - model_name='file', - name='torrent', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='files', to='torrent.torrent'), + model_name="file", + name="torrent", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="files", + to="torrent.torrent", + ), ), migrations.AlterUniqueTogether( - name='shareduser', - unique_together={('user', 'torrent')}, + name="shareduser", + unique_together={("user", "torrent")}, ), ] diff --git a/app/torrent/migrations/0003_torrent_date_modified.py b/app/torrent/migrations/0003_torrent_date_modified.py index 81b3ca5..dd0f08a 100644 --- a/app/torrent/migrations/0003_torrent_date_modified.py +++ b/app/torrent/migrations/0003_torrent_date_modified.py @@ -4,15 +4,14 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('torrent', '0002_initial'), + ("torrent", "0002_initial"), ] operations = [ migrations.AddField( - model_name='torrent', - name='date_modified', + model_name="torrent", + name="date_modified", field=models.DateTimeField(auto_now=True), ), ] diff --git a/app/torrent/migrations/0004_rename_date_shareduser_date_created.py b/app/torrent/migrations/0004_rename_date_shareduser_date_created.py index d7b1e87..f7ab215 100644 --- a/app/torrent/migrations/0004_rename_date_shareduser_date_created.py +++ b/app/torrent/migrations/0004_rename_date_shareduser_date_created.py @@ -4,15 +4,14 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('torrent', '0003_torrent_date_modified'), + ("torrent", "0003_torrent_date_modified"), ] operations = [ migrations.RenameField( - model_name='shareduser', - old_name='date', - new_name='date_created', + model_name="shareduser", + old_name="date", + new_name="date_created", ), ] diff --git a/app/torrent/migrations/0005_rename_date_added_torrent_date_created.py b/app/torrent/migrations/0005_rename_date_added_torrent_date_created.py index 88dddb9..8d8ea67 100644 --- a/app/torrent/migrations/0005_rename_date_added_torrent_date_created.py +++ b/app/torrent/migrations/0005_rename_date_added_torrent_date_created.py @@ -4,15 +4,14 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('torrent', '0004_rename_date_shareduser_date_created'), + ("torrent", "0004_rename_date_shareduser_date_created"), ] operations = [ migrations.RenameField( - model_name='torrent', - old_name='date_added', - new_name='date_created', + model_name="torrent", + old_name="date_added", + new_name="date_created", ), ] diff --git a/app/torrent/models.py b/app/torrent/models.py index c9c7e77..2546337 100644 --- a/app/torrent/models.py +++ b/app/torrent/models.py @@ -1,12 +1,11 @@ -from django.db import models -from django.conf import settings - +import mimetypes +import uuid from functools import cached_property from pathlib import Path from urllib.parse import quote -import mimetypes -import uuid -import shlex + +from django.conf import settings +from django.db import models class Torrent(models.Model): @@ -14,8 +13,12 @@ class Torrent(models.Model): name = models.CharField(max_length=255) date_created = models.DateTimeField(auto_now_add=True) date_modified = models.DateTimeField(auto_now=True) - user = models.ForeignKey("user.User", on_delete=models.CASCADE, related_name="torrents") - shared_users = models.ManyToManyField("user.User", related_name="torrents_shares", blank=True, through="SharedUser") + user = models.ForeignKey( + "user.User", on_delete=models.CASCADE, related_name="torrents" + ) + shared_users = models.ManyToManyField( + "user.User", related_name="torrents_shares", blank=True, through="SharedUser" + ) size = models.PositiveBigIntegerField() transmission_data = models.JSONField(default=dict) @@ -35,10 +38,7 @@ class Torrent(models.Model): @cached_property def related_users(self): - return [ - self.user_id, - *self.shared_users.values_list("id", flat=True) - ] + return [self.user_id, *self.shared_users.values_list("id", flat=True)] class SharedUser(models.Model): @@ -50,7 +50,6 @@ class SharedUser(models.Model): unique_together = ("user", "torrent") - class File(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4) torrent = models.ForeignKey("Torrent", models.CASCADE, related_name="files") @@ -86,7 +85,7 @@ class File(models.Model): def is_video(self): if self.mime_types.startswith("video/"): return True - video_extensions = ['.mp4', '.flv', '.webm', '.avi', '.mkv', '.mov', '.wmv'] + video_extensions = [".mp4", ".flv", ".webm", ".avi", ".mkv", ".mov", ".wmv"] return self.pathname.suffix.lower() in video_extensions @property @@ -95,13 +94,13 @@ class File(models.Model): encoded_parts = [] for part in self.pathname.parts: # Ignorer un slash initial si présent - if part == '/' or part == '\\': + if part == "/" or part == "\\": continue encoded_parts.append(quote(part)) # Construction du chemin final avec le préfixe Nginx - if settings.NGINX_ACCEL_BASE.endswith('/'): - base = settings.NGINX_ACCEL_BASE.rstrip('/') + if settings.NGINX_ACCEL_BASE.endswith("/"): + base = settings.NGINX_ACCEL_BASE.rstrip("/") else: base = settings.NGINX_ACCEL_BASE diff --git a/app/torrent/serializers.py b/app/torrent/serializers.py index f7d2dd0..ecb16cc 100644 --- a/app/torrent/serializers.py +++ b/app/torrent/serializers.py @@ -1,15 +1,14 @@ from django.urls import reverse from django.utils.text import slugify - from rest_framework import serializers -from user.serializers import UserSerializer -from .models import Torrent, File +from .models import File, Torrent class TorrentSerializer(serializers.ModelSerializer): count_files = serializers.IntegerField(read_only=True, source="len_files") download_url = serializers.SerializerMethodField(read_only=True) + class Meta: model = Torrent fields = "__all__" @@ -32,4 +31,4 @@ class FileSerializer(serializers.ModelSerializer): return reverse("torrent:download_file", kwargs={"file_id": obj.id}) def get_flux_url(self, obj: File): - return f'{reverse("torrent:flux_file", kwargs={"file_id": obj.id})}#{slugify(obj.filename)}' + return f"{reverse('torrent:flux_file', kwargs={'file_id': obj.id})}#{slugify(obj.filename)}" diff --git a/app/torrent/signals.py b/app/torrent/signals.py index 388aaad..9b20996 100644 --- a/app/torrent/signals.py +++ b/app/torrent/signals.py @@ -1,11 +1,14 @@ from app.utils import send_sync_channel_message + +from .models import Torrent from .utils import transmission_handler -from .models import Torrent, SharedUser def on_post_save_torrent(instance: Torrent, created, **kwargs): if created: - send_sync_channel_message(f"user_{instance.user_id}", "add_torrent", instance.id) + send_sync_channel_message( + f"user_{instance.user_id}", "add_torrent", instance.id + ) def on_pre_delete_torrent(instance: Torrent, **kwargs): @@ -25,20 +28,38 @@ def on_shared_user_changed(sender, instance: Torrent, action, pk_set, **kwargs): for user_id in pk_set: send_sync_channel_message(f"user_{user_id}", "add_torrent", instance.id) for user_id in instance.related_users: - send_sync_channel_message(f"user_{user_id}", "update_torrent", { - "torrent_id": instance.id, - "updated_fields": {"shared_users": list(instance.shared_users.all().values_list("id", flat=True))} - }) + send_sync_channel_message( + f"user_{user_id}", + "update_torrent", + { + "torrent_id": instance.id, + "updated_fields": { + "shared_users": list( + instance.shared_users.all().values_list("id", flat=True) + ) + }, + }, + ) case "pre_remove": pass case "post_remove": for user_id in pk_set: - send_sync_channel_message(f"user_{user_id}", "remove_torrent", instance.id) + send_sync_channel_message( + f"user_{user_id}", "remove_torrent", instance.id + ) for user_id in instance.related_users: - send_sync_channel_message(f"user_{user_id}", "update_torrent", { - "torrent_id": instance.id, - "updated_fields": {"shared_users": list(instance.shared_users.all().values_list("id", flat=True))} - }) + send_sync_channel_message( + f"user_{user_id}", + "update_torrent", + { + "torrent_id": instance.id, + "updated_fields": { + "shared_users": list( + instance.shared_users.all().values_list("id", flat=True) + ) + }, + }, + ) case "pre_clear": pass case "post_clear": diff --git a/app/torrent/tasks.py b/app/torrent/tasks.py index caeb91b..8930166 100644 --- a/app/torrent/tasks.py +++ b/app/torrent/tasks.py @@ -1,12 +1,10 @@ -from django.db import close_old_connections from celery import shared_task -from channels.layers import get_channel_layer -from asgiref.sync import async_to_sync -from .models import Torrent, File -from .utils import transmission_handler from app.utils import send_sync_channel_message +from .models import Torrent +from .utils import transmission_handler + @shared_task def update_transmission_data(): @@ -19,8 +17,8 @@ def update_transmission_data(): updated_torrents.append(torrent) if updated_torrents: Torrent.objects.bulk_update(updated_torrents, ["transmission_data"]) - send_sync_channel_message("torrent", "transmission_data_updated", { - torrent.id: torrent.transmission_data - for torrent in updated_torrents - }) - + send_sync_channel_message( + "torrent", + "transmission_data_updated", + {torrent.id: torrent.transmission_data for torrent in updated_torrents}, + ) diff --git a/app/torrent/tests.py b/app/torrent/tests.py index 516abd4..1dc0ae2 100644 --- a/app/torrent/tests.py +++ b/app/torrent/tests.py @@ -1,39 +1,34 @@ +from unittest.mock import MagicMock, patch + +from django.conf import settings from django.test import TestCase from django.urls import reverse -from rest_framework.test import APITestCase, APIClient from rest_framework import status -from django.conf import settings -from unittest.mock import patch, MagicMock +from rest_framework.test import APIClient, APITestCase -from .models import Torrent, SharedUser, File from user.models import User -from .views import TorrentViewSet, FileViewSet -from .utils import Transmission, torrent_proceed, torrent_share + +from .models import File, SharedUser, Torrent +from .utils import Transmission, torrent_proceed class TorrentModelTestCase(TestCase): def setUp(self): self.user = User.objects.create_user( - username='testuser', - email='test@example.com', - password='testpassword' + username="testuser", email="test@example.com", password="testpassword" ) self.torrent = Torrent.objects.create( - id='abc123', - name='Test Torrent', + id="abc123", + name="Test Torrent", user=self.user, size=1000, - transmission_data={} + transmission_data={}, ) self.shared_user = User.objects.create_user( - username='shareduser', - email='shared@example.com', - password='sharedpassword' + username="shareduser", email="shared@example.com", password="sharedpassword" ) self.file = File.objects.create( - torrent=self.torrent, - rel_name='test_file.txt', - size=100 + torrent=self.torrent, rel_name="test_file.txt", size=100 ) def test_len_files(self): @@ -41,15 +36,11 @@ class TorrentModelTestCase(TestCase): self.assertEqual(self.torrent.len_files, 1) # Add another file and test again - File.objects.create( - torrent=self.torrent, - rel_name='another_file.txt', - size=200 - ) + File.objects.create(torrent=self.torrent, rel_name="another_file.txt", size=200) # Clear cached_property - if hasattr(self.torrent, '_len_files'): - delattr(self.torrent, '_len_files') + if hasattr(self.torrent, "_len_files"): + delattr(self.torrent, "_len_files") self.assertEqual(self.torrent.len_files, 2) @@ -62,8 +53,8 @@ class TorrentModelTestCase(TestCase): self.torrent.shared_users.add(self.shared_user) # Clear cached_property - if hasattr(self.torrent, '_related_users'): - delattr(self.torrent, '_related_users') + if hasattr(self.torrent, "_related_users"): + delattr(self.torrent, "_related_users") # Should include both users now self.assertIn(self.user.id, self.torrent.related_users) @@ -73,30 +64,26 @@ class TorrentModelTestCase(TestCase): class FileModelTestCase(TestCase): def setUp(self): self.user = User.objects.create_user( - username='testuser', - email='test@example.com', - password='testpassword' + username="testuser", email="test@example.com", password="testpassword" ) self.torrent = Torrent.objects.create( - id='abc123', - name='Test Torrent', + id="abc123", + name="Test Torrent", user=self.user, size=1000, - transmission_data={} + transmission_data={}, ) self.file = File.objects.create( - torrent=self.torrent, - rel_name='test/path/file.mp4', - size=100 + torrent=self.torrent, rel_name="test/path/file.mp4", size=100 ) def test_pathname(self): """Test the pathname property returns the correct path""" - self.assertEqual(str(self.file.pathname), 'test/path/file.mp4') + self.assertEqual(str(self.file.pathname), "test/path/file.mp4") def test_filename(self): """Test the filename property returns the correct filename""" - self.assertEqual(self.file.filename, 'file.mp4') + self.assertEqual(self.file.filename, "file.mp4") def test_abs_pathname(self): """Test the abs_pathname property returns the correct absolute path""" @@ -109,9 +96,7 @@ class FileModelTestCase(TestCase): # Test non-video file non_video_file = File.objects.create( - torrent=self.torrent, - rel_name='test/path/document.pdf', - size=50 + torrent=self.torrent, rel_name="test/path/document.pdf", size=50 ) self.assertFalse(non_video_file.is_video) @@ -119,29 +104,22 @@ class FileModelTestCase(TestCase): class SharedUserModelTestCase(TestCase): def setUp(self): self.owner = User.objects.create_user( - username='owner', - email='owner@example.com', - password='ownerpassword' + username="owner", email="owner@example.com", password="ownerpassword" ) self.shared_user = User.objects.create_user( - username='shareduser', - email='shared@example.com', - password='sharedpassword' + username="shareduser", email="shared@example.com", password="sharedpassword" ) self.torrent = Torrent.objects.create( - id='abc123', - name='Test Torrent', + id="abc123", + name="Test Torrent", user=self.owner, size=1000, - transmission_data={} + transmission_data={}, ) def test_shared_user_creation(self): """Test creating a shared user relationship""" - shared = SharedUser.objects.create( - user=self.shared_user, - torrent=self.torrent - ) + shared = SharedUser.objects.create(user=self.shared_user, torrent=self.torrent) self.assertEqual(shared.user, self.shared_user) self.assertEqual(shared.torrent, self.torrent) @@ -152,109 +130,99 @@ class SharedUserModelTestCase(TestCase): class TorrentViewSetTestCase(APITestCase): def setUp(self): self.user = User.objects.create_user( - username='testuser', - email='test@example.com', - password='testpassword' + username="testuser", email="test@example.com", password="testpassword" ) self.client = APIClient() self.client.force_authenticate(user=self.user) self.torrent = Torrent.objects.create( - id='abc123', - name='Test Torrent', + id="abc123", + name="Test Torrent", user=self.user, size=1000, - transmission_data={} + transmission_data={}, ) self.file = File.objects.create( - torrent=self.torrent, - rel_name='test_file.txt', - size=100 + torrent=self.torrent, rel_name="test_file.txt", size=100 ) def test_list_torrents(self): """Test listing torrents""" - url = reverse('torrent-list') + url = reverse("torrent-list") response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), 1) - self.assertEqual(response.data[0]['id'], self.torrent.id) + self.assertEqual(response.data[0]["id"], self.torrent.id) def test_retrieve_torrent(self): """Test retrieving a specific torrent""" - url = reverse('torrent-detail', args=[self.torrent.id]) + url = reverse("torrent-detail", args=[self.torrent.id]) response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['id'], self.torrent.id) - self.assertEqual(response.data['name'], 'Test Torrent') + self.assertEqual(response.data["id"], self.torrent.id) + self.assertEqual(response.data["name"], "Test Torrent") - @patch('torrent.views.torrent_share') + @patch("torrent.views.torrent_share") def test_share_torrent(self, mock_torrent_share): """Test sharing a torrent with another user""" mock_torrent_share.return_value = True shared_user = User.objects.create_user( - username='shareduser', - email='shared@example.com', - password='sharedpassword' + username="shareduser", email="shared@example.com", password="sharedpassword" ) - url = reverse('torrent-share', args=[self.torrent.id]) - response = self.client.post(url, {'user_id': shared_user.id}) + url = reverse("torrent-share", args=[self.torrent.id]) + response = self.client.post(url, {"user_id": shared_user.id}) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(response.data['success']) + self.assertTrue(response.data["success"]) mock_torrent_share.assert_called_once() class FileViewSetTestCase(APITestCase): def setUp(self): self.user = User.objects.create_user( - username='testuser', - email='test@example.com', - password='testpassword' + username="testuser", email="test@example.com", password="testpassword" ) self.client = APIClient() self.client.force_authenticate(user=self.user) self.torrent = Torrent.objects.create( - id='abc123', - name='Test Torrent', + id="abc123", + name="Test Torrent", user=self.user, size=1000, - transmission_data={} + transmission_data={}, ) self.file = File.objects.create( - torrent=self.torrent, - rel_name='test_file.txt', - size=100 + torrent=self.torrent, rel_name="test_file.txt", size=100 ) def test_list_files(self): """Test listing files""" - url = reverse('file-list') + url = reverse("file-list") response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_retrieve_file(self): """Test retrieving a specific file""" - url = reverse('file-detail', args=[self.file.id]) + url = reverse("file-detail", args=[self.file.id]) response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['id'], str(self.file.id)) - self.assertEqual(response.data['rel_name'], 'test_file.txt') + self.assertEqual(response.data["id"], str(self.file.id)) + self.assertEqual(response.data["rel_name"], "test_file.txt") class TransmissionUtilsTestCase(TestCase): - @patch('torrent.utils.Client') + @patch("torrent.utils.Client") def test_transmission_init(self, mock_client): """Test Transmission class initialization""" transmission = Transmission() mock_client.assert_called_once_with(**settings.TRANSMISSION) - @patch('torrent.utils.Client') + @patch("torrent.utils.Client") def test_add_torrent(self, mock_client): """Test adding a torrent""" mock_instance = mock_client.return_value @@ -267,62 +235,64 @@ class TransmissionUtilsTestCase(TestCase): mock_instance.add_torrent.assert_called_once_with(file_obj) self.assertEqual(result, mock_instance.add_torrent.return_value) - @patch('torrent.utils.Client') + @patch("torrent.utils.Client") def test_get_data(self, mock_client): """Test getting torrent data""" mock_instance = mock_client.return_value mock_torrent = MagicMock() mock_torrent.progress = 50 - mock_torrent.fields = {'name': 'Test', 'size': 1000} + mock_torrent.fields = {"name": "Test", "size": 1000} mock_instance.get_torrent.return_value = mock_torrent transmission = Transmission() - result = transmission.get_data('hash123') + result = transmission.get_data("hash123") - mock_instance.get_torrent.assert_called_once_with('hash123', transmission.trpc_args) - self.assertEqual(result['progress'], 50) - self.assertEqual(result['name'], 'Test') - self.assertEqual(result['size'], 1000) + mock_instance.get_torrent.assert_called_once_with( + "hash123", transmission.trpc_args + ) + self.assertEqual(result["progress"], 50) + self.assertEqual(result["name"], "Test") + self.assertEqual(result["size"], 1000) class TorrentProceedTestCase(TestCase): def setUp(self): self.user = User.objects.create_user( - username='testuser', - email='test@example.com', - password='testpassword', - max_size=10000 + username="testuser", + email="test@example.com", + password="testpassword", + max_size=10000, ) - @patch('torrent.utils.transmission_handler') + @patch("torrent.utils.transmission_handler") def test_torrent_proceed_size_exceed(self, mock_transmission): """Test torrent_proceed when user size is exceeded""" # Set user's used size to exceed max_size self.user.max_size = 100 Torrent.objects.create( - id='abc123', - name='Test Torrent', + id="abc123", + name="Test Torrent", user=self.user, size=200, # Exceeds max_size - transmission_data={} + transmission_data={}, ) file_obj = MagicMock() result = torrent_proceed(self.user, file_obj) - self.assertEqual(result['status'], 'error') - self.assertEqual(result['message'], 'Size exceed') + self.assertEqual(result["status"], "error") + self.assertEqual(result["message"], "Size exceed") mock_transmission.add_torrent.assert_not_called() - @patch('torrent.utils.transmission_handler') + @patch("torrent.utils.transmission_handler") def test_torrent_proceed_transmission_error(self, mock_transmission): """Test torrent_proceed when transmission raises an error""" from transmission_rpc.error import TransmissionError - mock_transmission.add_torrent.side_effect = TransmissionError('Test error') + mock_transmission.add_torrent.side_effect = TransmissionError("Test error") file_obj = MagicMock() result = torrent_proceed(self.user, file_obj) - self.assertEqual(result['status'], 'error') - self.assertEqual(result['message'], 'Transmission Error') + self.assertEqual(result["status"], "error") + self.assertEqual(result["message"], "Transmission Error") diff --git a/app/torrent/urls.py b/app/torrent/urls.py index 2a97450..c554c35 100644 --- a/app/torrent/urls.py +++ b/app/torrent/urls.py @@ -1,12 +1,14 @@ from django.urls import path -from .views import HomeView, download_file, download_torrent, pping, flux_file +from .views import HomeView, download_file, download_torrent, flux_file, pping app_name = "torrent" urlpatterns = [ path("", HomeView.as_view(), name="home"), path("pping/", pping, name="pping"), path("download_file/", download_file, name="download_file"), - path("download_torrent/", download_torrent, name="download_torrent"), + path( + "download_torrent/", download_torrent, name="download_torrent" + ), path("flux_file/", flux_file, name="flux_file"), ] diff --git a/app/torrent/utils.py b/app/torrent/utils.py index 58404fa..bdf1ce5 100644 --- a/app/torrent/utils.py +++ b/app/torrent/utils.py @@ -1,24 +1,38 @@ -import os - -from django.conf import settings - -import traceback import base64 import io +import os +import traceback +from django.conf import settings from transmission_rpc import Client from transmission_rpc.error import TransmissionError -# from app.utils import send_sync_channel_message -from .models import Torrent, File from user.models import User +# from app.utils import send_sync_channel_message +from .models import File, Torrent + class Transmission: trpc_args = [ - "id", "percentDone", "uploadRatio", "rateUpload", "rateDownload", "hashString", "status", "sizeWhenDone", - "leftUntilDone", "name", "eta", "totalSize", "uploadedEver", "peersGettingFromUs", "peersSendingToUs", - "tracker", "trackerStats", "activityDate" + "id", + "percentDone", + "uploadRatio", + "rateUpload", + "rateDownload", + "hashString", + "status", + "sizeWhenDone", + "leftUntilDone", + "name", + "eta", + "totalSize", + "uploadedEver", + "peersGettingFromUs", + "peersSendingToUs", + "tracker", + "trackerStats", + "activityDate", ] def __init__(self): @@ -31,7 +45,12 @@ class Transmission: if os.path.exists(port_file): try: with open(port_file) as f: - vpn_port = int(f.read().strip()) + content = f.read().strip() + if ( + not content + ): # Si le fichier est vide, on attend la prochaine itération + return + vpn_port = int(content) # Récupère le port actuel configuré dans Transmission current_settings = self.client.get_session() @@ -55,15 +74,15 @@ class Transmission: def get_data(self, hash_string): data = self.client.get_torrent(hash_string, self.trpc_args) - return { - "progress": data.progress, - "status_str": data.status, - **data.fields - } + return {"progress": data.progress, "status_str": data.status, **data.fields} def get_all_data(self, hash_strings=None): return { - data.hashString: {"progress": data.progress, "status_str": data.status, **data.fields} + data.hashString: { + "progress": data.progress, + "status_str": data.status, + **data.fields, + } for data in self.client.get_torrents(hash_strings, self.trpc_args) } @@ -84,7 +103,7 @@ class Transmission: port_file = "/tmp/gluetun/forwarded_port" vpn_port = None if os.path.exists(port_file): - with open(port_file, "r") as f: + with open(port_file) as f: vpn_port = f.read().strip() # 2. Test de connectivité du port (via l'API Transmission) @@ -106,11 +125,7 @@ transmission_handler = Transmission() def torrent_proceed(user, file, file_mode="file_object"): - r = { - "torrent": None, - "status": "error", - "message": "Unexpected error" - } + r = {"torrent": None, "status": "error", "message": "Unexpected error"} user: User if user.size_used > user.max_size: @@ -149,16 +164,18 @@ def torrent_proceed(user, file, file_mode="file_object"): name=data["name"], user=user, size=data["totalSize"], - transmission_data=data + transmission_data=data, + ) + File.objects.bulk_create( + [ + File( + torrent=torrent, + rel_name=file.name, + size=file.size, + ) + for file in transmission_handler.get_files(torrent.id) + ] ) - File.objects.bulk_create([ - File( - torrent=torrent, - rel_name=file.name, - size=file.size, - ) - for file in transmission_handler.get_files(torrent.id) - ]) r["torrent"] = torrent r["status"] = "success" @@ -167,13 +184,22 @@ def torrent_proceed(user, file, file_mode="file_object"): def torrent_share(torrent, current_user, target_user_id): - from .models import Torrent, SharedUser + from .models import SharedUser torrent: Torrent - if (torrent.user_id != target_user_id and - any([torrent.user == current_user, torrent.shared_users.filter(id=current_user.id)]) and - not SharedUser.objects.filter(torrent_id=torrent.id, user_id=target_user_id).exists()): + if ( + torrent.user_id != target_user_id + and any( + [ + torrent.user == current_user, + torrent.shared_users.filter(id=current_user.id), + ] + ) + and not SharedUser.objects.filter( + torrent_id=torrent.id, user_id=target_user_id + ).exists() + ): torrent.shared_users.add(target_user_id) return True - return False \ No newline at end of file + return False diff --git a/app/torrent/views.py b/app/torrent/views.py index 54728e1..98c2265 100644 --- a/app/torrent/views.py +++ b/app/torrent/views.py @@ -1,21 +1,20 @@ +import anyio +from django.contrib.auth.mixins import LoginRequiredMixin +from django.db.models import Count, OuterRef, Q, Sum +from django.db.models.functions import Coalesce +from django.http import Http404, HttpResponse, StreamingHttpResponse from django.shortcuts import redirect from django.urls import reverse from django.views.generic import TemplateView -from django.contrib.auth.mixins import LoginRequiredMixin -from django.db.models import Q, Count, OuterRef, Sum -from django.db.models.functions import Coalesce -from django.http import HttpResponse, Http404, StreamingHttpResponse - -from rest_framework.viewsets import GenericViewSet from rest_framework import mixins -from rest_framework.response import Response from rest_framework.decorators import action -import anyio +from rest_framework.response import Response +from rest_framework.viewsets import GenericViewSet from app.utils import StreamingZipFileResponse -from user.models import User -from .models import Torrent, File, SharedUser -from .serializers import TorrentSerializer, FileSerializer + +from .models import File, SharedUser, Torrent +from .serializers import FileSerializer, TorrentSerializer from .utils import torrent_proceed, torrent_share @@ -35,7 +34,7 @@ async def download_file(request, file_id): | Q(torrent__user__friends=user) | Q(torrent__shared_users__friends=user), torrent__transmission_data__progress__gte=100, - pk=file_id + pk=file_id, ).distinct() try: @@ -44,10 +43,12 @@ async def download_file(request, file_id): raise Http404() else: if int(request.GET.get("dl_hotfix", 0)) == 1: + async def read_file(): async with await anyio.open_file(file.abs_pathname, "rb") as f: while chunk := await f.read(128 * 1024): yield chunk + response = StreamingHttpResponse(read_file()) response["Content-Length"] = file.size response["Content-Type"] = "application/octet-stream" @@ -86,7 +87,7 @@ async def secured_flux_file(request, file_id): | Q(torrent__user__friends=user) | Q(torrent__shared_users__friends=user), torrent__transmission_data__progress__gte=100, - pk=file_id + pk=file_id, ).distinct() try: @@ -105,39 +106,42 @@ async def secured_flux_file(request, file_id): async def download_torrent(request, torrent_id): # py version user = await request.auser() - qs = Torrent.objects.filter( - Q(user=user) - | Q(shared_users=user) - | Q(user__friends=user) - | Q(shared_users__friends=user), - transmission_data__progress__gte=100, - pk=torrent_id - ).annotate(count_files=Count("files")).distinct() + qs = ( + Torrent.objects.filter( + Q(user=user) + | Q(shared_users=user) + | Q(user__friends=user) + | Q(shared_users__friends=user), + transmission_data__progress__gte=100, + pk=torrent_id, + ) + .annotate(count_files=Count("files")) + .distinct() + ) torrent = await qs.aget() if await torrent.alen_files == 1: file = await torrent.files.afirst() - return redirect(reverse("torrent:download_file", kwargs={ - "file_id": file.pk - })) + return redirect(reverse("torrent:download_file", kwargs={"file_id": file.pk})) response = StreamingZipFileResponse( filename=f"{torrent.name}.zip", file_list=[ - (file.abs_pathname, file.rel_name) - async for file in torrent.files.all() + (file.abs_pathname, file.rel_name) async for file in torrent.files.all() ], - is_async=True + is_async=True, ) return response -class TorrentViewSet(mixins.CreateModelMixin, - mixins.RetrieveModelMixin, - mixins.DestroyModelMixin, - mixins.ListModelMixin, - GenericViewSet): +class TorrentViewSet( + mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + GenericViewSet, +): queryset = Torrent.objects.all().annotate(count_files=Count("files")) serializer_class = TorrentSerializer @@ -158,7 +162,9 @@ class TorrentViewSet(mixins.CreateModelMixin, else: user_id = self.request.user.id - sub = SharedUser.objects.filter(torrent_id=OuterRef("pk"), user_id=user_id).values("date_created") + sub = SharedUser.objects.filter( + torrent_id=OuterRef("pk"), user_id=user_id + ).values("date_created") qs = qs.annotate(last_date=Coalesce(sub, "date_created")).order_by("-last_date") search = self.request.query_params.get("search", None) @@ -188,7 +194,9 @@ class TorrentViewSet(mixins.CreateModelMixin, def share(self, request, pk): user_id = self.request.data.get("user_id") torrent = self.get_object() - is_share_success = torrent_share(torrent=torrent, current_user=self.request.user, target_user_id=user_id) + is_share_success = torrent_share( + torrent=torrent, current_user=self.request.user, target_user_id=user_id + ) return Response({"success": is_share_success}) @action(methods=["get"], detail=False) @@ -196,9 +204,7 @@ class TorrentViewSet(mixins.CreateModelMixin, Torrent.objects.filter(user=self.request.user).aggregate(total_size=Sum("size")) -class FileViewSet(mixins.RetrieveModelMixin, - mixins.ListModelMixin, - GenericViewSet): +class FileViewSet(mixins.RetrieveModelMixin, mixins.ListModelMixin, GenericViewSet): queryset = File.objects.all() serializer_class = FileSerializer filterset_fields = ["torrent"] diff --git a/app/user/admin.py b/app/user/admin.py index df4a846..12fa8d2 100644 --- a/app/user/admin.py +++ b/app/user/admin.py @@ -2,8 +2,8 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.template.defaultfilters import filesizeformat -from .forms import UserCreationForm, UserChangeForm -from .models import User, FriendRequest, Invitation +from .forms import UserChangeForm +from .models import FriendRequest, Invitation, User @admin.register(User) @@ -12,26 +12,36 @@ class UserAdmin(BaseUserAdmin): # add_form = UserCreationForm form = UserChangeForm fieldsets = BaseUserAdmin.fieldsets + ( - ["Custom Fields", { - "fields": ["max_size", "friends"] - }] - ,) - list_display = ["username", "email", "is_superuser", "is_active", "is_staff", "display_max_size", "size_used"] + ["Custom Fields", {"fields": ["max_size", "friends"]}], + ) + list_display = [ + "username", + "email", + "is_superuser", + "is_active", + "is_staff", + "display_max_size", + "size_used", + ] add_fieldsets = ( - (None, { - "classes": ("wide",), - "fields": ("username", "email", "max_size", "password1", "password2"), - }), + ( + None, + { + "classes": ("wide",), + "fields": ("username", "email", "max_size", "password1", "password2"), + }, + ), ) def display_max_size(self, obj: User): return filesizeformat(obj.max_size) + display_max_size.short_description = "Max size" def size_used(self, obj: User): return filesizeformat(obj.size_used) - size_used.short_description = "Size used" + size_used.short_description = "Size used" @admin.register(Invitation) diff --git a/app/user/apps.py b/app/user/apps.py index 36cce4c..578292c 100644 --- a/app/user/apps.py +++ b/app/user/apps.py @@ -2,5 +2,5 @@ from django.apps import AppConfig class UserConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'user' + default_auto_field = "django.db.models.BigAutoField" + name = "user" diff --git a/app/user/forms.py b/app/user/forms.py index 203f330..4942c33 100644 --- a/app/user/forms.py +++ b/app/user/forms.py @@ -15,6 +15,7 @@ class RegisterForm(base_auth_forms.UserCreationForm): class UserCreationForm(AdminUserCreationForm): max_size = forms.IntegerField(required=True) email = forms.EmailField(required=True) + class Meta(base_auth_forms.UserCreationForm): model = User fields = base_auth_forms.BaseUserCreationForm.Meta.fields + ("max_size",) @@ -24,4 +25,3 @@ class UserChangeForm(base_auth_forms.UserChangeForm): class Meta: model = User fields = ["max_size"] - diff --git a/app/user/management/commands/import_old_users.py b/app/user/management/commands/import_old_users.py index 27490f2..f729148 100644 --- a/app/user/management/commands/import_old_users.py +++ b/app/user/management/commands/import_old_users.py @@ -1,9 +1,9 @@ -from django.core.management.base import BaseCommand - -import json import base64 +import json import sys +from django.core.management.base import BaseCommand + from user.models import User @@ -26,7 +26,9 @@ class Command(BaseCommand): user_data = data["fields"] user_pk = data["pk"] if User.objects.filter(username__iexact=user_data["username"]).exists(): - old_new_users_maps[user_pk] = User.objects.filter(username__iexact=user_data["username"]).get() + old_new_users_maps[user_pk] = User.objects.filter( + username__iexact=user_data["username"] + ).get() else: old_new_users_maps[user_pk] = User.objects.create( email=user_data["email"], @@ -35,12 +37,14 @@ class Command(BaseCommand): is_superuser=user_data["is_superuser"], username=user_data["username"], password=user_data["password"], - max_size=user_data["limit_size"] + max_size=user_data["limit_size"], ) old_friends[user_pk] = user_data["friends"] for old_user, friends in old_friends.items(): current_user = old_new_users_maps[old_user] for friend in friends: - if not current_user.friends.filter(id=old_new_users_maps[friend].id).exists(): + if not current_user.friends.filter( + id=old_new_users_maps[friend].id + ).exists(): current_user.friends.add(old_new_users_maps[friend]) diff --git a/app/user/migrations/0001_initial.py b/app/user/migrations/0001_initial.py index 8783114..b96b97a 100644 --- a/app/user/migrations/0001_initial.py +++ b/app/user/migrations/0001_initial.py @@ -1,72 +1,205 @@ # Generated by Django 5.1.6 on 2025-03-04 23:41 +import uuid + import django.contrib.auth.validators import django.db.models.deletion import django.utils.timezone -import user.models -import uuid from django.conf import settings from django.db import migrations, models +import user.models + class Migration(migrations.Migration): - initial = True dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), + ("auth", "0012_alter_user_first_name_max_length"), ] operations = [ migrations.CreateModel( - name='User', + name="User", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), - ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), - ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('email', models.EmailField(max_length=254, unique=True)), - ('max_size', models.PositiveBigIntegerField(default=53687091200)), - ('is_trusted', models.BooleanField(default=False)), - ('friends', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL)), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={ + "unique": "A user with that username already exists." + }, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=150, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ("email", models.EmailField(max_length=254, unique=True)), + ("max_size", models.PositiveBigIntegerField(default=53687091200)), + ("is_trusted", models.BooleanField(default=False)), + ( + "friends", + models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL), + ), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.permission", + verbose_name="user permissions", + ), + ), ], options={ - 'verbose_name': 'user', - 'verbose_name_plural': 'users', - 'abstract': False, + "verbose_name": "user", + "verbose_name_plural": "users", + "abstract": False, }, managers=[ - ('objects', user.models.UsernameUserManager()), + ("objects", user.models.UsernameUserManager()), ], ), migrations.CreateModel( - name='Invitation', + name="Invitation", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('token', models.UUIDField(default=uuid.uuid4)), - ('date_created', models.DateTimeField(auto_now_add=True)), - ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invitations', to=settings.AUTH_USER_MODEL)), - ('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='invitation', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("token", models.UUIDField(default=uuid.uuid4)), + ("date_created", models.DateTimeField(auto_now_add=True)), + ( + "created_by", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="invitations", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "user", + models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="invitation", + to=settings.AUTH_USER_MODEL, + ), + ), ], ), migrations.CreateModel( - name='FriendRequest', + name="FriendRequest", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('date', models.DateTimeField(auto_now_add=True)), - ('receiver', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='friend_request_receives', to=settings.AUTH_USER_MODEL)), - ('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='friend_request_sends', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("date", models.DateTimeField(auto_now_add=True)), + ( + "receiver", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="friend_request_receives", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "sender", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="friend_request_sends", + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'unique_together': {('sender', 'receiver')}, + "unique_together": {("sender", "receiver")}, }, ), ] diff --git a/app/user/models.py b/app/user/models.py index 8efded8..ed7c490 100644 --- a/app/user/models.py +++ b/app/user/models.py @@ -1,9 +1,10 @@ -from django.db import models -from django.contrib.auth.models import AbstractUser, BaseUserManager -from django.db.models import Sum - import uuid from functools import cached_property + +from django.contrib.auth.models import AbstractUser, BaseUserManager +from django.db import models +from django.db.models import Sum + from torrent.models import Torrent @@ -53,7 +54,9 @@ class User(AbstractUser): if hasattr(self, "total_size"): return self.total_size else: - return Torrent.objects.filter(user=self).aggregate(total_size=Sum("size", default=0))["total_size"] + return Torrent.objects.filter(user=self).aggregate( + total_size=Sum("size", default=0) + )["total_size"] @property def min_infos(self): @@ -61,8 +64,12 @@ class User(AbstractUser): class FriendRequest(models.Model): - sender = models.ForeignKey("User", on_delete=models.CASCADE, related_name="friend_request_sends") - receiver = models.ForeignKey("User", on_delete=models.CASCADE, related_name="friend_request_receives") + sender = models.ForeignKey( + "User", on_delete=models.CASCADE, related_name="friend_request_sends" + ) + receiver = models.ForeignKey( + "User", on_delete=models.CASCADE, related_name="friend_request_receives" + ) date = models.DateTimeField(auto_now_add=True) class Meta: @@ -72,5 +79,7 @@ class FriendRequest(models.Model): class Invitation(models.Model): created_by = models.ForeignKey("User", models.CASCADE, related_name="invitations") token = models.UUIDField(default=uuid.uuid4) - user = models.OneToOneField("User", models.CASCADE, related_name="invitation", null=True, blank=True) + user = models.OneToOneField( + "User", models.CASCADE, related_name="invitation", null=True, blank=True + ) date_created = models.DateTimeField(auto_now_add=True) diff --git a/app/user/serializers.py b/app/user/serializers.py index 7493514..5d513eb 100644 --- a/app/user/serializers.py +++ b/app/user/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from .models import User, FriendRequest, Invitation +from .models import FriendRequest, Invitation, User class UserSerializer(serializers.ModelSerializer): diff --git a/app/user/tests.py b/app/user/tests.py index 3e82271..bd5e6e6 100644 --- a/app/user/tests.py +++ b/app/user/tests.py @@ -1,26 +1,25 @@ +from unittest.mock import MagicMock, patch + from django.test import TestCase from django.urls import reverse -from rest_framework.test import APITestCase, APIClient from rest_framework import status -from unittest.mock import patch, MagicMock +from rest_framework.test import APIClient, APITestCase -from .models import User, FriendRequest, Invitation, UsernameUserManager from torrent.models import Torrent -from .views import UserViewSet, FriendRequestViewSet + +from .models import FriendRequest, Invitation, User class UserModelTestCase(TestCase): def setUp(self): self.user = User.objects.create_user( - username='testuser', - email='test@example.com', - password='testpassword', - max_size=1000000 + username="testuser", + email="test@example.com", + password="testpassword", + max_size=1000000, ) self.friend = User.objects.create_user( - username='frienduser', - email='friend@example.com', - password='friendpassword' + username="frienduser", email="friend@example.com", password="friendpassword" ) def test_size_used_property(self): @@ -30,35 +29,32 @@ class UserModelTestCase(TestCase): # Create a torrent for the user Torrent.objects.create( - id='abc123', - name='Test Torrent', + id="abc123", + name="Test Torrent", user=self.user, size=5000, - transmission_data={} + transmission_data={}, ) # Create another torrent Torrent.objects.create( - id='def456', - name='Another Torrent', + id="def456", + name="Another Torrent", user=self.user, size=3000, - transmission_data={} + transmission_data={}, ) # Clear cached_property if it exists - if hasattr(self.user, 'total_size'): - delattr(self.user, 'total_size') + if hasattr(self.user, "total_size"): + delattr(self.user, "total_size") # Size used should be the sum of torrent sizes self.assertEqual(self.user.size_used, 8000) def test_min_infos_property(self): """Test the min_infos property returns the correct user info""" - expected_info = { - 'username': 'testuser', - 'id': self.user.id - } + expected_info = {"username": "testuser", "id": self.user.id} self.assertEqual(self.user.min_infos, expected_info) @@ -66,111 +62,86 @@ class UsernameUserManagerTestCase(TestCase): def test_create_user(self): """Test creating a regular user""" user = User.objects.create_user( - username='newuser', - email='new@example.com', - password='newpassword' + username="newuser", email="new@example.com", password="newpassword" ) self.assertFalse(user.is_staff) self.assertFalse(user.is_superuser) - self.assertEqual(user.username, 'newuser') - self.assertEqual(user.email, 'new@example.com') - self.assertTrue(user.check_password('newpassword')) + self.assertEqual(user.username, "newuser") + self.assertEqual(user.email, "new@example.com") + self.assertTrue(user.check_password("newpassword")) def test_create_superuser(self): """Test creating a superuser""" admin = User.objects.create_superuser( - username='admin', - email='admin@example.com', - password='adminpassword' + username="admin", email="admin@example.com", password="adminpassword" ) self.assertTrue(admin.is_staff) self.assertTrue(admin.is_superuser) - self.assertEqual(admin.username, 'admin') - self.assertEqual(admin.email, 'admin@example.com') + self.assertEqual(admin.username, "admin") + self.assertEqual(admin.email, "admin@example.com") def test_create_user_without_username(self): """Test creating a user without username raises error""" with self.assertRaises(ValueError): User.objects.create_user( - username='', - email='test@example.com', - password='testpassword' + username="", email="test@example.com", password="testpassword" ) def test_create_user_without_email(self): """Test creating a user without email raises error""" with self.assertRaises(ValueError): User.objects.create_user( - username='testuser', - email='', - password='testpassword' + username="testuser", email="", password="testpassword" ) class FriendRequestModelTestCase(TestCase): def setUp(self): self.sender = User.objects.create_user( - username='sender', - email='sender@example.com', - password='senderpassword' + username="sender", email="sender@example.com", password="senderpassword" ) self.receiver = User.objects.create_user( - username='receiver', - email='receiver@example.com', - password='receiverpassword' + username="receiver", + email="receiver@example.com", + password="receiverpassword", ) def test_friend_request_creation(self): """Test creating a friend request""" friend_request = FriendRequest.objects.create( - sender=self.sender, - receiver=self.receiver + sender=self.sender, receiver=self.receiver ) self.assertEqual(friend_request.sender, self.sender) self.assertEqual(friend_request.receiver, self.receiver) def test_unique_together_constraint(self): """Test that the unique_together constraint works""" - FriendRequest.objects.create( - sender=self.sender, - receiver=self.receiver - ) + FriendRequest.objects.create(sender=self.sender, receiver=self.receiver) # Creating another request with the same sender and receiver should raise an error with self.assertRaises(Exception): - FriendRequest.objects.create( - sender=self.sender, - receiver=self.receiver - ) + FriendRequest.objects.create(sender=self.sender, receiver=self.receiver) class InvitationModelTestCase(TestCase): def setUp(self): self.creator = User.objects.create_user( - username='creator', - email='creator@example.com', - password='creatorpassword' + username="creator", email="creator@example.com", password="creatorpassword" ) def test_invitation_creation(self): """Test creating an invitation""" - invitation = Invitation.objects.create( - created_by=self.creator - ) + invitation = Invitation.objects.create(created_by=self.creator) self.assertEqual(invitation.created_by, self.creator) self.assertIsNotNone(invitation.token) self.assertIsNone(invitation.user) def test_invitation_assignment(self): """Test assigning an invitation to a user""" - invitation = Invitation.objects.create( - created_by=self.creator - ) + invitation = Invitation.objects.create(created_by=self.creator) new_user = User.objects.create_user( - username='newuser', - email='new@example.com', - password='newpassword' + username="newuser", email="new@example.com", password="newpassword" ) invitation.user = new_user @@ -184,133 +155,127 @@ class InvitationModelTestCase(TestCase): class UserViewSetTestCase(APITestCase): def setUp(self): self.user = User.objects.create_user( - username='testuser', - email='test@example.com', - password='testpassword' + username="testuser", email="test@example.com", password="testpassword" ) self.friend = User.objects.create_user( - username='frienduser', - email='friend@example.com', - password='friendpassword' + username="frienduser", email="friend@example.com", password="friendpassword" ) self.client = APIClient() self.client.force_authenticate(user=self.user) def test_list_users(self): """Test listing users""" - url = reverse('user-list') + url = reverse("user-list") response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), 2) # Should include both users def test_retrieve_user(self): """Test retrieving a specific user""" - url = reverse('user-detail', args=[self.friend.id]) + url = reverse("user-detail", args=[self.friend.id]) response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['username'], 'frienduser') + self.assertEqual(response.data["username"], "frienduser") def test_add_friend_request(self): """Test adding a friend request""" - url = reverse('user-add-friend-request', args=[self.friend.username]) + url = reverse("user-add-friend-request", args=[self.friend.username]) response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(response.data['success']) - self.assertEqual(response.data['message'], 'Request sent') + self.assertTrue(response.data["success"]) + self.assertEqual(response.data["message"], "Request sent") # Verify the friend request was created - self.assertTrue(FriendRequest.objects.filter( - sender=self.user, - receiver=self.friend - ).exists()) + self.assertTrue( + FriendRequest.objects.filter( + sender=self.user, receiver=self.friend + ).exists() + ) def test_add_friend_request_nonexistent_user(self): """Test adding a friend request to a nonexistent user""" - url = reverse('user-add-friend-request', args=['nonexistentuser']) + url = reverse("user-add-friend-request", args=["nonexistentuser"]) response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertFalse(response.data['success']) - self.assertEqual(response.data['message'], "User 'nonexistentuser' doesn't exist") + self.assertFalse(response.data["success"]) + self.assertEqual( + response.data["message"], "User 'nonexistentuser' doesn't exist" + ) def test_remove_friend(self): """Test removing a friend""" # First add as friend self.user.friends.add(self.friend) - url = reverse('user-remove-friend', args=[self.friend.id]) + url = reverse("user-remove-friend", args=[self.friend.id]) response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(response.data['success']) + self.assertTrue(response.data["success"]) # Verify the friend was removed self.assertFalse(self.user.friends.filter(id=self.friend.id).exists()) - @patch('user.views.shutil.disk_usage') + @patch("user.views.shutil.disk_usage") def test_user_stats(self, mock_disk_usage): """Test getting user stats""" # Mock disk_usage return value mock_disk_usage.return_value = MagicMock( - total=1000000, - used=500000, - free=500000 + total=1000000, used=500000, free=500000 ) # Create torrents for the user Torrent.objects.create( - id='abc123', - name='Test Torrent', + id="abc123", + name="Test Torrent", user=self.user, size=5000, - transmission_data={} + transmission_data={}, ) - url = reverse('user-user-stats') + url = reverse("user-user-stats") response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) # Check that the response contains the expected fields - self.assertIn('torrents_size', response.data) - self.assertIn('torrents_len', response.data) - self.assertIn('user_max_size', response.data) - self.assertIn('disk_total', response.data) - self.assertIn('disk_used', response.data) - self.assertIn('disk_free', response.data) + self.assertIn("torrents_size", response.data) + self.assertIn("torrents_len", response.data) + self.assertIn("user_max_size", response.data) + self.assertIn("disk_total", response.data) + self.assertIn("disk_used", response.data) + self.assertIn("disk_free", response.data) class FriendRequestViewSetTestCase(APITestCase): def setUp(self): self.user = User.objects.create_user( - username='testuser', - email='test@example.com', - password='testpassword' + username="testuser", email="test@example.com", password="testpassword" ) self.sender = User.objects.create_user( - username='sender', - email='sender@example.com', - password='senderpassword' + username="sender", email="sender@example.com", password="senderpassword" ) self.client = APIClient() self.client.force_authenticate(user=self.user) # Create a friend request self.friend_request = FriendRequest.objects.create( - sender=self.sender, - receiver=self.user + sender=self.sender, receiver=self.user ) def test_list_friend_requests(self): """Test listing friend requests""" - url = reverse('friendrequest-list') + url = reverse("friendrequest-list") response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), 1) - self.assertEqual(response.data[0]['sender']['username'], 'sender') + self.assertEqual(response.data[0]["sender"]["username"], "sender") def test_delete_friend_request(self): """Test deleting a friend request""" - url = reverse('friendrequest-detail', args=[self.friend_request.id]) + url = reverse("friendrequest-detail", args=[self.friend_request.id]) response = self.client.delete(url) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) # Verify the friend request was deleted - self.assertFalse(FriendRequest.objects.filter(id=self.friend_request.id).exists()) + self.assertFalse( + FriendRequest.objects.filter(id=self.friend_request.id).exists() + ) diff --git a/app/user/urls.py b/app/user/urls.py index b07711a..b5a729e 100644 --- a/app/user/urls.py +++ b/app/user/urls.py @@ -1,6 +1,6 @@ from django.urls import path -from .views import UserLoginView, RegisterView +from .views import RegisterView, UserLoginView app_name = "user" urlpatterns = [ diff --git a/app/user/views.py b/app/user/views.py index a766a91..d0615a5 100644 --- a/app/user/views.py +++ b/app/user/views.py @@ -1,20 +1,19 @@ -from django.contrib.auth.views import LoginView -from django.views.generic import CreateView -from django.contrib.auth import login -from django.urls import reverse_lazy -from django.db.models import Count, Sum, F, IntegerField -from django.db.models.functions import Coalesce +import shutil -from rest_framework.viewsets import ModelViewSet, GenericViewSet +from django.contrib.auth import login +from django.contrib.auth.views import LoginView +from django.db.models import Count, Sum +from django.db.models.functions import Coalesce +from django.urls import reverse_lazy +from django.views.generic import CreateView from rest_framework import mixins from rest_framework.decorators import action from rest_framework.response import Response -import shutil +from rest_framework.viewsets import GenericViewSet -from .models import User, FriendRequest, Invitation from .forms import RegisterForm -from .serializers import UserSerializer, FriendRequestSerializer, InvitationSerializer - +from .models import FriendRequest, Invitation, User +from .serializers import FriendRequestSerializer, UserSerializer class UserLoginView(LoginView): @@ -31,7 +30,9 @@ class RegisterView(CreateView): invitation = None def get_form(self, form_class=None): - self.invitation = Invitation.objects.get(token=self.kwargs.get("token"), user__isnull=True) + self.invitation = Invitation.objects.get( + token=self.kwargs.get("token"), user__isnull=True + ) return super().get_form(form_class) def form_valid(self, form): @@ -42,9 +43,7 @@ class RegisterView(CreateView): return r -class UserViewSet(mixins.RetrieveModelMixin, - mixins.ListModelMixin, - GenericViewSet): +class UserViewSet(mixins.RetrieveModelMixin, mixins.ListModelMixin, GenericViewSet): queryset = User.objects.all().annotate( count_torrent=Count("torrents") + Count("torrents_shares") ) @@ -72,18 +71,22 @@ class UserViewSet(mixins.RetrieveModelMixin, return Response({"success": False, "message": "Already friend"}) elif FriendRequest.objects.filter(sender=user, receiver=receiver).exists(): # déjà une demande en attente - return Response({"success": False, "message": "Friend request Already sent"}) + return Response( + {"success": False, "message": "Friend request Already sent"} + ) elif FriendRequest.objects.filter(sender=receiver, receiver=user).exists(): # friend request en cours, on accepte FriendRequest.objects.filter(sender=receiver, receiver=user).delete() user.friends.add(receiver) - return Response({"success": True, "message": f"{receiver.username} added to your friend list"}) + return Response( + { + "success": True, + "message": f"{receiver.username} added to your friend list", + } + ) else: # aucune demande en cours, on créer un friend request - FriendRequest.objects.create( - sender=user, - receiver=receiver - ) + FriendRequest.objects.create(sender=user, receiver=receiver) return Response({"success": True, "message": "Request sent"}) @action(methods=["get"], detail=True) @@ -91,8 +94,13 @@ class UserViewSet(mixins.RetrieveModelMixin, friend = User.objects.get(pk=pk) if self.request.user.friends.filter(pk=friend.pk).exists(): self.request.user.friends.remove(friend) - return Response({"success": True, "message": f"The friend {friend.username} successfully removed"}) - return Response({"success": False, "message": f"error"}) + return Response( + { + "success": True, + "message": f"The friend {friend.username} successfully removed", + } + ) + return Response({"success": False, "message": "error"}) @action(methods=["get"], detail=False) def user_stats(self, request): @@ -104,23 +112,27 @@ class UserViewSet(mixins.RetrieveModelMixin, disk_usage = shutil.disk_usage("/") - return Response({ - "torrents_size": stats["total_size"], - "torrents_len": stats["total_torrent"], - "torrent_len_shared": stats["total_shared_torrent"], - "torrents_total_len": stats["total_torrent"] + stats["total_shared_torrent"], - "user_max_size": request.user.max_size, - "user_usage_percent": (stats["total_size"] / request.user.max_size) * 100, - "disk_total": disk_usage.total, - "disk_used": disk_usage.used, - "disk_free": disk_usage.free, - "disk_usage_percent": (disk_usage.used / disk_usage.total) * 100, - }) + return Response( + { + "torrents_size": stats["total_size"], + "torrents_len": stats["total_torrent"], + "torrent_len_shared": stats["total_shared_torrent"], + "torrents_total_len": stats["total_torrent"] + + stats["total_shared_torrent"], + "user_max_size": request.user.max_size, + "user_usage_percent": (stats["total_size"] / request.user.max_size) + * 100, + "disk_total": disk_usage.total, + "disk_used": disk_usage.used, + "disk_free": disk_usage.free, + "disk_usage_percent": (disk_usage.used / disk_usage.total) * 100, + } + ) -class FriendRequestViewSet(mixins.ListModelMixin, - mixins.DestroyModelMixin, - GenericViewSet): +class FriendRequestViewSet( + mixins.ListModelMixin, mixins.DestroyModelMixin, GenericViewSet +): queryset = FriendRequest.objects.all() serializer_class = FriendRequestSerializer diff --git a/app/watch_party/admin.py b/app/watch_party/admin.py index 8c38f3f..846f6b4 100644 --- a/app/watch_party/admin.py +++ b/app/watch_party/admin.py @@ -1,3 +1 @@ -from django.contrib import admin - # Register your models here. diff --git a/app/watch_party/apps.py b/app/watch_party/apps.py index 52bea26..2302efa 100644 --- a/app/watch_party/apps.py +++ b/app/watch_party/apps.py @@ -2,5 +2,5 @@ from django.apps import AppConfig class WatchPartyConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'watch_party' + default_auto_field = "django.db.models.BigAutoField" + name = "watch_party" diff --git a/app/watch_party/models.py b/app/watch_party/models.py index 1330efa..021758f 100644 --- a/app/watch_party/models.py +++ b/app/watch_party/models.py @@ -6,6 +6,8 @@ from django.db import models class Room(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4) date_created = models.DateTimeField(auto_now_add=True) - created_by = models.ForeignKey("user.User", on_delete=models.CASCADE, related_name="rooms_created") + created_by = models.ForeignKey( + "user.User", on_delete=models.CASCADE, related_name="rooms_created" + ) users = models.ManyToManyField("user.User", related_name="rooms") all_admin = models.BooleanField(default=False) diff --git a/app/watch_party/tests.py b/app/watch_party/tests.py index 7ce503c..a39b155 100644 --- a/app/watch_party/tests.py +++ b/app/watch_party/tests.py @@ -1,3 +1 @@ -from django.test import TestCase - # Create your tests here. diff --git a/app/watch_party/views.py b/app/watch_party/views.py index 91ea44a..60f00ef 100644 --- a/app/watch_party/views.py +++ b/app/watch_party/views.py @@ -1,3 +1 @@ -from django.shortcuts import render - # Create your views here.