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