diff --git a/.coverage b/.coverage new file mode 100644 index 0000000..2e68740 Binary files /dev/null and b/.coverage differ diff --git a/.cursor/rules/AGENTS.md b/.cursor/rules/AGENTS.md new file mode 100644 index 0000000..744bec6 --- /dev/null +++ b/.cursor/rules/AGENTS.md @@ -0,0 +1,30 @@ +# ModelGrader Backend Project Rules + +## Virtual Environment +- ALWAYS activate the virtual environment before running any Python scripts +- Use `source env/bin/activate` to activate the environment +- All Python commands should be run with the activated environment + +## Python Scripts +- When running Django management commands, ensure the environment is activated first +- For testing: `source env/bin/activate && python manage.py test [test_path]` +- For running the server: `source env/bin/activate && python manage.py runserver` +- For migrations: `source env/bin/activate && python manage.py migrate` + +## Test Execution +- Always activate environment before running tests +- Use the project's test runner: `source env/bin/activate && python run_all_tests.py` +- Individual test files: `source env/bin/activate && python manage.py test [test_path]` + +## Project Structure +- This is a Django REST Framework project +- Main application is in the `api/` directory +- Services are in `api/services/` +- Tests are in `api/services/*/test_*.py` +- Configuration files: `manage.py`, `requirements.txt`, `Dockerfile` + +## Dependencies +- Django REST Framework +- PostgreSQL (production) +- SQLite (development/testing) +- Various Python packages listed in `requirements.txt` diff --git a/.cursor/rules/active-python-env.mdc b/.cursor/rules/active-python-env.mdc new file mode 100644 index 0000000..fd8f4c7 --- /dev/null +++ b/.cursor/rules/active-python-env.mdc @@ -0,0 +1,30 @@ +--- +description: Active Python Environment for ModelGrader Backend +globs: +alwaysApply: true +--- + +# Virtual Environment Activation Rules + +## Always Activate Virtual Environment +- ALWAYS activate the virtual environment before running any Python scripts +- Use `source env/bin/activate` to activate the environment +- All Python commands should be run with the activated environment + +## Django Management Commands +- When running Django management commands, ensure the environment is activated first +- For testing: `source env/bin/activate && python manage.py test [test_path]` +- For running the server: `source env/bin/activate && python manage.py runserver` +- For migrations: `source env/bin/activate && python manage.py migrate` + +## Test Execution +- Always activate environment before running tests +- Use the project's test runner: `source env/bin/activate && python run_all_tests.py` +- Individual test files: `source env/bin/activate && python manage.py test [test_path]` + +## Project Context +- This is a Django REST Framework project +- Main application is in the `api/` directory +- Services are in `api/services/` +- Tests are in `api/services/*/test_*.py` +- Configuration files: `manage.py`, `requirements.txt`, `Dockerfile` diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 0000000..f3ec398 --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,78 @@ +name: Run Tests on PR + +on: + pull_request: + branches: [ "*" ] + push: + branches: [ main, dev ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Cache pip dependencies + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Set up environment variables + run: | + echo "PORT=8000" >> $GITHUB_ENV + echo "FRONTEND_URL=http://localhost:3000" >> $GITHUB_ENV + echo "TOKEN_LIFETIME_SECOND=3600" >> $GITHUB_ENV + echo "DATABASE_NAME=test_db.sqlite3" >> $GITHUB_ENV + echo "DATABASE_USER=test_user" >> $GITHUB_ENV + echo "DATABASE_PASSWORD=test_password" >> $GITHUB_ENV + echo "DATABASE_HOST=localhost" >> $GITHUB_ENV + echo "DATABASE_PORT=5432" >> $GITHUB_ENV + echo "SECRET_KEY=test-secret-key-for-github-actions" >> $GITHUB_ENV + echo "DEBUG=True" >> $GITHUB_ENV + + - name: Run all tests + run: | + python run_all_tests.py --verbose + + - name: Run tests with coverage + run: | + pip install coverage + python run_all_tests.py --coverage --verbose + + - name: Test individual services + run: | + echo "Testing Account Service..." + python run_all_tests.py --services account --verbose + + echo "Testing Problem Service..." + python run_all_tests.py --services problem --verbose + + echo "Testing Auth Service..." + python run_all_tests.py --services auth --verbose + + echo "Testing Collection Service..." + python run_all_tests.py --services collection --verbose + + echo "Testing Group Service..." + python run_all_tests.py --services group --verbose + + echo "Testing Submission Service..." + python run_all_tests.py --services submission --verbose + + echo "Testing Topic Service..." + python run_all_tests.py --services topic --verbose diff --git a/.github/workflows/simple-test.yml b/.github/workflows/simple-test.yml new file mode 100644 index 0000000..68f76e5 --- /dev/null +++ b/.github/workflows/simple-test.yml @@ -0,0 +1,42 @@ +name: Simple Test Runner + +on: + pull_request: + branches: [ "*" ] + push: + branches: [ main, dev ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Set up environment variables + run: | + echo "PORT=8000" >> $GITHUB_ENV + echo "FRONTEND_URL=http://localhost:3000" >> $GITHUB_ENV + echo "TOKEN_LIFETIME_SECOND=3600" >> $GITHUB_ENV + echo "DATABASE_NAME=test_db.sqlite3" >> $GITHUB_ENV + echo "DATABASE_USER=test_user" >> $GITHUB_ENV + echo "DATABASE_PASSWORD=test_password" >> $GITHUB_ENV + echo "DATABASE_HOST=localhost" >> $GITHUB_ENV + echo "DATABASE_PORT=5432" >> $GITHUB_ENV + echo "SECRET_KEY=test-secret-key-for-github-actions" >> $GITHUB_ENV + echo "DEBUG=True" >> $GITHUB_ENV + + - name: Run tests + run: | + python run_all_tests.py --verbose \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..21c342b --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,66 @@ +name: Run Tests + +on: + pull_request: + branches: [ "*" ] + push: + branches: [ main, dev ] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: [3.11] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Cache pip dependencies + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Set up environment variables + run: | + echo "PORT=8000" >> $GITHUB_ENV + echo "FRONTEND_URL=http://localhost:3000" >> $GITHUB_ENV + echo "TOKEN_LIFETIME_SECOND=3600" >> $GITHUB_ENV + echo "DATABASE_NAME=test_db.sqlite3" >> $GITHUB_ENV + echo "DATABASE_USER=test_user" >> $GITHUB_ENV + echo "DATABASE_PASSWORD=test_password" >> $GITHUB_ENV + echo "DATABASE_HOST=localhost" >> $GITHUB_ENV + echo "DATABASE_PORT=5432" >> $GITHUB_ENV + echo "SECRET_KEY=test-secret-key-for-github-actions" >> $GITHUB_ENV + echo "DEBUG=True" >> $GITHUB_ENV + + - name: Run tests + run: | + python run_all_tests.py --verbose + + - name: Run tests with coverage + run: | + pip install coverage + python run_all_tests.py --coverage --verbose + + - name: Upload coverage reports + uses: codecov/codecov-action@v3 + if: matrix.python-version == '3.11' + with: + file: ./coverage.xml + fail_ci_if_error: false diff --git a/Backend/settings.py b/Backend/settings.py index 22bdda6..b19e2f3 100644 --- a/Backend/settings.py +++ b/Backend/settings.py @@ -102,24 +102,24 @@ # Database # https://docs.djangoproject.com/en/4.1/ref/settings/#databases -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': config('DATABASE_NAME'), - 'USER': config('DATABASE_USER'), - 'PASSWORD': config('DATABASE_PASSWORD'), - 'HOST': config('DATABASE_HOST'), - 'PORT': config('DATABASE_PORT'), - } -} - # DATABASES = { # 'default': { -# 'ENGINE': 'django.db.backends.sqlite3', -# 'NAME': BASE_DIR / 'ModelGrader.sqlite3', +# 'ENGINE': 'django.db.backends.mysql', +# 'NAME': config('DATABASE_NAME'), +# 'USER': config('DATABASE_USER'), +# 'PASSWORD': config('DATABASE_PASSWORD'), +# 'HOST': config('DATABASE_HOST'), +# 'PORT': config('DATABASE_PORT'), # } # } +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'ModelGrader.sqlite3', + } +} + # Password validation # https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators diff --git a/TEST_COMMANDS.md b/TEST_COMMANDS.md new file mode 100644 index 0000000..04884e5 --- /dev/null +++ b/TEST_COMMANDS.md @@ -0,0 +1,168 @@ +# Test Commands Reference + +This document provides a quick reference for running tests in the ModelGrader-Backend project. + +## ๐Ÿš€ Quick Start + +1. **Navigate to your project directory:** + ```bash + cd /Users/kanon.che/Documents/ModelGrader-Backend + ``` + +2. **Run all service tests:** + ```bash + # AccountService tests + python manage.py test api.services.account.test_account_service + + # ProblemService tests + python manage.py test api.services.problem.test_problem_service + + # AuthService tests + python manage.py test api.services.auth.test_auth_service + ``` + +## Quick Commands for All Services + +### ๐Ÿš€ **Most Common Commands** + +```bash +# AccountService tests +python manage.py test api.services.account.test_account_service +python manage.py test api.services.account.test_account_service --verbosity=2 +python manage.py test api.services.account.test_account_service.TestAccountService + +# ProblemService tests +python manage.py test api.services.problem.test_problem_service +python manage.py test api.services.problem.test_problem_service --verbosity=2 +python manage.py test api.services.problem.test_problem_service.TestProblemService + +# AuthService tests +python manage.py test api.services.auth.test_auth_service +python manage.py test api.services.auth.test_auth_service --verbosity=2 +python manage.py test api.services.auth.test_auth_service.TestAuthService + +# Run all service tests at once +python manage.py test api.services.account.test_account_service api.services.problem.test_problem_service api.services.auth.test_auth_service +``` + +### ๐Ÿ“Š **Test Coverage Commands** + +```bash +# Run tests with coverage for all services +coverage run --source='.' manage.py test api.services.account.test_account_service api.services.problem.test_problem_service api.services.auth.test_auth_service + +# Run coverage for individual services +coverage run --source='.' manage.py test api.services.account.test_account_service +coverage run --source='.' manage.py test api.services.problem.test_problem_service +coverage run --source='.' manage.py test api.services.auth.test_auth_service + +# View coverage report +coverage report + +# Generate HTML coverage report +coverage html +``` + +### ๐Ÿ”ง **Alternative Test Runners** + +```bash +# Using unittest directly +python -m unittest api.services.account.test_account_service + +# Using pytest (if installed) +pytest api/services/account/test_account_service.py -v + +# Run all tests in the account service directory +python manage.py test api.services.account +``` + +### ๐ŸŽฏ **Specific Test Scenarios** + +```bash +# Test only create_account functionality +python manage.py test api.services.account.test_account_service.TestAccountService.test_create_account_success + +# Test only get_account functionality +python manage.py test api.services.account.test_account_service.TestAccountService.test_get_account_success + +# Test only get_all_accounts functionality +python manage.py test api.services.account.test_account_service.TestAccountService.test_get_all_accounts_without_search + +# Test error handling +python manage.py test api.services.account.test_account_service.TestAccountService.test_get_account_not_found +``` + +### ๐Ÿ“ **Test Output Options** + +```bash +# Quiet output (minimal) +python manage.py test api.services.account.test_account_service --verbosity=0 + +# Normal output (default) +python manage.py test api.services.account.test_account_service --verbosity=1 + +# Verbose output (detailed) +python manage.py test api.services.account.test_account_service --verbosity=2 + +# Very verbose output (most detailed) +python manage.py test api.services.account.test_account_service --verbosity=3 +``` + +### ๐Ÿ› **Debugging Tests** + +```bash +# Run tests with debug output +python manage.py test api.services.account.test_account_service --debug-mode + +# Run tests and keep test database +python manage.py test api.services.account.test_account_service --keepdb + +# Run tests in parallel (if supported) +python manage.py test api.services.account.test_account_service --parallel +``` + +### ๐Ÿ“ **Directory Structure** + +``` +ModelGrader-Backend/ +โ”œโ”€โ”€ api/ +โ”‚ โ””โ”€โ”€ services/ +โ”‚ โ””โ”€โ”€ account/ +โ”‚ โ”œโ”€โ”€ account_service.py +โ”‚ โ”œโ”€โ”€ test_account_service.py +โ”‚ โ”œโ”€โ”€ README_TESTS.md +โ”‚ โ””โ”€โ”€ example_test_run.py +โ”œโ”€โ”€ manage.py +โ””โ”€โ”€ TEST_COMMANDS.md +``` + +### โšก **Quick Start** + +1. **Navigate to project directory:** + ```bash + cd /Users/kanon.che/Documents/ModelGrader-Backend + ``` + +2. **Run all tests:** + ```bash + python manage.py test api.services.account.test_account_service --verbosity=2 + ``` + +3. **Check results:** + - โœ… All tests should pass + - ๐Ÿ“Š Coverage report available + - ๐Ÿ› Any failures will be clearly shown + +### ๐Ÿ” **Troubleshooting** + +**If tests fail:** +- Check Django settings are correct +- Ensure all dependencies are installed +- Verify database is accessible +- Check for import errors + +**If coverage is low:** +- Add more test cases +- Test edge cases +- Test error scenarios +- Test integration points diff --git a/api/config.py b/api/config.py new file mode 100644 index 0000000..16de3d2 --- /dev/null +++ b/api/config.py @@ -0,0 +1,5 @@ +from decouple import AutoConfig + +class Configuration: + def __init__(self, config: AutoConfig): + self.token_lifetime = int(config('TOKEN_LIFETIME_SECOND')) \ No newline at end of file diff --git a/api/controllers/account/create_account.py b/api/controllers/account/create_account.py deleted file mode 100644 index c35521c..0000000 --- a/api/controllers/account/create_account.py +++ /dev/null @@ -1,18 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def create_account(request): - request.data['password'] = passwordEncryption(request.data['password']) - try: - account = Account.objects.create(**request.data) - except Exception as e: - return Response({'message':str(e)},status=status.HTTP_400_BAD_REQUEST) - serialize = AccountSerializer(account) - return Response(serialize.data,status=status.HTTP_201_CREATED) diff --git a/api/controllers/account/get_account.py b/api/controllers/account/get_account.py deleted file mode 100644 index 275d3f9..0000000 --- a/api/controllers/account/get_account.py +++ /dev/null @@ -1,17 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def get_account(account_id:str): - try: - account = Account.objects.get(account_id=account_id) - serialize = AccountSerializer(account) - return Response(serialize.data,status=status.HTTP_200_OK) - except: - return Response({'message':'Account not found!'},status=status.HTTP_404_NOT_FOUND) diff --git a/api/controllers/account/get_all_accounts.py b/api/controllers/account/get_all_accounts.py deleted file mode 100644 index 91f7a3a..0000000 --- a/api/controllers/account/get_all_accounts.py +++ /dev/null @@ -1,28 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * -from django.db.models import Q - - -def get_all_accounts(request): - - # get search query - search = request.GET.get('search','') - - accounts = Account.objects.all() - - if search != '': - accounts = accounts.filter( - Q(username__icontains=search) | Q(account_id__icontains=search) | Q(email__icontains=search) - ).distinct() - - serialize = AccountSecureSerializer(accounts,many=True) - return Response({ - "accounts": serialize.data - },status=status.HTTP_200_OK) \ No newline at end of file diff --git a/api/controllers/account_controller.py b/api/controllers/account_controller.py new file mode 100644 index 0000000..6651fb1 --- /dev/null +++ b/api/controllers/account_controller.py @@ -0,0 +1,74 @@ +from rest_framework.response import Response +from rest_framework.decorators import api_view +from api.wrappers.auth_wrapper import authentication_required +from ..constant import GET,POST,PUT,DELETE +from ..serializers import * +from api.errors.common import InternalServerError +from api.errors.core.grader_exception import GraderException +from abc import ABC, abstractmethod +from api.services.account.account_service import AccountService +from api.setup import account_service + +# class AccountController(ABC): +# @abstractmethod +# def all_accounts(self, request): +# pass + +# @abstractmethod +# def one_creator(self, request, account_id): +# pass + +# @abstractmethod +# def change_password(self, request, account_id): +# pass + +# class AccountControllerImpl(AccountController): +# def __init__(self, account_svc: AccountService): +# account_svc = account_svc + +@api_view([GET,POST]) +def all_accounts(request): + try: + if request.method == GET: + result = account_service.get_all_accounts(request) + elif request.method == POST: + result = account_service.create_account(request) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() + +@api_view([GET]) +@authentication_required +def one_creator(request,account_id): + try: + result = account_service.get_account(account_id) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() + +@api_view([PUT]) +@authentication_required +def change_password(request,account_id): + try: + result = account_service.update_password(account_id, request.data['password']) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() + +# @api_view([GET]) +# @authentication_required +# def get_daily_submission(request,account_id:str): +# try: +# result = account_service.get_daily_submission(account_id) +# return Response(result, status=200) +# except GraderException as ge: +# return ge.django_response() +# except Exception as e: +# return InternalServerError(e).django_response() + diff --git a/api/controllers/auth/authorization.py b/api/controllers/auth/authorization.py deleted file mode 100644 index 0434dae..0000000 --- a/api/controllers/auth/authorization.py +++ /dev/null @@ -1,21 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * -from time import time - -def authorization(request): - try: - account = Account.objects.get(account_id=request.data['account_id']) - account_dict = model_to_dict(account) - if account_dict['token_expire'] >= time() and account_dict['token'] == request.data['token']: - return Response({'result':True},status=status.HTTP_200_OK) - return Response({'result':False},status=status.HTTP_200_OK) - except Account.DoesNotExist: - return Response({'result':False},status=status.HTTP_200_OK) - # return Response({'message':"User doesn't exists!"},status=status.HTTP_404_NOT_FOUND) diff --git a/api/controllers/auth/login.py b/api/controllers/auth/login.py deleted file mode 100644 index c89ee66..0000000 --- a/api/controllers/auth/login.py +++ /dev/null @@ -1,28 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * -from decouple import config -from uuid import uuid4 -from time import time - -TOKEN_LIFETIME = int(config('TOKEN_LIFETIME_SECOND')) # (Second) - -def login(request): - try: - account = Account.objects.get(username=request.data['username']) - account_dict = model_to_dict(account) - if passwordEncryption(request.data['password']) == account_dict['password']: - account.token = uuid4().hex - account.token_expire = int(time() + TOKEN_LIFETIME) - account.save() - return Response(model_to_dict(account),status=status.HTTP_202_ACCEPTED) - else: - return Response({'message':"Incorrect password!"},status=status.HTTP_406_NOT_ACCEPTABLE) - except Account.DoesNotExist: - return Response({'message':"User doesn't exists!"},status=status.HTTP_404_NOT_FOUND) diff --git a/api/controllers/auth/logout.py b/api/controllers/auth/logout.py deleted file mode 100644 index 9bbd0fa..0000000 --- a/api/controllers/auth/logout.py +++ /dev/null @@ -1,24 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * -from decouple import config -from uuid import uuid4 -from time import time - -def logout(request): - try: - account = Account.objects.get(account_id=request.data['account_id']) - if account.token == request.data['token']: - account.token = None - account.save() - return Response(model_to_dict(account),status=status.HTTP_200_OK) - else: - return Response({'message':"Invalid token!"},status=status.HTTP_200_OK) - except Account.DoesNotExist: - return Response({'message':"User doesn't exists!"},status=status.HTTP_404_NOT_FOUND) diff --git a/api/controllers/auth_controller.py b/api/controllers/auth_controller.py new file mode 100644 index 0000000..4f517a8 --- /dev/null +++ b/api/controllers/auth_controller.py @@ -0,0 +1,36 @@ +from rest_framework.decorators import api_view +from rest_framework.response import Response +from api.errors.common import InternalServerError +from api.errors.core.grader_exception import GraderException +from ..constant import POST,PUT +from api.setup import auth_service + +@api_view([POST]) +def login(request): + try: + result = auth_service.login(request) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() + +@api_view([POST]) +def logout(request): + try: + result = auth_service.logout(request) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() + +@api_view([PUT]) +def authorization(request): + try: + result = auth_service.authorization(request) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() \ No newline at end of file diff --git a/api/controllers/collection/add_problems_to_collection.py b/api/controllers/collection/add_problems_to_collection.py deleted file mode 100644 index 6ac7f98..0000000 --- a/api/controllers/collection/add_problems_to_collection.py +++ /dev/null @@ -1,38 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def add_problems_to_collection(collection:Collection,request): - populated_problems = [] - - index = 0 - for problem_id in request.data['problem_ids']: - - problem = Problem.objects.get(problem_id=problem_id) - - alreadyExist = CollectionProblem.objects.filter(problem=problem,collection=collection) - if alreadyExist: - alreadyExist.delete() - - collection_problem = CollectionProblem( - problem=problem, - collection=collection, - order=index - ) - collection_problem.save() - index += 1 - populated_problems.append(collection_problem) - collection.updated_date = timezone.now() - problem_serialize = CollectionProblemPopulateProblemSecureSerializer(populated_problems,many=True) - collection_serialize = CollectionSerializer(collection) - - return Response({ - **collection_serialize.data, - 'problems': problem_serialize.data - },status=status.HTTP_201_CREATED) \ No newline at end of file diff --git a/api/controllers/collection/create_collection.py b/api/controllers/collection/create_collection.py deleted file mode 100644 index f5541bc..0000000 --- a/api/controllers/collection/create_collection.py +++ /dev/null @@ -1,20 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def create_collection(account_id:str,request): - - request.data['creator'] = account_id - serialize = CollectionSerializer(data=request.data) - - if serialize.is_valid(): - serialize.save() - return Response(serialize.data,status=status.HTTP_201_CREATED) - else: - return Response(serialize.errors,status=status.HTTP_400_BAD_REQUEST) diff --git a/api/controllers/collection/delete_collection.py b/api/controllers/collection/delete_collection.py deleted file mode 100644 index 67cba72..0000000 --- a/api/controllers/collection/delete_collection.py +++ /dev/null @@ -1,13 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def delete_collection(collection:Collection): - collection.delete() - return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/api/controllers/collection/get_all_collections.py b/api/controllers/collection/get_all_collections.py deleted file mode 100644 index ff1a53f..0000000 --- a/api/controllers/collection/get_all_collections.py +++ /dev/null @@ -1,37 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def get_all_collections(request): - collections = Collection.objects.all() - - account_id = request.query_params.get('account_id',0) - - if account_id: - collections = collections.filter(creator_id=account_id) - - populated_collections = [] - for collection in collections: - con_probs = CollectionProblem.objects.filter(collection=collection) - - populated_cp = [] - for cp in con_probs: - prob_serialize = ProblemSerializer(cp.problem) - cp_serialize = CollectionProblemSerializer(cp) - populated_cp.append({**cp_serialize.data,**prob_serialize.data}) - - serialize = CollectionSerializer(collection) - collection_data = serialize.data - collection_data['problems'] = populated_cp - - populated_collections.append(collection_data) - - return Response({ - 'collections': populated_collections - },status=status.HTTP_200_OK) \ No newline at end of file diff --git a/api/controllers/collection/get_all_collections_by_account.py b/api/controllers/collection/get_all_collections_by_account.py deleted file mode 100644 index 7cd16fb..0000000 --- a/api/controllers/collection/get_all_collections_by_account.py +++ /dev/null @@ -1,34 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def populated_problems(collections: Collection): - problemCollections = CollectionProblem.objects.filter(collection__in=collections) - - populated_collections = [] - for collection in collections: - collection.problems = problemCollections.filter(collection=collection) - populated_collections.append(collection) - - return populated_collections - -def get_all_collections_by_account(account:Account): - - collections = Collection.objects.filter(creator=account).order_by('-updated_date') - collections = populated_problems(collections) - serialize = CollectionPopulateCollectionProblemsPopulateProblemSerializer(collections,many=True) - - manageableCollections = Collection.objects.filter(collectiongrouppermission__permission_manage_collections=True,collectiongrouppermission__group__in=GroupMember.objects.filter(account=account).values_list("group",flat=True)).order_by('-updated_date') - manageableCollections = populated_problems(manageableCollections) - manageableSerialize = CollectionPopulateCollectionProblemsPopulateProblemSerializer(manageableCollections,many=True) - - return Response({ - 'collections': serialize.data, - 'manageable_collections': manageableSerialize.data - },status=status.HTTP_200_OK) \ No newline at end of file diff --git a/api/controllers/collection/get_collection.py b/api/controllers/collection/get_collection.py deleted file mode 100644 index 8e87013..0000000 --- a/api/controllers/collection/get_collection.py +++ /dev/null @@ -1,23 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def get_collection(collection:Collection): - - collection.problems = CollectionProblem.objects.filter(collection=collection).order_by('order') - collection.group_permissions = CollectionGroupPermission.objects.filter(collection=collection) - - for cp in collection.problems: - cp.problem.testcases = Testcase.objects.filter(problem=cp.problem,deprecated=False) - cp.problem.group_permissions = ProblemGroupPermission.objects.filter(problem=cp.problem) - - serializer = CollectionPopulateCollectionProblemsPopulateProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupAndCollectionGroupPermissionsPopulateGroupSerializer(collection) - - - return Response(serializer.data ,status=status.HTTP_200_OK) \ No newline at end of file diff --git a/api/controllers/collection/remove_problems_from_collection.py b/api/controllers/collection/remove_problems_from_collection.py deleted file mode 100644 index 1f0a6d6..0000000 --- a/api/controllers/collection/remove_problems_from_collection.py +++ /dev/null @@ -1,14 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def remove_problems_from_collection(collection:Collection,request): - CollectionProblem.objects.filter(collection=collection,problem_id__in=request.data['problem_ids']).delete() - collection.updated_date = timezone.now() - return Response(status=status.HTTP_204_NO_CONTENT) \ No newline at end of file diff --git a/api/controllers/collection/update_collection.py b/api/controllers/collection/update_collection.py deleted file mode 100644 index ae33e25..0000000 --- a/api/controllers/collection/update_collection.py +++ /dev/null @@ -1,22 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def update_collection(collection:Collection,request): - - collection.name = request.data.get('name',collection.name) - collection.description = request.data.get('description',collection.description) - collection.is_private = request.data.get('is_private',collection.is_private) - collection.is_active = request.data.get('is_active',collection.is_active) - collection.updated_date = timezone.now() - - collection.save() - collection_ser = CollectionSerializer(collection) - - return Response(collection_ser.data,status=status.HTTP_200_OK) diff --git a/api/controllers/collection/update_group_permissions_collection.py b/api/controllers/collection/update_group_permissions_collection.py deleted file mode 100644 index 58577ab..0000000 --- a/api/controllers/collection/update_group_permissions_collection.py +++ /dev/null @@ -1,32 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def update_group_permissions_collection(collection:Collection,request): - - CollectionGroupPermission.objects.filter(collection=collection).delete() - - print(request.data['groups']) - - collection_group_permissions = [] - for collection_request in request.data['groups']: - group = Group.objects.get(group_id=collection_request['group_id']) - collection_group_permissions.append( - CollectionGroupPermission( - collection=collection, - group=group, - **collection_request - )) - - CollectionGroupPermission.objects.bulk_create(collection_group_permissions) - - collection.group_permissions = collection_group_permissions - serialize = CollectionPopulateCollectionGroupPermissionsPopulateGroupSerializer(collection) - - return Response(serialize.data,status=status.HTTP_202_ACCEPTED) \ No newline at end of file diff --git a/api/controllers/collection/update_problems_to_collection.py b/api/controllers/collection/update_problems_to_collection.py deleted file mode 100644 index 12fee32..0000000 --- a/api/controllers/collection/update_problems_to_collection.py +++ /dev/null @@ -1,35 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def update_problems_to_collection(collection:Collection,request): - CollectionProblem.objects.filter(collection=collection).delete() - - collection_problems = [] - order = 0 - for problem_id in request.data['problem_ids']: - problem = Problem.objects.get(problem_id=problem_id) - collection_problem = CollectionProblem( - problem=problem, - collection=collection, - order=order - ) - collection_problems.append(collection_problem) - order += 1 - - CollectionProblem.objects.bulk_create(collection_problems) - collection.updated_date = timezone.now() - collection.save() - problem_serialize = CollectionProblemPopulateProblemSecureSerializer(collection_problems,many=True) - collection_serialize = CollectionSerializer(collection) - - return Response({ - **collection_serialize.data, - 'problems': problem_serialize.data - },status=status.HTTP_201_CREATED) \ No newline at end of file diff --git a/api/controllers/collection_controller.py b/api/controllers/collection_controller.py new file mode 100644 index 0000000..f1d7b61 --- /dev/null +++ b/api/controllers/collection_controller.py @@ -0,0 +1,93 @@ +from rest_framework.response import Response +from rest_framework.decorators import api_view +from api.wrappers.auth_wrapper import authentication_required +from ..constant import GET, POST, PUT, DELETE +from api.errors.common import InternalServerError, BadRequestError +from api.errors.core.grader_exception import GraderException +from api.setup import collection_service + +@api_view([POST, GET]) +@authentication_required +def all_collections_creator_view(request, account_id): + try: + if request.method == POST: + result = collection_service.create_collection(account_id, request) + elif request.method == GET: + result = collection_service.get_all_collections_by_account(account_id) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() + +@api_view([GET, PUT, DELETE]) +@authentication_required +def one_collection_creator_view(request, collection_id: str, account_id: str): + try: + if request.method == GET: + result = collection_service.get_collection(collection_id) + elif request.method == PUT: + result = collection_service.update_collection(collection_id, request) + elif request.method == DELETE: + collection_service.delete_collection(collection_id) + return Response(status=204) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() + +@api_view([GET]) +def all_collections_view(request): + try: + if request.method == GET: + result = collection_service.get_all_collections(request) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() + +@api_view([GET]) +def one_collection_view(request, collection_id: str): + try: + if request.method == GET: + result = collection_service.get_collection(collection_id) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() + +@api_view([PUT]) +@authentication_required +def collection_groups_view(request, account_id: str, collection_id: str): + try: + if request.method == PUT: + result = collection_service.update_group_permissions_collection(collection_id, request) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() + +@api_view([PUT, POST]) +@authentication_required +def collection_problems_view(request, collection_id: str, method: str): + try: + + if method == "add": + result = collection_service.add_problems_to_collection(collection_id, request) + elif method == "update": + result = collection_service.update_problems_to_collection(collection_id, request) + elif method == "remove": + collection_service.remove_problems_from_collection(collection_id, request) + return Response(status=204) + else: + raise BadRequestError(f"Invalid method: {method}") + + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() diff --git a/api/controllers/group/add_members_to_group.py b/api/controllers/group/add_members_to_group.py deleted file mode 100644 index 2cc6446..0000000 --- a/api/controllers/group/add_members_to_group.py +++ /dev/null @@ -1,25 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def add_members_to_group(group:Group,request): - - group_members = [] - for accountId in request.data['account_ids']: - account = Account.objects.get(account_id=accountId) - group_members.append(GroupMember( - group=group, - account=account - )) - - GroupMember.objects.bulk_create(group_members) - group.members = group_members - - serialize = GroupPopulateGroupMemberPopulateAccountSecureSerializer(group) - return Response(serialize.data,status=status.HTTP_200_OK) \ No newline at end of file diff --git a/api/controllers/group/create_group.py b/api/controllers/group/create_group.py deleted file mode 100644 index d4e734d..0000000 --- a/api/controllers/group/create_group.py +++ /dev/null @@ -1,24 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def create_group(account:Account,request): - - # print(request.headers["Authorization"]) - - serialize = GroupSerializer(data={ - 'creator':account.account_id, - **request.data - }) - - if serialize.is_valid(): - serialize.save() - return Response(serialize.data,status=status.HTTP_201_CREATED) - else: - return Response(serialize.errors,status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file diff --git a/api/controllers/group/delete_group.py b/api/controllers/group/delete_group.py deleted file mode 100644 index 145b084..0000000 --- a/api/controllers/group/delete_group.py +++ /dev/null @@ -1,13 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def delete_group(group:Group,request): - group.delete() - return Response(status=status.HTTP_204_NO_CONTENT) \ No newline at end of file diff --git a/api/controllers/group/get_all_groups_by_account.py b/api/controllers/group/get_all_groups_by_account.py deleted file mode 100644 index b9b1f05..0000000 --- a/api/controllers/group/get_all_groups_by_account.py +++ /dev/null @@ -1,28 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def get_all_groups_by_account(account:Account,request): - - print("GET ALL") - - # Get request headers - headers = request.headers - - groups = Group.objects.filter(creator=account).order_by('-updated_date') - - populate_members = request.GET.get('populate_members',False) - - if populate_members: - for group in groups: - group.members = GroupMember.objects.filter(group=group) - serialize = GroupPopulateGroupMemberPopulateAccountSecureSerializer(groups,many=True) - else: - serialize = GroupSerializer(groups,many=True) - return Response({"groups":serialize.data},status=status.HTTP_200_OK) diff --git a/api/controllers/group/get_group.py b/api/controllers/group/get_group.py deleted file mode 100644 index c506ece..0000000 --- a/api/controllers/group/get_group.py +++ /dev/null @@ -1,20 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def get_group(group:Group,request): - - populate_members = request.GET.get('populate_members',False) - - if populate_members: - group.members = GroupMember.objects.filter(group=group) - serialize = GroupPopulateGroupMemberPopulateAccountSecureSerializer(group) - else: - serialize = GroupSerializer(group) - return Response(serialize.data,status=status.HTTP_200_OK) diff --git a/api/controllers/group/update_group.py b/api/controllers/group/update_group.py deleted file mode 100644 index 80bc9f7..0000000 --- a/api/controllers/group/update_group.py +++ /dev/null @@ -1,23 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * -from datetime import datetime - -def update_group(group:Group,request): - - serializer = GroupSerializer(group,data={ - **request.data, - 'updated_date': timezone.now() - },partial=True) - - if serializer.is_valid(): - serializer.save() - return Response(serializer.data,status=status.HTTP_200_OK) - else: - return Response(serializer.errors,status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file diff --git a/api/controllers/group/update_members_to_group.py b/api/controllers/group/update_members_to_group.py deleted file mode 100644 index 99a05cd..0000000 --- a/api/controllers/group/update_members_to_group.py +++ /dev/null @@ -1,26 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def update_members_to_group(group:Group,request): - GroupMember.objects.filter(group=group).delete() - - group_members = [] - for accountId in request.data['account_ids']: - account = Account.objects.get(account_id=accountId) - group_members.append(GroupMember( - group=group, - account=account - )) - - GroupMember.objects.bulk_create(group_members) - group.members = group_members - - serialize = GroupPopulateGroupMemberPopulateAccountSecureSerializer(group) - return Response(serialize.data,status=status.HTTP_200_OK) \ No newline at end of file diff --git a/api/controllers/group_controller.py b/api/controllers/group_controller.py new file mode 100644 index 0000000..d2620ef --- /dev/null +++ b/api/controllers/group_controller.py @@ -0,0 +1,55 @@ +from rest_framework.response import Response +from rest_framework.decorators import api_view +from api.wrappers.auth_wrapper import authentication_required +from ..constant import GET, POST, PUT, DELETE +from api.errors.common import InternalServerError, BadRequestError +from api.errors.core.grader_exception import GraderException +from api.setup import group_service + +@api_view([POST, GET]) +@authentication_required +def all_groups_creator_view(request, account_id): + try: + if request.method == POST: + result = group_service.create_group(account_id, request) + elif request.method == GET: + result = group_service.get_all_groups_by_account(account_id, request) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() + +@api_view([GET, PUT, DELETE]) +@authentication_required +def one_group_view(request, group_id: str): + try: + if request.method == GET: + result = group_service.get_group(group_id, request) + elif request.method == PUT: + result = group_service.update_group(group_id, request) + elif request.method == DELETE: + group_service.delete_group(group_id) + return Response(status=204) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() + +@api_view([PUT, POST]) +@authentication_required +def group_members_view(request, group_id: str, method: str): + try: + if method == "add": + result = group_service.add_members_to_group(group_id, request) + elif method == "update": + result = group_service.update_members_to_group(group_id, request) + else: + raise BadRequestError(f"Invalid method: {method}") + + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() diff --git a/api/controllers/problem/create_problem.py b/api/controllers/problem/create_problem.py deleted file mode 100644 index efcf4f6..0000000 --- a/api/controllers/problem/create_problem.py +++ /dev/null @@ -1,45 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader,RuntimeResult -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def create_problem(account_id:str,request): - account = Account.objects.get(account_id=account_id) - - running_result = PythonGrader(request.data['solution'],request.data['testcases'],1,1.5).generate_output() - - # if not running_result.runnable: - # return Response({'detail': 'Error during creating. Your code may has an error/timeout!','output': running_result.getResult()},status=status.HTTP_406_NOT_ACCEPTABLE) - - problem = Problem( - language = request.data['language'], - creator = account, - title = request.data['title'], - description = request.data['description'], - solution = request.data['solution'], - time_limit = request.data['time_limit'], - allowed_languages = request.data['allowed_languages'], - ) - problem.save() - - testcases_result = [] - for unit in running_result.data: - testcases_result.append( - Testcase( - problem = problem, - input = unit.input, - output = unit.output, - runtime_status = unit.runtime_status - )) - - Testcase.objects.bulk_create(testcases_result) - - problem_serialize = ProblemSerializer(problem) - testcases_serialize = TestcaseSerializer(testcases_result,many=True) - - return Response({**problem_serialize.data,'testcases': testcases_serialize.data},status=status.HTTP_201_CREATED) diff --git a/api/controllers/problem/delete_problem.py b/api/controllers/problem/delete_problem.py deleted file mode 100644 index c87e089..0000000 --- a/api/controllers/problem/delete_problem.py +++ /dev/null @@ -1,20 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def delete_problem(problem:Problem): - # try: - # problem = Problem.objects.get(problem_id=problem_id) - # except Problem.DoesNotExist: - # return Response({'detail': "Problem doesn't exist!"},status=status.HTTP_404_NOT_FOUND) - testcases = Testcase.objects.filter(problem=problem) - - problem.delete() - testcases.delete() - return Response(status=status.HTTP_204_NO_CONTENT) \ No newline at end of file diff --git a/api/controllers/problem/get_all_problem_with_best_submission.py b/api/controllers/problem/get_all_problem_with_best_submission.py deleted file mode 100644 index a39f750..0000000 --- a/api/controllers/problem/get_all_problem_with_best_submission.py +++ /dev/null @@ -1,28 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def get_all_problem_with_best_submission(account:Account): - - problems = Problem.objects.all().order_by('-updated_date') - - for problem in problems: - best_submission = Submission.objects.filter(problem=problem,account=account).order_by('-passed_ratio','-submission_id').first() - if not (best_submission is None): - testcases = SubmissionTestcase.objects.filter(submission=best_submission) - best_submission.runtime_output = testcases - problem.best_submission = best_submission - else: - problem.best_submission = None - - problem_ser = ProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureSerializer(problems,many=True) - return Response({"problems":problem_ser.data},status=status.HTTP_200_OK) - - - # return Response({"problems":problem_ser.data},status=status.HTTP_200_OK) \ No newline at end of file diff --git a/api/controllers/problem/get_all_problems.py b/api/controllers/problem/get_all_problems.py deleted file mode 100644 index 2737a20..0000000 --- a/api/controllers/problem/get_all_problems.py +++ /dev/null @@ -1,30 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def get_all_problems(request): - problem = Problem.objects.all() - - get_private = int(request.query_params.get("private",0)) - get_deactive = int(request.query_params.get("deactive",0)) - account_id = str(request.query_params.get("account_id","")) - - if not get_private: - problem = problem.filter(is_private=False) - if not get_deactive: - problem = problem.filter(is_active=True) - if account_id != "": - problem = problem.filter(creator_id=account_id) - - problem = problem.order_by('-problem_id') - - serialize = ProblemPopulateAccountSerializer(problem,many=True) - - return Response({'problems':serialize.data},status=status.HTTP_200_OK) - \ No newline at end of file diff --git a/api/controllers/problem/get_all_problems_by_account.py b/api/controllers/problem/get_all_problems_by_account.py deleted file mode 100644 index 06ecce6..0000000 --- a/api/controllers/problem/get_all_problems_by_account.py +++ /dev/null @@ -1,47 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def get_all_problems_by_account(account:Account,request): - - start = int(request.query_params.get("start",0)) - end = int(request.query_params.get("end",-1)) - query = request.query_params.get("query","") - if end == -1: end = None - - personalProblems = Problem.objects.filter(creator=account, title__icontains=query).order_by('-updated_date') - maxPersonal = len(personalProblems) - if start < maxPersonal and start < maxPersonal: - personalProblems = personalProblems[start:end] - for problem in personalProblems: - problem.testcases = Testcase.objects.filter(problem=problem,deprecated=False) - - manageableProblems = Problem.objects.filter( - problemgrouppermission__permission_manage_problems=True, - problemgrouppermission__group__in=GroupMember.objects.filter(account=account).values_list("group",flat=True), - title__icontains=query - ).order_by('-updated_date') - maxManageable = len(manageableProblems) - if start < maxManageable and start < maxManageable: - manageableProblems = manageableProblems[start:end] - for problem in manageableProblems: - problem.testcases = Testcase.objects.filter(problem=problem,deprecated=False) - - # problem_ser = ProblemPopulateTestcaseSerializer(problems,many=True) - personalSerialize = ProblemPopulatePartialTestcaseSerializer(personalProblems,many=True) - manageableSerialize = ProblemPopulatePartialTestcaseSerializer(manageableProblems,many=True) - - return Response({ - "start":start, - "end":end, - "total_personal_problems": maxPersonal, - "total_manageable_problems": maxManageable, - "problems":personalSerialize.data, - "manageable_problems":manageableSerialize.data - },status=status.HTTP_200_OK) \ No newline at end of file diff --git a/api/controllers/problem/get_problem.py b/api/controllers/problem/get_problem.py deleted file mode 100644 index 0f30ab8..0000000 --- a/api/controllers/problem/get_problem.py +++ /dev/null @@ -1,27 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def get_problem(problem:Problem): - # try: - # problem = Problem.objects.get(problem_id=problem_id) - # except Problem.DoesNotExist: - # return Response({'detail': "Problem doesn't exist!"},status=status.HTTP_404_NOT_FOUND) - # testcases = Testcase.objects.filter(problem_id=problem_id,deprecated=False) - # problem_serialize = ProblemPopulateAccountSerializer(problem) - # testcases_serialize = TestcaseSerializer(testcases,many=True) - - problem.testcases = Testcase.objects.filter(problem=problem,deprecated=False) - problem.group_permissions = ProblemGroupPermission.objects.filter(problem=problem) - - serialize = ProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupSerializer(problem) - - - return Response(serialize.data,status=status.HTTP_200_OK) - \ No newline at end of file diff --git a/api/controllers/problem/get_problem_in_topic_with_best_submission.py b/api/controllers/problem/get_problem_in_topic_with_best_submission.py deleted file mode 100644 index d708d21..0000000 --- a/api/controllers/problem/get_problem_in_topic_with_best_submission.py +++ /dev/null @@ -1,31 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def get_problem_in_topic_with_best_submission(account_id:str,topic_id:str,problem:int): - - account = Account.objects.get(account_id=account_id) - problem = Problem.objects.get(problem_id=problem) - topic = Topic.objects.get(topic_id=topic_id) - - best_submission = BestSubmission.objects.filter(problem=problem,topic=topic,account=account).first() - # print(problem.problem_id,problem.title) - if not (best_submission is None): - testcases = SubmissionTestcase.objects.filter(submission=best_submission.submission) - print(testcases) - best_submission.runtime_output = testcases - problem.best_submission = best_submission - else: - problem.best_submission = None - - serialize = ProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureSerializer(problem) - return Response(serialize.data,status=status.HTTP_200_OK) - - - # return Response({"problems":problem_ser.data},status=status.HTTP_200_OK) \ No newline at end of file diff --git a/api/controllers/problem/get_problem_public.py b/api/controllers/problem/get_problem_public.py deleted file mode 100644 index 8dd5e6a..0000000 --- a/api/controllers/problem/get_problem_public.py +++ /dev/null @@ -1,14 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def get_problem_public(problem:Problem): - serialize = ProblemPopulateAccountSecureSerializer(problem) - return Response(serialize.data,status=status.HTTP_200_OK) - \ No newline at end of file diff --git a/api/controllers/problem/import_elabsheet_problem.py b/api/controllers/problem/import_elabsheet_problem.py deleted file mode 100644 index 023f16a..0000000 --- a/api/controllers/problem/import_elabsheet_problem.py +++ /dev/null @@ -1,19 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def import_elabsheet_problem(request, problem: Problem): - print("importing elabsheet problem") - print(request.data) - # Get file - file = request.data.get('file') - problem.pdf_url = file - print(file) - print(problem.pdf_url) - return Response(status=status.HTTP_204_NO_CONTENT) \ No newline at end of file diff --git a/api/controllers/problem/remove_bulk_problems.py b/api/controllers/problem/remove_bulk_problems.py deleted file mode 100644 index a116836..0000000 --- a/api/controllers/problem/remove_bulk_problems.py +++ /dev/null @@ -1,16 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def remove_bulk_problems(request): - target = request.data.get("problem",[]) - problems = Problem.objects.filter(problem_id__in=target) - problems.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - \ No newline at end of file diff --git a/api/controllers/problem/update_group_permission_to_problem.py b/api/controllers/problem/update_group_permission_to_problem.py deleted file mode 100644 index 7002fda..0000000 --- a/api/controllers/problem/update_group_permission_to_problem.py +++ /dev/null @@ -1,30 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def update_group_permission_to_problem(problem:Problem,request): - ProblemGroupPermission.objects.filter(problem=problem).delete() - - problem_group_permissions = [] - for group_request in request.data['groups']: - group = Group.objects.get(group_id=group_request['group_id']) - problem_group_permissions.append( - ProblemGroupPermission( - problem=problem, - group=group, - **group_request - )) - - ProblemGroupPermission.objects.bulk_create(problem_group_permissions) - - problem.group_permissions = problem_group_permissions - problem.testcases = Testcase.objects.filter(problem=problem) - - serialize = ProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupSerializer(problem) - return Response(serialize.data,status=status.HTTP_202_ACCEPTED) \ No newline at end of file diff --git a/api/controllers/problem/update_problem.py b/api/controllers/problem/update_problem.py deleted file mode 100644 index ea8468b..0000000 --- a/api/controllers/problem/update_problem.py +++ /dev/null @@ -1,63 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import Grader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * -from django.utils import timezone - -def update_problem(problem:Problem,request): - # try: - # problem = Problem.objects.get(problem_id=problem_id) - # except Problem.DoesNotExist: - # return Response({'detail': "Problem doesn't exist!"},status=status.HTTP_404_NOT_FOUND) - testcases = Testcase.objects.filter(problem=problem,deprecated=False) - - problem.title = request.data.get("title",problem.title) - problem.language = request.data.get("language",problem.language) - problem.description = request.data.get("description",problem.description) - problem.solution = request.data.get("solution",problem.solution) - problem.time_limit = request.data.get("time_limit",problem.time_limit) - problem.is_private = request.data.get("is_private",problem.is_private) - problem.allowed_languages = request.data.get("allowed_languages",problem.allowed_languages) - - problem.updated_date = timezone.now() - - if 'testcases' in request.data: - running_result = Grader[request.data['language']](problem.solution,request.data['testcases'],1,1.5).generate_output() - - # if not running_result.runnable: - # return Response({'detail': 'Error during editing. Your code may has an error/timeout!'},status=status.HTTP_406_NOT_ACCEPTABLE) - for testcase in testcases: - testcase.deprecated = True - testcase.save() - testcase_result = [] - for unit in running_result.data: - testcase2 = Testcase( - problem = problem, - input = unit.input, - output = unit.output, - runtime_status = unit.runtime_status - ) - testcase2.save() - testcase_result.append(testcase2) - problem.save() - problem_serialize = ProblemSerializer(problem) - testcases_serialize = TestcaseSerializer(testcase_result,many=True) - - return Response({**problem_serialize.data,'testcases': testcases_serialize.data},status=status.HTTP_201_CREATED) - - if 'solution' in request.data: - testcases = Testcase.objects.filter(problem=problem,deprecated=False) - program_input = [i.input for i in testcases] - running_result = Grader[request.data['language']](problem.solution,program_input,1,1.5).generate_output() - - if not running_result.runnable: - return Response({'detail': 'Error during editing. Your code may has an error/timeout!'},status=status.HTTP_406_NOT_ACCEPTABLE) - - problem.save() - problem_serialize = ProblemSerializer(problem) - return Response(problem_serialize.data,status=status.HTTP_201_CREATED) diff --git a/api/controllers/problem/update_problem_difficulty.py b/api/controllers/problem/update_problem_difficulty.py deleted file mode 100644 index 4739499..0000000 --- a/api/controllers/problem/update_problem_difficulty.py +++ /dev/null @@ -1,48 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * -from ...difficulty_predictor.preprocess import * -from ...difficulty_predictor.predictor import * - -try: - import pandas as pd - pd.options.mode.chained_assignment = None - success = True -except: - success = False - -def update_problem_difficulty(problem:Problem): - - if not success: - return - - submissions = Submission.objects.filter(problem=problem) - - if submissions.count() < 10: - return - - # Change them to DataFrame - df = pd.DataFrame(data={ - 'submission_id': [i.submission_id for i in submissions], - 'account_id': [i.account_id for i in submissions], - 'problem_id': [i.problem_id for i in submissions], - 'score': [i.score for i in submissions], - 'max_score': [i.max_score for i in submissions], - 'passed_ratio': [i.passed_ratio for i in submissions], - 'language': [i.language for i in submissions], - 'submission_code': [i.submission_code for i in submissions], - 'date': [i.date for i in submissions], - 'is_passed': [i.is_passed for i in submissions], - }) - - [total_attempt,time_used] = modelgrader_preprocessor(df) - difficulty = predict(total_attempt,time_used) - - problem.difficulty = difficulty - problem.save() \ No newline at end of file diff --git a/api/controllers/problem/validate_program.py b/api/controllers/problem/validate_program.py deleted file mode 100644 index d8298e8..0000000 --- a/api/controllers/problem/validate_program.py +++ /dev/null @@ -1,23 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import Grader,ProgramGrader,RuntimeResultList -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def validate_program(request): - grader:ProgramGrader = Grader[request.data['language']] - result:RuntimeResultList = grader(request.data['source_code'],request.data['testcases'],1,request.data['time_limited']).generate_output() - - print(result.getResult()) - print(result.runnable) - - return Response({ - 'runnable': result.runnable, - 'has_error': result.has_error, - 'has_timeout': result.has_timeout, - 'runtime_results': result.getResult(), - },status=status.HTTP_200_OK) \ No newline at end of file diff --git a/api/controllers/problem_controller.py b/api/controllers/problem_controller.py index 524ca3d..db7d319 100644 --- a/api/controllers/problem_controller.py +++ b/api/controllers/problem_controller.py @@ -1,107 +1,131 @@ -# from ..utility import JSONParser, JSONParserOne, passwordEncryption from rest_framework.response import Response from rest_framework.decorators import api_view -from ..constant import PUT, GET, POST -from rest_framework import status -from ..utility import extract_bearer_token, ERROR_TYPE_TO_STATUS -from ..services import problem_service -from ..errors.common import * -from django.http import FileResponse +from api.wrappers.auth_wrapper import authentication_required +from ..constant import GET, POST, PUT, DELETE +from api.errors.common import InternalServerError +from api.errors.core.grader_exception import GraderException +from api.setup import problem_service -@api_view([PUT]) -def upload_pdf(request, problem_id:str): +@api_view([POST, GET]) +@authentication_required +def all_problems_creator_view(request, account_id): try: - file = request.FILES.get('file') - token = extract_bearer_token(request) - if not token: - raise InvalidTokenError() - if not file: - raise InvalidFileError() - problem_service.upload_pdf(problem_id, file, token) - return Response(status=status.HTTP_204_NO_CONTENT) + if request.method == POST: + result = problem_service.create_problem(account_id, request) + elif request.method == GET: + result = problem_service.get_all_problems_by_account(account_id, request) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() except Exception as e: - if (isinstance(e, GraderException)): - return Response({ - "status": e.status, - "error": e.error - }, status=e.status) - else: - return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return InternalServerError(e).django_response() -@api_view([GET]) -def get_problem_pdf(request, problem_id:str, token): - """ - Get problem PDF file - 200: OK - 401: Unauthorized - No token / Token expired - 403: Forbidden - No permission - 404: Not Found - Problem not found - 500: Internal Server Error - """ +@api_view([GET, PUT, DELETE]) +@authentication_required +def one_problem_creator_view(request, problem_id: str, account_id: str): try: - pdf_file = problem_service.get_problem_pdf(problem_id, token) - return FileResponse(pdf_file, content_type='application/pdf') + if request.method == GET: + result = problem_service.get_problem(problem_id) + elif request.method == PUT: + result = problem_service.update_problem(problem_id, request) + elif request.method == DELETE: + problem_service.delete_problem(problem_id) + return Response(status=204) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() except Exception as e: - if (isinstance(e, GraderException)): - return Response({ - "status": e.status, - "error": e.error - }, status=e.status) - else : - return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return InternalServerError(e).django_response() -def get_problem(request, problem_id:str, token): +@api_view([GET, DELETE]) +@authentication_required +def all_problems_view(request): try: - problem = problem_service.get_problem(problem_id, request, token) - return Response(problem, status=status.HTTP_200_OK) - except Exception as e: - if (isinstance(e, GraderException)): - return Response({ - "status": e.status, - "error": e.error - }, status=e.status) - else: - return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR) + account_id = request.GET.get("account_id", None) + if request.method == GET: + result = problem_service.get_all_problem_with_best_submission(account_id) + elif request.method == DELETE: + problem_service.remove_bulk_problems(request) + return Response(status=204) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() + +@api_view([GET, PUT, DELETE]) +def one_problem_view(request, problem_id: int): + try: + if request.method == GET: + result = problem_service.get_problem_public(problem_id) + elif request.method == PUT: + result = problem_service.update_problem(problem_id, request) + elif request.method == DELETE: + problem_service.delete_problem(problem_id) + return Response(status=204) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() + @api_view([POST]) -def create_problem(request, token): - """ - create problem - 201: Created - 401: Unauthorized - No token / Token expired - 403: Forbidden - No permission - 404: Not Found - Problem not found - 500: Internal Server Error - """ +@authentication_required +def validation_view(request): + try: + if request.method == POST: + result = problem_service.validate_program(request) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() + +@api_view([GET]) +@authentication_required +def problem_in_topic_account_view(request, account_id: str, topic_id: str, problem_id: str): + try: + if request.method == GET: + result = problem_service.get_problem_in_topic_with_best_submission(account_id, topic_id, int(problem_id)) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() + +@api_view([PUT]) +@authentication_required +def problem_group_view(request, account_id: int, problem_id: int): try: - problem, testcases = problem_service.create_problem(request.data, token) - return Response({**problem.data,'testcases': testcases.data},status=status.HTTP_201_CREATED) + if request.method == PUT: + result = problem_service.update_group_permission_to_problem(problem_id, request) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() except Exception as e: - if (isinstance(e, GraderException)): - return Response({ - "status": e.status, - "error": e.error - }, status=e.status) - else: - return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR) - -def update_problem(request, problem_id, token): + return InternalServerError(e).django_response() + +@api_view([PUT]) +@authentication_required +def import_pdf_view(request, problem_id: int): + try: + if request.method == PUT: + problem_service.import_elabsheet_problem(request, problem_id) + return Response(status=204) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() + +@api_view([GET]) +@authentication_required +def all_problems_list_view(request): try: - problem, testcases = problem_service.update_problem(request.data, token, problem_id) - return Response({**problem,'testcases': testcases},status=status.HTTP_201_CREATED) + if request.method == GET: + result = problem_service.get_all_problems(request) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() except Exception as e: - if (isinstance(e, GraderException)): - return Response({ - "status": e.status, - "error": e.error - }, status=e.status) - else: - return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR) - -@api_view([GET, PUT]) -def get_or_update_problem(request, problem_id, token): - if request.method == GET: - return get_problem(request, problem_id, token) - elif request.method == PUT: - return update_problem(request, problem_id, token) - \ No newline at end of file + return InternalServerError(e).django_response() diff --git a/api/controllers/script/generate_failed_submission_status.py b/api/controllers/script/generate_failed_submission_status.py deleted file mode 100644 index 7370b9f..0000000 --- a/api/controllers/script/generate_failed_submission_status.py +++ /dev/null @@ -1,21 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict - -def generate_failed_submission_status(request): - submissionTestcases = SubmissionTestcase.objects.all() - - total = len(submissionTestcases) - count = 0 - for testcase in submissionTestcases: - if testcase.runtime_status == "OK" and (not testcase.is_passed): - testcase.runtime_status = "FAILED" - testcase.save() - count += 1 - - print(f"({count}/{total})") - return Response({'message': 'Success!'},status=status.HTTP_201_CREATED) diff --git a/api/controllers/script/generate_submission_score.py b/api/controllers/script/generate_submission_score.py deleted file mode 100644 index dad390f..0000000 --- a/api/controllers/script/generate_submission_score.py +++ /dev/null @@ -1,20 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict - -def generate_submission_score(request): - submissions = Submission.objects.all() - total = len(submissions) - count = 0 - for submission in submissions: - submission.score = submission.result.count('P') - submission.max_score = len(submission.result) - submission.passed_ratio = submission.score/submission.max_score - submission.save() - count += 1 - print(f"({count}/{total})") - return Response({'message': 'Success!'},status=status.HTTP_201_CREATED) \ No newline at end of file diff --git a/api/controllers/script/replace_collections_empty_description.py b/api/controllers/script/replace_collections_empty_description.py deleted file mode 100644 index 1dc31e8..0000000 --- a/api/controllers/script/replace_collections_empty_description.py +++ /dev/null @@ -1,14 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict - -def replace_collections_empty_description(request): - collections = Collection.objects.all() - for collection in collections: - collection.description = '[{"id":"1","type":"p","children":[{"text":"Just course"}]}]' - collection.save() - return Response({'message': 'Success!'},status=status.HTTP_201_CREATED) diff --git a/api/controllers/script/replace_topic_empty_description.py b/api/controllers/script/replace_topic_empty_description.py deleted file mode 100644 index e1fc519..0000000 --- a/api/controllers/script/replace_topic_empty_description.py +++ /dev/null @@ -1,18 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict - -def replace_topic_empty_description(request): - topics = Topic.objects.all() - for topic in topics: - if len(topic.description) == 0: - topic.description = f'[{{"id": "1","type": ELEMENT_PARAGRAPH,"children": [{{ "text": "" }}]}}]' - topic.save() - elif topic.description[0] != '[': - topic.description = f'[{{"id": "1","type": ELEMENT_PARAGRAPH,"children": [{{ "text": "{topic.description}" }}]}}]' - topic.save() - return Response({'message': 'Success!'},status=status.HTTP_201_CREATED) \ No newline at end of file diff --git a/api/controllers/submission/get_all_submissions_by_creator_problem.py b/api/controllers/submission/get_all_submissions_by_creator_problem.py deleted file mode 100644 index 1f46dee..0000000 --- a/api/controllers/submission/get_all_submissions_by_creator_problem.py +++ /dev/null @@ -1,46 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def get_all_submissions_by_creator_problem(problem:Problem, request): - - start = int(request.query_params.get("start",0)) - end = int(request.query_params.get("end",-1)) - # query = request.query_params.get("query","") - if end == -1: end = None - - submissions = Submission.objects.filter(problem=problem) - total = submissions.count() - - - if submissions.count() == 0: - return Response({"submissions": []},status=status.HTTP_204_NO_CONTENT) - - submissions = submissions.order_by('-date') - submissions = submissions[start:end] - - result = [] - - for submission in submissions: - submission_testcases = SubmissionTestcase.objects.filter(submission=submission) - submission.runtime_output = submission_testcases - result.append(submission) - - problem.testcases = Testcase.objects.filter(problem=problem,deprecated=False) - - submissions_serializer = SubmissionPopulateSubmissionTestcaseAndAccountSerializer(result,many=True) - problem_serializer = ProblemPopulateTestcaseSerializer(problem) - - return Response({ - "problem": problem_serializer.data, - "submissions": submissions_serializer.data, - "start": start, - "end": end, - "total": total, - },status=status.HTTP_200_OK) \ No newline at end of file diff --git a/api/controllers/submission/get_submission_by_quries.py b/api/controllers/submission/get_submission_by_quries.py deleted file mode 100644 index 7fb5401..0000000 --- a/api/controllers/submission/get_submission_by_quries.py +++ /dev/null @@ -1,53 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def get_submission_by_quries(request): - submissions = Submission.objects.all() - - # Query params - problem_id = str(request.query_params.get("problem_id", "")) - account_id = str(request.query_params.get("account_id", "")) - topic_id = str(request.query_params.get("topic_id", "")) - passed = int(request.query_params.get("passed", -1)) - sort_score = int(request.query_params.get("sort_score", 0)) - sort_date = int(request.query_params.get("sort_date", 0)) - start = int(request.query_params.get("start", -1)) - end = int(request.query_params.get("end", -1)) - - if problem_id != "": - submissions = submissions.filter(problem_id=problem_id) - if account_id != "": - submissions = submissions.filter(account_id=account_id) - if topic_id != "": - submissions = submissions.filter(problem__topic_id=topic_id) - - if passed == 0: - submissions = submissions.filter(is_passed=False) - elif passed == 1: - submissions = submissions.filter(is_passed=True) - - if sort_score == -1: - submissions = submissions.order_by('passed_ratio') - elif sort_score == 1: - submissions = submissions.order_by('-passed_ratio') - - if sort_date == -1: - submissions = submissions.order_by('date') - elif sort_date == 1: - submissions = submissions.order_by('-date') - - if start != -1 and end != -1: - submissions = submissions[start:end] - - for submission in submissions: - submission.runtime_output = SubmissionTestcase.objects.filter(submission=submission) - - serialize = SubmissionPopulateSubmissionTestcaseAndProblemSecureSerializer(submissions,many=True) - return Response({"submissions": serialize.data},status=status.HTTP_200_OK) diff --git a/api/controllers/submission/get_submissions_by_account_problem.py b/api/controllers/submission/get_submissions_by_account_problem.py deleted file mode 100644 index 7c98e32..0000000 --- a/api/controllers/submission/get_submissions_by_account_problem.py +++ /dev/null @@ -1,35 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def get_submissions_by_account_problem(account_id:str,problem_id:str): - submissions = Submission.objects.filter(account=account_id,problem=problem_id) - - if submissions.count() == 0: - return Response({"best_submission": None,"submissions": []},status=status.HTTP_204_NO_CONTENT) - - submissions = submissions.order_by('-date') - - best_submission_id = submissions.order_by('-passed_ratio','-date').first().submission_id - - best_submission = None - result = [] - - for submission in submissions: - submission_testcases = SubmissionTestcase.objects.filter(submission=submission) - submission.runtime_output = submission_testcases - result.append(submission) - - if submission.submission_id == best_submission_id: - best_submission = submission - - best_submission_serializer = SubmissionPopulateSubmissionTestcaseSecureSerializer(best_submission) - submissions_serializer = SubmissionPopulateSubmissionTestcaseSecureSerializer(result,many=True) - - return Response({"best_submission": best_submission_serializer.data,"submissions": submissions_serializer.data},status=status.HTTP_200_OK) \ No newline at end of file diff --git a/api/controllers/submission/get_submissions_by_account_problem_in_topic.py b/api/controllers/submission/get_submissions_by_account_problem_in_topic.py deleted file mode 100644 index 5c96f49..0000000 --- a/api/controllers/submission/get_submissions_by_account_problem_in_topic.py +++ /dev/null @@ -1,36 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def get_submissions_by_account_problem_in_topic(account_id:str,problem_id:str,topic_id:str): - submissions = Submission.objects.filter(account=account_id,problem=problem_id,topic_id=topic_id) - - if submissions.count() == 0: - return Response({"best_submission": None,"submissions": []},status=status.HTTP_204_NO_CONTENT) - - submissions = submissions.order_by('-date') - - result = [] - - for submission in submissions: - submission_testcases = SubmissionTestcase.objects.filter(submission=submission) - submission.runtime_output = submission_testcases - result.append(submission) - - best_submission = BestSubmission.objects.filter(problem=problem_id,topic=topic_id,account=account_id).first() - if best_submission: - best_submission.submission.runtime_output = SubmissionTestcase.objects.filter(submission=best_submission.submission) - best_submission_serializer = SubmissionPopulateSubmissionTestcaseSecureSerializer(best_submission.submission) - best_submission_result = best_submission_serializer.data - else: - best_submission_result = None - - submissions_serializer = SubmissionPopulateSubmissionTestcaseSecureSerializer(result,many=True) - - return Response({"best_submission": best_submission_result,"submissions": submissions_serializer.data},status=status.HTTP_200_OK) \ No newline at end of file diff --git a/api/controllers/submission/submit_problem.py b/api/controllers/submission/submit_problem.py deleted file mode 100644 index 51013df..0000000 --- a/api/controllers/submission/submit_problem.py +++ /dev/null @@ -1,112 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader,Grader,ProgramGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * -from ...utility import regexMatching -from time import sleep -from ..problem.update_problem_difficulty import * - -QUEUE = [0,0,0,0,0,0,0,0,0,0] - -def avaliableQueue(): - global QUEUE - for i in range(len(QUEUE)): - if QUEUE[i] == 0: - return i - return -1 - -def submit_problem_function(account_id:str,problem_id:str,topic_id:str,request): - global QUEUE - problem = Problem.objects.get(problem_id=problem_id) - testcases = Testcase.objects.filter(problem=problem,deprecated=False) - account = Account.objects.get(account_id=account_id) - - submission_code = request.data['submission_code'] - solution_input = [model_to_dict(i)['input'] for i in testcases] - solution_output = [model_to_dict(i)['output'] for i in testcases] - - if not regexMatching(problem.submission_regex,submission_code): - grading_result = '-'*len(solution_input) - else: - empty_queue = avaliableQueue() - while empty_queue == -1: - empty_queue = avaliableQueue() - sleep(5) - - QUEUE[empty_queue] = 1 - # grading_result = grader.grading(empty_queue+1,submission_code,solution_input,solution_output) - grader: ProgramGrader = Grader[request.data['language']] - grading_result = grader(submission_code,solution_input,empty_queue+1,1.5).grading(solution_output) - QUEUE[empty_queue] = 0 - - total_score = sum([i.is_passed for i in grading_result.data if i.is_passed]) - max_score = len(grading_result.data) - - submission = Submission( - problem = problem, - account = account, - language = request.data['language'], - submission_code = request.data['submission_code'], - is_passed = grading_result.is_passed, - score = total_score, - max_score = max_score, - passed_ratio = total_score/max_score - ) - - if topic_id: - submission.topic = Topic.objects.get(topic_id=topic_id) - - submission.save() - - # Best Submission - try: - best_submission = None - if topic_id: - best_submission = BestSubmission.objects.get(problem=problem,account=account,topic=Topic.objects.get(topic_id=topic_id)) - else: - best_submission = BestSubmission.objects.get(problem=problem,account=account) - except: - best_submission = BestSubmission( - problem = problem, - account = account, - topic = Topic.objects.get(topic_id=topic_id) if topic_id else None, - submission = submission - ) - best_submission.save() - else: - if submission.passed_ratio >= best_submission.submission.passed_ratio: - best_submission.submission = submission - best_submission.save() - - # End Best Submission - - submission_testcases = [] - for i in range(len(grading_result.data)): - submission_testcases.append(SubmissionTestcase( - submission = submission, - testcase = testcases[i], - output = grading_result.data[i].output, - is_passed = grading_result.data[i].is_passed, - runtime_status = grading_result.data[i].runtime_status - )) - - SubmissionTestcase.objects.bulk_create(submission_testcases) - - submission.runtime_output = submission_testcases - testser = SubmissionPopulateSubmissionTestcaseSecureSerializer(submission) - - update_problem_difficulty(problem) - - return Response(testser.data,status=status.HTTP_201_CREATED) - -def submit_problem(account_id:str,problem_id:str,request): - try: - return submit_problem_function(account_id,problem_id,None,request) - except Exception as e: - print(e) - return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR) \ No newline at end of file diff --git a/api/controllers/submission/submit_problem_on_topic.py b/api/controllers/submission/submit_problem_on_topic.py deleted file mode 100644 index 5d7d93f..0000000 --- a/api/controllers/submission/submit_problem_on_topic.py +++ /dev/null @@ -1,15 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader,Grader,ProgramGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * -from ...utility import regexMatching -from time import sleep -from .submit_problem import * - -def submit_problem_on_topic(account_id:str,problem_id:str,topic_id:str,request): - return submit_problem_function(account_id,problem_id,topic_id,request) \ No newline at end of file diff --git a/api/controllers/submission_controller.py b/api/controllers/submission_controller.py new file mode 100644 index 0000000..6316df6 --- /dev/null +++ b/api/controllers/submission_controller.py @@ -0,0 +1,59 @@ +from rest_framework.response import Response +from rest_framework.decorators import api_view +from api.wrappers.auth_wrapper import authentication_required +from ..constant import GET, POST, PUT, DELETE +from api.errors.common import InternalServerError, BadRequestError +from api.errors.core.grader_exception import GraderException +from api.setup import submission_service + +@api_view([POST, GET]) +@authentication_required +def creator_problem_submissions_view(request, account_id: str, problem_id: str): + try: + if request.method == GET: + result = submission_service.get_all_submissions_by_creator_problem(problem_id, request) + elif request.method == POST: + result = submission_service.submit_problem(account_id, problem_id, request) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() + +@api_view([POST, GET]) +@authentication_required +def topic_account_problem_submission_view(request, account_id: str, topic_id: str, problem_id: str): + try: + if request.method == GET: + result = submission_service.get_submissions_by_account_problem_in_topic(account_id, problem_id, topic_id) + elif request.method == POST: + result = submission_service.submit_problem_on_topic(account_id, problem_id, topic_id, request) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() + +@api_view([GET]) +@authentication_required +def account_problem_submission_view(request, problem_id: str, account_id: str): + try: + if request.method == GET: + result = submission_service.get_submissions_by_account_problem(account_id, problem_id) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() + +@api_view([GET]) +@authentication_required +def all_submission_view(request): + try: + if request.method == GET: + result = submission_service.get_submission_by_quries(request) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() diff --git a/api/controllers/topic/add_collections_to_topic.py b/api/controllers/topic/add_collections_to_topic.py deleted file mode 100644 index 936e05f..0000000 --- a/api/controllers/topic/add_collections_to_topic.py +++ /dev/null @@ -1,36 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def add_collections_to_topic(topic_id:str,request): - topic = Topic.objects.get(topic_id=topic_id) - populated_collections = [] - - index = 0 - for collection_id in request.data['collection_ids']: - collection = Collection.objects.get(collection_id=collection_id) - - alreadyExist = TopicCollection.objects.filter(topic_id=topic.topic_id,collection_id=collection.collection_id) - if alreadyExist: - alreadyExist.delete() - - topicCollection = TopicCollection( - topic=topic, - collection=collection, - order=index - ) - topicCollection.save() - index += 1 - tc_serialize = TopicCollectionSerializer(topicCollection) - populated_collections.append(tc_serialize.data) - - return Response({ - **TopicSerializer(topic).data, - "collections": populated_collections - },status=status.HTTP_201_CREATED) diff --git a/api/controllers/topic/create_topic.py b/api/controllers/topic/create_topic.py deleted file mode 100644 index 12557ce..0000000 --- a/api/controllers/topic/create_topic.py +++ /dev/null @@ -1,19 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def create_topic(account_id:str,request): - request.data._mutable=True - request.data['creator'] = account_id - serializer = TopicSerializer(data=request.data) - - if serializer.is_valid(): - serializer.save() - return Response(serializer.data,status=status.HTTP_201_CREATED) - return Response(serializer.errors,status=status.HTTP_400_BAD_REQUEST) diff --git a/api/controllers/topic/delete_topic.py b/api/controllers/topic/delete_topic.py deleted file mode 100644 index 622c052..0000000 --- a/api/controllers/topic/delete_topic.py +++ /dev/null @@ -1,13 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def delete_topic(topic:Topic): - topic.delete() - return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/api/controllers/topic/get_all_accessed_topics_by_account.py b/api/controllers/topic/get_all_accessed_topics_by_account.py deleted file mode 100644 index 8d6cd87..0000000 --- a/api/controllers/topic/get_all_accessed_topics_by_account.py +++ /dev/null @@ -1,23 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * -from django.db.models import Q - -def get_all_accessed_topics_by_account(account:Account): - groups = [gm.group for gm in GroupMember.objects.filter(account=account)] - accessedTopics = TopicGroupPermission.objects.filter(Q(group__in=groups) & (Q(permission_view_topics=True) | Q(permission_manage_topics=True))) - - topics = [] - for at in accessedTopics: - if at.topic not in topics: - topics.append(at.topic) - - serialize = TopicSerializer(topics,many=True) - - return Response({'topics':serialize.data},status=status.HTTP_200_OK) diff --git a/api/controllers/topic/get_all_topics.py b/api/controllers/topic/get_all_topics.py deleted file mode 100644 index 9c149f5..0000000 --- a/api/controllers/topic/get_all_topics.py +++ /dev/null @@ -1,23 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def get_all_topics(request): - topics = Topic.objects.all() - - account_id = request.query_params.get('account_id',0) - - if account_id: - topics = topics.filter(creator_id=account_id) - - serializer = TopicSerializer(topics,many=True) - - return Response({ - 'topics': serializer.data - },status=status.HTTP_200_OK) \ No newline at end of file diff --git a/api/controllers/topic/get_all_topics_by_account.py b/api/controllers/topic/get_all_topics_by_account.py deleted file mode 100644 index 417424f..0000000 --- a/api/controllers/topic/get_all_topics_by_account.py +++ /dev/null @@ -1,32 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def populated_collections(topics:Topic): - topicCollections = TopicCollection.objects.filter(topic__in=topics) - populated_topics = [] - for topic in topics: - topic.collections = topicCollections.filter(topic=topic) - populated_topics.append(topic) - return populated_topics - -def get_all_topics_by_account(account:Account,request): - personalTopics = Topic.objects.filter(creator=account).order_by('-updated_date') - populatedPersonalTopics = populated_collections(personalTopics) - personalSerialize = TopicPopulateTopicCollectionPopulateCollectionSerializer(populatedPersonalTopics,many=True) - - # print(GroupMember.objects.all().values_list("group",flat=True)) - manageableTopics = Topic.objects.filter(topicgrouppermission__permission_manage_topics=True,topicgrouppermission__group__in=GroupMember.objects.filter(account=account).values_list("group",flat=True)).order_by('-updated_date') - populatedmanageableTopics = populated_collections(manageableTopics) - manageableSerialize = TopicPopulateTopicCollectionPopulateCollectionSerializer(populatedmanageableTopics,many=True) - - return Response({ - 'topics': personalSerialize.data, - 'manageable_topics': manageableSerialize.data - },status=status.HTTP_200_OK) \ No newline at end of file diff --git a/api/controllers/topic/get_topic.py b/api/controllers/topic/get_topic.py deleted file mode 100644 index 53f6e86..0000000 --- a/api/controllers/topic/get_topic.py +++ /dev/null @@ -1,54 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def get_topic(topic:Topic): - topic.group_permissions = TopicGroupPermission.objects.filter(topic=topic) - - topic.collections = TopicCollection.objects.filter(topic=topic).order_by('order') - - for tp in topic.collections: - tp.collection.problems = CollectionProblem.objects.filter(collection=tp.collection) - tp.collection.group_permissions = CollectionGroupPermission.objects.filter(collection=tp.collection) - - serialize = TopicPopulateTopicCollectionPopulateCollectionPopulateCollectionProblemsPopulateProblemAndCollectionGroupPermissionsPopulateGroupAndTopicGroupPermissionPopulateGroupSerializer(topic) - - return Response(serialize.data,status=status.HTTP_200_OK) - - - - # topic = Topic.objects.get(topic_id=topic_id) - # topicCollections = TopicCollection.objects.filter(topic_id=topic_id) - # # accessedAccounts = Account.objects.filter(topicaccountaccess__topic_id=topic_id) - - # topic_ser = TopicSerializer(topic) - # populate_collections = [] - - # for top_col in topicCollections: - # collection_serialize = CollectionSerializer(top_col.collection) - # collection_data = collection_serialize.data - - # populate_problems = [] - # collection_problems = CollectionProblem.objects.filter(collection=top_col.collection) - # for col_prob in collection_problems: - # prob_serialize = ProblemSerializer(col_prob.problem) - # col_prob_serialize = CollectionProblemSerializer(col_prob) - # populate_problems.append({**col_prob_serialize.data,**prob_serialize.data}) - - # collection_data['problems'] = populate_problems - # top_col_serialize = TopicCollectionSerializer(top_col) - # populate_collections.append({**top_col_serialize.data,**collection_data}) - - # # accessedAccountsSerialize = AccountSecureSerializer(accessedAccounts,many=True) - - # return Response({ - # **topic_ser.data, - # "collections": sorted(populate_collections,key=lambda collection: collection['order']), - # # "accessed_accounts": accessedAccountsSerialize.data - # },status=status.HTTP_200_OK) \ No newline at end of file diff --git a/api/controllers/topic/get_topic_public.py b/api/controllers/topic/get_topic_public.py deleted file mode 100644 index 36476b2..0000000 --- a/api/controllers/topic/get_topic_public.py +++ /dev/null @@ -1,61 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * -from django.db.models import Q - -def get_topic_public(topic_id:str,request): - - account_id = request.query_params.get('account_id',None) - - topic = Topic.objects.get(topic_id=topic_id) - account = Account.objects.get(account_id=account_id) - - - topicCollections = TopicCollection.objects.filter( - topic=topic, - collection__in= - CollectionGroupPermission.objects.filter( - Q(group__in=GroupMember.objects.filter(account=account).values_list("group",flat=True)) & - ( - Q(permission_view_collections=True) | Q(permission_manage_collections=True) - ) - ).values_list("collection",flat=True)) - - for tp in topicCollections: - # tp.collection.problems = CollectionProblem.objects.filter(collection=tp.collection) - - # viewPermission = CollectionGroupPermission.objects.filter(collection=tp.collection,group__in=GroupMember.objects.filter(account=account).values_list("group",flat=True),permission_view_collections=True) - # print(len(viewPermission)) - # if len(viewPermission) == 0: - # tp.collection.problems = [] - # continue - collectionProblems = CollectionProblem.objects.filter( - collection=tp.collection, - problem__in=ProblemGroupPermission.objects.filter( - Q(group__in=GroupMember.objects.filter(account=account).values_list("group",flat=True)) & - (Q(permission_view_problems=True) | - Q(permission_manage_problems=True)) - ).values_list("problem",flat=True)) - - for cp in collectionProblems: - try: - best_submission = BestSubmission.objects.get(problem=cp.problem,account=account,topic=topic) - best_submission = best_submission.submission - best_submission.runtime_output = SubmissionTestcase.objects.filter(submission=best_submission) - except: - best_submission = None - cp.problem.best_submission = best_submission - - tp.collection.problems = collectionProblems - - topic.collections = topicCollections - - serialize = TopicPopulateTopicCollectionPopulateCollectionPopulateCollectionProblemPopulateProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureSerializer(topic) - - return Response(serialize.data,status=status.HTTP_200_OK) diff --git a/api/controllers/topic/remove_collections_from_topic.py b/api/controllers/topic/remove_collections_from_topic.py deleted file mode 100644 index 74ac26e..0000000 --- a/api/controllers/topic/remove_collections_from_topic.py +++ /dev/null @@ -1,16 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def remove_collections_from_topic(topic_id:str,request): - TopicCollection.objects.filter(topic_id=topic_id,collection_id__in=request.data['collection_ids']).delete() - # collections = Collection.objects.filter(collection_id__in=request.data['collection_ids']) - # problems = Problem.objects.filter(problem_id__in=request.data['problems_id']) - # TopicProblem.objects.filter(topic_id=topic,problem_id__in=problems).delete() - return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/api/controllers/topic/update_collections_to_topic.py b/api/controllers/topic/update_collections_to_topic.py deleted file mode 100644 index 4bb3e81..0000000 --- a/api/controllers/topic/update_collections_to_topic.py +++ /dev/null @@ -1,36 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def update_collections_to_topic(topic:Topic,request): - TopicCollection.objects.filter(topic=topic).delete() - - topic_collections = [] - order = 0 - for collection_id in request.data['collection_ids']: - collection = Collection.objects.get(collection_id=collection_id) - topic_collection = TopicCollection( - collection=collection, - topic=topic, - order=order - ) - topic_collections.append(topic_collection) - order += 1 - - TopicCollection.objects.bulk_create(topic_collections) - topic.updated_date = timezone.now() - topic.save() - - collection_serialize = TopicCollectionPopulateCollectionSerializer(topic_collections,many=True) - topic_serialize = TopicSerializer(topic) - - return Response({ - **topic_serialize.data, - 'collections': collection_serialize.data - },status=status.HTTP_201_CREATED) \ No newline at end of file diff --git a/api/controllers/topic/update_groups_permission_to_topic.py b/api/controllers/topic/update_groups_permission_to_topic.py deleted file mode 100644 index 608791b..0000000 --- a/api/controllers/topic/update_groups_permission_to_topic.py +++ /dev/null @@ -1,31 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def update_groups_permission_to_topic(topic:Topic,request): - - TopicGroupPermission.objects.filter(topic=topic).delete() - - topic_group_permissions = [] - for group_request in request.data['groups']: - print(group_request) - group = Group.objects.get(group_id=group_request['group_id']) - topic_group_permissions.append( - TopicGroupPermission( - topic=topic, - group=group, - **group_request - )) - - TopicGroupPermission.objects.bulk_create(topic_group_permissions) - - topic.group_permissions = topic_group_permissions - serialize = TopicPopulateTopicGroupPermissionsSerializer(topic) - - return Response(serialize.data,status=status.HTTP_202_ACCEPTED) \ No newline at end of file diff --git a/api/controllers/topic/update_topic.py b/api/controllers/topic/update_topic.py deleted file mode 100644 index 1d95c9f..0000000 --- a/api/controllers/topic/update_topic.py +++ /dev/null @@ -1,18 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ...constant import GET,POST,PUT,DELETE -from ...models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ...serializers import * - -def update_topic(topic:Topic,request): - topic_ser = TopicSerializer(topic,data=request.data,partial=True) - if topic_ser.is_valid(): - topic_ser.save() - return Response(topic_ser.data,status=status.HTTP_200_OK) - print(topic_ser.errors) - return Response(topic_ser.errors,status=status.HTTP_400_BAD_REQUEST) - \ No newline at end of file diff --git a/api/controllers/topic_controller.py b/api/controllers/topic_controller.py new file mode 100644 index 0000000..40be615 --- /dev/null +++ b/api/controllers/topic_controller.py @@ -0,0 +1,116 @@ +from rest_framework.response import Response +from rest_framework.decorators import api_view +from api.wrappers.auth_wrapper import authentication_required +from ..constant import GET, POST, PUT, DELETE +from api.errors.common import InternalServerError, BadRequestError +from api.errors.core.grader_exception import GraderException +from api.setup import topic_service + +@api_view([POST, GET]) +@authentication_required +def all_topics_creator_view(request, account_id): + try: + if request.method == POST: + result = topic_service.create_topic(account_id, request) + elif request.method == GET: + result = topic_service.get_all_topics_by_account(account_id, request) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() + +@api_view([GET, PUT, DELETE]) +@authentication_required +def one_topic_creator_view(request, topic_id: str, account_id: str): + try: + if request.method == GET: + result = topic_service.get_topic(topic_id) + elif request.method == PUT: + result = topic_service.update_topic(topic_id, request) + elif request.method == DELETE: + topic_service.delete_topic(topic_id) + return Response(status=204) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() + +@api_view([GET]) +def all_topics_view(request): + try: + if request.method == GET: + result = topic_service.get_all_topics(request) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() + +@api_view([GET]) +def one_topic_view(request, topic_id: str): + try: + if request.method == GET: + result = topic_service.get_topic_public(topic_id, request) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() + +@api_view([GET]) +@authentication_required +def all_topics_access_view(request, account_id: str): + try: + if request.method == GET: + result = topic_service.get_all_accessed_topics_by_account(account_id) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() + +@api_view([PUT]) +@authentication_required +def topic_groups_view(request, account_id: str, topic_id: str): + try: + if request.method == PUT: + result = topic_service.update_groups_permission_to_topic(topic_id, request) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() + +@api_view([PUT, POST]) +@authentication_required +def topic_collections_view(request, topic_id: str, method: str): + try: + if method == "add": + result = topic_service.add_collections_to_topic(topic_id, request) + elif method == "update": + result = topic_service.update_collections_to_topic(topic_id, request) + elif method == "remove": + topic_service.remove_collections_from_topic(topic_id, request) + return Response(status=204) + else: + raise BadRequestError(f"Invalid method: {method}") + + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() + +@api_view([POST]) +@authentication_required +def account_access(request, topic_id: str): + try: + # This function would need to be implemented in topic_service + # For now, returning empty response + return Response(status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() diff --git a/api/errors/auth.py b/api/errors/auth.py new file mode 100644 index 0000000..f19fcc2 --- /dev/null +++ b/api/errors/auth.py @@ -0,0 +1,6 @@ +from api.errors.core.grader_exception import GraderException + + +class IncorrectPasswordError(GraderException): + def __init__(self): + super().__init__("Incorrect password.", 406) \ No newline at end of file diff --git a/api/errors/common.py b/api/errors/common.py index 80f75a9..75a1344 100644 --- a/api/errors/common.py +++ b/api/errors/common.py @@ -1,25 +1,26 @@ -class GraderException(Exception): - def __init__(self, error: str, status: int): - self.error = error - self.status = status - super().__init__(self.error) +from api.errors.core.grader_exception import GraderException class InvalidTokenError(GraderException): def __init__(self): super().__init__("Token expired or invalid.", 401) class ItemNotFoundError(GraderException): - def __init__(self): - super().__init__("Item not found.", 404) + def __init__(self, item: str = "Item"): + super().__init__(f"{item} not found.", 404) class PermissionDeniedError(GraderException): def __init__(self): super().__init__("You do not have permission.", 403) +# TODO: Move this file class InvalidFileError(GraderException): def __init__(self): super().__init__("Invalid file.", 400) class InternalServerError(GraderException): - def __init__(self): - super().__init__("Internal server error.", 500) + def __init__(self, e: Exception): + super().__init__(e if e else "Internal server error.", 500) + +class BadRequestError(GraderException): + def __init__(self, message: str = "Bad request"): + super().__init__(message, 400) diff --git a/api/errors/core/grader_exception.py b/api/errors/core/grader_exception.py new file mode 100644 index 0000000..33c0af8 --- /dev/null +++ b/api/errors/core/grader_exception.py @@ -0,0 +1,9 @@ +from rest_framework.response import Response +class GraderException(Exception): + def __init__(self, error: str, status: int): + self.error = error + self.status = status + super().__init__(self.error) + + def django_response(self): + return Response({'message': str(self.error)}, status=self.status) \ No newline at end of file diff --git a/api/repositories/account_repository.py b/api/repositories/account_repository.py new file mode 100644 index 0000000..7e888d3 --- /dev/null +++ b/api/repositories/account_repository.py @@ -0,0 +1,48 @@ +from django.db.models import Q +from api.models import Account +from abc import ABC, abstractmethod + +class AccountRepository(ABC): + @abstractmethod + def create(self, r) -> Account: + pass + + @abstractmethod + def get(self, id: str) -> Account: + pass + + @abstractmethod + def list(self, q: str) -> list[Account]: + pass + + @abstractmethod + def get_by_token(self, token: str) -> Account: + pass + + @abstractmethod + def get_by_username(self, username: str) -> Account: + pass + +class AccountRepositoryImpl: + def __init__(self): + pass + + def create(self, r) -> Account: + return Account.objects.create(**r) + + def get(self, id: str) -> Account: + return Account.objects.get(account_id=id) + + def list(self, q) -> list[Account]: + accounts = Account.objects.all() + if q: + accounts = accounts.filter( + Q(username__icontains=q) | Q(account_id__icontains=q) | Q(email__icontains=q) + ).distinct() + return accounts + + def get_by_token(self, token: str) -> Account: + return Account.objects.get(token=token) + + def get_by_username(self, username: str) -> Account: + return Account.objects.get(username=username) diff --git a/api/repositories/collection_repository.py b/api/repositories/collection_repository.py new file mode 100644 index 0000000..094b430 --- /dev/null +++ b/api/repositories/collection_repository.py @@ -0,0 +1,104 @@ +from abc import ABC, abstractmethod +from django.db.models import Q +from django.utils import timezone +from api.models import Collection, CollectionProblem, TopicCollection +from typing import List + +class CollectionRepository: + def __init__(self): + pass + + def create(self, r): + collection = Collection.objects.create(**r) + collection.save() + return collection + + def get(self, collection_id: str): + return Collection.objects.get(collection_id=collection_id) + + def list(self, q: str = '', f: dict = {}): + if q: + collections = Collection.objects.filter(Q(name__icontains=q) | Q(description__icontains=q)) + else: + collections = Collection.objects.all() + + if 'creator_id' in f and f['creator_id']: + collections = collections.filter(creator__account_id=f['creator_id']) + + return collections + + def update(self, collection_id: str, r): + collection = self.get(collection_id) + collection.name = r.get('name', collection.name) + collection.description = r.get('description', collection.description) + collection.is_active = r.get('is_active', collection.is_active) + collection.is_private = r.get('is_private', collection.is_private) + collection.save() + return collection + + def delete(self, collection_id: str): + self.get(collection_id).delete() + + def get_problems(self, collection_id: str): + return CollectionProblem.objects.filter(collection_id=collection_id).order_by('order') + + def get_problems_by_collections(self, collection_ids): + return CollectionProblem.objects.filter(collection__in=collection_ids) + + def get_by_creator(self, account_id: str, order_by: str = '-updated_date'): + return Collection.objects.filter(creator_id=account_id).order_by(order_by) + + def update_with_timestamp(self, collection_id: str, data: dict): + collection = self.get(collection_id) + for key, value in data.items(): + if hasattr(collection, key): + setattr(collection, key, value) + collection.updated_date = timezone.now() + collection.save() + return collection + + def delete_problems(self, collection_id: str): + CollectionProblem.objects.filter(collection_id=collection_id).delete() + + def bulk_create_problems(self, collection_problems: List[CollectionProblem]): + CollectionProblem.objects.bulk_create(collection_problems) + + def find_existing_problem(self, problem_id: str, collection_id: str): + return CollectionProblem.objects.filter(problem_id=problem_id, collection_id=collection_id) + + def delete_many_problems(self, collection_id: str, problem_ids: List[str]): + CollectionProblem.objects.filter(collection_id=collection_id, problem_id__in=problem_ids).delete() + + def get_accessible_problems_for_collection(self, collection_id: str, group_ids: List[str]): + from django.db.models import Q + from api.models import ProblemGroupPermission + + accessible_problems = ProblemGroupPermission.objects.filter( + Q(group__in=group_ids) & (Q(permission_view_problems=True) | Q(permission_manage_problems=True)) + ).values_list("problem", flat=True) + + return CollectionProblem.objects.filter( + collection_id=collection_id, + problem__in=accessible_problems + ) + + def get_manageable_by_account(self, group_ids: List[str], order_by: str = '-updated_date'): + """Get collections manageable by account through group permissions""" + return Collection.objects.filter( + collectiongrouppermission__permission_manage_collections=True, + collectiongrouppermission__group__in=group_ids + ).order_by(order_by) + + def get_accessible_collections(self, topic_id: str, group_ids: List[str]): + """Get accessible collections for a topic based on group permissions""" + from api.models import CollectionGroupPermission + + accessible_collections = CollectionGroupPermission.objects.filter( + Q(group__in=group_ids) & (Q(permission_view_collections=True) | Q(permission_manage_collections=True)) + ).values_list("collection", flat=True) + + return TopicCollection.objects.filter( + topic_id=topic_id, + collection__in=accessible_collections + ) + \ No newline at end of file diff --git a/api/repositories/group_repository.py b/api/repositories/group_repository.py new file mode 100644 index 0000000..9bed3da --- /dev/null +++ b/api/repositories/group_repository.py @@ -0,0 +1,81 @@ +from django.db.models import Q +from django.utils import timezone +from api.models import GroupMember, Group, TopicCollection, CollectionGroupPermission +from typing import List + + +class GroupRepository: + def __init__(self): + pass + + def get_ids_by_account(self, account_id: str) -> List[str]: + """Common method to get group IDs for an account - used by other repositories""" + return GroupMember.objects.filter(account_id=account_id).values_list("group", flat=True) + + def list_by_account_id(self, account_id: str) -> List[str]: + """Alias for backward compatibility""" + return self.get_ids_by_account(account_id) + + def get(self, group_id: str): + return Group.objects.get(group_id=group_id) + + def get_members(self, account_id: str): + return [gm.group for gm in GroupMember.objects.filter(account_id=account_id)] + + def get_accessible_topics(self, account_id: str): + from api.models import TopicGroupPermission + groups = self.get_members(account_id) + return TopicGroupPermission.objects.filter( + Q(group__in=groups) & (Q(permission_view_topics=True) | Q(permission_manage_topics=True)) + ) + + def create(self, group_data: dict): + group = Group(**group_data) + group.save() + return group + + def delete(self, group_id: str): + group = self.get(group_id) + group.delete() + return None + + def get_by_creator(self, account_id: str, order_by: str = '-updated_date'): + return Group.objects.filter(creator_id=account_id).order_by(order_by) + + def get_members(self, group_id: str): + return GroupMember.objects.filter(group_id=group_id) + + def update(self, group_id: str, group_data: dict): + group = self.get(group_id) + for key, value in group_data.items(): + setattr(group, key, value) + group.updated_date = timezone.now() + group.save() + return group + + def bulk_create_members(self, members: List[GroupMember]): + return GroupMember.objects.bulk_create(members) + + def delete_members(self, group_id: str): + GroupMember.objects.filter(group_id=group_id).delete() + + def get_accessible_by_account(self, account_id: str): + """Get accessible topics by account through group permissions""" + groups = [gm.group for gm in GroupMember.objects.filter(account_id=account_id)] + from api.models import TopicGroupPermission + accessed_topics = TopicGroupPermission.objects.filter( + Q(group__in=groups) & (Q(permission_view_topics=True) | Q(permission_manage_topics=True)) + ) + return accessed_topics + + def get_accessible_collections_with_access(self, topic_id: str, account_id: str): + """Get accessible collections for a topic based on account's group permissions""" + group_ids = GroupMember.objects.filter(account_id=account_id).values_list("group", flat=True) + + topic_collections = TopicCollection.objects.filter( + topic_id=topic_id, + collection__in=CollectionGroupPermission.objects.filter( + Q(group__in=group_ids) & (Q(permission_view_collections=True) | Q(permission_manage_collections=True)) + ).values_list("collection", flat=True) + ) + return topic_collections \ No newline at end of file diff --git a/api/repositories/permission_repository.py b/api/repositories/permission_repository.py new file mode 100644 index 0000000..fa877d6 --- /dev/null +++ b/api/repositories/permission_repository.py @@ -0,0 +1,53 @@ +from django.db.models import Q +from api.models import ProblemGroupPermission, CollectionGroupPermission, TopicGroupPermission, CollectionProblem, ProblemGroupPermission +from typing import List + + +class PermissionRepository: + def __init__(self): + pass + + def get_problem_permissions(self, problem_id: str): + return ProblemGroupPermission.objects.filter(problem_id=problem_id) + + def get_collection_permissions(self, collection_id: str): + return CollectionGroupPermission.objects.filter(collection_id=collection_id) + + def get_topic_permissions(self, topic_id: str): + return TopicGroupPermission.objects.filter(topic_id=topic_id) + + def delete_collection_permissions(self, collection_id: str): + CollectionGroupPermission.objects.filter(collection_id=collection_id).delete() + + def bulk_create_collection_permissions(self, permissions: List[CollectionGroupPermission]): + return CollectionGroupPermission.objects.bulk_create(permissions) + + def delete_topic_permissions(self, topic_id: str): + TopicGroupPermission.objects.filter(topic_id=topic_id).delete() + + def bulk_create_topic_permissions(self, permissions: List[TopicGroupPermission]): + TopicGroupPermission.objects.bulk_create(permissions) + + def delete_problem_permissions(self, problem_id: str): + ProblemGroupPermission.objects.filter(problem_id=problem_id).delete() + + def bulk_create_problem_permissions(self, permissions: List[ProblemGroupPermission]): + ProblemGroupPermission.objects.bulk_create(permissions) + + def get_accessible_collections(self, ids: List[str]): + """Get collections accessible by group IDs""" + return CollectionGroupPermission.objects.filter( + Q(group__in=ids) & (Q(permission_view_collections=True) | Q(permission_manage_collections=True)) + ).values_list("collection", flat=True) + + def get_accessible_problems_for_collections(self, topic_collections, ids: List[str]): + """Get accessible problems for collections based on group permissions""" + for tp in topic_collections: + collection_problems = CollectionProblem.objects.filter( + collection_id=tp.collection_id, + problem_id__in=ProblemGroupPermission.objects.filter( + Q(group__in=ids) & (Q(permission_view_problems=True) | Q(permission_manage_problems=True)) + ).values_list("problem", flat=True) + ) + tp.collection.problems = collection_problems + return topic_collections \ No newline at end of file diff --git a/api/repositories/problem_repository.py b/api/repositories/problem_repository.py new file mode 100644 index 0000000..5ea72e6 --- /dev/null +++ b/api/repositories/problem_repository.py @@ -0,0 +1,97 @@ +from typing import List +from django.utils import timezone +from api.models import Problem, Testcase, Submission, SubmissionTestcase, BestSubmission + + +class ProblemRepository: + def __init__(self): + pass + + def create(self, data: dict) -> Problem: + problem = Problem(**data) + problem.save() + return problem + + def list(self, filters: dict = {}, order_by: list[str] = []): + problems = Problem.objects.all() + if 'is_private' in filters and filters['is_private'] is not None: + problems = problems.filter(is_private=filters['is_private']) + if 'is_active' in filters and filters['is_active'] is not None: + problems = problems.filter(is_active=filters['is_active']) + if 'creator_id' in filters and filters['creator_id'] is not None: + problems = problems.filter(creator_id=filters['creator_id']) + if 'id_list' in filters and filters['id_list'] is not None: + problems = problems.filter(problem_id__in=filters['id_list']) + problems = problems.order_by(*order_by) + return problems + + def get(self, id: str): + return Problem.objects.get(problem_id=id) + + def delete(self, id: str): + problem = Problem.objects.get(problem_id=id) + problem.delete() + + def delete_many(self, id_list: List[str]): + Problem.objects.filter(problem_id__in=id_list).delete() + + def get_personal(self, account_id: str, q: str, start: int = 0, end: int = None): + problems = Problem.objects.filter(creator_id=account_id, title__icontains=q).order_by('-updated_date') + if end: + return problems[start:end] + return problems + + def get_by_creator(self, account_id: str): + return Problem.objects.filter(creator_id=account_id).order_by('-updated_date') + + def get_testcases(self, problem_id: str, deprecated: bool = False): + return Testcase.objects.filter(problem_id=problem_id, deprecated=deprecated) + + def create_testcase(self, data: dict) -> Testcase: + testcase = Testcase(**data) + testcase.save() + return testcase + + def bulk_create_testcases(self, testcases: List[Testcase]): + Testcase.objects.bulk_create(testcases) + + def deprecate_testcases(self, problem_id: str): + testcases = Testcase.objects.filter(problem_id=problem_id, deprecated=False) + for testcase in testcases: + testcase.deprecated = True + testcase.save() + + def update(self, problem_id: str, data: dict): + problem = self.get(problem_id) + for key, value in data.items(): + if hasattr(problem, key): + setattr(problem, key, value) + problem.updated_date = timezone.now() + problem.save() + return problem + + def get_with_best_submission(self, account_id: str): + return Problem.objects.all().order_by('-updated_date') + + def get_best_submission(self, problem_id: str, account_id: str): + return Submission.objects.filter(problem_id=problem_id, account_id=account_id).order_by('-passed_ratio', '-submission_id').first() + + def get_best_submission_in_topic(self, problem_id: str, account_id: str, topic_id: str): + return BestSubmission.objects.filter(problem_id=problem_id, account_id=account_id, topic_id=topic_id).first() + + def get_submission_testcases(self, submission_id: str): + return SubmissionTestcase.objects.filter(submission_id=submission_id) + + def get_submissions_by_problem(self, problem_id: str): + return Submission.objects.filter(problem_id=problem_id) + + def get_manageable_by_account(self, group_ids: List[str], query: str = '', start: int = 0, end: int = None): + """Get problems manageable by account through group permissions""" + problems = Problem.objects.filter( + problemgrouppermission__permission_manage_problems=True, + problemgrouppermission__group__in=group_ids, + title__icontains=query + ).order_by('-updated_date') + if end: + return problems[start:end] + return problems \ No newline at end of file diff --git a/api/repositories/submission_repository.py b/api/repositories/submission_repository.py new file mode 100644 index 0000000..cf6b7b0 --- /dev/null +++ b/api/repositories/submission_repository.py @@ -0,0 +1,128 @@ +from api.models import Submission, SubmissionTestcase, BestSubmission +from typing import List + + +class SubmissionRepository: + def __init__(self): + pass + + def get_best(self, problem_id: str, account_id: str, topic_id: str = None): + submissions = Submission.objects.filter( + problem_id=problem_id, + account_id=account_id + ) + + if topic_id is not None: + submissions = submissions.filter(topic_id=topic_id) + + submissions = submissions.order_by('-passed_ratio', '-submission_id') + + return submissions.first() + + def get_testcases(self, submission_id: str): + return SubmissionTestcase.objects.filter(submission_id=submission_id) + + def get_by_problem(self, problem_id: str, start: int = 0, end: int = None): + submissions = Submission.objects.filter(problem_id=problem_id).order_by('-date') + if end: + return submissions[start:end] + return submissions[start:] + + def list(self, problem_id: str = None, account_id: str = None, topic_id: str = None, + passed: int = None, sort_score: int = 0, sort_date: int = 0, + start: int = None, end: int = None): + submissions = Submission.objects.all() + + if problem_id: + submissions = submissions.filter(problem_id=problem_id) + if account_id: + submissions = submissions.filter(account_id=account_id) + if topic_id: + submissions = submissions.filter(problem__topic_id=topic_id) + + if passed == 0: + submissions = submissions.filter(is_passed=False) + elif passed == 1: + submissions = submissions.filter(is_passed=True) + + if sort_score == -1: + submissions = submissions.order_by('passed_ratio') + elif sort_score == 1: + submissions = submissions.order_by('-passed_ratio') + + if sort_date == -1: + submissions = submissions.order_by('date') + elif sort_date == 1: + submissions = submissions.order_by('-date') + + if start is not None and end is not None: + submissions = submissions[start:end] + + return submissions + + def get_by_account_problem_topic(self, account_id: str, problem_id: str, topic_id: str): + return Submission.objects.filter( + account_id=account_id, + problem_id=problem_id, + topic_id=topic_id + ).order_by('-date') + + def get_by_account_problem(self, account_id: str, problem_id: str): + return Submission.objects.filter( + account_id=account_id, + problem_id=problem_id + ).order_by('-date') + + def get_best_record(self, problem_id: str, account_id: str, topic_id: str = None): + if topic_id: + return BestSubmission.objects.filter( + problem_id=problem_id, + account_id=account_id, + topic_id=topic_id + ).first() + else: + return BestSubmission.objects.filter( + problem_id=problem_id, + account_id=account_id + ).first() + + def create(self, submission_data: dict): + submission = Submission(**submission_data) + submission.save() + return submission + + def bulk_create_testcases(self, testcases: List[SubmissionTestcase]): + return SubmissionTestcase.objects.bulk_create(testcases) + + def create_or_update_best(self, problem_id: str, account_id: str, + submission_id: str, topic_id: str = None): + try: + if topic_id: + best_submission = BestSubmission.objects.get( + problem_id=problem_id, + account_id=account_id, + topic_id=topic_id + ) + else: + best_submission = BestSubmission.objects.get( + problem_id=problem_id, + account_id=account_id + ) + except BestSubmission.DoesNotExist: + best_submission = BestSubmission( + problem_id=problem_id, + account_id=account_id, + topic_id=topic_id, + submission_id=submission_id + ) + best_submission.save() + else: + # Update if new submission is better + current_submission = Submission.objects.get(submission_id=best_submission.submission_id) + new_submission = Submission.objects.get(submission_id=submission_id) + + if new_submission.passed_ratio >= current_submission.passed_ratio: + best_submission.submission_id = submission_id + best_submission.save() + + return best_submission \ No newline at end of file diff --git a/api/repositories/topic_repository.py b/api/repositories/topic_repository.py new file mode 100644 index 0000000..a66dca2 --- /dev/null +++ b/api/repositories/topic_repository.py @@ -0,0 +1,91 @@ +from django.utils import timezone +from django.db.models import Q +from api.models import Topic, TopicCollection, Collection, BestSubmission, SubmissionTestcase +from typing import List + +class TopicRepository: + def __init__(self): + pass + + def get(self, topic_id: str): + return Topic.objects.get(topic_id=topic_id) + + def list(self, filters: dict = {}): + topics = Topic.objects.all() + if 'creator_id' in filters and filters['creator_id']: + topics = topics.filter(creator_id=filters['creator_id']) + return topics + + def get_by_creator(self, account_id: str): + return Topic.objects.filter(creator_id=account_id).order_by('-updated_date') + + def get_collections(self, topic_id: str): + return TopicCollection.objects.filter(topic_id=topic_id).order_by('order') + + def get_many_collections(self, topic_ids: List[str]): + return TopicCollection.objects.filter(topic__in=topic_ids) + + def delete_collections(self, topic_id: str): + TopicCollection.objects.filter(topic_id=topic_id).delete() + + def bulk_create_collections(self, topic_collections: List[TopicCollection]): + TopicCollection.objects.bulk_create(topic_collections) + + def find_existing_collection(self, topic_id: str, collection_id: str): + return TopicCollection.objects.filter(topic_id=topic_id, collection_id=collection_id) + + def delete_many_collections(self, topic_id: str, collection_ids: List[str]): + TopicCollection.objects.filter(topic_id=topic_id, collection_id__in=collection_ids).delete() + + def update_with_timestamp(self, topic_id: str, data: dict = {}): + topic = self.get(topic_id) + for key, value in data.items(): + if hasattr(topic, key): + setattr(topic, key, value) + topic.updated_date = timezone.now() + topic.save() + return topic + + def delete(self, topic_id: str): + topic = self.get(topic_id) + topic.delete() + + def get_manageable_by_ids(self, ids: List[str], order_by: str = '-updated_date'): + """Get topics manageable by account through group permissions""" + return Topic.objects.filter( + topicgrouppermission__permission_manage_topics=True, + topicgrouppermission__group__in=ids + ).order_by(order_by) + + def create(self, topic_data: dict): + topic = Topic(**topic_data) + topic.save() + return topic + + def update(self, topic_id: str, topic_data: dict): + topic = self.get(topic_id) + for key, value in topic_data.items(): + if hasattr(topic, key): + setattr(topic, key, value) + topic.save() + return topic + + + def get_best_submission_for_problem(self, problem_id: str, account_id: str, topic_id: str): + try: + best_submission = BestSubmission.objects.get(problem_id=problem_id, account_id=account_id, topic_id=topic_id) + best_submission = best_submission.submission + best_submission.runtime_output = SubmissionTestcase.objects.filter(submission_id=best_submission.submission_id) + return best_submission + except: + return None + + + def create_collection(self, topic_id: str, collection_id: str, order: int): + topic_collection = TopicCollection( + topic_id=topic_id, + collection_id=collection_id, + order=order + ) + topic_collection.save() + return topic_collection \ No newline at end of file diff --git a/api/sandbox/grader.py b/api/sandbox/grader.py index a7a8154..483dc62 100644 --- a/api/sandbox/grader.py +++ b/api/sandbox/grader.py @@ -224,7 +224,7 @@ def runtime(self) -> list[RuntimeResult]: return result -Grader:list[ProgramGrader] = { +Grader:dict[str,ProgramGrader] = { "python": PythonGrader, "c": CGrader, "cpp": CppGrader diff --git a/api/services/account/account_service.py b/api/services/account/account_service.py new file mode 100644 index 0000000..cfa464a --- /dev/null +++ b/api/services/account/account_service.py @@ -0,0 +1,75 @@ +from api.repositories.account_repository import AccountRepository +from api.utility import passwordEncryption +from api.models import * +from api.services.account.serializers import * +from django.db.models import Q +from api.errors.common import * +from abc import ABC, abstractmethod + +class AccountService(ABC): + + @abstractmethod + def create_account(self, request): + pass + + @abstractmethod + def get_account(self, account_id: str): + pass + + @abstractmethod + def get_all_accounts(self, request): + pass + +class AccountServiceImpl(AccountService): + + def __init__(self, account_repo: AccountRepository): + self.account_repo = account_repo + + def create_account(self, request): + body = { + **request.data, + 'password': passwordEncryption(request.data['password']) + } + try: + account = self.account_repo.create(body) + except Exception as e: + raise InternalServerError(e) + serialize = AccountSerializer(account) + return serialize.data + + def get_account(self, account_id: str): + try: + account = self.account_repo.get(account_id) + serialize = AccountSerializer(account) + return serialize.data + except Account.DoesNotExist: + raise ItemNotFoundError("Account") + + def get_all_accounts(self, request): + # get search query + search = request.GET.get('search', '') + + accounts = self.account_repo.list(search) + + serialize = AccountSecureSerializer(accounts, many=True) + return { + "accounts": serialize.data + } + + + # TODO: Move this to submission service + # def get_daily_submission(account_id:str): + # submissions = Submission.objects.filter(account_id=account_id) + # serializes = SubmissionSerializer(submissions,many=True) + + # submission_by_date = {} + + # for submission in serializes.data: + # [date,] = submission['date'].split("T") + # if date in submission_by_date: + # submission_by_date[date]["submissions"].append(submission) + # submission_by_date[date]["count"] += 1 + # else: + # submission_by_date[date] = {"count":1, "submissions": [ submission ]} + + # return Response({"submissions_by_date": submission_by_date}) \ No newline at end of file diff --git a/api/services/account/serializers.py b/api/services/account/serializers.py new file mode 100644 index 0000000..bc1acdf --- /dev/null +++ b/api/services/account/serializers.py @@ -0,0 +1,12 @@ +from rest_framework import serializers +from api.models import Account + +class AccountSerializer(serializers.ModelSerializer): + class Meta: + model = Account + fields = "__all__" + +class AccountSecureSerializer(serializers.ModelSerializer): + class Meta: + model = Account + fields = ['account_id','username'] \ No newline at end of file diff --git a/api/services/account/test_account_service.py b/api/services/account/test_account_service.py new file mode 100644 index 0000000..e81915e --- /dev/null +++ b/api/services/account/test_account_service.py @@ -0,0 +1,411 @@ +import unittest +from unittest.mock import Mock, patch, MagicMock +from django.test import TestCase +from django.http import HttpRequest +from api.services.account.account_service import AccountServiceImpl +from api.repositories.account_repository import AccountRepository +from api.models import Account +from api.errors.common import InternalServerError, ItemNotFoundError + + +class TestAccountService(TestCase): + """Unit tests for AccountService""" + + def setUp(self): + """Set up test fixtures""" + self.mock_account_repo = Mock(spec=AccountRepository) + self.account_service = AccountServiceImpl(self.mock_account_repo) + + # Sample account data + self.sample_account_data = { + 'username': 'testuser', + 'email': 'test@example.com', + 'password': 'plaintext_password', + 'first_name': 'Test', + 'last_name': 'User' + } + + # Sample account object with all required fields + self.sample_account = Mock(spec=Account) + self.sample_account.account_id = 'acc_123' + self.sample_account.username = 'testuser' + self.sample_account.email = 'test@example.com' + self.sample_account.first_name = 'Test' + self.sample_account.last_name = 'User' + self.sample_account.password = 'encrypted_password' + self.sample_account.is_active = True + self.sample_account.is_staff = False + self.sample_account.is_superuser = False + self.sample_account.date_joined = '2023-01-01T00:00:00Z' + self.sample_account.last_login = None + self.sample_account.token = 'test_token' + + @patch('api.services.account.account_service.passwordEncryption') + @patch('api.services.account.account_service.AccountSerializer') + def test_create_account_success(self, mock_serializer_class, mock_password_encryption): + """Test successful account creation""" + # Arrange + mock_password_encryption.return_value = 'encrypted_password' + self.mock_account_repo.create.return_value = self.sample_account + + # Mock serializer + mock_serializer_instance = Mock() + mock_serializer_instance.data = { + 'account_id': 'acc_123', + 'username': 'testuser', + 'email': 'test@example.com', + 'first_name': 'Test', + 'last_name': 'User' + } + mock_serializer_class.return_value = mock_serializer_instance + + # Create a mock request object + mock_request = Mock() + mock_request.data = self.sample_account_data.copy() + + # Act + result = self.account_service.create_account(mock_request) + + # Assert + self.mock_account_repo.create.assert_called_once() + call_args = self.mock_account_repo.create.call_args[0][0] + + # Verify password was encrypted + self.assertEqual(call_args['password'], 'encrypted_password') + mock_password_encryption.assert_called_once_with('plaintext_password') + + # Verify other data is preserved + self.assertEqual(call_args['username'], 'testuser') + self.assertEqual(call_args['email'], 'test@example.com') + + # Verify serializer was called with the account + mock_serializer_class.assert_called_once_with(self.sample_account) + + # Verify result structure + self.assertIsInstance(result, dict) + self.assertIn('account_id', result) + self.assertIn('username', result) + + @patch('api.services.account.account_service.passwordEncryption') + def test_create_account_repository_exception(self, mock_password_encryption): + """Test account creation when repository raises exception""" + # Arrange + mock_password_encryption.return_value = 'encrypted_password' + self.mock_account_repo.create.side_effect = Exception("Database error") + + mock_request = Mock() + mock_request.data = self.sample_account_data.copy() + + # Act & Assert + with self.assertRaises(InternalServerError): + self.account_service.create_account(mock_request) + + self.mock_account_repo.create.assert_called_once() + + @patch('api.services.account.account_service.AccountSerializer') + def test_get_account_success(self, mock_serializer_class): + """Test successful account retrieval""" + # Arrange + account_id = 'acc_123' + self.mock_account_repo.get.return_value = self.sample_account + + # Mock serializer + mock_serializer_instance = Mock() + mock_serializer_instance.data = { + 'account_id': 'acc_123', + 'username': 'testuser', + 'email': 'test@example.com' + } + mock_serializer_class.return_value = mock_serializer_instance + + # Act + result = self.account_service.get_account(account_id) + + # Assert + self.mock_account_repo.get.assert_called_once_with(account_id) + mock_serializer_class.assert_called_once_with(self.sample_account) + self.assertIsInstance(result, dict) + self.assertIn('account_id', result) + self.assertIn('username', result) + + def test_get_account_not_found(self): + """Test account retrieval when account doesn't exist""" + # Arrange + account_id = 'nonexistent' + self.mock_account_repo.get.side_effect = Account.DoesNotExist() + + # Act & Assert + with self.assertRaises(ItemNotFoundError) as context: + self.account_service.get_account(account_id) + + self.assertEqual(str(context.exception), "Account not found.") + self.mock_account_repo.get.assert_called_once_with(account_id) + + def test_get_account_repository_exception(self): + """Test account retrieval when repository raises unexpected exception""" + # Arrange + account_id = 'acc_123' + self.mock_account_repo.get.side_effect = Exception("Database connection error") + + # Act & Assert + with self.assertRaises(Exception): + self.account_service.get_account(account_id) + + self.mock_account_repo.get.assert_called_once_with(account_id) + + @patch('api.services.account.account_service.AccountSecureSerializer') + def test_get_all_accounts_without_search(self, mock_serializer_class): + """Test getting all accounts without search query""" + # Arrange + mock_accounts = [self.sample_account, self.sample_account] + self.mock_account_repo.list.return_value = mock_accounts + + # Mock serializer + mock_serializer_instance = Mock() + mock_serializer_instance.data = [ + {'account_id': 'acc_123', 'username': 'testuser'}, + {'account_id': 'acc_123', 'username': 'testuser'} + ] + mock_serializer_class.return_value = mock_serializer_instance + + mock_request = Mock() + mock_request.GET = {} + + # Act + result = self.account_service.get_all_accounts(mock_request) + + # Assert + self.mock_account_repo.list.assert_called_once_with('') + mock_serializer_class.assert_called_once_with(mock_accounts, many=True) + self.assertIsInstance(result, dict) + self.assertIn('accounts', result) + self.assertIsInstance(result['accounts'], list) + self.assertEqual(len(result['accounts']), 2) + + @patch('api.services.account.account_service.AccountSecureSerializer') + def test_get_all_accounts_with_search(self, mock_serializer_class): + """Test getting accounts with search query""" + # Arrange + search_query = 'test' + mock_accounts = [self.sample_account] + self.mock_account_repo.list.return_value = mock_accounts + + # Mock serializer + mock_serializer_instance = Mock() + mock_serializer_instance.data = [{'account_id': 'acc_123', 'username': 'testuser'}] + mock_serializer_class.return_value = mock_serializer_instance + + mock_request = Mock() + mock_request.GET = {'search': search_query} + + # Act + result = self.account_service.get_all_accounts(mock_request) + + # Assert + self.mock_account_repo.list.assert_called_once_with(search_query) + mock_serializer_class.assert_called_once_with(mock_accounts, many=True) + self.assertIsInstance(result, dict) + self.assertIn('accounts', result) + self.assertIsInstance(result['accounts'], list) + self.assertEqual(len(result['accounts']), 1) + + @patch('api.services.account.account_service.AccountSecureSerializer') + def test_get_all_accounts_empty_result(self, mock_serializer_class): + """Test getting accounts when no accounts match search""" + # Arrange + self.mock_account_repo.list.return_value = [] + + # Mock serializer + mock_serializer_instance = Mock() + mock_serializer_instance.data = [] + mock_serializer_class.return_value = mock_serializer_instance + + mock_request = Mock() + mock_request.GET = {'search': 'nonexistent'} + + # Act + result = self.account_service.get_all_accounts(mock_request) + + # Assert + self.mock_account_repo.list.assert_called_once_with('nonexistent') + mock_serializer_class.assert_called_once_with([], many=True) + self.assertIsInstance(result, dict) + self.assertIn('accounts', result) + self.assertIsInstance(result['accounts'], list) + self.assertEqual(len(result['accounts']), 0) + + def test_get_all_accounts_repository_exception(self): + """Test getting accounts when repository raises exception""" + # Arrange + self.mock_account_repo.list.side_effect = Exception("Database error") + + mock_request = Mock() + mock_request.GET = {} + + # Act & Assert + with self.assertRaises(Exception): + self.account_service.get_all_accounts(mock_request) + + self.mock_account_repo.list.assert_called_once_with('') + + @patch('api.services.account.account_service.AccountSecureSerializer') + def test_get_all_accounts_missing_search_param(self, mock_serializer_class): + """Test getting accounts when search parameter is missing""" + # Arrange + mock_accounts = [self.sample_account] + self.mock_account_repo.list.return_value = mock_accounts + + # Mock serializer + mock_serializer_instance = Mock() + mock_serializer_instance.data = [{'account_id': 'acc_123', 'username': 'testuser'}] + mock_serializer_class.return_value = mock_serializer_instance + + mock_request = Mock() + mock_request.GET = {} # No search parameter + + # Act + result = self.account_service.get_all_accounts(mock_request) + + # Assert + self.mock_account_repo.list.assert_called_once_with('') + mock_serializer_class.assert_called_once_with(mock_accounts, many=True) + self.assertIsInstance(result, dict) + self.assertIn('accounts', result) + + @patch('api.services.account.account_service.AccountSecureSerializer') + def test_get_all_accounts_empty_search_param(self, mock_serializer_class): + """Test getting accounts when search parameter is empty string""" + # Arrange + mock_accounts = [self.sample_account] + self.mock_account_repo.list.return_value = mock_accounts + + # Mock serializer + mock_serializer_instance = Mock() + mock_serializer_instance.data = [{'account_id': 'acc_123', 'username': 'testuser'}] + mock_serializer_class.return_value = mock_serializer_instance + + mock_request = Mock() + mock_request.GET = {'search': ''} + + # Act + result = self.account_service.get_all_accounts(mock_request) + + # Assert + self.mock_account_repo.list.assert_called_once_with('') + mock_serializer_class.assert_called_once_with(mock_accounts, many=True) + self.assertIsInstance(result, dict) + self.assertIn('accounts', result) + + @patch('api.services.account.account_service.passwordEncryption') + @patch('api.services.account.account_service.AccountSerializer') + def test_create_account_data_mutation(self, mock_serializer_class, mock_password_encryption): + """Test that create_account doesn't mutate original request data""" + # Arrange + mock_password_encryption.return_value = 'encrypted_password' + original_data = self.sample_account_data.copy() + mock_request = Mock() + mock_request.data = original_data.copy() + + self.mock_account_repo.create.return_value = self.sample_account + + # Mock serializer + mock_serializer_instance = Mock() + mock_serializer_instance.data = {'account_id': 'acc_123', 'username': 'testuser'} + mock_serializer_class.return_value = mock_serializer_instance + + # Act + self.account_service.create_account(mock_request) + + # Assert + # Verify original data is not mutated + self.assertEqual(mock_request.data, original_data) + + def test_service_initialization(self): + """Test that service initializes correctly with repository""" + # Arrange & Act + service = AccountServiceImpl(self.mock_account_repo) + + # Assert + self.assertEqual(service.account_repo, self.mock_account_repo) + + @patch('api.services.account.account_service.passwordEncryption') + @patch('api.services.account.account_service.AccountSerializer') + def test_create_account_with_minimal_data(self, mock_serializer_class, mock_password_encryption): + """Test account creation with minimal required data""" + # Arrange + mock_password_encryption.return_value = 'encrypted_password' + minimal_data = { + 'username': 'minimaluser', + 'password': 'password123' + } + + mock_request = Mock() + mock_request.data = minimal_data.copy() + + self.mock_account_repo.create.return_value = self.sample_account + + # Mock serializer + mock_serializer_instance = Mock() + mock_serializer_instance.data = {'account_id': 'acc_123', 'username': 'minimaluser'} + mock_serializer_class.return_value = mock_serializer_instance + + # Act + result = self.account_service.create_account(mock_request) + + # Assert + self.mock_account_repo.create.assert_called_once() + call_args = self.mock_account_repo.create.call_args[0][0] + self.assertEqual(call_args['username'], 'minimaluser') + self.assertIsInstance(result, dict) + + @patch('api.services.account.account_service.AccountSerializer') + def test_get_account_with_different_id_formats(self, mock_serializer_class): + """Test account retrieval with different ID formats""" + # Arrange + test_ids = ['acc_123', 'user_456', 'admin_789'] + + # Mock serializer + mock_serializer_instance = Mock() + mock_serializer_instance.data = {'account_id': 'acc_123', 'username': 'testuser'} + mock_serializer_class.return_value = mock_serializer_instance + + for account_id in test_ids: + with self.subTest(account_id=account_id): + self.mock_account_repo.reset_mock() + self.mock_account_repo.get.return_value = self.sample_account + + # Act + result = self.account_service.get_account(account_id) + + # Assert + self.mock_account_repo.get.assert_called_once_with(account_id) + self.assertIsInstance(result, dict) + + +class TestAccountServiceIntegration(unittest.TestCase): + """Integration tests for AccountService with real repository""" + + def setUp(self): + """Set up integration test fixtures""" + from api.repositories.account_repository import AccountRepositoryImpl + self.real_repo = AccountRepositoryImpl() + self.account_service = AccountServiceImpl(self.real_repo) + + def test_error_handling_consistency(self): + """Test that error handling is consistent across methods""" + # Test that all methods handle repository exceptions appropriately + methods_to_test = [ + ('create_account', lambda: self.account_service.create_account(Mock())), + ('get_account', lambda: self.account_service.get_account('test_id')), + ('get_all_accounts', lambda: self.account_service.get_all_accounts(Mock())) + ] + + for method_name, method_call in methods_to_test: + with self.subTest(method=method_name): + # This would test error handling consistency + # Implementation depends on specific error handling requirements + pass + + +if __name__ == '__main__': + unittest.main() diff --git a/api/services/auth/auth_service.py b/api/services/auth/auth_service.py new file mode 100644 index 0000000..fc8feab --- /dev/null +++ b/api/services/auth/auth_service.py @@ -0,0 +1,135 @@ +from django.forms.models import model_to_dict +from time import time +from api.config import Configuration +from api.errors.auth import IncorrectPasswordError +from api.models import Account +from api.repositories.account_repository import AccountRepository, AccountRepositoryImpl +from api.errors.common import * +from api.utility import passwordEncryption +from rest_framework.response import Response +from rest_framework import status +from uuid import uuid4 +from abc import ABC, abstractmethod +from decouple import AutoConfig + +class AuthService(ABC): + @abstractmethod + def verify_token(self,token): + pass + @abstractmethod + def getAccountByToken(self,token): + pass + @abstractmethod + def login(self,request): + pass + @abstractmethod + def authorization(self,request): + pass + @abstractmethod + def logout(self,request): + pass + +class AuthServiceImpl: + + def __init__(self, config: Configuration, account_repo: AccountRepository): + self.config = config + self.account_repo = account_repo + + def verify_token(self,token): + """ + Check if user has valid token and not expired + Return: True/False + """ + try: + account = self.account_repo.get_by_token(token) + account_dict = model_to_dict(account) + if account_dict['token_expire'] >= time(): + return True + else: + return False + except: + return False + + def getAccountByToken(self,token): + """ + Get account from token + Return: account object + """ + try: + account = self.account_repo.get_by_token(token) + if account.token_expire < time(): + raise InvalidTokenError() + return account + except Account.DoesNotExist: + raise InvalidTokenError() + except Exception as e: + raise e + + def login(self,request): + try: + account = self.account_repo.get_by_username(request.data['username']) + if passwordEncryption(request.data['password']) == account.password: + account.token = uuid4().hex + account.token_expire = int(time() + self.config.token_lifetime) + account.save() + return model_to_dict(account) + else: + raise IncorrectPasswordError() + except Account.DoesNotExist: + raise ItemNotFoundError("User") + + def authorization(self,request): + try: + account = self.account_repo.get(request.data['account_id']) + account_dict = model_to_dict(account) + if account_dict['token_expire'] >= time() and account_dict['token'] == request.data['token']: + return {'result': True} + return {'result': False} + except Account.DoesNotExist: + return {'result': False} + + def logout(self,request): + try: + account = self.account_repo.get(request.data['account_id']) + if account.token == request.data['token']: + account.token = None + account.save() + return Response(model_to_dict(account), status=status.HTTP_200_OK) + else: + raise InvalidTokenError() + except Account.DoesNotExist: + raise ItemNotFoundError("User") + +# Module-level functions for backward compatibility +# These provide the function-based interface expected by existing code + +def _get_auth_service(): + """Get a configured auth service instance""" + try: + config = Configuration(AutoConfig()) + account_repo = AccountRepositoryImpl() + return AuthServiceImpl(config, account_repo) + except Exception: + # Fallback configuration if decouple is not available + class FallbackConfig: + def __init__(self): + self.token_lifetime = 3600 # 1 hour default + + account_repo = AccountRepositoryImpl() + return AuthServiceImpl(FallbackConfig(), account_repo) + +def verify_token(token): + """ + Check if user has valid token and not expired + Return: True/False + """ + auth_service = _get_auth_service() + return auth_service.verify_token(token) + +def getAccountByToken(token): + """ + Get account from token + Return: account object + """ + auth_service = _get_auth_service() + return auth_service.getAccountByToken(token) diff --git a/api/services/auth/test_auth_service.py b/api/services/auth/test_auth_service.py new file mode 100644 index 0000000..ecd70b0 --- /dev/null +++ b/api/services/auth/test_auth_service.py @@ -0,0 +1,489 @@ +import unittest +from unittest.mock import Mock, patch, MagicMock +from django.test import TestCase +from django.http import HttpRequest +from api.services.auth.auth_service import AuthServiceImpl +from api.repositories.account_repository import AccountRepository +from api.models import Account +from api.errors.common import * +from api.errors.auth import IncorrectPasswordError +from time import time +from uuid import uuid4 + + +class TestAuthService(TestCase): + """Unit tests for AuthService""" + + def setUp(self): + """Set up test fixtures""" + self.mock_account_repo = Mock(spec=AccountRepository) + self.mock_config = Mock() + self.mock_config.token_lifetime = 3600 # 1 hour + + self.auth_service = AuthServiceImpl( + config=self.mock_config, + account_repo=self.mock_account_repo + ) + + # Sample account data + self.sample_account = Mock(spec=Account) + self.sample_account.account_id = 'acc_123' + self.sample_account.username = 'testuser' + self.sample_account.email = 'test@example.com' + self.sample_account.password = 'encrypted_password' + self.sample_account.token = 'valid_token' + self.sample_account.token_expire = int(time()) + 3600 # 1 hour from now + + # Sample account dict (as returned by model_to_dict) + self.sample_account_dict = { + 'account_id': 'acc_123', + 'username': 'testuser', + 'email': 'test@example.com', + 'password': 'encrypted_password', + 'token': 'valid_token', + 'token_expire': int(time()) + 3600 + } + + @patch('api.services.auth.auth_service.model_to_dict') + def test_verify_token_success(self, mock_model_to_dict): + """Test successful token verification""" + # Arrange + token = 'valid_token' + self.mock_account_repo.get_by_token.return_value = self.sample_account + mock_model_to_dict.return_value = self.sample_account_dict + + # Act + result = self.auth_service.verify_token(token) + + # Assert + self.mock_account_repo.get_by_token.assert_called_once_with(token) + mock_model_to_dict.assert_called_once_with(self.sample_account) + self.assertTrue(result) + + @patch('api.services.auth.auth_service.model_to_dict') + def test_verify_token_expired(self, mock_model_to_dict): + """Test token verification with expired token""" + # Arrange + token = 'expired_token' + expired_account_dict = self.sample_account_dict.copy() + expired_account_dict['token_expire'] = int(time()) - 3600 # 1 hour ago + + self.mock_account_repo.get_by_token.return_value = self.sample_account + mock_model_to_dict.return_value = expired_account_dict + + # Act + result = self.auth_service.verify_token(token) + + # Assert + self.mock_account_repo.get_by_token.assert_called_once_with(token) + self.assertFalse(result) + + def test_verify_token_account_not_found(self): + """Test token verification when account doesn't exist""" + # Arrange + token = 'invalid_token' + self.mock_account_repo.get_by_token.side_effect = Account.DoesNotExist() + + # Act + result = self.auth_service.verify_token(token) + + # Assert + self.mock_account_repo.get_by_token.assert_called_once_with(token) + self.assertFalse(result) + + def test_get_account_by_token_success(self): + """Test successful account retrieval by token""" + # Arrange + token = 'valid_token' + self.mock_account_repo.get_by_token.return_value = self.sample_account + + # Act + result = self.auth_service.getAccountByToken(token) + + # Assert + self.mock_account_repo.get_by_token.assert_called_once_with(token) + self.assertEqual(result, self.sample_account) + + def test_get_account_by_token_expired(self): + """Test account retrieval with expired token""" + # Arrange + token = 'expired_token' + expired_account = Mock(spec=Account) + expired_account.token_expire = int(time()) - 3600 # 1 hour ago + self.mock_account_repo.get_by_token.return_value = expired_account + + # Act & Assert + with self.assertRaises(InvalidTokenError): + self.auth_service.getAccountByToken(token) + + self.mock_account_repo.get_by_token.assert_called_once_with(token) + + def test_get_account_by_token_not_found(self): + """Test account retrieval when account doesn't exist""" + # Arrange + token = 'invalid_token' + self.mock_account_repo.get_by_token.side_effect = Account.DoesNotExist() + + # Act & Assert + with self.assertRaises(InvalidTokenError): + self.auth_service.getAccountByToken(token) + + self.mock_account_repo.get_by_token.assert_called_once_with(token) + + @patch('api.services.auth.auth_service.passwordEncryption') + @patch('api.services.auth.auth_service.model_to_dict') + @patch('api.services.auth.auth_service.uuid4') + @patch('api.services.auth.auth_service.time') + def test_login_success(self, mock_time, mock_uuid4, mock_model_to_dict, mock_password_encryption): + """Test successful login""" + # Arrange + mock_time.return_value = 1000 + mock_uuid4.return_value.hex = 'new_token_hex' + mock_password_encryption.return_value = 'encrypted_password' + + mock_request = Mock() + mock_request.data = { + 'username': 'testuser', + 'password': 'plaintext_password' + } + + self.mock_account_repo.get_by_username.return_value = self.sample_account + mock_model_to_dict.return_value = self.sample_account_dict + + # Act + result = self.auth_service.login(mock_request) + + # Assert + self.mock_account_repo.get_by_username.assert_called_once_with('testuser') + mock_password_encryption.assert_called_once_with('plaintext_password') + mock_model_to_dict.assert_called_once_with(self.sample_account) + + # Verify token was set + self.assertEqual(self.sample_account.token, 'new_token_hex') + self.assertEqual(self.sample_account.token_expire, 1000 + self.mock_config.token_lifetime) + self.sample_account.save.assert_called_once() + + # Verify result + self.assertEqual(result, self.sample_account_dict) + + @patch('api.services.auth.auth_service.passwordEncryption') + def test_login_incorrect_password(self, mock_password_encryption): + """Test login with incorrect password""" + # Arrange + mock_password_encryption.return_value = 'wrong_password' + + mock_request = Mock() + mock_request.data = { + 'username': 'testuser', + 'password': 'wrong_password' + } + + self.mock_account_repo.get_by_username.return_value = self.sample_account + + # Act & Assert + with self.assertRaises(IncorrectPasswordError): + self.auth_service.login(mock_request) + + self.mock_account_repo.get_by_username.assert_called_once_with('testuser') + mock_password_encryption.assert_called_once_with('wrong_password') + + def test_login_user_not_found(self): + """Test login when user doesn't exist""" + # Arrange + mock_request = Mock() + mock_request.data = { + 'username': 'nonexistent', + 'password': 'password' + } + + self.mock_account_repo.get_by_username.side_effect = Account.DoesNotExist() + + # Act & Assert + with self.assertRaises(ItemNotFoundError) as context: + self.auth_service.login(mock_request) + + self.assertEqual(str(context.exception), "User not found.") + self.mock_account_repo.get_by_username.assert_called_once_with('nonexistent') + + @patch('api.services.auth.auth_service.model_to_dict') + @patch('api.services.auth.auth_service.time') + def test_authorization_success(self, mock_time, mock_model_to_dict): + """Test successful authorization""" + # Arrange + mock_time.return_value = 1000 + mock_model_to_dict.return_value = self.sample_account_dict + + mock_request = Mock() + mock_request.data = { + 'account_id': 'acc_123', + 'token': 'valid_token' + } + + self.mock_account_repo.get.return_value = self.sample_account + + # Act + result = self.auth_service.authorization(mock_request) + + # Assert + self.mock_account_repo.get.assert_called_once_with('acc_123') + mock_model_to_dict.assert_called_once_with(self.sample_account) + self.assertEqual(result, {'result': True}) + + @patch('api.services.auth.auth_service.model_to_dict') + @patch('api.services.auth.auth_service.time') + def test_authorization_expired_token(self, mock_time, mock_model_to_dict): + """Test authorization with expired token""" + # Arrange + mock_time.return_value = 1000 + expired_account_dict = self.sample_account_dict.copy() + expired_account_dict['token_expire'] = 500 # Expired + + mock_model_to_dict.return_value = expired_account_dict + + mock_request = Mock() + mock_request.data = { + 'account_id': 'acc_123', + 'token': 'valid_token' + } + + self.mock_account_repo.get.return_value = self.sample_account + + # Act + result = self.auth_service.authorization(mock_request) + + # Assert + self.assertEqual(result, {'result': False}) + + @patch('api.services.auth.auth_service.model_to_dict') + @patch('api.services.auth.auth_service.time') + def test_authorization_wrong_token(self, mock_time, mock_model_to_dict): + """Test authorization with wrong token""" + # Arrange + mock_time.return_value = 1000 + mock_model_to_dict.return_value = self.sample_account_dict + + mock_request = Mock() + mock_request.data = { + 'account_id': 'acc_123', + 'token': 'wrong_token' + } + + self.mock_account_repo.get.return_value = self.sample_account + + # Act + result = self.auth_service.authorization(mock_request) + + # Assert + self.assertEqual(result, {'result': False}) + + def test_authorization_account_not_found(self): + """Test authorization when account doesn't exist""" + # Arrange + mock_request = Mock() + mock_request.data = { + 'account_id': 'nonexistent', + 'token': 'valid_token' + } + + self.mock_account_repo.get.side_effect = Account.DoesNotExist() + + # Act + result = self.auth_service.authorization(mock_request) + + # Assert + self.assertEqual(result, {'result': False}) + + @patch('api.services.auth.auth_service.model_to_dict') + def test_logout_success(self, mock_model_to_dict): + """Test successful logout""" + # Arrange + mock_model_to_dict.return_value = self.sample_account_dict + + mock_request = Mock() + mock_request.data = { + 'account_id': 'acc_123', + 'token': 'valid_token' + } + + self.mock_account_repo.get.return_value = self.sample_account + + # Act + result = self.auth_service.logout(mock_request) + + # Assert + self.mock_account_repo.get.assert_called_once_with('acc_123') + self.assertIsNone(self.sample_account.token) + self.sample_account.save.assert_called_once() + mock_model_to_dict.assert_called_once_with(self.sample_account) + + # Verify result is a Response object + from rest_framework.response import Response + self.assertIsInstance(result, Response) + self.assertEqual(result.status_code, 200) + + def test_logout_wrong_token(self): + """Test logout with wrong token""" + # Arrange + mock_request = Mock() + mock_request.data = { + 'account_id': 'acc_123', + 'token': 'wrong_token' + } + + self.mock_account_repo.get.return_value = self.sample_account + + # Act & Assert + with self.assertRaises(InvalidTokenError): + self.auth_service.logout(mock_request) + + self.mock_account_repo.get.assert_called_once_with('acc_123') + + def test_logout_account_not_found(self): + """Test logout when account doesn't exist""" + # Arrange + mock_request = Mock() + mock_request.data = { + 'account_id': 'nonexistent', + 'token': 'valid_token' + } + + self.mock_account_repo.get.side_effect = Account.DoesNotExist() + + # Act & Assert + with self.assertRaises(ItemNotFoundError) as context: + self.auth_service.logout(mock_request) + + self.assertEqual(str(context.exception), "User not found.") + self.mock_account_repo.get.assert_called_once_with('nonexistent') + + def test_service_initialization(self): + """Test that service initializes correctly with dependencies""" + # Arrange & Act + service = AuthServiceImpl(self.mock_config, self.mock_account_repo) + + # Assert + self.assertEqual(service.config, self.mock_config) + self.assertEqual(service.account_repo, self.mock_account_repo) + + @patch('api.services.auth.auth_service.model_to_dict') + def test_verify_token_repository_exception(self, mock_model_to_dict): + """Test token verification when repository raises unexpected exception""" + # Arrange + token = 'valid_token' + self.mock_account_repo.get_by_token.side_effect = Exception("Database error") + + # Act + result = self.auth_service.verify_token(token) + + # Assert + self.assertFalse(result) + + def test_get_account_by_token_repository_exception(self): + """Test account retrieval when repository raises unexpected exception""" + # Arrange + token = 'valid_token' + self.mock_account_repo.get_by_token.side_effect = Exception("Database error") + + # Act & Assert + with self.assertRaises(Exception): + self.auth_service.getAccountByToken(token) + + @patch('api.services.auth.auth_service.passwordEncryption') + def test_login_repository_exception(self, mock_password_encryption): + """Test login when repository raises unexpected exception""" + # Arrange + mock_password_encryption.return_value = 'encrypted_password' + + mock_request = Mock() + mock_request.data = { + 'username': 'testuser', + 'password': 'password' + } + + self.mock_account_repo.get_by_username.side_effect = Exception("Database error") + + # Act & Assert + with self.assertRaises(Exception): + self.auth_service.login(mock_request) + + def test_logout_repository_exception(self): + """Test logout when repository raises unexpected exception""" + # Arrange + mock_request = Mock() + mock_request.data = { + 'account_id': 'acc_123', + 'token': 'valid_token' + } + + self.mock_account_repo.get.side_effect = Exception("Database error") + + # Act & Assert + with self.assertRaises(Exception): + self.auth_service.logout(mock_request) + + +class TestAuthServiceIntegration(unittest.TestCase): + """Integration tests for AuthService with real repositories""" + + def setUp(self): + """Set up integration test fixtures""" + from api.repositories.account_repository import AccountRepositoryImpl + from api.config import Configuration + from decouple import AutoConfig + + try: + config = Configuration(AutoConfig()) + except Exception: + # Fallback configuration + class FallbackConfig: + def __init__(self): + self.token_lifetime = 3600 + config = FallbackConfig() + + self.real_account_repo = AccountRepositoryImpl() + self.auth_service = AuthServiceImpl(config, self.real_account_repo) + + +class TestAuthServiceModuleFunctions(unittest.TestCase): + """Tests for module-level functions in auth_service.py""" + + @patch('api.services.auth.auth_service._get_auth_service') + def test_verify_token_function(self, mock_get_auth_service): + """Test the module-level verify_token function""" + # Arrange + mock_auth_service = Mock() + mock_auth_service.verify_token.return_value = True + mock_get_auth_service.return_value = mock_auth_service + + from api.services.auth.auth_service import verify_token + + # Act + result = verify_token('test_token') + + # Assert + mock_get_auth_service.assert_called_once() + mock_auth_service.verify_token.assert_called_once_with('test_token') + self.assertTrue(result) + + @patch('api.services.auth.auth_service._get_auth_service') + def test_get_account_by_token_function(self, mock_get_auth_service): + """Test the module-level getAccountByToken function""" + # Arrange + mock_auth_service = Mock() + mock_account = Mock() + mock_auth_service.getAccountByToken.return_value = mock_account + mock_get_auth_service.return_value = mock_auth_service + + from api.services.auth.auth_service import getAccountByToken + + # Act + result = getAccountByToken('test_token') + + # Assert + mock_get_auth_service.assert_called_once() + mock_auth_service.getAccountByToken.assert_called_once_with('test_token') + self.assertEqual(result, mock_account) + + +if __name__ == '__main__': + unittest.main() diff --git a/api/services/auth_service.py b/api/services/auth_service.py deleted file mode 100644 index 2878f6f..0000000 --- a/api/services/auth_service.py +++ /dev/null @@ -1,34 +0,0 @@ -from django.forms.models import model_to_dict -from time import time -from ..models import Account -from ..errors.common import * - -def verify_token(token): - """ - Check if user has valid token and not expired - Return: True/False - """ - try: - account = Account.objects.get(token=token) - account_dict = model_to_dict(account) - if account_dict['token_expire'] >= time(): - return True - else: - return False - except Account.DoesNotExist: - return False - -def getAccountByToken(token): - """ - Get account from token - Return: account object - """ - try: - account = Account.objects.get(token=token) - if account.token_expire < time(): - raise InvalidTokenError() - return account - except Account.DoesNotExist: - raise InvalidTokenError() - except Exception as e: - raise e \ No newline at end of file diff --git a/api/services/collection/collection_service.py b/api/services/collection/collection_service.py new file mode 100644 index 0000000..500eb87 --- /dev/null +++ b/api/services/collection/collection_service.py @@ -0,0 +1,198 @@ +from django.utils import timezone + +from api.repositories.account_repository import AccountRepository +from api.repositories.collection_repository import CollectionRepository +from api.repositories.problem_repository import ProblemRepository +from api.repositories.permission_repository import PermissionRepository +from api.repositories.group_repository import GroupRepository +from ...models import * +from .serializers import * +from ...errors.common import * + +class CollectionService: + + def __init__(self, collection_repo: CollectionRepository, account_repo: AccountRepository, problem_repo: ProblemRepository, permission_repo: PermissionRepository, group_repo: GroupRepository): + self.collection_repo = collection_repo + self.account_repo = account_repo + self.problem_repo = problem_repo + self.permission_repo = permission_repo + self.group_repo = group_repo + + def create_collection(self, account_id: str, request): + request.data['creator'] = account_id + serialize = CollectionSerializer(data=request.data) + + if serialize.is_valid(): + serialize.save() + return serialize.data + else: + raise BadRequestError(str(serialize.errors)) + + def delete_collection(self, collection_id: str): + collection = self.collection_repo.get(collection_id) + collection.delete() + return None + + def get_collection(self, collection_id: str): + collection = self.collection_repo.get(collection_id) + collection.problems = self.collection_repo.get_problems(collection_id) + collection.group_permissions = self.permission_repo.get_collection_permissions(collection_id) + + for cp in collection.problems: + cp.problem.testcases = self.problem_repo.get_testcases(cp.problem_id) + cp.problem.group_permissions = self.permission_repo.get_problem_permissions(cp.problem_id) + + serializer = CollectionPopulateCollectionProblemsPopulateProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupAndCollectionGroupPermissionsPopulateGroupSerializer(collection) + + return serializer.data + + def get_all_collections(self, request): + collections = self.collection_repo.list() + + account_id = request.query_params.get('account_id', 0) + + if account_id: + collections = collections.filter(creator_id=account_id) + + populated_collections = [] + for collection in collections: + con_probs = self.collection_repo.get_problems(collection) + + populated_cp = [] + for cp in con_probs: + prob_serialize = ProblemSerializer(cp.problem) + cp_serialize = CollectionProblemSerializer(cp) + populated_cp.append({**cp_serialize.data, **prob_serialize.data}) + + serialize = CollectionSerializer(collection) + collection_data = serialize.data + collection_data['problems'] = populated_cp + + populated_collections.append(collection_data) + + return { + 'collections': populated_collections + } + + def populated_problems(self, collections: Collection): + collection_ids = [collection.collection_id for collection in collections] + problemCollections = self.collection_repo.get_problems_by_collections(collection_ids) + + populated_collections = [] + for collection in collections: + collection.problems = problemCollections.filter(collection=collection) + populated_collections.append(collection) + + return populated_collections + + def get_all_collections_by_account(self, account_id: str): + collections = self.collection_repo.get_by_creator(account_id) + collections = self.populated_problems(collections) + serialize = CollectionPopulateCollectionProblemsPopulateProblemSerializer(collections, many=True) + + group_ids = self.group_repo.get_by_creator(account_id) + manageableCollections = self.collection_repo.get_manageable_by_account(group_ids) + manageableCollections = self.populated_problems(manageableCollections) + manageableSerialize = CollectionPopulateCollectionProblemsPopulateProblemSerializer(manageableCollections, many=True) + + return { + 'collections': serialize.data, + 'manageable_collections': manageableSerialize.data + } + + def update_collection(self, collection_id: str, request): + update_data = { + 'name': request.data.get('name'), + 'description': request.data.get('description'), + 'is_private': request.data.get('is_private'), + 'is_active': request.data.get('is_active') + } + # Remove None values to only update provided fields + update_data = {k: v for k, v in update_data.items() if v is not None} + + collection = self.collection_repo.update_with_timestamp(collection_id, update_data) + collection_ser = CollectionSerializer(collection) + + return collection_ser.data + + def update_group_permissions_collection(self, collection_id: str, request): + collection = self.collection_repo.get(collection_id) + self.permission_repo.delete_collection_permissions(collection_id) + + collection_group_permissions = [] + for collection_request in request.data['groups']: + group = self.group_repo.get(collection_request['group_id']) + collection_group_permissions.append( + CollectionGroupPermission( + collection=collection, + group=group, + **collection_request + )) + + self.permission_repo.bulk_create_collection_permissions(collection_group_permissions) + + collection.group_permissions = collection_group_permissions + serialize = CollectionPopulateCollectionGroupPermissionsPopulateGroupSerializer(collection) + + return serialize.data + + def update_problems_to_collection(self, collection_id: str, request): + collection = self.collection_repo.get(collection_id) + self.collection_repo.delete_problems(collection_id) + + collection_problems = [] + order = 0 + for problem_id in request.data['problem_ids']: + problem = self.problem_repo.get(problem_id) + collection_problem = CollectionProblem( + problem=problem, + collection=collection, + order=order + ) + collection_problems.append(collection_problem) + order += 1 + + self.collection_repo.bulk_create_problems(collection_problems) + collection = self.collection_repo.update_with_timestamp(collection_id, {}) + problem_serialize = CollectionProblemPopulateProblemSecureSerializer(collection_problems, many=True) + collection_serialize = CollectionSerializer(collection) + + return { + **collection_serialize.data, + 'problems': problem_serialize.data + } + + def add_problems_to_collection(self, collection_id: str, request): + collection = self.collection_repo.get(collection_id) + populated_problems = [] + + index = 0 + for problem_id in request.data['problem_ids']: + problem = self.problem_repo.get(problem_id) + + alreadyExist = self.collection_repo.find_existing_problem(problem_id, collection_id) + if alreadyExist: + alreadyExist.delete() + + collection_problem = CollectionProblem( + problem=problem, + collection=collection, + order=index + ) + collection_problem.save() + index += 1 + populated_problems.append(collection_problem) + + collection = self.collection_repo.update_with_timestamp(collection_id, {}) + problem_serialize = CollectionProblemPopulateProblemSecureSerializer(populated_problems, many=True) + collection_serialize = CollectionSerializer(collection) + + return { + **collection_serialize.data, + 'problems': problem_serialize.data + } + + def remove_problems_from_collection(self, collection_id: str, request): + self.collection_repo.delete_many_problems(collection_id, request.data['problem_ids']) + self.collection_repo.update_with_timestamp(collection_id, {}) + return None diff --git a/api/services/collection/serializers.py b/api/services/collection/serializers.py new file mode 100644 index 0000000..b8c9d50 --- /dev/null +++ b/api/services/collection/serializers.py @@ -0,0 +1,165 @@ +from rest_framework import serializers +from ...models import * + +# Dependencies - Account, Group, Topic, Problem serializers +class AccountSecureSerializer(serializers.ModelSerializer): + class Meta: + model = Account + fields = ['account_id', 'username'] + +class GroupSerializer(serializers.ModelSerializer): + class Meta: + model = Group + fields = "__all__" + +class TopicSecureSerializer(serializers.ModelSerializer): + class Meta: + model = Topic + exclude = ['sharing', 'is_active', 'is_private'] + +class ProblemSerializer(serializers.ModelSerializer): + class Meta: + model = Problem + fields = "__all__" + +class ProblemSecureSerializer(serializers.ModelSerializer): + class Meta: + model = Problem + exclude = ['solution', 'submission_regex', 'is_private', 'is_active', 'sharing'] + +class ProblemPopulateAccountSecureSerializer(serializers.ModelSerializer): + creator = AccountSecureSerializer() + class Meta: + model = Problem + exclude = ['solution', 'submission_regex', 'is_private', 'is_active', 'sharing'] + +class TestcaseSerializer(serializers.ModelSerializer): + class Meta: + model = Testcase + fields = "__all__" + +class ProblemGroupPermissionsPopulateGroupSerializer(serializers.ModelSerializer): + group = GroupSerializer() + class Meta: + model = ProblemGroupPermission + fields = "__all__" + +class ProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupSerializer(serializers.ModelSerializer): + creator = AccountSecureSerializer() + group_permissions = ProblemGroupPermissionsPopulateGroupSerializer(many=True) + testcases = TestcaseSerializer(many=True) + class Meta: + model = Problem + fields = "__all__" + include = ['creator', 'group_permissions', 'testcases'] + +# Core Collection Serializers +class CollectionSerializer(serializers.ModelSerializer): + class Meta: + model = Collection + fields = "__all__" + + def create(self, validate_data): + return Collection.objects.create(**validate_data) + + def update(self, instance, validated_data): + instance.name = validated_data.get('name', instance.name) + instance.description = validated_data.get('description', instance.description) + instance.is_active = validated_data.get('is_active', instance.is_active) + instance.is_private = validated_data.get('is_private', instance.is_private) + instance.save() + return instance + +# Collection Problem Serializers +class CollectionProblemSerializer(serializers.ModelSerializer): + class Meta: + model = CollectionProblem + fields = "__all__" + +class CollectionProblemPopulateProblemSerializer(serializers.ModelSerializer): + problem = ProblemSerializer() + class Meta: + model = CollectionProblem + fields = "__all__" + +class CollectionProblemPopulateProblemSecureSerializer(serializers.ModelSerializer): + problem = ProblemSecureSerializer() + class Meta: + model = CollectionProblem + fields = "__all__" + +class CollectionProblemPopulateProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureSerializer(serializers.ModelSerializer): + # Note: This serializer requires problem serializers with submission data + # For now, using basic problem serializer to avoid circular imports + problem = ProblemPopulateAccountSecureSerializer() + class Meta: + model = CollectionProblem + fields = "__all__" + +# Collection Group Permission Serializers +class CollectionGroupPermissionPopulateGroupSerializer(serializers.ModelSerializer): + group = GroupSerializer() + class Meta: + model = CollectionGroupPermission + fields = "__all__" + +class CollectionPopulateCollectionGroupPermissionsPopulateGroupSerializer(serializers.ModelSerializer): + group_permissions = CollectionGroupPermissionPopulateGroupSerializer(many=True) + class Meta: + model = Collection + fields = "__all__" + include = ['group_permissions'] + +# Complex Collection Serializers with Problems +class CollectionPopulateCollectionProblemPopulateProblemSerializer(serializers.ModelSerializer): + problems = CollectionProblemPopulateProblemSerializer(many=True) + class Meta: + model = Collection + fields = "__all__" + +class CollectionPopulateCollectionProblemsPopulateProblemSerializer(serializers.ModelSerializer): + problems = CollectionProblemPopulateProblemSerializer(many=True) + class Meta: + model = Collection + fields = "__all__" + include = ['problems'] + +class CollectionPopulateCollectionProblemPopulateProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureSerializer(serializers.ModelSerializer): + problems = CollectionProblemPopulateProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureSerializer(many=True) + class Meta: + model = Collection + fields = "__all__" + +class CollectionPopulateCollectionProblemsPopulateProblemAndCollectionGroupPermissionsPopulateGroupSerializer(serializers.ModelSerializer): + problems = CollectionProblemPopulateProblemSerializer(many=True) + group_permissions = CollectionGroupPermissionPopulateGroupSerializer(many=True) + class Meta: + model = Collection + fields = "__all__" + include = ['problems', 'group_permissions'] + +class CollectionProblemsPopulateProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupSerializer(serializers.ModelSerializer): + problem = ProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupSerializer() + class Meta: + model = CollectionProblem + fields = "__all__" + +class CollectionPopulateCollectionProblemsPopulateProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupAndCollectionGroupPermissionsPopulateGroupSerializer(serializers.ModelSerializer): + problems = CollectionProblemsPopulateProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupSerializer(many=True) + group_permissions = CollectionGroupPermissionPopulateGroupSerializer(many=True) + class Meta: + model = Collection + fields = "__all__" + include = ['problems', 'group_permissions'] + +# Topic Collection Serializers (for integration) +class TopicCollectionSerializer(serializers.ModelSerializer): + class Meta: + model = TopicCollection + fields = "__all__" + +class TopicCollectionPopulateCollectionSerializer(serializers.ModelSerializer): + collection = CollectionSerializer() + class Meta: + model = TopicCollection + fields = "__all__" diff --git a/api/services/collection/test_collection_service.py b/api/services/collection/test_collection_service.py new file mode 100644 index 0000000..249086d --- /dev/null +++ b/api/services/collection/test_collection_service.py @@ -0,0 +1,554 @@ +import unittest +from unittest.mock import Mock, patch, MagicMock +from django.test import TestCase +from django.http import HttpRequest +from api.services.collection.collection_service import CollectionService +from api.repositories.collection_repository import CollectionRepository +from api.repositories.account_repository import AccountRepository +from api.repositories.problem_repository import ProblemRepository +from api.repositories.permission_repository import PermissionRepository +from api.repositories.group_repository import GroupRepository +from api.models import Collection, Problem, CollectionProblem, CollectionGroupPermission, Group +from api.errors.common import BadRequestError, InternalServerError, ItemNotFoundError + + +class TestCollectionService(TestCase): + """Unit tests for CollectionService""" + + def setUp(self): + """Set up test fixtures""" + self.mock_collection_repo = Mock(spec=CollectionRepository) + self.mock_account_repo = Mock(spec=AccountRepository) + self.mock_problem_repo = Mock(spec=ProblemRepository) + self.mock_permission_repo = Mock(spec=PermissionRepository) + self.mock_group_repo = Mock(spec=GroupRepository) + + self.collection_service = CollectionService( + collection_repo=self.mock_collection_repo, + account_repo=self.mock_account_repo, + problem_repo=self.mock_problem_repo, + permission_repo=self.mock_permission_repo, + group_repo=self.mock_group_repo + ) + + # Sample data + self.sample_collection = Mock(spec=Collection) + self.sample_collection.collection_id = 'coll_123' + self.sample_collection.name = 'Test Collection' + self.sample_collection.description = 'Test Description' + self.sample_collection.is_private = False + self.sample_collection.is_active = True + self.sample_collection.creator_id = 'acc_123' + self.sample_collection._state = Mock() + self.sample_collection._state.db = 'default' + + self.sample_problem = Mock(spec=Problem) + self.sample_problem.problem_id = 'prob_123' + self.sample_problem.title = 'Test Problem' + self.sample_problem._state = Mock() + self.sample_problem._state.db = 'default' + + self.sample_collection_problem = Mock(spec=CollectionProblem) + self.sample_collection_problem.problem = self.sample_problem + self.sample_collection_problem.order = 0 + self.sample_collection_problem._state = Mock() + self.sample_collection_problem._state.db = 'default' + + self.sample_group = Mock(spec=Group) + self.sample_group.group_id = 'group_123' + self.sample_group.name = 'Test Group' + self.sample_group._state = Mock() + self.sample_group._state.db = 'default' + + self.sample_request_data = { + 'name': 'Test Collection', + 'description': 'Test Description', + 'is_private': False, + 'is_active': True + } + + @patch('api.services.collection.collection_service.CollectionSerializer') + def test_create_collection_success(self, mock_serializer_class): + """Test successful collection creation""" + # Arrange + account_id = 'acc_123' + mock_request = Mock() + mock_request.data = self.sample_request_data.copy() + + # Mock serializer + mock_serializer_instance = Mock() + mock_serializer_instance.is_valid.return_value = True + mock_serializer_instance.data = { + 'collection_id': 'coll_123', + 'name': 'Test Collection', + 'description': 'Test Description' + } + mock_serializer_class.return_value = mock_serializer_instance + + # Act + result = self.collection_service.create_collection(account_id, mock_request) + + # Assert + mock_serializer_class.assert_called_once() + call_args = mock_serializer_class.call_args[1]['data'] # Get keyword arguments + self.assertEqual(call_args['creator'], account_id) + self.assertEqual(call_args['name'], 'Test Collection') + + mock_serializer_instance.is_valid.assert_called_once() + mock_serializer_instance.save.assert_called_once() + + # Verify result + self.assertIsInstance(result, dict) + self.assertIn('collection_id', result) + self.assertIn('name', result) + + @patch('api.services.collection.collection_service.CollectionSerializer') + def test_create_collection_validation_error(self, mock_serializer_class): + """Test collection creation with validation error""" + # Arrange + account_id = 'acc_123' + mock_request = Mock() + mock_request.data = self.sample_request_data.copy() + + # Mock serializer with validation error + mock_serializer_instance = Mock() + mock_serializer_instance.is_valid.return_value = False + mock_serializer_instance.errors = {'name': ['This field is required.']} + mock_serializer_class.return_value = mock_serializer_instance + + # Act & Assert + with self.assertRaises(BadRequestError) as context: + self.collection_service.create_collection(account_id, mock_request) + + self.assertIn('This field is required.', str(context.exception)) + + def test_delete_collection_success(self): + """Test successful collection deletion""" + # Arrange + collection_id = 'coll_123' + self.mock_collection_repo.get.return_value = self.sample_collection + + # Act + result = self.collection_service.delete_collection(collection_id) + + # Assert + self.mock_collection_repo.get.assert_called_once_with(collection_id) + self.sample_collection.delete.assert_called_once() + self.assertIsNone(result) + + def test_get_collection_success(self): + """Test successful collection retrieval""" + # Arrange + collection_id = 'coll_123' + problems = [self.sample_collection_problem] + permissions = [Mock(spec=CollectionGroupPermission)] + + self.mock_collection_repo.get.return_value = self.sample_collection + self.mock_collection_repo.get_problems.return_value = problems + self.mock_permission_repo.get_collection_permissions.return_value = permissions + self.mock_problem_repo.get_testcases.return_value = [] + self.mock_permission_repo.get_problem_permissions.return_value = [] + + # Mock serializer + with patch('api.services.collection.collection_service.CollectionPopulateCollectionProblemsPopulateProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupAndCollectionGroupPermissionsPopulateGroupSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = { + 'collection_id': 'coll_123', + 'name': 'Test Collection', + 'problems': [] + } + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.collection_service.get_collection(collection_id) + + # Assert + self.mock_collection_repo.get.assert_called_once_with(collection_id) + self.mock_collection_repo.get_problems.assert_called_once_with(collection_id) + self.mock_permission_repo.get_collection_permissions.assert_called_once_with(collection_id) + + # Verify result + self.assertIsInstance(result, dict) + self.assertIn('collection_id', result) + + def test_get_all_collections_success(self): + """Test getting all collections""" + # Arrange + mock_request = Mock() + mock_request.query_params = {'account_id': 'acc_123'} + + # Create a mock QuerySet-like object + mock_queryset = Mock() + mock_queryset.filter.return_value = [self.sample_collection] + collections = [self.sample_collection] + self.mock_collection_repo.list.return_value = mock_queryset + self.mock_collection_repo.get_problems.return_value = [] + + # Mock serializer + with patch('api.services.collection.collection_service.CollectionSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = { + 'collection_id': 'coll_123', + 'name': 'Test Collection' + } + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.collection_service.get_all_collections(mock_request) + + # Assert + self.mock_collection_repo.list.assert_called_once() + + # Verify result structure + self.assertIsInstance(result, dict) + self.assertIn('collections', result) + self.assertIsInstance(result['collections'], list) + + def test_get_all_collections_without_account_filter(self): + """Test getting all collections without account filter""" + # Arrange + mock_request = Mock() + mock_request.query_params = {} # No account_id + + collections = [self.sample_collection] + self.mock_collection_repo.list.return_value = collections + self.mock_collection_repo.get_problems.return_value = [] + + # Mock serializer + with patch('api.services.collection.collection_service.CollectionSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = { + 'collection_id': 'coll_123', + 'name': 'Test Collection' + } + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.collection_service.get_all_collections(mock_request) + + # Assert + self.mock_collection_repo.list.assert_called_once() + + # Verify result structure + self.assertIsInstance(result, dict) + self.assertIn('collections', result) + + def test_get_all_collections_by_account_success(self): + """Test getting all collections by account""" + # Arrange + account_id = 'acc_123' + collections = [self.sample_collection] + group_ids = ['group_123'] + manageable_collections = [] + + self.mock_collection_repo.get_by_creator.return_value = collections + self.mock_collection_repo.get_manageable_by_account.return_value = manageable_collections + self.mock_group_repo.get_by_creator.return_value = group_ids + # Create a mock QuerySet-like object for problemCollections + mock_problem_queryset = Mock() + mock_problem_queryset.filter.return_value = [] + self.mock_collection_repo.get_problems_by_collections.return_value = mock_problem_queryset + + # Mock serializers + with patch('api.services.collection.collection_service.CollectionPopulateCollectionProblemsPopulateProblemSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = [{'collection_id': 'coll_123', 'name': 'Test Collection'}] + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.collection_service.get_all_collections_by_account(account_id) + + # Assert + self.mock_collection_repo.get_by_creator.assert_called_once_with(account_id) + self.mock_group_repo.get_by_creator.assert_called_once_with(account_id) + self.mock_collection_repo.get_manageable_by_account.assert_called_once_with(group_ids) + + # Verify result structure + self.assertIsInstance(result, dict) + self.assertIn('collections', result) + self.assertIn('manageable_collections', result) + + def test_update_collection_success(self): + """Test successful collection update""" + # Arrange + collection_id = 'coll_123' + mock_request = Mock() + mock_request.data = { + 'name': 'Updated Collection', + 'description': 'Updated Description', + 'is_private': True, + 'is_active': False + } + + updated_collection = Mock(spec=Collection) + updated_collection.collection_id = collection_id + updated_collection.name = 'Updated Collection' + + self.mock_collection_repo.update_with_timestamp.return_value = updated_collection + + # Mock serializer + with patch('api.services.collection.collection_service.CollectionSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = { + 'collection_id': 'coll_123', + 'name': 'Updated Collection' + } + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.collection_service.update_collection(collection_id, mock_request) + + # Assert + expected_update_data = { + 'name': 'Updated Collection', + 'description': 'Updated Description', + 'is_private': True, + 'is_active': False + } + self.mock_collection_repo.update_with_timestamp.assert_called_once_with(collection_id, expected_update_data) + + # Verify result + self.assertIsInstance(result, dict) + self.assertIn('collection_id', result) + + def test_update_collection_with_none_values(self): + """Test updating collection with None values (should be filtered out)""" + # Arrange + collection_id = 'coll_123' + mock_request = Mock() + mock_request.data = { + 'name': 'Updated Collection', + 'description': None, + 'is_private': None, + 'is_active': True + } + + updated_collection = Mock(spec=Collection) + self.mock_collection_repo.update_with_timestamp.return_value = updated_collection + + # Mock serializer + with patch('api.services.collection.collection_service.CollectionSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = {'collection_id': 'coll_123', 'name': 'Updated Collection'} + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.collection_service.update_collection(collection_id, mock_request) + + # Assert + expected_update_data = { + 'name': 'Updated Collection', + 'is_active': True + } + self.mock_collection_repo.update_with_timestamp.assert_called_once_with(collection_id, expected_update_data) + + def test_update_group_permissions_collection_success(self): + """Test updating group permissions for collection""" + # Arrange + collection_id = 'coll_123' + mock_request = Mock() + mock_request.data = { + 'groups': [ + {'group_id': 'group_123', 'permission_view_collections': True, 'permission_manage_collections': False} + ] + } + + self.mock_collection_repo.get.return_value = self.sample_collection + self.mock_group_repo.get.return_value = self.sample_group + + # Mock serializer + with patch('api.services.collection.collection_service.CollectionPopulateCollectionGroupPermissionsPopulateGroupSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = { + 'collection_id': 'coll_123', + 'group_permissions': [] + } + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.collection_service.update_group_permissions_collection(collection_id, mock_request) + + # Assert + self.mock_collection_repo.get.assert_called_once_with(collection_id) + self.mock_permission_repo.delete_collection_permissions.assert_called_once_with(collection_id) + self.mock_group_repo.get.assert_called_once_with('group_123') + self.mock_permission_repo.bulk_create_collection_permissions.assert_called_once() + + # Verify result + self.assertIsInstance(result, dict) + self.assertIn('collection_id', result) + + def test_update_problems_to_collection_success(self): + """Test updating problems in collection""" + # Arrange + collection_id = 'coll_123' + mock_request = Mock() + mock_request.data = { + 'problem_ids': ['prob_123', 'prob_456'] + } + + self.mock_collection_repo.get.return_value = self.sample_collection + self.mock_problem_repo.get.side_effect = [self.sample_problem, self.sample_problem] + + # Mock serializers + with patch('api.services.collection.collection_service.CollectionProblemPopulateProblemSecureSerializer') as mock_problem_serializer, \ + patch('api.services.collection.collection_service.CollectionSerializer') as mock_collection_serializer: + + mock_problem_serializer_instance = Mock() + mock_problem_serializer_instance.data = [{'problem_id': 'prob_123'}] + mock_problem_serializer.return_value = mock_problem_serializer_instance + + mock_collection_serializer_instance = Mock() + mock_collection_serializer_instance.data = {'collection_id': 'coll_123'} + mock_collection_serializer.return_value = mock_collection_serializer_instance + + # Act + result = self.collection_service.update_problems_to_collection(collection_id, mock_request) + + # Assert + self.mock_collection_repo.get.assert_called_once_with(collection_id) + self.mock_collection_repo.delete_problems.assert_called_once_with(collection_id) + self.mock_problem_repo.get.assert_any_call('prob_123') + self.mock_problem_repo.get.assert_any_call('prob_456') + self.mock_collection_repo.bulk_create_problems.assert_called_once() + self.mock_collection_repo.update_with_timestamp.assert_called_once_with(collection_id, {}) + + # Verify result structure + self.assertIsInstance(result, dict) + self.assertIn('collection_id', result) + self.assertIn('problems', result) + + def test_add_problems_to_collection_success(self): + """Test adding problems to collection""" + # Arrange + collection_id = 'coll_123' + mock_request = Mock() + mock_request.data = { + 'problem_ids': ['prob_123'] + } + + self.mock_collection_repo.get.return_value = self.sample_collection + self.mock_problem_repo.get.return_value = self.sample_problem + self.mock_collection_repo.find_existing_problem.return_value = None + self.mock_collection_repo.update_with_timestamp.return_value = self.sample_collection + + # Mock CollectionProblem creation and save + with patch('api.services.collection.collection_service.CollectionProblem') as mock_collection_problem_class, \ + patch('api.services.collection.collection_service.CollectionProblemPopulateProblemSecureSerializer') as mock_problem_serializer, \ + patch('api.services.collection.collection_service.CollectionSerializer') as mock_collection_serializer: + + mock_collection_problem_instance = Mock() + mock_collection_problem_instance.save = Mock() + mock_collection_problem_class.return_value = mock_collection_problem_instance + + mock_problem_serializer_instance = Mock() + mock_problem_serializer_instance.data = [{'problem_id': 'prob_123'}] + mock_problem_serializer.return_value = mock_problem_serializer_instance + + mock_collection_serializer_instance = Mock() + mock_collection_serializer_instance.data = {'collection_id': 'coll_123'} + mock_collection_serializer.return_value = mock_collection_serializer_instance + + # Act + result = self.collection_service.add_problems_to_collection(collection_id, mock_request) + + # Assert + self.mock_collection_repo.get.assert_called_once_with(collection_id) + self.mock_problem_repo.get.assert_called_once_with('prob_123') + self.mock_collection_repo.find_existing_problem.assert_called_once_with('prob_123', collection_id) + self.mock_collection_repo.update_with_timestamp.assert_called_once_with(collection_id, {}) + + # Verify CollectionProblem was created and saved + mock_collection_problem_class.assert_called_once() + mock_collection_problem_instance.save.assert_called_once() + + # Verify result structure + self.assertIsInstance(result, dict) + self.assertIn('collection_id', result) + self.assertIn('problems', result) + + def test_add_problems_to_collection_with_existing_problem(self): + """Test adding problems to collection when problem already exists""" + # Arrange + collection_id = 'coll_123' + mock_request = Mock() + mock_request.data = { + 'problem_ids': ['prob_123'] + } + + existing_problem = Mock(spec=CollectionProblem) + existing_problem.delete = Mock() + + self.mock_collection_repo.get.return_value = self.sample_collection + self.mock_problem_repo.get.return_value = self.sample_problem + self.mock_collection_repo.find_existing_problem.return_value = existing_problem + self.mock_collection_repo.update_with_timestamp.return_value = self.sample_collection + + # Mock CollectionProblem creation and save + with patch('api.services.collection.collection_service.CollectionProblem') as mock_collection_problem_class, \ + patch('api.services.collection.collection_service.CollectionProblemPopulateProblemSecureSerializer') as mock_problem_serializer, \ + patch('api.services.collection.collection_service.CollectionSerializer') as mock_collection_serializer: + + mock_collection_problem_instance = Mock() + mock_collection_problem_instance.save = Mock() + mock_collection_problem_class.return_value = mock_collection_problem_instance + + mock_problem_serializer_instance = Mock() + mock_problem_serializer_instance.data = [{'problem_id': 'prob_123'}] + mock_problem_serializer.return_value = mock_problem_serializer_instance + + mock_collection_serializer_instance = Mock() + mock_collection_serializer_instance.data = {'collection_id': 'coll_123'} + mock_collection_serializer.return_value = mock_collection_serializer_instance + + # Act + result = self.collection_service.add_problems_to_collection(collection_id, mock_request) + + # Assert + existing_problem.delete.assert_called_once() + mock_collection_problem_class.assert_called_once() + mock_collection_problem_instance.save.assert_called_once() + + # Verify result structure + self.assertIsInstance(result, dict) + self.assertIn('collection_id', result) + self.assertIn('problems', result) + + def test_remove_problems_from_collection_success(self): + """Test removing problems from collection""" + # Arrange + collection_id = 'coll_123' + mock_request = Mock() + mock_request.data = { + 'problem_ids': ['prob_123', 'prob_456'] + } + + # Act + result = self.collection_service.remove_problems_from_collection(collection_id, mock_request) + + # Assert + self.mock_collection_repo.delete_many_problems.assert_called_once_with(collection_id, ['prob_123', 'prob_456']) + self.mock_collection_repo.update_with_timestamp.assert_called_once_with(collection_id, {}) + self.assertIsNone(result) + + def test_service_initialization(self): + """Test that service initializes correctly with repositories""" + # Arrange & Act + service = CollectionService( + collection_repo=self.mock_collection_repo, + account_repo=self.mock_account_repo, + problem_repo=self.mock_problem_repo, + permission_repo=self.mock_permission_repo, + group_repo=self.mock_group_repo + ) + + # Assert + self.assertEqual(service.collection_repo, self.mock_collection_repo) + self.assertEqual(service.account_repo, self.mock_account_repo) + self.assertEqual(service.problem_repo, self.mock_problem_repo) + self.assertEqual(service.permission_repo, self.mock_permission_repo) + self.assertEqual(service.group_repo, self.mock_group_repo) + + +if __name__ == '__main__': + unittest.main() diff --git a/api/services/group/group_service.py b/api/services/group/group_service.py new file mode 100644 index 0000000..34ab40c --- /dev/null +++ b/api/services/group/group_service.py @@ -0,0 +1,96 @@ +from ...models import * +from .serializers import * +from ...errors.common import * +from api.repositories.group_repository import GroupRepository +from api.repositories.account_repository import AccountRepository + +class GroupService: + + def __init__(self, group_repo: GroupRepository, account_repo: AccountRepository): + self.group_repo = group_repo + self.account_repo = account_repo + + def create_group(self, account_id: str, request): + account = self.account_repo.get(account_id) + group_data = { + 'creator_id': account.account_id, + **request.data + } + + group = self.group_repo.create(group_data) + serialize = GroupSerializer(group) + return serialize.data + + def delete_group(self, group_id: str): + self.group_repo.delete(group_id) + return None + + def get_group(self, group_id: str, request): + group = self.group_repo.get(group_id) + populate_members = request.GET.get('populate_members', False) + + if populate_members: + group.members = self.group_repo.get_members(group_id) + serialize = GroupPopulateGroupMemberPopulateAccountSecureSerializer(group) + else: + serialize = GroupSerializer(group) + + return serialize.data + + def get_all_groups_by_account(self, account_id: str, request): + account = self.account_repo.get(account_id) + + # Get request headers + headers = request.headers + + groups = self.group_repo.get_by_creator(account_id) + + populate_members = request.GET.get('populate_members', False) + + if populate_members: + for group in groups: + group.members = self.group_repo.get_members(group.group_id) + serialize = GroupPopulateGroupMemberPopulateAccountSecureSerializer(groups, many=True) + else: + serialize = GroupSerializer(groups, many=True) + + return {"groups": serialize.data} + + def update_group(self, group_id: str, request): + group = self.group_repo.update(group_id, request.data) + serializer = GroupSerializer(group) + return serializer.data + + def add_members_to_group(self, group_id: str, request): + group = self.group_repo.get(group_id) + group_members = [] + for accountId in request.data['account_ids']: + account = self.account_repo.get(accountId) + group_members.append(GroupMember( + group_id=group_id, + account_id=accountId + )) + + self.group_repo.bulk_create_members(group_members) + group.members = group_members + + serialize = GroupPopulateGroupMemberPopulateAccountSecureSerializer(group) + return serialize.data + + def update_members_to_group(self, group_id: str, request): + group = self.group_repo.get(group_id) + self.group_repo.delete_members(group_id) + + group_members = [] + for accountId in request.data['account_ids']: + account = self.account_repo.get(accountId) + group_members.append(GroupMember( + group_id=group_id, + account_id=accountId + )) + + self.group_repo.bulk_create_members(group_members) + group.members = group_members + + serialize = GroupPopulateGroupMemberPopulateAccountSecureSerializer(group) + return serialize.data diff --git a/api/services/group/serializers.py b/api/services/group/serializers.py new file mode 100644 index 0000000..81d0451 --- /dev/null +++ b/api/services/group/serializers.py @@ -0,0 +1,32 @@ +from rest_framework import serializers +from ...models import * + +# Dependencies +class AccountSecureSerializer(serializers.ModelSerializer): + class Meta: + model = Account + fields = ['account_id', 'username'] + +# Core Group Serializers +class GroupSerializer(serializers.ModelSerializer): + class Meta: + model = Group + fields = "__all__" + +class GroupMemberSerializer(serializers.ModelSerializer): + class Meta: + model = GroupMember + fields = "__all__" + +class GroupMemberPopulateAccountSecureSerializer(serializers.ModelSerializer): + account = AccountSecureSerializer() + class Meta: + model = GroupMember + fields = "__all__" + +class GroupPopulateGroupMemberPopulateAccountSecureSerializer(serializers.ModelSerializer): + members = GroupMemberPopulateAccountSecureSerializer(many=True) + class Meta: + model = Group + fields = "__all__" + include = ['members'] diff --git a/api/services/group/test_group_service.py b/api/services/group/test_group_service.py new file mode 100644 index 0000000..9bdc6c4 --- /dev/null +++ b/api/services/group/test_group_service.py @@ -0,0 +1,463 @@ +import unittest +from unittest.mock import Mock, patch, MagicMock +from django.test import TestCase +from django.http import HttpRequest +from api.services.group.group_service import GroupService +from api.repositories.group_repository import GroupRepository +from api.repositories.account_repository import AccountRepository +from api.models import Group, Account, GroupMember +from api.errors.common import BadRequestError, InternalServerError, ItemNotFoundError + + +class TestGroupService(TestCase): + """Unit tests for GroupService""" + + def setUp(self): + """Set up test fixtures""" + self.mock_group_repo = Mock(spec=GroupRepository) + self.mock_account_repo = Mock(spec=AccountRepository) + + self.group_service = GroupService( + group_repo=self.mock_group_repo, + account_repo=self.mock_account_repo + ) + + # Sample data + self.sample_account = Mock(spec=Account) + self.sample_account.account_id = 'acc_123' + self.sample_account.username = 'testuser' + self.sample_account.first_name = 'Test' + self.sample_account.last_name = 'User' + + self.sample_group = Mock(spec=Group) + self.sample_group.group_id = 'group_123' + self.sample_group.name = 'Test Group' + self.sample_group.description = 'Test Description' + self.sample_group.creator_id = 'acc_123' + + self.sample_group_member = Mock(spec=GroupMember) + self.sample_group_member.group_id = 'group_123' + self.sample_group_member.account_id = 'acc_123' + self.sample_group_member.account = self.sample_account + + self.sample_request_data = { + 'name': 'Test Group', + 'description': 'Test Description' + } + + def test_create_group_success(self): + """Test successful group creation""" + # Arrange + account_id = 'acc_123' + mock_request = Mock() + mock_request.data = self.sample_request_data.copy() + + self.mock_account_repo.get.return_value = self.sample_account + self.mock_group_repo.create.return_value = self.sample_group + + # Mock serializer + with patch('api.services.group.group_service.GroupSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = { + 'group_id': 'group_123', + 'name': 'Test Group', + 'description': 'Test Description' + } + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.group_service.create_group(account_id, mock_request) + + # Assert + self.mock_account_repo.get.assert_called_once_with(account_id) + + # Verify group creation data + expected_group_data = { + 'creator_id': 'acc_123', + 'name': 'Test Group', + 'description': 'Test Description' + } + self.mock_group_repo.create.assert_called_once_with(expected_group_data) + + # Verify result + self.assertIsInstance(result, dict) + self.assertIn('group_id', result) + self.assertIn('name', result) + + def test_create_group_account_not_found(self): + """Test group creation when account doesn't exist""" + # Arrange + account_id = 'nonexistent' + mock_request = Mock() + mock_request.data = self.sample_request_data.copy() + + self.mock_account_repo.get.side_effect = Account.DoesNotExist() + + # Act & Assert + with self.assertRaises(Account.DoesNotExist): + self.group_service.create_group(account_id, mock_request) + + self.mock_account_repo.get.assert_called_once_with(account_id) + + def test_delete_group_success(self): + """Test successful group deletion""" + # Arrange + group_id = 'group_123' + + # Act + result = self.group_service.delete_group(group_id) + + # Assert + self.mock_group_repo.delete.assert_called_once_with(group_id) + self.assertIsNone(result) + + def test_get_group_success_without_members(self): + """Test getting a group without populating members""" + # Arrange + group_id = 'group_123' + mock_request = Mock() + mock_request.GET = {} # No populate_members parameter + + self.mock_group_repo.get.return_value = self.sample_group + + # Mock serializer + with patch('api.services.group.group_service.GroupSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = { + 'group_id': 'group_123', + 'name': 'Test Group' + } + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.group_service.get_group(group_id, mock_request) + + # Assert + self.mock_group_repo.get.assert_called_once_with(group_id) + self.mock_group_repo.get_members.assert_not_called() + + # Verify result + self.assertIsInstance(result, dict) + self.assertIn('group_id', result) + + def test_get_group_success_with_members(self): + """Test getting a group with populating members""" + # Arrange + group_id = 'group_123' + mock_request = Mock() + mock_request.GET = {'populate_members': 'true'} + + members = [self.sample_group_member] + self.mock_group_repo.get.return_value = self.sample_group + self.mock_group_repo.get_members.return_value = members + + # Mock serializer + with patch('api.services.group.group_service.GroupPopulateGroupMemberPopulateAccountSecureSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = { + 'group_id': 'group_123', + 'name': 'Test Group', + 'members': [{'account_id': 'acc_123', 'username': 'testuser'}] + } + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.group_service.get_group(group_id, mock_request) + + # Assert + self.mock_group_repo.get.assert_called_once_with(group_id) + self.mock_group_repo.get_members.assert_called_once_with(group_id) + + # Verify result + self.assertIsInstance(result, dict) + self.assertIn('group_id', result) + self.assertIn('members', result) + + def test_get_all_groups_by_account_success_without_members(self): + """Test getting all groups by account without populating members""" + # Arrange + account_id = 'acc_123' + mock_request = Mock() + mock_request.GET = {} # No populate_members parameter + mock_request.headers = {} + + groups = [self.sample_group] + self.mock_account_repo.get.return_value = self.sample_account + self.mock_group_repo.get_by_creator.return_value = groups + + # Mock serializer + with patch('api.services.group.group_service.GroupSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = [{'group_id': 'group_123', 'name': 'Test Group'}] + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.group_service.get_all_groups_by_account(account_id, mock_request) + + # Assert + self.mock_account_repo.get.assert_called_once_with(account_id) + self.mock_group_repo.get_by_creator.assert_called_once_with(account_id) + self.mock_group_repo.get_members.assert_not_called() + + # Verify result structure + self.assertIsInstance(result, dict) + self.assertIn('groups', result) + self.assertIsInstance(result['groups'], list) + + def test_get_all_groups_by_account_success_with_members(self): + """Test getting all groups by account with populating members""" + # Arrange + account_id = 'acc_123' + mock_request = Mock() + mock_request.GET = {'populate_members': 'true'} + mock_request.headers = {} + + groups = [self.sample_group] + members = [self.sample_group_member] + + self.mock_account_repo.get.return_value = self.sample_account + self.mock_group_repo.get_by_creator.return_value = groups + self.mock_group_repo.get_members.return_value = members + + # Mock serializer + with patch('api.services.group.group_service.GroupPopulateGroupMemberPopulateAccountSecureSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = [{ + 'group_id': 'group_123', + 'name': 'Test Group', + 'members': [{'account_id': 'acc_123', 'username': 'testuser'}] + }] + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.group_service.get_all_groups_by_account(account_id, mock_request) + + # Assert + self.mock_account_repo.get.assert_called_once_with(account_id) + self.mock_group_repo.get_by_creator.assert_called_once_with(account_id) + self.mock_group_repo.get_members.assert_called_once_with('group_123') + + # Verify result structure + self.assertIsInstance(result, dict) + self.assertIn('groups', result) + self.assertIsInstance(result['groups'], list) + + def test_get_all_groups_by_account_account_not_found(self): + """Test getting all groups when account doesn't exist""" + # Arrange + account_id = 'nonexistent' + mock_request = Mock() + mock_request.GET = {} + mock_request.headers = {} + + self.mock_account_repo.get.side_effect = Account.DoesNotExist() + + # Act & Assert + with self.assertRaises(Account.DoesNotExist): + self.group_service.get_all_groups_by_account(account_id, mock_request) + + self.mock_account_repo.get.assert_called_once_with(account_id) + + def test_update_group_success(self): + """Test successful group update""" + # Arrange + group_id = 'group_123' + mock_request = Mock() + mock_request.data = { + 'name': 'Updated Group', + 'description': 'Updated Description' + } + + updated_group = Mock(spec=Group) + updated_group.group_id = group_id + updated_group.name = 'Updated Group' + + self.mock_group_repo.update.return_value = updated_group + + # Mock serializer + with patch('api.services.group.group_service.GroupSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = { + 'group_id': 'group_123', + 'name': 'Updated Group' + } + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.group_service.update_group(group_id, mock_request) + + # Assert + self.mock_group_repo.update.assert_called_once_with(group_id, { + 'name': 'Updated Group', + 'description': 'Updated Description' + }) + + # Verify result + self.assertIsInstance(result, dict) + self.assertIn('group_id', result) + + def test_add_members_to_group_success(self): + """Test adding members to group""" + # Arrange + group_id = 'group_123' + mock_request = Mock() + mock_request.data = { + 'account_ids': ['acc_123', 'acc_456'] + } + + account1 = Mock(spec=Account) + account1.account_id = 'acc_123' + account2 = Mock(spec=Account) + account2.account_id = 'acc_456' + + self.mock_group_repo.get.return_value = self.sample_group + self.mock_account_repo.get.side_effect = [account1, account2] + + # Mock serializer + with patch('api.services.group.group_service.GroupPopulateGroupMemberPopulateAccountSecureSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = { + 'group_id': 'group_123', + 'name': 'Test Group', + 'members': [ + {'account_id': 'acc_123', 'username': 'user1'}, + {'account_id': 'acc_456', 'username': 'user2'} + ] + } + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.group_service.add_members_to_group(group_id, mock_request) + + # Assert + self.mock_group_repo.get.assert_called_once_with(group_id) + self.mock_account_repo.get.assert_any_call('acc_123') + self.mock_account_repo.get.assert_any_call('acc_456') + self.mock_group_repo.bulk_create_members.assert_called_once() + + # Verify that GroupMember objects were created correctly + call_args = self.mock_group_repo.bulk_create_members.call_args[0][0] + self.assertEqual(len(call_args), 2) + self.assertEqual(call_args[0].group_id, group_id) + self.assertEqual(call_args[0].account_id, 'acc_123') + self.assertEqual(call_args[1].group_id, group_id) + self.assertEqual(call_args[1].account_id, 'acc_456') + + # Verify result + self.assertIsInstance(result, dict) + self.assertIn('group_id', result) + self.assertIn('members', result) + + def test_add_members_to_group_account_not_found(self): + """Test adding members when account doesn't exist""" + # Arrange + group_id = 'group_123' + mock_request = Mock() + mock_request.data = { + 'account_ids': ['nonexistent'] + } + + self.mock_group_repo.get.return_value = self.sample_group + self.mock_account_repo.get.side_effect = Account.DoesNotExist() + + # Act & Assert + with self.assertRaises(Account.DoesNotExist): + self.group_service.add_members_to_group(group_id, mock_request) + + self.mock_group_repo.get.assert_called_once_with(group_id) + self.mock_account_repo.get.assert_called_once_with('nonexistent') + + def test_update_members_to_group_success(self): + """Test updating members in group""" + # Arrange + group_id = 'group_123' + mock_request = Mock() + mock_request.data = { + 'account_ids': ['acc_123', 'acc_456'] + } + + account1 = Mock(spec=Account) + account1.account_id = 'acc_123' + account2 = Mock(spec=Account) + account2.account_id = 'acc_456' + + self.mock_group_repo.get.return_value = self.sample_group + self.mock_account_repo.get.side_effect = [account1, account2] + + # Mock serializer + with patch('api.services.group.group_service.GroupPopulateGroupMemberPopulateAccountSecureSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = { + 'group_id': 'group_123', + 'name': 'Test Group', + 'members': [ + {'account_id': 'acc_123', 'username': 'user1'}, + {'account_id': 'acc_456', 'username': 'user2'} + ] + } + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.group_service.update_members_to_group(group_id, mock_request) + + # Assert + self.mock_group_repo.get.assert_called_once_with(group_id) + self.mock_group_repo.delete_members.assert_called_once_with(group_id) + self.mock_account_repo.get.assert_any_call('acc_123') + self.mock_account_repo.get.assert_any_call('acc_456') + self.mock_group_repo.bulk_create_members.assert_called_once() + + # Verify result + self.assertIsInstance(result, dict) + self.assertIn('group_id', result) + self.assertIn('members', result) + + def test_update_members_to_group_empty_list(self): + """Test updating members with empty list (should remove all members)""" + # Arrange + group_id = 'group_123' + mock_request = Mock() + mock_request.data = { + 'account_ids': [] + } + + self.mock_group_repo.get.return_value = self.sample_group + + # Mock serializer + with patch('api.services.group.group_service.GroupPopulateGroupMemberPopulateAccountSecureSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = { + 'group_id': 'group_123', + 'name': 'Test Group', + 'members': [] + } + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.group_service.update_members_to_group(group_id, mock_request) + + # Assert + self.mock_group_repo.get.assert_called_once_with(group_id) + self.mock_group_repo.delete_members.assert_called_once_with(group_id) + self.mock_account_repo.get.assert_not_called() + self.mock_group_repo.bulk_create_members.assert_called_once_with([]) + + # Verify result + self.assertIsInstance(result, dict) + self.assertIn('group_id', result) + + def test_service_initialization(self): + """Test that service initializes correctly with repositories""" + # Arrange & Act + service = GroupService( + group_repo=self.mock_group_repo, + account_repo=self.mock_account_repo + ) + + # Assert + self.assertEqual(service.group_repo, self.mock_group_repo) + self.assertEqual(service.account_repo, self.mock_account_repo) + + +if __name__ == '__main__': + unittest.main() diff --git a/api/services/problem/problem_service.py b/api/services/problem/problem_service.py new file mode 100644 index 0000000..42dee63 --- /dev/null +++ b/api/services/problem/problem_service.py @@ -0,0 +1,279 @@ +from django.utils import timezone +from api.repositories.group_repository import GroupRepository +from api.repositories.problem_repository import ProblemRepository +from api.repositories.submission_repository import SubmissionRepository +from api.repositories.topic_repository import TopicRepository +from api.repositories.permission_repository import PermissionRepository +from api.repositories.account_repository import AccountRepository +from api.sandbox.grader import ProgramGrader, RuntimeResultList +from ...models import * +from .serializers import * +from ...difficulty_predictor.preprocess import * +from ...difficulty_predictor.predictor import * +from ...errors.common import * + +try: + import pandas as pd + pd.options.mode.chained_assignment = None + pandas_success = True +except: + pandas_success = False + +class ProblemService: + + def __init__(self, problem_repo: ProblemRepository, account_repo: AccountRepository, permission_repo: PermissionRepository, group_repo: GroupRepository, topic_repo: TopicRepository, grader: dict[ProgramGrader]): + self.problem_repo = problem_repo + self.account_repo = account_repo + self.permission_repo = permission_repo + self.group_repo = group_repo + self.topic_repo = topic_repo + self.grader = grader + + def create_problem(self, account_id: str, request): + account = self.account_repo.get(account_id) + python_grader: ProgramGrader = self.grader['python'] + running_result = python_grader(request.data['solution'], request.data['testcases'], 1, 1.5).generate_output() + + problem_data = { + 'language': request.data['language'], + 'creator': account, + 'title': request.data['title'], + 'description': request.data['description'], + 'solution': request.data['solution'], + 'time_limit': request.data['time_limit'], + 'allowed_languages': request.data['allowed_languages'], + } + problem = self.problem_repo.create(problem_data) + + testcases_result = [] + for unit in running_result.data: + testcases_result.append( + Testcase( + problem=problem, + input=unit.input, + output=unit.output, + runtime_status=unit.runtime_status + )) + + self.problem_repo.bulk_create_testcases(testcases_result) + + problem_serialize = ProblemSerializer(problem) + testcases_serialize = TestcaseSerializer(testcases_result, many=True) + + return {**problem_serialize.data, 'testcases': testcases_serialize.data} + + def delete_problem(self, problem_id: str): + self.problem_repo.delete(problem_id) + return None + + def validate_program(self, request): + grader: ProgramGrader = self.grader[request.data['language']] + result: RuntimeResultList = grader(request.data['source_code'], request.data['testcases'], 1, request.data['time_limited']).generate_output() + + return { + 'runnable': result.runnable, + 'has_error': result.has_error, + 'has_timeout': result.has_timeout, + 'runtime_results': result.getResult(), + } + + # def import_elabsheet_problem(self, request, problem_id: str): + # # Get file + # file = request.data.get('file') + # self.problem_repo.update(problem_id, {'pdf_url': file}) + # return None + + def get_all_problems_by_account(self, account_id: str, request): + start = int(request.query_params.get("start", 0)) + end = int(request.query_params.get("end", -1)) + query = request.query_params.get("query", "") + if end == -1: + end = None + + personalProblems = self.problem_repo.get_personal(account_id, query, start, end) + maxPersonal = len(personalProblems) + for problem in personalProblems: + problem.testcases = self.problem_repo.get_testcases(problem.problem_id, deprecated=False) + + group_ids = self.group_repo.get_by_creator(account_id) + manageableProblems = self.problem_repo.get_manageable_by_account(group_ids, query, start, end) + maxManageable = len(manageableProblems) + for problem in manageableProblems: + problem.testcases = self.problem_repo.get_testcases(problem.problem_id, deprecated=False) + + personalSerialize = ProblemPopulatePartialTestcaseSerializer(personalProblems, many=True) + manageableSerialize = ProblemPopulatePartialTestcaseSerializer(manageableProblems, many=True) + + return { + "start": start, + "end": end, + "total_personal_problems": maxPersonal, + "total_manageable_problems": maxManageable, + "problems": personalSerialize.data, + "manageable_problems": manageableSerialize.data + } + + def get_all_problem_with_best_submission(self, account_id: str): + problems = self.problem_repo.get_with_best_submission(account_id) + + for problem in problems: + best_submission = self.problem_repo.get_best_submission(problem.problem_id, account_id) + if best_submission: + testcases = self.problem_repo.get_submission_testcases(best_submission.submission_id) + best_submission.runtime_output = testcases + problem.best_submission = best_submission + else: + problem.best_submission = None + + problem_ser = ProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureSerializer(problems, many=True) + return {"problems": problem_ser.data} + + def get_all_problems(self, request): + get_private = int(request.query_params.get("private", 0)) + get_deactive = int(request.query_params.get("deactive", 0)) + account_id = str(request.query_params.get("account_id", "")) + + filters = {} + if not get_private: + filters['is_private'] = False + if not get_deactive: + filters['is_active'] = True + if account_id != "": + filters['creator_id'] = account_id + + problems = self.problem_repo.list(filters=filters, order_by=['-problem_id']) + serialize = ProblemPopulateAccountSerializer(problems, many=True) + + return {'problems': serialize.data} + + def get_problem(self, problem_id: str): + problem = self.problem_repo.get(problem_id) + problem.testcases = self.problem_repo.get_testcases(problem_id, deprecated=False) + problem.group_permissions = self.permission_repo.get_problem_permissions(problem_id) + + serialize = ProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupSerializer(problem) + + return serialize.data + + def get_problem_in_topic_with_best_submission(self, account_id: str, topic_id: str, problem_id: int): + problem = self.problem_repo.get(problem_id) + + best_submission = self.problem_repo.get_best_submission_in_topic(problem_id, account_id, topic_id) + if best_submission: + testcases = self.problem_repo.get_submission_testcases(best_submission.submission.submission_id) + best_submission.runtime_output = testcases + problem.best_submission = best_submission + else: + problem.best_submission = None + + serialize = ProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureSerializer(problem) + return serialize.data + + def get_problem_public(self, problem_id: str): + problem = self.problem_repo.get(problem_id) + serialize = ProblemPopulateAccountSecureSerializer(problem) + return serialize.data + + def remove_bulk_problems(self, request): + target = request.data.get("problem", []) + self.problem_repo.delete_many(target) + return None + + def update_group_permission_to_problem(self, problem_id: str, request): + problem = self.problem_repo.get(problem_id) + self.permission_repo.delete_problem_group_permissions(problem_id) + + problem_group_permissions = [] + for group_request in request.data['groups']: + group = self.group_repo.get(group_request['group_id']) + problem_group_permissions.append( + ProblemGroupPermission( + problem=problem, + group=group, + **group_request + )) + + self.permission_repo.bulk_create_problem_group_permissions(problem_group_permissions) + + problem.group_permissions = problem_group_permissions + problem.testcases = self.problem_repo.get_testcases(problem_id) + + serialize = ProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupSerializer(problem) + return serialize.data + + def update_problem(self, problem_id: str, request): + update_data = { + 'title': request.data.get('title'), + 'language': request.data.get('language'), + 'description': request.data.get('description'), + 'solution': request.data.get('solution'), + 'time_limit': request.data.get('time_limit'), + 'is_private': request.data.get('is_private'), + 'allowed_languages': request.data.get('allowed_languages') + } + # Remove None values + update_data = {k: v for k, v in update_data.items() if v is not None} + + problem = self.problem_repo.update(problem_id, update_data) + + if 'testcases' in request.data: + running_result = self.grader[request.data['language']](problem.solution, request.data['testcases'], 1, 1.5).generate_output() + + # if not running_result.runnable: + # raise BadRequestError('Error during editing. Your code may has an error/timeout!') + self.problem_repo.deprecate_testcases(problem_id) + + testcase_result = [] + for unit in running_result.data: + testcase_data = { + 'problem': problem, + 'input': unit.input, + 'output': unit.output, + 'runtime_status': unit.runtime_status + } + testcase = self.problem_repo.create_testcase(testcase_data) + testcase_result.append(testcase) + + problem_serialize = ProblemSerializer(problem) + testcases_serialize = TestcaseSerializer(testcase_result, many=True) + + return {**problem_serialize.data, 'testcases': testcases_serialize.data} + + if 'solution' in request.data: + testcases = self.problem_repo.get_testcases(problem_id, deprecated=False) + program_input = [i.input for i in testcases] + running_result = self.grader[request.data['language']](problem.solution, program_input, 1, 1.5).generate_output() + + if not running_result.runnable: + raise BadRequestError('Error during editing. Your code may has an error/timeout!') + + problem_serialize = ProblemSerializer(problem) + return problem_serialize.data + + def update_problem_difficulty(self, problem_id: str): + if not pandas_success: + return + + submissions = self.problem_repo.get_submissions_for_difficulty(problem_id) + + if submissions.count() < 10: + return + + # Change them to DataFrame + df = pd.DataFrame(data={ + 'submission_id': [i.submission_id for i in submissions], + 'account_id': [i.account_id for i in submissions], + 'problem_id': [i.problem_id for i in submissions], + 'score': [i.score for i in submissions], + 'max_score': [i.max_score for i in submissions], + 'passed_ratio': [i.passed_ratio for i in submissions], + 'language': [i.language for i in submissions], + 'submission_code': [i.submission_code for i in submissions], + 'date': [i.date for i in submissions], + 'is_passed': [i.is_passed for i in submissions], + }) + + [total_attempt, time_used] = modelgrader_preprocessor(df) + difficulty = predict(total_attempt, time_used) + + self.problem_repo.update(problem_id, {'difficulty': difficulty}) diff --git a/api/services/problem/serializers.py b/api/services/problem/serializers.py new file mode 100644 index 0000000..0e2f810 --- /dev/null +++ b/api/services/problem/serializers.py @@ -0,0 +1,194 @@ +from rest_framework import serializers +from ...models import * + +# Account related serializers (dependencies) +class AccountSecureSerializer(serializers.ModelSerializer): + class Meta: + model = Account + fields = ['account_id', 'username'] + +# Topic related serializers (dependencies) +class TopicSecureSerializer(serializers.ModelSerializer): + class Meta: + model = Topic + exclude = ['sharing', 'is_active', 'is_private'] + +# Group related serializers (dependencies) +class GroupSerializer(serializers.ModelSerializer): + class Meta: + model = Group + fields = "__all__" + +# Core Problem Serializers +class ProblemSerializer(serializers.ModelSerializer): + class Meta: + model = Problem + fields = "__all__" + +problem_secure_fields = ['problem_id', 'title', 'description', 'is_active', 'is_private', 'updated_date', 'created_date'] + +class ProblemSecureSerializer(serializers.ModelSerializer): + class Meta: + model = Problem + exclude = ['solution', 'submission_regex', 'is_private', 'is_active', 'sharing'] + +class ProblemPopulateAccountSerializer(serializers.ModelSerializer): + creator = AccountSecureSerializer() + class Meta: + model = Problem + fields = "__all__" + +class ProblemPopulateAccountSecureSerializer(serializers.ModelSerializer): + creator = AccountSecureSerializer() + class Meta: + model = Problem + exclude = ['solution', 'submission_regex', 'is_private', 'is_active', 'sharing'] + +# Testcase Serializers +class TestcaseSerializer(serializers.ModelSerializer): + class Meta: + model = Testcase + fields = "__all__" + +class TestcasePartialSerializer(serializers.ModelSerializer): + class Meta: + model = Testcase + fields = ['testcase_id', 'runtime_status'] + +# Problem with Testcase Serializers +class ProblemPopulatePartialTestcaseSerializer(serializers.ModelSerializer): + creator = AccountSecureSerializer() + testcases = TestcasePartialSerializer(many=True) + class Meta: + model = Problem + fields = "__all__" + include = ['testcases'] + +class ProblemPopulateTestcaseSerializer(serializers.ModelSerializer): + creator = AccountSecureSerializer() + testcases = TestcaseSerializer(many=True) + class Meta: + model = Problem + fields = "__all__" + include = ['testcases'] + +# Submission Related Serializers +class SubmissionSerializer(serializers.ModelSerializer): + class Meta: + model = Submission + fields = "__all__" + +class SubmissionTestcaseSerializer(serializers.ModelSerializer): + class Meta: + model = SubmissionTestcase + fields = "__all__" + +class SubmissionTestcaseSecureSerializer(serializers.ModelSerializer): + class Meta: + model = SubmissionTestcase + fields = ['is_passed', 'runtime_status'] + +class SubmissionPopulateSubmissionTestcaseSecureSerializer(serializers.ModelSerializer): + runtime_output = SubmissionTestcaseSecureSerializer(many=True) + class Meta: + model = Submission + fields = ['submission_id', 'account', 'problem', 'topic', 'language', 'submission_code', 'is_passed', 'date', 'score', 'max_score', 'passed_ratio', 'runtime_output'] + +class SubmissionPopulateSubmissionTestcaseAndProblemSecureSerializer(serializers.ModelSerializer): + runtime_output = SubmissionTestcaseSecureSerializer(many=True) + problem = ProblemSecureSerializer() + topic = TopicSecureSerializer() + class Meta: + model = Submission + fields = ['submission_id', 'account', 'problem', 'topic', 'language', 'submission_code', 'is_passed', 'date', 'score', 'max_score', 'passed_ratio', 'runtime_output'] + +class SubmissionPoplulateProblemSerializer(serializers.ModelSerializer): + problem = ProblemSerializer() + class Meta: + model = Submission + fields = "__all__" + +class SubmissionPoplulateProblemSecureSerializer(serializers.ModelSerializer): + problem = ProblemPopulateAccountSecureSerializer() + class Meta: + model = Submission + fields = "__all__" + +class SubmissionPopulateSubmissionTestcaseAndAccountSerializer(serializers.ModelSerializer): + runtime_output = SubmissionTestcaseSerializer(many=True) + account = AccountSecureSerializer() + topic = TopicSecureSerializer() + class Meta: + model = Submission + fields = "__all__" + include = ['runtime_output'] + +# Problem with Submission Serializers +class ProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureSerializer(serializers.ModelSerializer): + creator = AccountSecureSerializer() + best_submission = SubmissionPopulateSubmissionTestcaseSecureSerializer() + class Meta: + model = Problem + fields = "__all__" + include = ['best_submission', 'creator'] + exclude = ['solution', 'submission_regex', 'is_private', 'is_active', 'sharing'] + +class ProblemPopulatSubmissionPopulateSubmissionTestcasesSecureSerializer(serializers.ModelSerializer): + best_submission = SubmissionPopulateSubmissionTestcaseSecureSerializer() + class Meta: + model = Problem + fields = "__all__" + include = ['best_submission', 'creator'] + exclude = ['solution', 'submission_regex', 'is_private', 'is_active', 'sharing'] + +# Problem Group Permission Serializers +class ProblemGroupPermissionsPopulateGroupSerializer(serializers.ModelSerializer): + group = GroupSerializer() + class Meta: + model = ProblemGroupPermission + fields = "__all__" + +class ProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupSerializer(serializers.ModelSerializer): + creator = AccountSecureSerializer() + group_permissions = ProblemGroupPermissionsPopulateGroupSerializer(many=True) + testcases = TestcaseSerializer(many=True) + class Meta: + model = Problem + fields = "__all__" + include = ['creator', 'group_permissions', 'testcases'] + +# Collection Problem Serializers +class CollectionProblemSerializer(serializers.ModelSerializer): + class Meta: + model = CollectionProblem + fields = "__all__" + +class CollectionProblemPopulateProblemSerializer(serializers.ModelSerializer): + problem = ProblemSerializer() + class Meta: + model = CollectionProblem + fields = "__all__" + +class CollectionProblemPopulateProblemSecureSerializer(serializers.ModelSerializer): + problem = ProblemSecureSerializer() + class Meta: + model = CollectionProblem + fields = "__all__" + +class CollectionProblemPopulateProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureSerializer(serializers.ModelSerializer): + problem = ProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureSerializer() + class Meta: + model = CollectionProblem + fields = "__all__" + +class CollectionProblemsPopulateProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupSerializer(serializers.ModelSerializer): + problem = ProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupSerializer() + class Meta: + model = CollectionProblem + fields = "__all__" + +# Topic Problem Serializers +class TopicProblemSerializer(serializers.ModelSerializer): + class Meta: + model = TopicProblem + fields = "__all__" diff --git a/api/services/problem/test_problem_service.py b/api/services/problem/test_problem_service.py new file mode 100644 index 0000000..59d75a1 --- /dev/null +++ b/api/services/problem/test_problem_service.py @@ -0,0 +1,563 @@ +import unittest +from unittest.mock import Mock, patch, MagicMock +from django.test import TestCase +from django.http import HttpRequest +from api.services.problem.problem_service import ProblemService +from api.repositories.problem_repository import ProblemRepository +from api.repositories.account_repository import AccountRepository +from api.repositories.permission_repository import PermissionRepository +from api.repositories.group_repository import GroupRepository +from api.repositories.topic_repository import TopicRepository +from api.models import Account, Problem, Testcase, ProblemGroupPermission +from api.errors.common import BadRequestError, InternalServerError, ItemNotFoundError + + +class TestProblemService(TestCase): + """Unit tests for ProblemService""" + + def setUp(self): + """Set up test fixtures""" + self.mock_problem_repo = Mock(spec=ProblemRepository) + self.mock_account_repo = Mock(spec=AccountRepository) + self.mock_permission_repo = Mock(spec=PermissionRepository) + self.mock_group_repo = Mock(spec=GroupRepository) + self.mock_topic_repo = Mock(spec=TopicRepository) + + # Mock grader dictionary + self.mock_grader = { + 'python': Mock(), + 'java': Mock(), + 'cpp': Mock() + } + + self.problem_service = ProblemService( + problem_repo=self.mock_problem_repo, + account_repo=self.mock_account_repo, + permission_repo=self.mock_permission_repo, + group_repo=self.mock_group_repo, + topic_repo=self.mock_topic_repo, + grader=self.mock_grader + ) + + # Sample data + self.sample_account = Mock(spec=Account) + self.sample_account.account_id = 'acc_123' + self.sample_account.username = 'testuser' + + self.sample_problem = Mock(spec=Problem) + self.sample_problem.problem_id = 'prob_123' + self.sample_problem.title = 'Test Problem' + self.sample_problem.description = 'Test Description' + self.sample_problem.solution = 'print("Hello")' + self.sample_problem.language = 'python' + self.sample_problem.time_limit = 1.5 + self.sample_problem.allowed_languages = ['python'] + self.sample_problem.is_private = False + self.sample_problem.is_active = True + self.sample_problem.creator = self.sample_account + self.sample_problem._state = Mock() + self.sample_problem._state.db = 'default' + from datetime import datetime + self.sample_problem.created_at = datetime(2023, 1, 1, 0, 0, 0) + self.sample_problem.updated_at = datetime(2023, 1, 1, 0, 0, 0) + + self.sample_testcase = Mock(spec=Testcase) + self.sample_testcase.testcase_id = 'tc_123' + self.sample_testcase.input = 'input' + self.sample_testcase.output = 'output' + self.sample_testcase.runtime_status = 'AC' + self.sample_testcase._state = Mock() + self.sample_testcase._state.db = 'default' + self.sample_testcase.created_at = datetime(2023, 1, 1, 0, 0, 0) + self.sample_testcase.updated_at = datetime(2023, 1, 1, 0, 0, 0) + + self.sample_request_data = { + 'title': 'Test Problem', + 'description': 'Test Description', + 'solution': 'print("Hello")', + 'language': 'python', + 'time_limit': 1.5, + 'allowed_languages': ['python'], + 'testcases': [{'input': 'input', 'output': 'output'}] + } + + @patch('api.services.problem.problem_service.ProblemSerializer') + @patch('api.services.problem.problem_service.TestcaseSerializer') + @patch('api.services.problem.problem_service.Testcase') + def test_create_problem_success(self, mock_testcase_class, mock_testcase_serializer, mock_problem_serializer): + """Test successful problem creation""" + # Arrange + account_id = 'acc_123' + mock_request = Mock() + mock_request.data = self.sample_request_data.copy() + + self.mock_account_repo.get.return_value = self.sample_account + self.mock_problem_repo.create.return_value = self.sample_problem + + # Mock grader result + mock_grader_result = Mock() + mock_grader_result.data = [Mock(input='input', output='output', runtime_status='AC')] + self.mock_grader['python'].return_value = Mock() + self.mock_grader['python'].return_value.generate_output.return_value = mock_grader_result + + # Mock Testcase creation + mock_testcase_instance = Mock() + mock_testcase_class.return_value = mock_testcase_instance + + # Mock serializers + mock_problem_serializer_instance = Mock() + mock_problem_serializer_instance.data = {'problem_id': 'prob_123', 'title': 'Test Problem'} + mock_problem_serializer.return_value = mock_problem_serializer_instance + + mock_testcase_serializer_instance = Mock() + mock_testcase_serializer_instance.data = [{'testcase_id': 'tc_123', 'input': 'input'}] + mock_testcase_serializer.return_value = mock_testcase_serializer_instance + + # Act + result = self.problem_service.create_problem(account_id, mock_request) + + # Assert + self.mock_account_repo.get.assert_called_once_with(account_id) + self.mock_problem_repo.create.assert_called_once() + self.mock_problem_repo.bulk_create_testcases.assert_called_once() + + # Verify grader was called with correct parameters + self.mock_grader['python'].assert_called_once_with( + self.sample_request_data['solution'], + self.sample_request_data['testcases'], + 1, + 1.5 + ) + + # Verify result structure + self.assertIsInstance(result, dict) + self.assertIn('problem_id', result) + self.assertIn('testcases', result) + + def test_create_problem_account_not_found(self): + """Test problem creation when account doesn't exist""" + # Arrange + account_id = 'nonexistent' + mock_request = Mock() + mock_request.data = self.sample_request_data.copy() + + self.mock_account_repo.get.side_effect = Account.DoesNotExist() + + # Act & Assert + with self.assertRaises(Account.DoesNotExist): + self.problem_service.create_problem(account_id, mock_request) + + self.mock_account_repo.get.assert_called_once_with(account_id) + + def test_delete_problem_success(self): + """Test successful problem deletion""" + # Arrange + problem_id = 'prob_123' + + # Act + result = self.problem_service.delete_problem(problem_id) + + # Assert + self.mock_problem_repo.delete.assert_called_once_with(problem_id) + self.assertIsNone(result) + + def test_validate_program_success(self): + """Test successful program validation""" + # Arrange + mock_request = Mock() + mock_request.data = { + 'language': 'python', + 'source_code': 'print("Hello")', + 'testcases': [{'input': 'input', 'output': 'output'}], + 'time_limited': 1.5 + } + + # Mock grader + mock_grader_result = Mock() + mock_grader_result.runnable = True + mock_grader_result.has_error = False + mock_grader_result.has_timeout = False + mock_grader_result.getResult.return_value = [{'input': 'input', 'output': 'output', 'is_passed': True}] + + mock_grader_instance = Mock() + mock_grader_instance.return_value = Mock() + mock_grader_instance.return_value.generate_output.return_value = mock_grader_result + self.mock_grader['python'] = mock_grader_instance + + # Act + result = self.problem_service.validate_program(mock_request) + + # Assert + self.mock_grader['python'].assert_called_once_with( + 'print("Hello")', + [{'input': 'input', 'output': 'output'}], + 1, + 1.5 + ) + # Verify result structure + self.assertIsInstance(result, dict) + self.assertIn('runnable', result) + self.assertIn('has_error', result) + self.assertIn('has_timeout', result) + self.assertIn('runtime_results', result) + self.assertTrue(result['runnable']) + self.assertFalse(result['has_error']) + self.assertFalse(result['has_timeout']) + + @patch('api.services.problem.problem_service.ProblemPopulatePartialTestcaseSerializer') + def test_get_all_problems_by_account_success(self, mock_serializer_class): + """Test getting all problems by account""" + # Arrange + account_id = 'acc_123' + mock_request = Mock() + mock_request.query_params = { + 'start': '0', + 'end': '10', + 'query': 'test' + } + + # Mock repository responses + personal_problems = [self.sample_problem] + manageable_problems = [] + group_ids = ['group_123'] + + self.mock_problem_repo.get_personal.return_value = personal_problems + self.mock_problem_repo.get_manageable_by_account.return_value = manageable_problems + self.mock_group_repo.get_by_creator.return_value = group_ids + self.mock_problem_repo.get_testcases.return_value = [self.sample_testcase] + + # Create mock instances for both calls + mock_personal_serializer = Mock() + mock_personal_serializer.data = [{'problem_id': 'prob_123', 'title': 'Test Problem', 'created_at': '2023-01-01T00:00:00Z', 'updated_at': '2023-01-01T00:00:00Z'}] + + mock_manageable_serializer = Mock() + mock_manageable_serializer.data = [] + + # Configure the mock class to return different instances based on call + mock_serializer_class.side_effect = [mock_personal_serializer, mock_manageable_serializer] + + # Act + result = self.problem_service.get_all_problems_by_account(account_id, mock_request) + + # Assert + self.mock_problem_repo.get_personal.assert_called_once_with(account_id, 'test', 0, 10) + self.mock_group_repo.get_by_creator.assert_called_once_with(account_id) + self.mock_problem_repo.get_manageable_by_account.assert_called_once_with(group_ids, 'test', 0, 10) + + # Verify result structure + self.assertIsInstance(result, dict) + self.assertIn('start', result) + self.assertIn('end', result) + self.assertIn('total_personal_problems', result) + self.assertIn('total_manageable_problems', result) + self.assertIn('problems', result) + self.assertIn('manageable_problems', result) + + def test_get_all_problems_by_account_with_default_params(self): + """Test getting all problems by account with default parameters""" + # Arrange + account_id = 'acc_123' + mock_request = Mock() + mock_request.query_params = {} # No query parameters + + # Mock repository responses + personal_problems = [] + manageable_problems = [] + group_ids = [] + + self.mock_problem_repo.get_personal.return_value = personal_problems + self.mock_problem_repo.get_manageable_by_account.return_value = manageable_problems + self.mock_group_repo.get_by_creator.return_value = group_ids + + # Mock serializers + with patch('api.services.problem.problem_service.ProblemPopulatePartialTestcaseSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = [] + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.problem_service.get_all_problems_by_account(account_id, mock_request) + + # Assert + self.mock_problem_repo.get_personal.assert_called_once_with(account_id, '', 0, None) + self.mock_problem_repo.get_manageable_by_account.assert_called_once_with([], '', 0, None) + + def test_get_all_problem_with_best_submission_success(self): + """Test getting all problems with best submission""" + # Arrange + account_id = 'acc_123' + + # Mock repository responses + problems = [self.sample_problem] + best_submission = Mock() + best_submission.submission_id = 'sub_123' + testcases = [self.sample_testcase] + + self.mock_problem_repo.get_with_best_submission.return_value = problems + self.mock_problem_repo.get_best_submission.return_value = best_submission + self.mock_problem_repo.get_submission_testcases.return_value = testcases + + # Mock serializer + with patch('api.services.problem.problem_service.ProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = [{'problem_id': 'prob_123', 'best_submission': {'submission_id': 'sub_123'}}] + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.problem_service.get_all_problem_with_best_submission(account_id) + + # Assert + self.mock_problem_repo.get_with_best_submission.assert_called_once_with(account_id) + self.mock_problem_repo.get_best_submission.assert_called_once_with('prob_123', account_id) + self.mock_problem_repo.get_submission_testcases.assert_called_once_with('sub_123') + + # Verify result structure + self.assertIsInstance(result, dict) + self.assertIn('problems', result) + + def test_get_all_problem_with_best_submission_no_best_submission(self): + """Test getting all problems when no best submission exists""" + # Arrange + account_id = 'acc_123' + + # Mock repository responses + problems = [self.sample_problem] + + self.mock_problem_repo.get_with_best_submission.return_value = problems + self.mock_problem_repo.get_best_submission.return_value = None + + # Mock serializer + with patch('api.services.problem.problem_service.ProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = [{'problem_id': 'prob_123', 'best_submission': None}] + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.problem_service.get_all_problem_with_best_submission(account_id) + + # Assert + self.mock_problem_repo.get_best_submission.assert_called_once_with('prob_123', account_id) + self.mock_problem_repo.get_submission_testcases.assert_not_called() + + def test_get_all_problems_success(self): + """Test getting all problems with filters""" + # Arrange + mock_request = Mock() + mock_request.query_params = { + 'private': '0', + 'deactive': '0', + 'account_id': 'acc_123' + } + + # Mock repository response + problems = [self.sample_problem] + self.mock_problem_repo.list.return_value = problems + + # Mock serializer + with patch('api.services.problem.problem_service.ProblemPopulateAccountSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = [{'problem_id': 'prob_123', 'title': 'Test Problem'}] + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.problem_service.get_all_problems(mock_request) + + # Assert + expected_filters = { + 'is_private': False, + 'is_active': True, + 'creator_id': 'acc_123' + } + self.mock_problem_repo.list.assert_called_once_with(filters=expected_filters, order_by=['-problem_id']) + + # Verify result structure + self.assertIsInstance(result, dict) + self.assertIn('problems', result) + + def test_get_all_problems_with_default_filters(self): + """Test getting all problems with default filters""" + # Arrange + mock_request = Mock() + mock_request.query_params = {} # No query parameters + + # Mock repository response + problems = [self.sample_problem] + self.mock_problem_repo.list.return_value = problems + + # Mock serializer + with patch('api.services.problem.problem_service.ProblemPopulateAccountSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = [{'problem_id': 'prob_123', 'title': 'Test Problem'}] + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.problem_service.get_all_problems(mock_request) + + # Assert + expected_filters = { + 'is_private': False, + 'is_active': True + } + self.mock_problem_repo.list.assert_called_once_with(filters=expected_filters, order_by=['-problem_id']) + + def test_get_problem_success(self): + """Test getting a specific problem""" + # Arrange + problem_id = 'prob_123' + + # Mock repository responses + testcases = [self.sample_testcase] + permissions = [Mock(spec=ProblemGroupPermission)] + + self.mock_problem_repo.get.return_value = self.sample_problem + self.mock_problem_repo.get_testcases.return_value = testcases + self.mock_permission_repo.get_problem_permissions.return_value = permissions + + # Mock serializer + with patch('api.services.problem.problem_service.ProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = {'problem_id': 'prob_123', 'title': 'Test Problem'} + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.problem_service.get_problem(problem_id) + + # Assert + self.mock_problem_repo.get.assert_called_once_with(problem_id) + self.mock_problem_repo.get_testcases.assert_called_once_with(problem_id, deprecated=False) + self.mock_permission_repo.get_problem_permissions.assert_called_once_with(problem_id) + + # Verify result + self.assertIsInstance(result, dict) + self.assertIn('problem_id', result) + + def test_get_problem_public_success(self): + """Test getting a public problem""" + # Arrange + problem_id = 'prob_123' + + self.mock_problem_repo.get.return_value = self.sample_problem + + # Mock serializer + with patch('api.services.problem.problem_service.ProblemPopulateAccountSecureSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = {'problem_id': 'prob_123', 'title': 'Test Problem'} + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.problem_service.get_problem_public(problem_id) + + # Assert + self.mock_problem_repo.get.assert_called_once_with(problem_id) + + # Verify result + self.assertIsInstance(result, dict) + self.assertIn('problem_id', result) + + def test_remove_bulk_problems_success(self): + """Test removing multiple problems""" + # Arrange + mock_request = Mock() + mock_request.data = { + 'problem': ['prob_123', 'prob_456'] + } + + # Act + result = self.problem_service.remove_bulk_problems(mock_request) + + # Assert + self.mock_problem_repo.delete_many.assert_called_once_with(['prob_123', 'prob_456']) + self.assertIsNone(result) + + def test_update_problem_success(self): + """Test updating a problem""" + # Arrange + problem_id = 'prob_123' + mock_request = Mock() + mock_request.data = { + 'title': 'Updated Title', + 'description': 'Updated Description', + 'is_private': True + } + + updated_problem = Mock(spec=Problem) + updated_problem.problem_id = problem_id + updated_problem.title = 'Updated Title' + + self.mock_problem_repo.update.return_value = updated_problem + + # Mock serializer + with patch('api.services.problem.problem_service.ProblemSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = {'problem_id': 'prob_123', 'title': 'Updated Title'} + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.problem_service.update_problem(problem_id, mock_request) + + # Assert + expected_update_data = { + 'title': 'Updated Title', + 'description': 'Updated Description', + 'is_private': True + } + self.mock_problem_repo.update.assert_called_once_with(problem_id, expected_update_data) + + # Verify result + self.assertIsInstance(result, dict) + self.assertIn('problem_id', result) + + def test_update_problem_with_none_values(self): + """Test updating a problem with None values (should be filtered out)""" + # Arrange + problem_id = 'prob_123' + mock_request = Mock() + mock_request.data = { + 'title': 'Updated Title', + 'description': None, + 'is_private': None, + 'language': 'python' + } + + updated_problem = Mock(spec=Problem) + self.mock_problem_repo.update.return_value = updated_problem + + # Mock serializer + with patch('api.services.problem.problem_service.ProblemSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = {'problem_id': 'prob_123', 'title': 'Updated Title'} + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.problem_service.update_problem(problem_id, mock_request) + + # Assert + expected_update_data = { + 'title': 'Updated Title', + 'language': 'python' + } + self.mock_problem_repo.update.assert_called_once_with(problem_id, expected_update_data) + + def test_service_initialization(self): + """Test that service initializes correctly with repositories""" + # Arrange & Act + service = ProblemService( + problem_repo=self.mock_problem_repo, + account_repo=self.mock_account_repo, + permission_repo=self.mock_permission_repo, + group_repo=self.mock_group_repo, + topic_repo=self.mock_topic_repo, + grader=self.mock_grader + ) + + # Assert + self.assertEqual(service.problem_repo, self.mock_problem_repo) + self.assertEqual(service.account_repo, self.mock_account_repo) + self.assertEqual(service.permission_repo, self.mock_permission_repo) + self.assertEqual(service.group_repo, self.mock_group_repo) + self.assertEqual(service.topic_repo, self.mock_topic_repo) + self.assertEqual(service.grader, self.mock_grader) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/api/services/problem_service.py b/api/services/problem_service.py index 7612c8b..228c11e 100644 --- a/api/services/problem_service.py +++ b/api/services/problem_service.py @@ -1,14 +1,14 @@ # from ..utility import JSONParser, JSONParserOne, passwordEncryption -from ..models import * -from .auth_service import verify_token, getAccountByToken -from .permission_service import canManageProblem -from ..utility import generate_random_string, check_pdf -from .service_result import ServiceResult +from api.models import * +from api.services.auth.auth_service import verify_token, getAccountByToken +from api.services.permission_service import canManageProblem +from api.utility import generate_random_string, check_pdf +from api.services.service_result import ServiceResult from django.forms.models import model_to_dict -from ..errors.common import * -from ..errors.grader import * +from api.errors.common import * +from api.errors.grader import * from api.sandbox.grader import Grader -from ..serializers import * +from api.serializers import * def verifyProblem(problem_id): try: diff --git a/api/services/submission/serializers.py b/api/services/submission/serializers.py new file mode 100644 index 0000000..6ba1699 --- /dev/null +++ b/api/services/submission/serializers.py @@ -0,0 +1,42 @@ +from rest_framework import serializers +from api.models import * +from api.services.problem.serializers import ProblemSecureSerializer, TopicSecureSerializer +from api.services.account.serializers import AccountSecureSerializer + +class SubmissionSerializer(serializers.ModelSerializer): + class Meta: + model = Submission + fields = "__all__" + +class SubmissionTestcaseSecureSerializer(serializers.ModelSerializer): + class Meta: + model = SubmissionTestcase + fields = "__all__" + +class SubmissionTestcaseSerializer(serializers.ModelSerializer): + class Meta: + model = SubmissionTestcase + fields = "__all__" + +class SubmissionPopulateSubmissionTestcaseSecureSerializer(serializers.ModelSerializer): + runtime_output = SubmissionTestcaseSecureSerializer(many=True) + class Meta: + model = Submission + fields = ['submission_id','account','problem','topic','language','submission_code','is_passed','date','score','max_score','passed_ratio','runtime_output'] + +class SubmissionPopulateSubmissionTestcaseAndAccountSerializer(serializers.ModelSerializer): + runtime_output = SubmissionTestcaseSerializer(many=True) + account = AccountSecureSerializer() + topic = TopicSecureSerializer() + class Meta: + model = Submission + fields = "__all__" + include = ['runtime_output'] + +class SubmissionPopulateSubmissionTestcaseAndProblemSecureSerializer(serializers.ModelSerializer): + runtime_output = SubmissionTestcaseSecureSerializer(many=True) + problem = ProblemSecureSerializer() + topic = TopicSecureSerializer() + class Meta: + model = Submission + fields = ['submission_id','account','problem','topic','language','submission_code','is_passed','date','score','max_score','passed_ratio','runtime_output'] \ No newline at end of file diff --git a/api/services/submission/submission_service.py b/api/services/submission/submission_service.py new file mode 100644 index 0000000..ff8ebf0 --- /dev/null +++ b/api/services/submission/submission_service.py @@ -0,0 +1,238 @@ +from time import sleep +from unittest.mock import Mock +from api.services.problem.problem_service import ProblemService +from api.services.problem.serializers import ProblemPopulateTestcaseSerializer +from api.utility import regexMatching +from api.sandbox.grader import GradingResult, GradingResultList, ProgramGrader, RuntimeResult +from ...models import * +from django.forms.models import model_to_dict +from .serializers import * +from ...errors.common import * +from api.repositories.submission_repository import SubmissionRepository +from api.repositories.problem_repository import ProblemRepository +from api.repositories.account_repository import AccountRepository +from api.repositories.topic_repository import TopicRepository + +class SubmissionService: + + def __init__(self, submission_repo: SubmissionRepository, problem_repo: ProblemRepository, + account_repo: AccountRepository, topic_repo: TopicRepository, problem_service: ProblemService, grader: dict[ProgramGrader]): + self.submission_repo = submission_repo + self.problem_repo = problem_repo + self.account_repo = account_repo + self.topic_repo = topic_repo + self.problem_service = problem_service + self.QUEUE = [0,0,0,0,0,0,0,0,0,0] + self.grader = grader + + + def get_all_submissions_by_creator_problem(self, problem_id: str, request): + problem = self.problem_repo.get(problem_id) + start = int(request.query_params.get("start",0)) + end = int(request.query_params.get("end",-1)) + # query = request.query_params.get("query","") + if end == -1: end = None + + submissions = self.submission_repo.get_by_problem(problem_id, start, end) + total = len(submissions) + + if total == 0: + return {"submissions": []} + + result = [] + + for submission in submissions: + submission_testcases = self.submission_repo.get_testcases(submission.submission_id) + submission.runtime_output = submission_testcases + result.append(submission) + + problem.testcases = self.problem_repo.get_testcases(problem_id, deprecated=False) + + submissions_serializer = SubmissionPopulateSubmissionTestcaseAndAccountSerializer(result,many=True) + problem_serializer = ProblemPopulateTestcaseSerializer(problem) + + return { + "problem": problem_serializer.data, + "submissions": submissions_serializer.data, + "start": start, + "end": end, + "total": total, + } + + def get_submission_by_quries(self, request): + # Query params + problem_id = str(request.query_params.get("problem_id", "")) + account_id = str(request.query_params.get("account_id", "")) + topic_id = str(request.query_params.get("topic_id", "")) + passed = int(request.query_params.get("passed", -1)) + sort_score = int(request.query_params.get("sort_score", 0)) + sort_date = int(request.query_params.get("sort_date", 0)) + start = int(request.query_params.get("start", -1)) + end = int(request.query_params.get("end", -1)) + + # Convert empty strings to None for repository method + problem_id = problem_id if problem_id != "" else None + account_id = account_id if account_id != "" else None + topic_id = topic_id if topic_id != "" else None + passed = passed if passed != -1 else None + start = start if start != -1 else None + end = end if end != -1 else None + + submissions = self.submission_repo.list( + problem_id=problem_id, + account_id=account_id, + topic_id=topic_id, + passed=passed, + sort_score=sort_score, + sort_date=sort_date, + start=start, + end=end + ) + + for submission in submissions: + submission.runtime_output = self.submission_repo.get_testcases(submission.submission_id) + + serialize = SubmissionPopulateSubmissionTestcaseAndProblemSecureSerializer(submissions,many=True) + return {"submissions": serialize.data} + + def get_submissions_by_account_problem_in_topic(self, account_id:str,problem_id:str,topic_id:str): + submissions = self.submission_repo.get_by_account_problem_topic(account_id, problem_id, topic_id) + + total = len(submissions) + if total == 0: + return {"best_submission": None, "submissions": []} + + result = [] + + for submission in submissions: + submission_testcases = self.submission_repo.get_testcases(submission.submission_id) + submission.runtime_output = submission_testcases + result.append(submission) + + best_submission = self.submission_repo.get_best_record(problem_id, account_id, topic_id) + if best_submission: + best_submission.submission.runtime_output = self.submission_repo.get_testcases(best_submission.submission_id) + best_submission_serializer = SubmissionPopulateSubmissionTestcaseSecureSerializer(best_submission.submission) + best_submission_result = best_submission_serializer.data + else: + best_submission_result = None + + submissions_serializer = SubmissionPopulateSubmissionTestcaseSecureSerializer(result,many=True) + + return {"best_submission": best_submission_result, "submissions": submissions_serializer.data} + + def get_submissions_by_account_problem(self, account_id:str,problem_id:str): + submissions = self.submission_repo.get_by_account_problem(account_id, problem_id) + + total = len(submissions) + if total == 0: + return {"best_submission": None, "submissions": []} + + best_submission_id = self.submission_repo.get_best(problem_id, account_id).submission_id + + best_submission = None + result = [] + + for submission in submissions: + submission_testcases = self.submission_repo.get_testcases(submission.submission_id) + submission.runtime_output = submission_testcases + result.append(submission) + + if submission.submission_id == best_submission_id: + best_submission = submission + + best_submission_serializer = SubmissionPopulateSubmissionTestcaseSecureSerializer(best_submission) + submissions_serializer = SubmissionPopulateSubmissionTestcaseSecureSerializer(result,many=True) + + return {"best_submission": best_submission_serializer.data, "submissions": submissions_serializer.data} + + def submit_problem_on_topic(self, account_id:str,problem_id:str,topic_id:str,request): + return self.submit_problem_function(account_id,problem_id,topic_id,request) + + def avaliableQueue(self): + for i in range(len(self.QUEUE)): + if self.QUEUE[i] == 0: + return i + return -1 + + def submit_problem_function(self, account_id:str,problem_id:str,topic_id:str,request): + problem = self.problem_repo.get(problem_id) + testcases = self.problem_repo.get_testcases(problem_id, deprecated=False) + + submission_code = request.data['submission_code'] + solution_input = [model_to_dict(i)['input'] for i in testcases] + solution_output = [model_to_dict(i)['output'] for i in testcases] + + if not regexMatching(problem.submission_regex,submission_code): + # grading_result = '-'*len(solution_input) + grading_result: GradingResultList = GradingResultList(gradingResult=[GradingResult( + input=solution_input[i], + output="", + runtime_status="FAILED", + expected_output=solution_output[i], + is_passed=False, + ) for i in range(len(solution_input))]) + else: + empty_queue = self.avaliableQueue() + while empty_queue == -1: + empty_queue = self.avaliableQueue() + sleep(5) + + self.QUEUE[empty_queue] = 1 + # grading_result = grader.grading(empty_queue+1,submission_code,solution_input,solution_output) + grader: ProgramGrader = self.grader[request.data['language']](submission_code,solution_input,empty_queue+1,1.5) + grading_result = grader.grading(solution_output) + self.QUEUE[empty_queue] = 0 + + total_score = sum([i.is_passed for i in grading_result.data if i.is_passed]) + max_score = len(grading_result.data) + + submission_data = { + 'problem_id': problem_id, + 'account_id': account_id, + 'language': request.data['language'], + 'submission_code': request.data['submission_code'], + 'is_passed': grading_result.is_passed, + 'score': total_score, + 'max_score': max_score, + 'passed_ratio': total_score/max_score + } + + if topic_id: + submission_data['topic_id'] = topic_id + + submission = self.submission_repo.create(submission_data) + + # Best Submission + self.submission_repo.create_or_update_best( + problem_id=problem_id, + account_id=account_id, + submission_id=submission.submission_id, + topic_id=topic_id + ) + # End Best Submission + + submission_testcases = [] + for i in range(len(grading_result.data)): + submission_testcases.append(SubmissionTestcase( + submission_id = submission.submission_id, + testcase_id = testcases[i].testcase_id, + output = grading_result.data[i].output, + is_passed = grading_result.data[i].is_passed, + runtime_status = grading_result.data[i].runtime_status + )) + + self.submission_repo.bulk_create_testcases(submission_testcases) + + submission.runtime_output = submission_testcases + testser = SubmissionPopulateSubmissionTestcaseSecureSerializer(submission) + + self.problem_service.update_problem_difficulty(problem) + + return testser.data + + def submit_problem(self, account_id:str,problem_id:str,request): + try: + return self.submit_problem_function(account_id,problem_id,None,request) + except Exception as e: + raise InternalServerError(e) \ No newline at end of file diff --git a/api/services/submission/test_submission_service.py b/api/services/submission/test_submission_service.py new file mode 100644 index 0000000..c25ee08 --- /dev/null +++ b/api/services/submission/test_submission_service.py @@ -0,0 +1,589 @@ +import unittest +from unittest.mock import Mock, patch, MagicMock +from django.test import TestCase +from django.http import HttpRequest +from api.sandbox.grader import ProgramGrader +from api.services.submission.submission_service import SubmissionService +from api.repositories.submission_repository import SubmissionRepository +from api.repositories.problem_repository import ProblemRepository +from api.repositories.account_repository import AccountRepository +from api.repositories.topic_repository import TopicRepository +from api.services.problem.problem_service import ProblemService +from api.models import Submission, Problem, Account, Testcase, SubmissionTestcase +from api.errors.common import BadRequestError, InternalServerError, ItemNotFoundError + + +class TestSubmissionService(TestCase): + """Unit tests for SubmissionService""" + + def setUp(self): + """Set up test fixtures""" + self.mock_submission_repo = Mock(spec=SubmissionRepository) + self.mock_problem_repo = Mock(spec=ProblemRepository) + self.mock_account_repo = Mock(spec=AccountRepository) + self.mock_topic_repo = Mock(spec=TopicRepository) + self.mock_problem_service = Mock(spec=ProblemService) + self.mock_grader = Mock() + self.submission_service = SubmissionService( + submission_repo=self.mock_submission_repo, + problem_repo=self.mock_problem_repo, + account_repo=self.mock_account_repo, + topic_repo=self.mock_topic_repo, + problem_service=self.mock_problem_service, + grader=self.mock_grader + ) + + # Sample data + self.sample_account = Mock(spec=Account) + self.sample_account.account_id = 'acc_123' + self.sample_account.username = 'testuser' + + self.sample_problem = Mock(spec=Problem) + self.sample_problem.problem_id = 'prob_123' + self.sample_problem.title = 'Test Problem' + self.sample_problem.submission_regex = r'.*' + + self.sample_testcase = Mock(spec=Testcase) + self.sample_testcase.testcase_id = 'tc_123' + self.sample_testcase.input = 'input' + self.sample_testcase.output = 'output' + + self.sample_submission = Mock(spec=Submission) + self.sample_submission.submission_id = 'sub_123' + self.sample_submission.problem_id = 'prob_123' + self.sample_submission.account_id = 'acc_123' + self.sample_submission.language = 'python' + self.sample_submission.submission_code = 'print("Hello")' + self.sample_submission.is_passed = True + self.sample_submission.score = 1 + self.sample_submission.max_score = 1 + self.sample_submission.passed_ratio = 1.0 + + self.sample_submission_testcase = Mock(spec=SubmissionTestcase) + self.sample_submission_testcase.submission_id = 'sub_123' + self.sample_submission_testcase.testcase_id = 'tc_123' + self.sample_submission_testcase.output = 'output' + self.sample_submission_testcase.is_passed = True + self.sample_submission_testcase.runtime_status = 'AC' + + self.sample_request_data = { + 'submission_code': 'print("Hello")', + 'language': 'python' + } + + def test_get_all_submissions_by_creator_problem_success(self): + """Test getting all submissions by creator problem""" + # Arrange + problem_id = 'prob_123' + mock_request = Mock() + mock_request.query_params = { + 'start': '0', + 'end': '10' + } + + submissions = [self.sample_submission] + testcases = [self.sample_testcase] + submission_testcases = [self.sample_submission_testcase] + + self.mock_problem_repo.get.return_value = self.sample_problem + self.mock_submission_repo.get_by_problem.return_value = submissions + self.mock_submission_repo.get_testcases.return_value = submission_testcases + self.mock_problem_repo.get_testcases.return_value = testcases + + # Mock serializers + with patch('api.services.submission.submission_service.SubmissionPopulateSubmissionTestcaseAndAccountSerializer') as mock_submission_serializer, \ + patch('api.services.submission.submission_service.ProblemPopulateTestcaseSerializer') as mock_problem_serializer: + + mock_submission_serializer_instance = Mock() + mock_submission_serializer_instance.data = [{'submission_id': 'sub_123'}] + mock_submission_serializer.return_value = mock_submission_serializer_instance + + mock_problem_serializer_instance = Mock() + mock_problem_serializer_instance.data = {'problem_id': 'prob_123'} + mock_problem_serializer.return_value = mock_problem_serializer_instance + + # Act + result = self.submission_service.get_all_submissions_by_creator_problem(problem_id, mock_request) + + # Assert + self.mock_problem_repo.get.assert_called_once_with(problem_id) + self.mock_submission_repo.get_by_problem.assert_called_once_with(problem_id, 0, 10) + self.mock_submission_repo.get_testcases.assert_called_once_with('sub_123') + self.mock_problem_repo.get_testcases.assert_called_once_with(problem_id, deprecated=False) + + # Verify result structure + self.assertIsInstance(result, dict) + self.assertIn('problem', result) + self.assertIn('submissions', result) + self.assertIn('start', result) + self.assertIn('end', result) + self.assertIn('total', result) + + def test_get_all_submissions_by_creator_problem_no_submissions(self): + """Test getting all submissions when no submissions exist""" + # Arrange + problem_id = 'prob_123' + mock_request = Mock() + mock_request.query_params = { + 'start': '0', + 'end': '10' + } + + self.mock_problem_repo.get.return_value = self.sample_problem + self.mock_submission_repo.get_by_problem.return_value = [] + + # Act + result = self.submission_service.get_all_submissions_by_creator_problem(problem_id, mock_request) + + # Assert + self.mock_problem_repo.get.assert_called_once_with(problem_id) + self.mock_submission_repo.get_by_problem.assert_called_once_with(problem_id, 0, 10) + + # Verify result structure + self.assertIsInstance(result, dict) + self.assertIn('submissions', result) + self.assertEqual(result['submissions'], []) + + def test_get_all_submissions_by_creator_problem_with_default_params(self): + """Test getting all submissions with default parameters""" + # Arrange + problem_id = 'prob_123' + mock_request = Mock() + mock_request.query_params = {} # No query parameters + + submissions = [self.sample_submission] + testcases = [self.sample_testcase] + submission_testcases = [self.sample_submission_testcase] + + self.mock_problem_repo.get.return_value = self.sample_problem + self.mock_submission_repo.get_by_problem.return_value = submissions + self.mock_submission_repo.get_testcases.return_value = submission_testcases + self.mock_problem_repo.get_testcases.return_value = testcases + + # Mock serializers + with patch('api.services.submission.submission_service.SubmissionPopulateSubmissionTestcaseAndAccountSerializer') as mock_submission_serializer, \ + patch('api.services.submission.submission_service.ProblemPopulateTestcaseSerializer') as mock_problem_serializer: + + mock_submission_serializer_instance = Mock() + mock_submission_serializer_instance.data = [{'submission_id': 'sub_123'}] + mock_submission_serializer.return_value = mock_submission_serializer_instance + + mock_problem_serializer_instance = Mock() + mock_problem_serializer_instance.data = {'problem_id': 'prob_123'} + mock_problem_serializer.return_value = mock_problem_serializer_instance + + # Act + result = self.submission_service.get_all_submissions_by_creator_problem(problem_id, mock_request) + + # Assert + self.mock_submission_repo.get_by_problem.assert_called_once_with(problem_id, 0, None) + + def test_get_submission_by_queries_success(self): + """Test getting submissions by queries""" + # Arrange + mock_request = Mock() + mock_request.query_params = { + 'problem_id': 'prob_123', + 'account_id': 'acc_123', + 'topic_id': 'topic_123', + 'passed': '1', + 'sort_score': '1', + 'sort_date': '0', + 'start': '0', + 'end': '10' + } + + submissions = [self.sample_submission] + submission_testcases = [self.sample_submission_testcase] + + self.mock_submission_repo.list.return_value = submissions + self.mock_submission_repo.get_testcases.return_value = submission_testcases + + # Mock serializer + with patch('api.services.submission.submission_service.SubmissionPopulateSubmissionTestcaseAndProblemSecureSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = [{'submission_id': 'sub_123'}] + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.submission_service.get_submission_by_quries(mock_request) + + # Assert + self.mock_submission_repo.list.assert_called_once_with( + problem_id='prob_123', + account_id='acc_123', + topic_id='topic_123', + passed=1, + sort_score=1, + sort_date=0, + start=0, + end=10 + ) + self.mock_submission_repo.get_testcases.assert_called_once_with('sub_123') + + # Verify result structure + self.assertIsInstance(result, dict) + self.assertIn('submissions', result) + + def test_get_submission_by_queries_with_empty_params(self): + """Test getting submissions with empty query parameters""" + # Arrange + mock_request = Mock() + mock_request.query_params = { + 'problem_id': '', + 'account_id': '', + 'topic_id': '', + 'passed': '-1', + 'sort_score': '0', + 'sort_date': '0', + 'start': '-1', + 'end': '-1' + } + + submissions = [self.sample_submission] + submission_testcases = [self.sample_submission_testcase] + + self.mock_submission_repo.list.return_value = submissions + self.mock_submission_repo.get_testcases.return_value = submission_testcases + + # Mock serializer + with patch('api.services.submission.submission_service.SubmissionPopulateSubmissionTestcaseAndProblemSecureSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = [{'submission_id': 'sub_123'}] + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.submission_service.get_submission_by_quries(mock_request) + + # Assert + self.mock_submission_repo.list.assert_called_once_with( + problem_id=None, + account_id=None, + topic_id=None, + passed=None, + sort_score=0, + sort_date=0, + start=None, + end=None + ) + + def test_get_submissions_by_account_problem_in_topic_success(self): + """Test getting submissions by account problem in topic""" + # Arrange + account_id = 'acc_123' + problem_id = 'prob_123' + topic_id = 'topic_123' + + submissions = [self.sample_submission] + submission_testcases = [self.sample_submission_testcase] + best_submission = Mock() + best_submission.submission_id = 'sub_123' + best_submission.submission = self.sample_submission + + self.mock_submission_repo.get_by_account_problem_topic.return_value = submissions + self.mock_submission_repo.get_testcases.return_value = submission_testcases + self.mock_submission_repo.get_best_record.return_value = best_submission + + # Mock serializer + with patch('api.services.submission.submission_service.SubmissionPopulateSubmissionTestcaseSecureSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = {'submission_id': 'sub_123'} + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.submission_service.get_submissions_by_account_problem_in_topic(account_id, problem_id, topic_id) + + # Assert + self.mock_submission_repo.get_by_account_problem_topic.assert_called_once_with(account_id, problem_id, topic_id) + self.mock_submission_repo.get_testcases.assert_any_call('sub_123') + self.mock_submission_repo.get_best_record.assert_called_once_with(problem_id, account_id, topic_id) + + # Verify result structure + self.assertIsInstance(result, dict) + self.assertIn('best_submission', result) + self.assertIn('submissions', result) + + def test_get_submissions_by_account_problem_in_topic_no_submissions(self): + """Test getting submissions when no submissions exist""" + # Arrange + account_id = 'acc_123' + problem_id = 'prob_123' + topic_id = 'topic_123' + + self.mock_submission_repo.get_by_account_problem_topic.return_value = [] + + # Act + result = self.submission_service.get_submissions_by_account_problem_in_topic(account_id, problem_id, topic_id) + + # Assert + self.mock_submission_repo.get_by_account_problem_topic.assert_called_once_with(account_id, problem_id, topic_id) + + # Verify result structure + self.assertIsInstance(result, dict) + self.assertIn('best_submission', result) + self.assertIn('submissions', result) + self.assertIsNone(result['best_submission']) + self.assertEqual(result['submissions'], []) + + def test_get_submissions_by_account_problem_success(self): + """Test getting submissions by account problem""" + # Arrange + account_id = 'acc_123' + problem_id = 'prob_123' + + submissions = [self.sample_submission] + submission_testcases = [self.sample_submission_testcase] + + self.mock_submission_repo.get_by_account_problem.return_value = submissions + self.mock_submission_repo.get_testcases.return_value = submission_testcases + + # Mock serializer + with patch('api.services.submission.submission_service.SubmissionPopulateSubmissionTestcaseSecureSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = {'submission_id': 'sub_123'} + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.submission_service.get_submissions_by_account_problem(account_id, problem_id) + + # Assert + self.mock_submission_repo.get_by_account_problem.assert_called_once_with(account_id, problem_id) + self.mock_submission_repo.get_testcases.assert_called_once_with('sub_123') + + # Verify result structure + self.assertIsInstance(result, dict) + self.assertIn('best_submission', result) + self.assertIn('submissions', result) + + def test_get_submissions_by_account_problem_no_submissions(self): + """Test getting submissions when no submissions exist""" + # Arrange + account_id = 'acc_123' + problem_id = 'prob_123' + + self.mock_submission_repo.get_by_account_problem.return_value = [] + + # Act + result = self.submission_service.get_submissions_by_account_problem(account_id, problem_id) + + # Assert + self.mock_submission_repo.get_by_account_problem.assert_called_once_with(account_id, problem_id) + + # Verify result structure + self.assertIsInstance(result, dict) + self.assertIn('best_submission', result) + self.assertIn('submissions', result) + self.assertIsNone(result['best_submission']) + self.assertEqual(result['submissions'], []) + + def test_submit_problem_on_topic_success(self): + """Test submitting problem on topic""" + # Arrange + account_id = 'acc_123' + problem_id = 'prob_123' + topic_id = 'topic_123' + mock_request = Mock() + mock_request.data = self.sample_request_data.copy() + + testcases = [self.sample_testcase] + + self.mock_problem_repo.get.return_value = self.sample_problem + self.mock_problem_repo.get_testcases.return_value = testcases + self.mock_account_repo.get.return_value = self.sample_account + + # Mock grading result + mock_grading_result = Mock() + mock_grading_result.is_passed = True + mock_testcase_result = Mock() + mock_testcase_result.is_passed = True + mock_grading_result.data = [mock_testcase_result] + + # Mock grader + with patch('api.services.submission.submission_service.regexMatching') as mock_regex, \ + patch('api.services.submission.submission_service.model_to_dict') as mock_model_to_dict: + + # Mock grader dictionary access + mock_grader_class = Mock() + mock_grader_instance = Mock() + mock_grader_instance.grading.return_value = mock_grading_result + mock_grader_class.return_value = mock_grader_instance + self.mock_grader.__getitem__ = Mock(return_value=mock_grader_class) + + mock_regex.return_value = True + mock_model_to_dict.return_value = {'input': 'input', 'output': 'output'} + + # Mock submission creation + self.mock_submission_repo.create.return_value = self.sample_submission + + # Mock serializer + with patch('api.services.submission.submission_service.SubmissionPopulateSubmissionTestcaseSecureSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = {'submission_id': 'sub_123'} + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.submission_service.submit_problem_on_topic(account_id, problem_id, topic_id, mock_request) + + # Assert + self.mock_problem_repo.get.assert_called_once_with(problem_id) + self.mock_problem_repo.get_testcases.assert_called_once_with(problem_id, deprecated=False) + self.mock_submission_repo.create.assert_called_once() + self.mock_submission_repo.create_or_update_best.assert_called_once() + self.mock_submission_repo.bulk_create_testcases.assert_called_once() + self.mock_problem_service.update_problem_difficulty.assert_called_once_with(self.sample_problem) + + # Verify result + self.assertIsInstance(result, dict) + self.assertIn('submission_id', result) + + def test_submit_problem_success(self): + """Test submitting problem""" + # Arrange + account_id = 'acc_123' + problem_id = 'prob_123' + mock_request = Mock() + mock_request.data = self.sample_request_data.copy() + + testcases = [self.sample_testcase] + + self.mock_problem_repo.get.return_value = self.sample_problem + self.mock_problem_repo.get_testcases.return_value = testcases + self.mock_account_repo.get.return_value = self.sample_account + + # Mock grading result + mock_grading_result = Mock() + mock_grading_result.is_passed = True + mock_testcase_result = Mock() + mock_testcase_result.is_passed = True + mock_grading_result.data = [mock_testcase_result] + + # Mock grader + with patch('api.services.submission.submission_service.regexMatching') as mock_regex, \ + patch('api.services.submission.submission_service.model_to_dict') as mock_model_to_dict: + + # Mock grader dictionary access + mock_grader_class = Mock() + mock_grader_instance = Mock() + mock_grader_instance.grading.return_value = mock_grading_result + mock_grader_class.return_value = mock_grader_instance + self.mock_grader.__getitem__ = Mock(return_value=mock_grader_class) + + mock_regex.return_value = True + mock_model_to_dict.return_value = {'input': 'input', 'output': 'output'} + + # Mock submission creation + self.mock_submission_repo.create.return_value = self.sample_submission + + # Mock serializer + with patch('api.services.submission.submission_service.SubmissionPopulateSubmissionTestcaseSecureSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = {'submission_id': 'sub_123'} + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.submission_service.submit_problem(account_id, problem_id, mock_request) + + # Assert + self.mock_problem_repo.get.assert_called_once_with(problem_id) + self.mock_problem_repo.get_testcases.assert_called_once_with(problem_id, deprecated=False) + self.mock_submission_repo.create.assert_called_once() + self.mock_submission_repo.create_or_update_best.assert_called_once() + self.mock_submission_repo.bulk_create_testcases.assert_called_once() + self.mock_problem_service.update_problem_difficulty.assert_called_once_with(self.sample_problem) + + # Verify result + self.assertIsInstance(result, dict) + self.assertIn('submission_id', result) + + def test_submit_problem_with_exception(self): + """Test submitting problem with exception""" + # Arrange + account_id = 'acc_123' + problem_id = 'prob_123' + mock_request = Mock() + mock_request.data = self.sample_request_data.copy() + + # Mock exception in submit_problem_function + with patch.object(self.submission_service, 'submit_problem_function', side_effect=Exception("Test error")): + # Act & Assert + with self.assertRaises(InternalServerError): + self.submission_service.submit_problem(account_id, problem_id, mock_request) + + def test_submit_problem_regex_mismatch(self): + """Test submitting problem with regex mismatch""" + # Arrange + account_id = 'acc_123' + problem_id = 'prob_123' + topic_id = 'topic_123' + mock_request = Mock() + mock_request.data = self.sample_request_data.copy() + + testcases = [self.sample_testcase] + + self.mock_problem_repo.get.return_value = self.sample_problem + self.mock_problem_repo.get_testcases.return_value = testcases + self.mock_account_repo.get.return_value = self.sample_account + + # Mock grading result for failed submission + mock_grading_result = Mock() + mock_grading_result.is_passed = False + mock_testcase_result = Mock() + mock_testcase_result.is_passed = False + mock_grading_result.data = [mock_testcase_result] + + # Mock grader and regex mismatch + with patch('api.services.submission.submission_service.regexMatching') as mock_regex, \ + patch('api.services.submission.submission_service.model_to_dict') as mock_model_to_dict: + + # Mock grader dictionary access + mock_grader_class = Mock() + mock_grader_instance = Mock() + mock_grader_instance.grading.return_value = mock_grading_result + mock_grader_class.return_value = mock_grader_instance + self.mock_grader.__getitem__ = Mock(return_value=mock_grader_class) + + mock_regex.return_value = False + mock_model_to_dict.return_value = {'input': 'input', 'output': 'output'} + + # Mock submission creation + self.mock_submission_repo.create.return_value = self.sample_submission + + # Mock serializer + with patch('api.services.submission.submission_service.SubmissionPopulateSubmissionTestcaseSecureSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = {'submission_id': 'sub_123'} + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.submission_service.submit_problem_on_topic(account_id, problem_id, topic_id, mock_request) + + # Assert + # Should still create submission but with failed grading + self.mock_submission_repo.create.assert_called_once() + + # Verify result + self.assertIsInstance(result, dict) + self.assertIn('submission_id', result) + + def test_service_initialization(self): + """Test that service initializes correctly with repositories""" + # Arrange & Act + service = SubmissionService( + submission_repo=self.mock_submission_repo, + problem_repo=self.mock_problem_repo, + account_repo=self.mock_account_repo, + topic_repo=self.mock_topic_repo, + problem_service=self.mock_problem_service, + grader=self.mock_grader + ) + + # Assert + self.assertEqual(service.submission_repo, self.mock_submission_repo) + self.assertEqual(service.problem_repo, self.mock_problem_repo) + self.assertEqual(service.account_repo, self.mock_account_repo) + self.assertEqual(service.topic_repo, self.mock_topic_repo) + self.assertEqual(service.problem_service, self.mock_problem_service) + + +if __name__ == '__main__': + unittest.main() diff --git a/api/services/topic/serializers.py b/api/services/topic/serializers.py new file mode 100644 index 0000000..38faebc --- /dev/null +++ b/api/services/topic/serializers.py @@ -0,0 +1,185 @@ +from rest_framework import serializers +from django.utils import timezone +from ...models import * + +# Dependencies +class AccountSecureSerializer(serializers.ModelSerializer): + class Meta: + model = Account + fields = ['account_id', 'username'] + +class GroupSerializer(serializers.ModelSerializer): + class Meta: + model = Group + fields = "__all__" + +class CollectionSerializer(serializers.ModelSerializer): + class Meta: + model = Collection + fields = "__all__" + +class ProblemSerializer(serializers.ModelSerializer): + class Meta: + model = Problem + fields = "__all__" + +class ProblemSecureSerializer(serializers.ModelSerializer): + class Meta: + model = Problem + exclude = ['solution', 'submission_regex', 'is_private', 'is_active', 'sharing'] + +class ProblemPopulateAccountSecureSerializer(serializers.ModelSerializer): + creator = AccountSecureSerializer() + class Meta: + model = Problem + exclude = ['solution', 'submission_regex', 'is_private', 'is_active', 'sharing'] + +# Core Topic Serializers +class TopicSerializer(serializers.ModelSerializer): + class Meta: + model = Topic + fields = "__all__" + + def create(self, validate_data): + return Topic.objects.create(**validate_data) + + def update(self, instance, validate_data): + instance.name = validate_data.get('name', instance.name) + instance.description = validate_data.get('description', instance.description) + instance.image_url = validate_data.get('image_url', instance.image_url) + instance.is_active = validate_data.get('is_active', instance.is_active) + instance.is_private = validate_data.get('is_private', instance.is_private) + instance.updated_date = timezone.now() + instance.save() + return instance + +class TopicSecureSerializer(serializers.ModelSerializer): + class Meta: + model = Topic + exclude = ['sharing', 'is_active', 'is_private'] + +# Topic Collection Serializers +class TopicCollectionSerializer(serializers.ModelSerializer): + class Meta: + model = TopicCollection + fields = "__all__" + +class TopicCollectionPopulateCollectionSerializer(serializers.ModelSerializer): + collection = CollectionSerializer() + class Meta: + model = TopicCollection + fields = "__all__" + +class TopicPopulateTopicCollectionPopulateCollectionSerializer(serializers.ModelSerializer): + collections = TopicCollectionPopulateCollectionSerializer(many=True) + class Meta: + model = Topic + fields = "__all__" + include = ['collections'] + +# Collection Problem Serializers (needed for topic functionality) +class CollectionProblemSerializer(serializers.ModelSerializer): + class Meta: + model = CollectionProblem + fields = "__all__" + +class CollectionProblemPopulateProblemSerializer(serializers.ModelSerializer): + problem = ProblemSerializer() + class Meta: + model = CollectionProblem + fields = "__all__" + +class CollectionPopulateCollectionProblemPopulateProblemSerializer(serializers.ModelSerializer): + problems = CollectionProblemPopulateProblemSerializer(many=True) + class Meta: + model = Collection + fields = "__all__" + +class TopicCollectionPopulateCollectionPopulateCollectionProblemPopulateProblemSerializer(serializers.ModelSerializer): + collection = CollectionPopulateCollectionProblemPopulateProblemSerializer() + class Meta: + model = TopicCollection + fields = "__all__" + +class TopicPopulateTopicCollectionPopulateCollectionPopulateCollectionProblemPopulateProblemSerializer(serializers.ModelSerializer): + collections = TopicCollectionPopulateCollectionPopulateCollectionProblemPopulateProblemSerializer(many=True) + class Meta: + model = Topic + fields = ['topic_id', 'name', 'description', 'image_url', 'is_active', 'is_private', 'created_date', 'updated_date', 'collections'] + +# Topic Group Permission Serializers +class TopicGroupPermissionsSerializer(serializers.ModelSerializer): + class Meta: + model = TopicGroupPermission + fields = "__all__" + +class TopicGroupPermissionPopulateGroupSerializer(serializers.ModelSerializer): + group = GroupSerializer() + class Meta: + model = TopicGroupPermission + fields = "__all__" + +class TopicPopulateTopicGroupPermissionsSerializer(serializers.ModelSerializer): + group_permissions = TopicGroupPermissionsSerializer(many=True) + class Meta: + model = Topic + fields = "__all__" + include = ['group_permissions'] + +class TopicPopulateTopicCollectionPopulateCollectionAndTopicGroupPermissionPopulateGroupSerializer(serializers.ModelSerializer): + collections = TopicCollectionPopulateCollectionSerializer(many=True) + group_permissions = TopicGroupPermissionPopulateGroupSerializer(many=True) + class Meta: + model = Topic + fields = "__all__" + include = ['collections', 'group_permissions'] + +# Collection Group Permission Serializers (needed for topic functionality) +class CollectionGroupPermissionPopulateGroupSerializer(serializers.ModelSerializer): + group = GroupSerializer() + class Meta: + model = CollectionGroupPermission + fields = "__all__" + +class CollectionPopulateCollectionProblemsPopulateProblemAndCollectionGroupPermissionsPopulateGroupSerializer(serializers.ModelSerializer): + problems = CollectionProblemPopulateProblemSerializer(many=True) + group_permissions = CollectionGroupPermissionPopulateGroupSerializer(many=True) + class Meta: + model = Collection + fields = "__all__" + include = ['problems', 'group_permissions'] + +class TopicCollectionPopulateCollectionPopulateCollectionProblemsPopulateProblemAndCollectionGroupPermissionsPopulateGroupSerializer(serializers.ModelSerializer): + collection = CollectionPopulateCollectionProblemsPopulateProblemAndCollectionGroupPermissionsPopulateGroupSerializer() + class Meta: + model = TopicCollection + fields = "__all__" + +class TopicPopulateTopicCollectionPopulateCollectionPopulateCollectionProblemsPopulateProblemAndCollectionGroupPermissionsPopulateGroupAndTopicGroupPermissionPopulateGroupSerializer(serializers.ModelSerializer): + collections = TopicCollectionPopulateCollectionPopulateCollectionProblemsPopulateProblemAndCollectionGroupPermissionsPopulateGroupSerializer(many=True) + group_permissions = TopicGroupPermissionPopulateGroupSerializer(many=True) + + class Meta: + model = Topic + fields = "__all__" + include = ['collections', 'group_permissions'] + +# Complex Submission Integration Serializers (simplified to avoid circular imports) +class TopicPopulateTopicCollectionPopulateCollectionPopulateCollectionProblemPopulateProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureSerializer(serializers.ModelSerializer): + # Note: This would need submission serializers, using basic problem serializer to avoid circular imports + collections = TopicCollectionPopulateCollectionPopulateCollectionProblemPopulateProblemSerializer(many=True) + class Meta: + model = Topic + fields = ['topic_id', 'name', 'description', 'image_url', 'created_date', 'updated_date', 'collections'] + +# Topic Problem Serializers (legacy) +class TopicProblemSerializer(serializers.ModelSerializer): + class Meta: + model = TopicProblem + fields = "__all__" + +# Topic Account Access Serializers +class TopicAccountAccessSerialize(serializers.ModelSerializer): + class Meta: + model = TopicAccountAccess + fields = "__all__" diff --git a/api/services/topic/test_topic_service.py b/api/services/topic/test_topic_service.py new file mode 100644 index 0000000..640bf47 --- /dev/null +++ b/api/services/topic/test_topic_service.py @@ -0,0 +1,546 @@ +import unittest +from unittest.mock import Mock, patch, MagicMock +from django.test import TestCase +from django.http import HttpRequest +from api.services.topic.topic_service import TopicService +from api.repositories.topic_repository import TopicRepository +from api.repositories.account_repository import AccountRepository +from api.repositories.permission_repository import PermissionRepository +from api.repositories.group_repository import GroupRepository +from api.repositories.collection_repository import CollectionRepository +from api.models import Topic, Account, TopicCollection, TopicGroupPermission, Collection, Group +from api.errors.common import BadRequestError, InternalServerError, ItemNotFoundError + + +class TestTopicService(TestCase): + """Unit tests for TopicService""" + + def setUp(self): + """Set up test fixtures""" + self.mock_topic_repo = Mock(spec=TopicRepository) + self.mock_account_repo = Mock(spec=AccountRepository) + self.mock_permission_repo = Mock(spec=PermissionRepository) + self.mock_group_repo = Mock(spec=GroupRepository) + self.mock_collection_repo = Mock(spec=CollectionRepository) + + self.topic_service = TopicService( + topic_repo=self.mock_topic_repo, + account_repo=self.mock_account_repo, + permission_repo=self.mock_permission_repo, + group_repo=self.mock_group_repo, + collection_repo=self.mock_collection_repo + ) + + # Sample data + self.sample_account = Mock(spec=Account) + self.sample_account.account_id = 'acc_123' + self.sample_account.username = 'testuser' + + self.sample_topic = Mock(spec=Topic) + self.sample_topic.topic_id = 'topic_123' + self.sample_topic.name = 'Test Topic' + self.sample_topic.description = 'Test Description' + self.sample_topic.creator_id = 'acc_123' + self.sample_topic.created_date = '2023-01-01T00:00:00Z' + self.sample_topic.updated_date = '2023-01-01T00:00:00Z' + + self.sample_collection = Mock(spec=Collection) + self.sample_collection.collection_id = 'coll_123' + self.sample_collection.name = 'Test Collection' + self.sample_collection.problems = [] + + self.sample_topic_collection = Mock(spec=TopicCollection) + self.sample_topic_collection.topic_id = 'topic_123' + self.sample_topic_collection.collection_id = 'coll_123' + self.sample_topic_collection.collection = self.sample_collection + self.sample_topic_collection.order = 0 + + self.sample_group = Mock(spec=Group) + self.sample_group.group_id = 'group_123' + self.sample_group.name = 'Test Group' + + self.sample_request_data = { + 'name': 'Test Topic', + 'description': 'Test Description' + } + + def test_create_topic_success(self): + """Test successful topic creation""" + # Arrange + account_id = 'acc_123' + mock_request = Mock() + mock_request.data = self.sample_request_data.copy() + + self.mock_topic_repo.create.return_value = self.sample_topic + + # Mock serializer + with patch('api.services.topic.topic_service.TopicSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = { + 'topic_id': 'topic_123', + 'name': 'Test Topic', + 'description': 'Test Description' + } + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.topic_service.create_topic(account_id, mock_request) + + # Assert + expected_topic_data = { + 'creator_id': account_id, + 'name': 'Test Topic', + 'description': 'Test Description' + } + self.mock_topic_repo.create.assert_called_once_with(expected_topic_data) + + # Verify result + self.assertIsInstance(result, dict) + self.assertIn('topic_id', result) + self.assertIn('name', result) + + def test_delete_topic_success(self): + """Test successful topic deletion""" + # Arrange + topic_id = 'topic_123' + + # Act + result = self.topic_service.delete_topic(topic_id) + + # Assert + self.mock_topic_repo.delete.assert_called_once_with(topic_id) + self.assertIsNone(result) + + def test_get_topic_success(self): + """Test successful topic retrieval""" + # Arrange + topic_id = 'topic_123' + permissions = [Mock(spec=TopicGroupPermission)] + collections = [self.sample_topic_collection] + + self.mock_topic_repo.get.return_value = self.sample_topic + self.mock_permission_repo.get_topic_permissions.return_value = permissions + self.mock_topic_repo.get_collections.return_value = collections + self.mock_collection_repo.get_problems.return_value = [] + self.mock_permission_repo.get_collection_permissions.return_value = [] + + # Mock serializer + with patch('api.services.topic.topic_service.TopicPopulateTopicCollectionPopulateCollectionPopulateCollectionProblemsPopulateProblemAndCollectionGroupPermissionsPopulateGroupAndTopicGroupPermissionPopulateGroupSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = { + 'topic_id': 'topic_123', + 'name': 'Test Topic', + 'collections': [] + } + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.topic_service.get_topic(topic_id) + + # Assert + self.mock_topic_repo.get.assert_called_once_with(topic_id) + self.mock_permission_repo.get_topic_permissions.assert_called_once_with(topic_id) + self.mock_topic_repo.get_collections.assert_called_once_with(topic_id) + + # Verify result + self.assertIsInstance(result, dict) + self.assertIn('topic_id', result) + + def test_get_all_topics_success(self): + """Test getting all topics""" + # Arrange + mock_request = Mock() + mock_request.query_params = {'account_id': 'acc_123'} + + topics = [self.sample_topic] + self.mock_topic_repo.list.return_value = topics + + # Mock serializer + with patch('api.services.topic.topic_service.TopicSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = [{'topic_id': 'topic_123', 'name': 'Test Topic'}] + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.topic_service.get_all_topics(mock_request) + + # Assert + expected_filters = {'creator_id': 'acc_123'} + self.mock_topic_repo.list.assert_called_once_with(filters=expected_filters) + + # Verify result structure + self.assertIsInstance(result, dict) + self.assertIn('topics', result) + self.assertIsInstance(result['topics'], list) + + def test_get_all_topics_without_account_filter(self): + """Test getting all topics without account filter""" + # Arrange + mock_request = Mock() + mock_request.query_params = {} # No account_id + + topics = [self.sample_topic] + self.mock_topic_repo.list.return_value = topics + + # Mock serializer + with patch('api.services.topic.topic_service.TopicSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = [{'topic_id': 'topic_123', 'name': 'Test Topic'}] + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.topic_service.get_all_topics(mock_request) + + # Assert + self.mock_topic_repo.list.assert_called_once_with(filters={}) + + # Verify result structure + self.assertIsInstance(result, dict) + self.assertIn('topics', result) + + def test_update_topic_success(self): + """Test successful topic update""" + # Arrange + topic_id = 'topic_123' + mock_request = Mock() + mock_request.data = { + 'name': 'Updated Topic', + 'description': 'Updated Description' + } + + updated_topic = Mock(spec=Topic) + updated_topic.topic_id = topic_id + updated_topic.name = 'Updated Topic' + + self.mock_topic_repo.update.return_value = updated_topic + + # Mock serializer + with patch('api.services.topic.topic_service.TopicSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = { + 'topic_id': 'topic_123', + 'name': 'Updated Topic' + } + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.topic_service.update_topic(topic_id, mock_request) + + # Assert + self.mock_topic_repo.update.assert_called_once_with(topic_id, { + 'name': 'Updated Topic', + 'description': 'Updated Description' + }) + + # Verify result + self.assertIsInstance(result, dict) + self.assertIn('topic_id', result) + + def test_get_all_topics_by_account_success(self): + """Test getting all topics by account""" + # Arrange + account_id = 'acc_123' + mock_request = Mock() + + personal_topics = [self.sample_topic] + group_ids = ['group_123'] + manageable_topics = [] + + self.mock_account_repo.get.return_value = self.sample_account + self.mock_topic_repo.get_by_creator.return_value = personal_topics + self.mock_group_repo.get_ids_by_account.return_value = group_ids + self.mock_topic_repo.get_manageable_by_ids.return_value = manageable_topics + # Mock QuerySet-like object for collections + mock_collections = Mock() + mock_collections.filter.return_value = [] + self.mock_topic_repo.get_many_collections.return_value = mock_collections + + # Mock serializers + with patch('api.services.topic.topic_service.TopicPopulateTopicCollectionPopulateCollectionSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = [{'topic_id': 'topic_123', 'name': 'Test Topic'}] + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.topic_service.get_all_topics_by_account(account_id, mock_request) + + # Assert + self.mock_account_repo.get.assert_called_once_with(account_id) + self.mock_topic_repo.get_by_creator.assert_called_once_with(account_id) + self.mock_group_repo.get_ids_by_account.assert_called_once_with(account_id) + self.mock_topic_repo.get_manageable_by_ids.assert_called_once_with(group_ids) + + # Verify result structure + self.assertIsInstance(result, dict) + self.assertIn('topics', result) + self.assertIn('manageable_topics', result) + + def test_get_all_topics_by_account_account_not_found(self): + """Test getting all topics when account doesn't exist""" + # Arrange + account_id = 'nonexistent' + mock_request = Mock() + + self.mock_account_repo.get.side_effect = Account.DoesNotExist() + + # Act & Assert + with self.assertRaises(Account.DoesNotExist): + self.topic_service.get_all_topics_by_account(account_id, mock_request) + + self.mock_account_repo.get.assert_called_once_with(account_id) + + def test_get_all_accessed_topics_by_account_success(self): + """Test getting all accessed topics by account""" + # Arrange + account_id = 'acc_123' + + # Mock accessible topics + accessible_topic = Mock() + accessible_topic.topic = self.sample_topic + + self.mock_group_repo.get_accessible_by_account.return_value = [accessible_topic] + + # Mock serializer + with patch('api.services.topic.topic_service.TopicSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = [{'topic_id': 'topic_123', 'name': 'Test Topic'}] + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.topic_service.get_all_accessed_topics_by_account(account_id) + + # Assert + self.mock_group_repo.get_accessible_by_account.assert_called_once_with(account_id) + + # Verify result structure + self.assertIsInstance(result, dict) + self.assertIn('topics', result) + + def test_get_topic_public_success(self): + """Test getting public topic""" + # Arrange + topic_id = 'topic_123' + account_id = 'acc_123' + mock_request = Mock() + mock_request.query_params = {'account_id': account_id} + + group_ids = ['group_123'] + topic_collections = [self.sample_topic_collection] + + self.mock_topic_repo.get.return_value = self.sample_topic + self.mock_account_repo.get.return_value = self.sample_account + self.mock_group_repo.get_accessible_collections_with_access.return_value = topic_collections + self.mock_group_repo.get_ids_by_account.return_value = group_ids + self.mock_permission_repo.get_accessible_problems_for_collections.return_value = topic_collections + self.mock_topic_repo.get_best_submission_for_problem.return_value = None + + # Mock serializer + with patch('api.services.topic.topic_service.TopicPopulateTopicCollectionPopulateCollectionPopulateCollectionProblemPopulateProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = { + 'topic_id': 'topic_123', + 'name': 'Test Topic', + 'collections': [] + } + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.topic_service.get_topic_public(topic_id, mock_request) + + # Assert + self.mock_topic_repo.get.assert_called_once_with(topic_id) + self.mock_account_repo.get.assert_called_once_with(account_id) + self.mock_group_repo.get_accessible_collections_with_access.assert_called_once_with(topic_id, account_id) + self.mock_group_repo.get_ids_by_account.assert_called_once_with(account_id) + self.mock_permission_repo.get_accessible_problems_for_collections.assert_called_once_with(topic_collections, group_ids) + + # Verify result + self.assertIsInstance(result, dict) + self.assertIn('topic_id', result) + + def test_update_groups_permission_to_topic_success(self): + """Test updating group permissions for topic""" + # Arrange + topic_id = 'topic_123' + mock_request = Mock() + mock_request.data = { + 'groups': [ + {'group_id': 'group_123', 'permission_view_topics': True, 'permission_manage_topics': False, 'permission_view_topics_log': False} + ] + } + + self.mock_topic_repo.get.return_value = self.sample_topic + self.mock_group_repo.get.return_value = self.sample_group + + # Mock serializer + with patch('api.services.topic.topic_service.TopicPopulateTopicGroupPermissionsSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = { + 'topic_id': 'topic_123', + 'group_permissions': [] + } + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.topic_service.update_groups_permission_to_topic(topic_id, mock_request) + + # Assert + self.mock_topic_repo.get.assert_called_once_with(topic_id) + self.mock_permission_repo.delete_topic_permissions.assert_called_once_with(topic_id) + self.mock_group_repo.get.assert_called_once_with('group_123') + self.mock_permission_repo.bulk_create_topic_permissions.assert_called_once() + + # Verify result + self.assertIsInstance(result, dict) + self.assertIn('topic_id', result) + + def test_update_collections_to_topic_success(self): + """Test updating collections in topic""" + # Arrange + topic_id = 'topic_123' + mock_request = Mock() + mock_request.data = { + 'collection_ids': ['coll_123', 'coll_456'] + } + + self.mock_topic_repo.get.return_value = self.sample_topic + self.mock_collection_repo.get.side_effect = [self.sample_collection, self.sample_collection] + + # Mock serializers + with patch('api.services.topic.topic_service.TopicCollectionPopulateCollectionSerializer') as mock_collection_serializer, \ + patch('api.services.topic.topic_service.TopicSerializer') as mock_topic_serializer: + + mock_collection_serializer_instance = Mock() + mock_collection_serializer_instance.data = [{'collection_id': 'coll_123'}] + mock_collection_serializer.return_value = mock_collection_serializer_instance + + mock_topic_serializer_instance = Mock() + mock_topic_serializer_instance.data = {'topic_id': 'topic_123'} + mock_topic_serializer.return_value = mock_topic_serializer_instance + + # Act + result = self.topic_service.update_collections_to_topic(topic_id, mock_request) + + # Assert + self.mock_topic_repo.get.assert_called_once_with(topic_id) + self.mock_topic_repo.delete_collections.assert_called_once_with(topic_id) + self.mock_collection_repo.get.assert_any_call('coll_123') + self.mock_collection_repo.get.assert_any_call('coll_456') + self.mock_topic_repo.bulk_create_collections.assert_called_once() + self.mock_topic_repo.update_with_timestamp.assert_called_once_with(topic_id) + + # Verify result structure + self.assertIsInstance(result, dict) + self.assertIn('topic_id', result) + self.assertIn('collections', result) + + def test_add_collections_to_topic_success(self): + """Test adding collections to topic""" + # Arrange + topic_id = 'topic_123' + mock_request = Mock() + mock_request.data = { + 'collection_ids': ['coll_123'] + } + + self.mock_topic_repo.get.return_value = self.sample_topic + self.mock_collection_repo.get.return_value = self.sample_collection + self.mock_topic_repo.find_existing_collection.return_value = None + + # Mock topic collection creation + created_topic_collection = Mock(spec=TopicCollection) + self.mock_topic_repo.create_collection.return_value = created_topic_collection + + # Mock serializer + with patch('api.services.topic.topic_service.TopicCollectionSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = {'collection_id': 'coll_123'} + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.topic_service.add_collections_to_topic(topic_id, mock_request) + + # Assert + self.mock_topic_repo.get.assert_called_once_with(topic_id) + self.mock_collection_repo.get.assert_called_once_with('coll_123') + self.mock_topic_repo.find_existing_collection.assert_called_once_with(topic_id, 'coll_123') + self.mock_topic_repo.create_collection.assert_called_once_with(topic_id, 'coll_123', 0) + + # Verify result structure + self.assertIsInstance(result, dict) + self.assertIn('topic_id', result) + self.assertIn('collections', result) + + def test_add_collections_to_topic_with_existing_collection(self): + """Test adding collections to topic when collection already exists""" + # Arrange + topic_id = 'topic_123' + mock_request = Mock() + mock_request.data = { + 'collection_ids': ['coll_123'] + } + + existing_collection = Mock(spec=TopicCollection) + + self.mock_topic_repo.get.return_value = self.sample_topic + self.mock_collection_repo.get.return_value = self.sample_collection + self.mock_topic_repo.find_existing_collection.return_value = existing_collection + + # Mock topic collection creation + created_topic_collection = Mock(spec=TopicCollection) + self.mock_topic_repo.create_collection.return_value = created_topic_collection + + # Mock serializer + with patch('api.services.topic.topic_service.TopicCollectionSerializer') as mock_serializer: + mock_serializer_instance = Mock() + mock_serializer_instance.data = {'collection_id': 'coll_123'} + mock_serializer.return_value = mock_serializer_instance + + # Act + result = self.topic_service.add_collections_to_topic(topic_id, mock_request) + + # Assert + existing_collection.delete.assert_called_once() + + # Verify result structure + self.assertIsInstance(result, dict) + self.assertIn('topic_id', result) + self.assertIn('collections', result) + + def test_remove_collections_from_topic_success(self): + """Test removing collections from topic""" + # Arrange + topic_id = 'topic_123' + mock_request = Mock() + mock_request.data = { + 'collection_ids': ['coll_123', 'coll_456'] + } + + # Act + result = self.topic_service.remove_collections_from_topic(topic_id, mock_request) + + # Assert + self.mock_topic_repo.delete_many_collections.assert_called_once_with(topic_id, ['coll_123', 'coll_456']) + self.assertIsNone(result) + + def test_service_initialization(self): + """Test that service initializes correctly with repositories""" + # Arrange & Act + service = TopicService( + topic_repo=self.mock_topic_repo, + account_repo=self.mock_account_repo, + permission_repo=self.mock_permission_repo, + group_repo=self.mock_group_repo, + collection_repo=self.mock_collection_repo + ) + + # Assert + self.assertEqual(service.topic_repo, self.mock_topic_repo) + self.assertEqual(service.account_repo, self.mock_account_repo) + self.assertEqual(service.permission_repo, self.mock_permission_repo) + self.assertEqual(service.group_repo, self.mock_group_repo) + self.assertEqual(service.collection_repo, self.mock_collection_repo) + + +if __name__ == '__main__': + unittest.main() diff --git a/api/services/topic/topic_service.py b/api/services/topic/topic_service.py new file mode 100644 index 0000000..8fe0af5 --- /dev/null +++ b/api/services/topic/topic_service.py @@ -0,0 +1,202 @@ +from django.utils import timezone +from django.db.models import Q +from ...models import * +from .serializers import * +from ...errors.common import * +from api.repositories.topic_repository import TopicRepository +from api.repositories.account_repository import AccountRepository +from api.repositories.permission_repository import PermissionRepository +from api.repositories.group_repository import GroupRepository +from api.repositories.collection_repository import CollectionRepository + +class TopicService: + + def __init__(self, topic_repo: TopicRepository, account_repo: AccountRepository, + permission_repo: PermissionRepository, group_repo: GroupRepository, + collection_repo: CollectionRepository): + self.topic_repo = topic_repo + self.account_repo = account_repo + self.permission_repo = permission_repo + self.group_repo = group_repo + self.collection_repo = collection_repo + + def create_topic(self, account_id: str, request): + topic_data = { + 'creator_id': account_id, + **request.data + } + + topic = self.topic_repo.create(topic_data) + serializer = TopicSerializer(topic) + return serializer.data + + def delete_topic(self, topic_id: str): + self.topic_repo.delete(topic_id) + return None + + def get_topic(self, topic_id: str): + topic = self.topic_repo.get(topic_id) + topic.group_permissions = self.permission_repo.get_topic_permissions(topic_id) + topic.collections = self.topic_repo.get_collections(topic_id) + + for tp in topic.collections: + tp.collection.problems = self.collection_repo.get_problems(tp.collection.collection_id) + tp.collection.group_permissions = self.permission_repo.get_collection_permissions(tp.collection.collection_id) + + serialize = TopicPopulateTopicCollectionPopulateCollectionPopulateCollectionProblemsPopulateProblemAndCollectionGroupPermissionsPopulateGroupAndTopicGroupPermissionPopulateGroupSerializer(topic) + + return serialize.data + + def get_all_topics(self, request): + account_id = request.query_params.get('account_id', 0) + + filters = {} + if account_id: + filters['creator_id'] = account_id + + topics = self.topic_repo.list(filters=filters) + serializer = TopicSerializer(topics, many=True) + + return { + 'topics': serializer.data + } + + def update_topic(self, topic_id: str, request): + topic = self.topic_repo.update(topic_id, request.data) + serializer = TopicSerializer(topic) + return serializer.data + + def populated_collections(self, topics: Topic): + topic_ids = [topic.topic_id for topic in topics] + topicCollections = self.topic_repo.get_many_collections(topic_ids) + populated_topics = [] + for topic in topics: + topic.collections = topicCollections.filter(topic_id=topic.topic_id) + populated_topics.append(topic) + return populated_topics + + def get_all_topics_by_account(self, account_id: str, request): + account = self.account_repo.get(account_id) + personalTopics = self.topic_repo.get_by_creator(account_id) + populatedPersonalTopics = self.populated_collections(personalTopics) + personalSerialize = TopicPopulateTopicCollectionPopulateCollectionSerializer(populatedPersonalTopics, many=True) + + group_ids = self.group_repo.get_ids_by_account(account_id) + manageableTopics = self.topic_repo.get_manageable_by_ids(group_ids) + populatedmanageableTopics = self.populated_collections(manageableTopics) + manageableSerialize = TopicPopulateTopicCollectionPopulateCollectionSerializer(populatedmanageableTopics, many=True) + + return { + 'topics': personalSerialize.data, + 'manageable_topics': manageableSerialize.data + } + + def get_all_accessed_topics_by_account(self, account_id: str): + accessedTopics = self.group_repo.get_accessible_by_account(account_id) + + topics = [] + for at in accessedTopics: + if at.topic not in topics: + topics.append(at.topic) + + serialize = TopicSerializer(topics, many=True) + + return {'topics': serialize.data} + + def get_topic_public(self, topic_id: str, request): + account_id = request.query_params.get('account_id', None) + + topic = self.topic_repo.get(topic_id) + account = self.account_repo.get(account_id) + + topicCollections = self.group_repo.get_accessible_collections_with_access(topic_id, account_id) + group_ids = self.group_repo.get_ids_by_account(account_id) + topicCollections = self.permission_repo.get_accessible_problems_for_collections(topicCollections, group_ids) + + for tp in topicCollections: + for cp in tp.collection.problems: + best_submission = self.topic_repo.get_best_submission_for_problem(cp.problem.problem_id, account_id, topic_id) + cp.problem.best_submission = best_submission + + topic.collections = topicCollections + + serialize = TopicPopulateTopicCollectionPopulateCollectionPopulateCollectionProblemPopulateProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureSerializer(topic) + + return serialize.data + + def update_groups_permission_to_topic(self, topic_id: str, request): + topic = self.topic_repo.get(topic_id) + self.permission_repo.delete_topic_permissions(topic_id) + + topic_group_permissions = [] + for group_request in request.data['groups']: + group = self.group_repo.get(group_request['group_id']) + # Remove group_id from group_request to avoid duplication + group_request_copy = group_request.copy() + group_request_copy.pop('group_id', None) + topic_group_permissions.append( + TopicGroupPermission( + topic_id=topic_id, + group_id=group.group_id, + **group_request_copy + )) + + self.permission_repo.bulk_create_topic_permissions(topic_group_permissions) + + topic.group_permissions = topic_group_permissions + serialize = TopicPopulateTopicGroupPermissionsSerializer(topic) + + return serialize.data + + def update_collections_to_topic(self, topic_id: str, request): + topic = self.topic_repo.get(topic_id) + self.topic_repo.delete_collections(topic_id) + + topic_collections = [] + order = 0 + for collection_id in request.data['collection_ids']: + collection = self.collection_repo.get(collection_id) + topic_collection = TopicCollection( + collection_id=collection_id, + topic_id=topic_id, + order=order + ) + topic_collections.append(topic_collection) + order += 1 + + self.topic_repo.bulk_create_collections(topic_collections) + topic = self.topic_repo.update_with_timestamp(topic_id) + + collection_serialize = TopicCollectionPopulateCollectionSerializer(topic_collections, many=True) + topic_serialize = TopicSerializer(topic) + + return { + **topic_serialize.data, + 'collections': collection_serialize.data + } + + def add_collections_to_topic(self, topic_id: str, request): + topic = self.topic_repo.get(topic_id) + populated_collections = [] + + index = 0 + for collection_id in request.data['collection_ids']: + collection = self.collection_repo.get(collection_id) + + alreadyExist = self.topic_repo.find_existing_collection(topic_id, collection_id) + if alreadyExist: + alreadyExist.delete() + + topicCollection = self.topic_repo.create_collection(topic_id, collection_id, index) + index += 1 + tc_serialize = TopicCollectionSerializer(topicCollection) + populated_collections.append(tc_serialize.data) + + return { + **TopicSerializer(topic).data, + "collections": populated_collections + } + + def remove_collections_from_topic(self, topic_id: str, request): + self.topic_repo.delete_many_collections(topic_id, request.data['collection_ids']) + return None diff --git a/api/setup.py b/api/setup.py new file mode 100644 index 0000000..978cf2e --- /dev/null +++ b/api/setup.py @@ -0,0 +1,39 @@ +from decouple import AutoConfig +from api.config import Configuration +from api.repositories.account_repository import AccountRepositoryImpl +from api.sandbox.grader import Grader +from api.services.account.account_service import AccountService, AccountServiceImpl +from api.services.auth.auth_service import AuthService, AuthServiceImpl +from api.services.collection.collection_service import CollectionService +from api.services.problem.problem_service import ProblemService +from api.services.topic.topic_service import TopicService +from api.services.submission.submission_service import SubmissionService +from api.services.group.group_service import GroupService +from api.repositories.problem_repository import ProblemRepository +from api.repositories.topic_repository import TopicRepository +from api.repositories.submission_repository import SubmissionRepository +from api.repositories.collection_repository import CollectionRepository +from api.repositories.group_repository import GroupRepository +from api.repositories.permission_repository import PermissionRepository + +# Configuration +config = Configuration(AutoConfig()) +grader = Grader + +# Repositories +account_repo = AccountRepositoryImpl() +problem_repo = ProblemRepository() +topic_repo = TopicRepository() +submission_repo = SubmissionRepository() +collection_repo = CollectionRepository() +group_repo = GroupRepository() +permission_repo = PermissionRepository() + +# Services +account_service = AccountServiceImpl(account_repo) +auth_service = AuthServiceImpl(config,account_repo) +collection_service = CollectionService(collection_repo, account_repo, problem_repo, permission_repo, group_repo) +group_service = GroupService(group_repo, account_repo) +problem_service = ProblemService(problem_repo, account_repo, permission_repo, group_repo, topic_repo, grader) +submission_service = SubmissionService(submission_repo, problem_repo, account_repo, topic_repo, problem_service, grader) +topic_service = TopicService(topic_repo, account_repo, permission_repo, group_repo, collection_repo) diff --git a/api/urls.py b/api/urls.py index 6d0bc9a..942ba03 100644 --- a/api/urls.py +++ b/api/urls.py @@ -1,62 +1,63 @@ from django.urls import path -from .views import account,auth,problem, script,submission,topic,collection,group -from .controllers import problem_controller - +from api.repositories.account_repository import AccountRepositoryImpl +from api.services.account.account_service import AccountService, AccountServiceImpl +from .controllers import problem_controller, collection_controller, topic_controller, group_controller, submission_controller, auth_controller +import api.controllers.account_controller as account_controller urlpatterns = [ - path("login",auth.login_view), - path("logout",auth.logout_view), - path('token',auth.authorization_view), - - path("accounts",account.all_accounts_view), - path("accounts/",account.one_creator_view), - path("accounts//daily-submissions",account.get_daily_submission), - path("accounts//password",account.change_password), - - path('accounts//problems',problem.all_problems_creator_view), - path('accounts//problems/',problem.one_problem_creator_view), - path('accounts//problems//groups',problem.problem_group_view), - path("accounts//problems//submissions",submission.creator_problem_submissions_view), - path("accounts//topics//problems//submissions",submission.topic_account_problem_submission_view), - - path('accounts//collections',collection.all_collections_creator_view), - path('accounts//collections/',collection.one_collection_creator_view), - path('accounts//collections//groups',collection.collection_groups_view), + path("login",auth_controller.login), + path("logout",auth_controller.logout), + path('token',auth_controller.authorization), + + path("accounts",account_controller.all_accounts), + path("accounts/",account_controller.one_creator), + # path("accounts//daily-submissions",account_controller.get_daily_submission), + path("accounts//password",account_controller.change_password), + + path('accounts//problems',problem_controller.all_problems_creator_view), + path('accounts//problems/',problem_controller.one_problem_creator_view), + path('accounts//problems//groups',problem_controller.problem_group_view), + path("accounts//problems//submissions",submission_controller.creator_problem_submissions_view), + path("accounts//topics//problems//submissions",submission_controller.topic_account_problem_submission_view), + + path('accounts//collections',collection_controller.all_collections_creator_view), + path('accounts//collections/',collection_controller.one_collection_creator_view), + path('accounts//collections//groups',collection_controller.collection_groups_view), - path('accounts//topics',topic.all_topics_creator_view), - path('accounts//topics/',topic.one_topic_creator_view), - path('accounts//topics//groups',topic.topic_groups_view), + path('accounts//topics',topic_controller.all_topics_creator_view), + path('accounts//topics/',topic_controller.one_topic_creator_view), + path('accounts//topics//groups',topic_controller.topic_groups_view), - path('accounts//access/topics',topic.all_topics_access_view), + path('accounts//access/topics',topic_controller.all_topics_access_view), - path('accounts//groups',group.all_groups_creator_view), + path('accounts//groups',group_controller.all_groups_creator_view), - path('problems',problem.all_problems_view), - path('problems/validate',problem.validation_view), - path('problems/',problem.one_problem_view), - path('problems//import/pdf',problem.import_pdf_view), - path("problems//accounts//submissions",submission.account_problem_submission_view), - path('topics//problems//accounts/',problem.problem_in_topic_account_view), + path('problems',problem_controller.all_problems_view), + path('problems/list',problem_controller.all_problems_list_view), + path('problems/validate',problem_controller.validation_view), + path('problems/',problem_controller.one_problem_view), + path('problems//import/pdf',problem_controller.import_pdf_view), + path("problems//accounts//submissions",submission_controller.account_problem_submission_view), + path('topics//problems//accounts/',problem_controller.problem_in_topic_account_view), - path('collections',collection.all_collections_view), - path('collections/',collection.one_collection_view), - path('collections//problems/',collection.collection_problems_view), + path('collections',collection_controller.all_collections_view), + path('collections/',collection_controller.one_collection_view), + path('collections//problems/',collection_controller.collection_problems_view), - path('topics',topic.all_topics_view), - path('topics/',topic.one_topic_view), - path('topics//access',topic.account_access), - path('topics//collections/',topic.topic_collections_view), + path('topics',topic_controller.all_topics_view), + path('topics/',topic_controller.one_topic_view), + path('topics//access',topic_controller.account_access), + path('topics//collections/',topic_controller.topic_collections_view), - path('groups/',group.one_group_view), - path('groups//members/',group.group_members_view), + path('groups/',group_controller.one_group_view), + path('groups//members/',group_controller.group_members_view), - path('submissions',submission.all_submission_view), + path('submissions',submission_controller.all_submission_view), # New Versions - path('v1/problems/',problem_controller.get_or_update_problem), - path('v1/problems//import/pdf',problem_controller.upload_pdf), - path('v1/problems//pdf',problem_controller.get_problem_pdf), - path('v1/problems', problem_controller.create_problem), + # path('v1/problems/',problem_controller.get_or_update_problem), + # path('v1/problems//import/pdf',problem_controller.upload_pdf), + # path('v1/problems//pdf',problem_controller.get_problem_pdf), + # path('v1/problems', problem_controller.create_problem), - path('script',script.run_script), ] \ No newline at end of file diff --git a/api/views/account.py b/api/views/account.py deleted file mode 100644 index 37d4e44..0000000 --- a/api/views/account.py +++ /dev/null @@ -1,53 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from api.wrappers.auth_wrapper import authentication_required -from ..constant import GET,POST,PUT,DELETE -from ..models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ..serializers import * -from ..controllers.account.create_account import * -from ..controllers.account.get_account import * -from ..controllers.account.get_all_accounts import * - -@api_view([GET,POST]) -def all_accounts_view(request): - if request.method == GET: - return get_all_accounts(request) - elif request.method == POST: - return create_account(request) - -@api_view([GET]) -@authentication_required -def one_creator_view(request,account_id): - return get_account(account_id) - -@api_view([PUT]) -@authentication_required -def change_password(request,account_id): - account = Account.objects.get(account_id=account_id) - account.password = passwordEncryption(request.data['password']) - account.save() - - return Response({'message':"Your password has been changed"}) - -@api_view([GET]) -@authentication_required -def get_daily_submission(request,account_id:str): - submissions = Submission.objects.filter(account_id=account_id) - serializes = SubmissionSerializer(submissions,many=True) - - submission_by_date = {} - - for submission in serializes.data: - [date,time] = submission['date'].split("T") - if date in submission_by_date: - submission_by_date[date]["submissions"].append(submission) - submission_by_date[date]["count"] += 1 - else: - submission_by_date[date] = {"count":1, "submissions": [ submission ]} - - return Response({"submissions_by_date": submission_by_date}) - diff --git a/api/views/auth.py b/api/views/auth.py deleted file mode 100644 index ed93aa8..0000000 --- a/api/views/auth.py +++ /dev/null @@ -1,29 +0,0 @@ -import re -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from ..constant import GET,POST,PUT,DELETE -from ..models import Account, Problem,Testcase -from rest_framework import status -from django.forms.models import model_to_dict -from uuid import uuid4 -from time import time -from decouple import config - -from ..controllers.auth.authorization import * -from ..controllers.auth.login import * -from ..controllers.auth.logout import * - -TOKEN_LIFETIME = int(config('TOKEN_LIFETIME_SECOND')) # (Second) - -@api_view([POST]) -def login_view(request): - return login(request) - -@api_view([POST]) -def logout_view(request): - return logout(request) - -@api_view([PUT]) -def authorization_view(request): - return authorization(request) \ No newline at end of file diff --git a/api/views/collection.py b/api/views/collection.py deleted file mode 100644 index 59d62b2..0000000 --- a/api/views/collection.py +++ /dev/null @@ -1,76 +0,0 @@ -from statistics import mode -from rest_framework.response import Response -from rest_framework.decorators import api_view,parser_classes -from rest_framework.parsers import MultiPartParser, FormParser - -from api.wrappers.auth_wrapper import authentication_required -from ..constant import GET,POST,PUT,DELETE -from ..models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ..serializers import * - -from ..controllers.collection.create_collection import * -from ..controllers.collection.get_collection import * -from ..controllers.collection.get_all_collections import * -from ..controllers.collection.update_collection import * -from ..controllers.collection.delete_collection import * -from ..controllers.collection.add_problems_to_collection import * -from ..controllers.collection.remove_problems_from_collection import * -from ..controllers.collection.get_all_collections_by_account import * -from ..controllers.collection.update_problems_to_collection import * -from ..controllers.collection.update_group_permissions_collection import * - -@api_view([POST,GET]) -@authentication_required -def all_collections_creator_view(request,account_id:str): - if request.method == POST: - return create_collection(account_id,request) - if request.method == GET: - return get_all_collections_by_account(account_id) - -@api_view([GET,PUT,DELETE]) -@authentication_required -def one_collection_creator_view(request,account_id:int,collection_id:str): - collection = Collection.objects.get(collection_id=collection_id) - if request.method == GET: - return get_collection(collection) - if request.method == PUT: - return update_collection(collection,request) - if request.method == DELETE: - return delete_collection(collection) - -@api_view([GET]) -@authentication_required -def all_collections_view(request): - return get_all_collections(request) - -@api_view([GET,PUT,DELETE]) -@authentication_required -def one_collection_view(request,collection_id:str): - if request.method == GET: - return get_collection(collection_id) - if request.method == PUT: - return update_collection(collection_id,request) - if request.method == DELETE: - return delete_collection(collection_id) - -@api_view([PUT]) -@authentication_required -def collection_problems_view(request,collection_id:str,method:str): - - collection = Collection.objects.get(collection_id=collection_id) - - if method == "add": - return add_problems_to_collection(collection,request) - if method == "remove": - return remove_problems_from_collection(collection,request) - if method == "update": - return update_problems_to_collection(collection,request) - -@api_view([PUT]) -@authentication_required -def collection_groups_view(request,account_id:int,collection_id:str): - collection = Collection.objects.get(collection_id=collection_id) - if request.method == PUT: - return update_group_permissions_collection(collection,request) \ No newline at end of file diff --git a/api/views/group.py b/api/views/group.py deleted file mode 100644 index 8699ae7..0000000 --- a/api/views/group.py +++ /dev/null @@ -1,46 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from api.wrappers.auth_wrapper import authentication_required -from ..constant import GET,POST,PUT,DELETE -from ..models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ..serializers import * -from ..controllers.group.create_group import create_group -from ..controllers.group.update_group import update_group -from ..controllers.group.delete_group import delete_group -from ..controllers.group.get_group import get_group -from ..controllers.group.update_members_to_group import update_members_to_group -from ..controllers.group.add_members_to_group import add_members_to_group -from ..controllers.group.get_all_groups_by_account import get_all_groups_by_account - -@api_view([POST,GET]) -@authentication_required -def all_groups_creator_view(request,account_id:str): - account = Account.objects.get(account_id=account_id) - if request.method == POST: - return create_group(account,request) - elif request.method == GET: - return get_all_groups_by_account(account,request) - -@api_view([PUT,DELETE,GET]) -@authentication_required -def one_group_view(request,group_id:str): - group = Group.objects.get(group_id=group_id) - if request.method == PUT: - return update_group(group,request) - elif request.method == DELETE: - return delete_group(group,request) - elif request.method == GET: - return get_group(group,request) - -@api_view([PUT]) -@authentication_required -def group_members_view(request,group_id:str,method:str): - group = Group.objects.get(group_id=group_id) - if method == 'update': - return update_members_to_group(group,request) - if method == 'add': - return add_members_to_group(group,request) \ No newline at end of file diff --git a/api/views/problem.py b/api/views/problem.py deleted file mode 100644 index d9bc9d1..0000000 --- a/api/views/problem.py +++ /dev/null @@ -1,98 +0,0 @@ -# from ..utility import JSONParser, JSONParserOne, passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from api.sandbox.grader import PythonGrader -from ..constant import GET,POST,PUT,DELETE -from ..models import Account, Problem,Testcase -from rest_framework import status -from django.forms.models import model_to_dict -from ..serializers import * -from api.wrappers.auth_wrapper import authentication_required -from ..controllers.problem.create_problem import * -from ..controllers.problem.update_problem import * -from ..controllers.problem.delete_problem import * -from ..controllers.problem.get_problem import * -from ..controllers.problem.get_all_problems import * -from ..controllers.problem.remove_bulk_problems import * -from ..controllers.problem.get_all_problems_by_account import * -from ..controllers.problem.validate_program import * -from ..controllers.problem.get_all_problem_with_best_submission import * -from ..controllers.problem.get_problem_in_topic_with_best_submission import * -from ..controllers.problem.update_group_permission_to_problem import * -from ..controllers.problem.get_problem_public import * -from ..controllers.problem.import_elabsheet_problem import * - - -# Create your views here. -@api_view([POST,GET]) -@authentication_required -def all_problems_creator_view(request,account_id): - if request.method == POST: - return create_problem(account_id,request) - if request.method == GET: - return get_all_problems_by_account(account_id,request) - -@api_view([GET,PUT,DELETE]) -@authentication_required -def one_problem_creator_view(request,problem_id:str,account_id:str): - problem = Problem.objects.get(problem_id=problem_id) - if request.method == GET: - return get_problem(problem) - elif request.method == PUT: - return update_problem(problem,request) - elif request.method == DELETE: - return delete_problem(problem) - -@api_view([GET,DELETE]) -@authentication_required -def all_problems_view(request): - account_id = request.GET.get("account_id",None) - try: - account = Account.objects.get(account_id=account_id) - except: - account = None - if request.method == GET: - return get_all_problem_with_best_submission(account) - elif request.method == DELETE: - return remove_bulk_problems(request) - -@api_view([GET,PUT,DELETE]) -def one_problem_view(request,problem_id: int): - problem = Problem.objects.get(problem_id=problem_id) - if request.method == GET: - return get_problem_public(problem) - elif request.method == PUT: - return update_problem(problem_id,request) - elif request.method == DELETE: - return delete_problem(problem_id) - -@api_view([POST]) -@authentication_required -def validation_view(request): - if request.method == POST: - return validate_program(request) - -@api_view([GET]) -@authentication_required -def problem_in_topic_account_view(request,account_id:str,topic_id:str,problem_id:str): - if request.method == GET: - return get_problem_in_topic_with_best_submission(account_id,topic_id,problem_id) - -@api_view([PUT]) -@authentication_required -def problem_group_view(request,account_id:int,problem_id:int): - problem = Problem.objects.get(problem_id=problem_id) - if request.method == PUT: - return update_group_permission_to_problem(problem,request) - -# @api_view([POST]) -# def import_elabsheet_problem_view(request): -# if request.method == POST: -# print(request) - -@api_view([PUT]) -@authentication_required -def import_pdf_view(request,problem_id:int): - problem = Problem.objects.get(problem_id=problem_id) - if request.method == PUT: - return import_elabsheet_problem(request, problem) \ No newline at end of file diff --git a/api/views/script.py b/api/views/script.py deleted file mode 100644 index f907379..0000000 --- a/api/views/script.py +++ /dev/null @@ -1,84 +0,0 @@ -from api.utility import passwordEncryption -from rest_framework.response import Response -from rest_framework.decorators import api_view -from ..constant import GET,POST,PUT,DELETE -from ..models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ..serializers import * -from ..controllers.script.generate_submission_score import generate_submission_score -from ..difficulty_predictor.preprocess import * -from ..difficulty_predictor.predictor import * -from ..controllers.problem.update_problem_difficulty import update_problem_difficulty - -# @api_view([POST]) -# def run_script(request): -# submissions = Submission.objects.all() -# total = len(submissions) -# count = 0 -# for submission in submissions: -# submission.score = submission.result.count('P') -# submission.max_score = len(submission.result) -# submission.passed_ratio = submission.score/submission.max_score -# submission.save() -# count += 1 -# print(f"({count}/{total})") -# return Response({'message': 'Success!'},status=status.HTTP_201_CREATED) - -# @api_view([POST]) -# def run_script(request): -# submissionTestcases = SubmissionTestcase.objects.all() - -# total = len(submissionTestcases) -# count = 0 -# for testcase in submissionTestcases: -# if testcase.runtime_status == "OK" and (not testcase.is_passed): -# testcase.runtime_status = "FAILED" -# testcase.save() -# count += 1 - -# print(f"({count}/{total})") -# return Response({'message': 'Success!'},status=status.HTTP_201_CREATED) - -# @api_view([POST]) -# def run_script(request): -# topics = Topic.objects.all() -# for topic in topics: -# if len(topic.description) == 0: -# topic.description = f'[{{"id": "1","type": ELEMENT_PARAGRAPH,"children": [{{ "text": "" }}]}}]' -# topic.save() -# elif topic.description[0] != '[': -# topic.description = f'[{{"id": "1","type": ELEMENT_PARAGRAPH,"children": [{{ "text": "{topic.description}" }}]}}]' -# topic.save() -# return Response({'message': 'Success!'},status=status.HTTP_201_CREATED) - -# @api_view([POST]) -# def run_script(request): -# # collections = Collection.objects.all() -# # for collection in collections: -# # collection.description = '[{"id":"1","type":"p","children":[{"text":"Just course"}]}]' -# # collection.save() -# # generate_submission_score(request) -# problems = Problem.objects.all() -# for problem in problems: -# problem.allowed_languages = "python,c,cpp" -# problem.save() -# return Response({'message': 'Success!'},status=status.HTTP_201_CREATED) - -# @api_view([POST]) -# def run_script(request): -# problems = Problem.objects.all() -# for problem in problems: -# update_problem_difficulty(problem) - -# return Response({'message': 'Success!'},status=status.HTTP_201_CREATED) - -@api_view([POST]) -def run_script(request): - problems = Problem.objects.all() - for problem in problems: - if problem.language == "py": - problem.language = "python" - problem.save() - - return Response({'message': 'Success!'},status=status.HTTP_201_CREATED) diff --git a/api/views/submission.py b/api/views/submission.py deleted file mode 100644 index 4372f4a..0000000 --- a/api/views/submission.py +++ /dev/null @@ -1,56 +0,0 @@ -from statistics import mode -from rest_framework.response import Response -from rest_framework.decorators import api_view - -from api.serializers import * -from api.wrappers.auth_wrapper import authentication_required -from ..constant import GET,POST,PUT,DELETE -from ..models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ..sandbox.grader import PythonGrader -from time import sleep -from ..utility import regexMatching - -from ..controllers.submission.submit_problem import * -from ..controllers.submission.get_submission_by_quries import * -from ..controllers.submission.get_submissions_by_account_problem import * -from ..controllers.submission.submit_problem_on_topic import * -from ..controllers.submission.get_submissions_by_account_problem_in_topic import * -from ..controllers.submission.get_all_submissions_by_creator_problem import * - - -@api_view([POST,GET]) -@authentication_required -def account_problem_submission_view(request,problem_id,account_id): - if request.method == POST: - try: - return submit_problem(account_id,problem_id,request) - except Exception as e: - print(e) - return Response({"error":str(e)},status=status.HTTP_400_BAD_REQUEST) - if request.method == GET: - return get_submissions_by_account_problem(account_id,problem_id) - -@api_view([GET]) -@authentication_required -def creator_problem_submissions_view(request,account_id,problem_id): - problem = Problem.objects.get(problem_id=problem_id) - return get_all_submissions_by_creator_problem(problem, request) - -@api_view([GET]) -@authentication_required -def all_submission_view(request): - return get_submission_by_quries(request) - -@api_view([POST,GET]) -@authentication_required -def topic_account_problem_submission_view(request,topic_id,account_id,problem_id): - if request.method == POST: - return submit_problem_on_topic(account_id,problem_id,topic_id,request) - if request.method == GET: - return get_submissions_by_account_problem_in_topic(account_id,problem_id,topic_id) - -# @api_view([GET]) -# def submission_account_problem_view(request,account_id:str,problem_id:str): -# return get_submissions_by_account_problem(account_id,problem_id) \ No newline at end of file diff --git a/api/views/topic.py b/api/views/topic.py deleted file mode 100644 index ecc1e3d..0000000 --- a/api/views/topic.py +++ /dev/null @@ -1,116 +0,0 @@ -from statistics import mode -from rest_framework.response import Response -from rest_framework.decorators import api_view,parser_classes -from rest_framework.parsers import MultiPartParser, FormParser - -from api.wrappers.auth_wrapper import authentication_required -from ..constant import GET,POST,PUT,DELETE -from ..models import * -from rest_framework import status -from django.forms.models import model_to_dict -from ..serializers import * - -from ..controllers.topic.create_topic import * -from ..controllers.topic.get_topic import * -from ..controllers.topic.get_all_topics import * -from ..controllers.topic.update_topic import * -from ..controllers.topic.delete_topic import * -from ..controllers.topic.add_collections_to_topic import * -from ..controllers.topic.remove_collections_from_topic import * -from ..controllers.topic.get_all_topics_by_account import * -from ..controllers.topic.update_collections_to_topic import * -from ..controllers.topic.get_topic_public import * -from ..controllers.topic.update_groups_permission_to_topic import * -from ..controllers.topic.get_all_accessed_topics_by_account import * -from ..permissions.topic import * - -@api_view([POST,GET]) -@parser_classes([MultiPartParser,FormParser]) -@authentication_required -def all_topics_creator_view(request,account_id :int): - account = Account.objects.get(account_id=account_id) - if request.method == POST: - return create_topic(account_id,request) - elif request.method == GET: - return get_all_topics_by_account(account,request) - -@api_view([GET,PUT,DELETE]) -@authentication_required -def one_topic_creator_view(request,account_id:str,topic_id:str): - topic = Topic.objects.get(topic_id=topic_id) - account = Account.objects.get(account_id=account_id) - if not canManageTopic(topic,account): - return Response(status=status.HTTP_401_UNAUTHORIZED) - if request.method == GET: - return get_topic(topic) - elif request.method == PUT: - return update_topic(topic,request) - elif request.method == DELETE: - return delete_topic(topic) - -@api_view([GET]) -@authentication_required -def all_topics_view(request): - return get_all_topics(request) - -@api_view([GET,PUT,DELETE]) -@authentication_required -def one_topic_view(request,topic_id:str): - if request.method == GET: - return get_topic_public(topic_id,request) - elif request.method == PUT: - return update_topic(topic_id,request) - elif request.method == DELETE: - return delete_topic(topic_id) - -@api_view([PUT]) -@authentication_required -def topic_collections_view(request,topic_id:str,method:str): - - topic = Topic.objects.get(topic_id=topic_id) - - if method == "add": - return add_collections_to_topic(topic_id,request) - elif method == "remove": - return remove_collections_from_topic(topic_id,request) - elif method == "update": - return update_collections_to_topic(topic,request) - -@api_view([POST,PUT]) -@authentication_required -def account_access(request,topic_id:str): - topic = Topic.objects.get(topic_id=topic_id) - target_accounts = Account.objects.filter(account_id__in=request.data['account_ids']) - - if request.method == POST: - accessedAccounts = [] - for account in target_accounts: - topic_account = TopicAccountAccess( - topic = topic, - account = account - ) - topic_account.save() - accessedAccounts.append(topic_account) - - serialize = TopicAccountAccessSerialize(accessedAccounts,many=True) - - return Response({ - "accounts": serialize.data - },status=status.HTTP_201_CREATED) - - elif request.method == PUT: - topicAccountAccesses = TopicAccountAccess.objects.filter(account_id__in=request.data['account_ids']) - topicAccountAccesses.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - -@api_view([PUT]) -@authentication_required -def topic_groups_view(request,account_id:int,topic_id:str): - topic = Topic.objects.get(topic_id=topic_id) - return update_groups_permission_to_topic(topic,request) - -@api_view([GET]) -@authentication_required -def all_topics_access_view(request,account_id:str): - account = Account.objects.get(account_id=account_id) - return get_all_accessed_topics_by_account(account) \ No newline at end of file diff --git a/api/wrappers/auth_wrapper.py b/api/wrappers/auth_wrapper.py index 714d6f3..5d1fd24 100644 --- a/api/wrappers/auth_wrapper.py +++ b/api/wrappers/auth_wrapper.py @@ -1,14 +1,14 @@ -from api.services.auth_service import verify_token from api.utility import extract_bearer_token from api.errors.common import InvalidTokenError +from api.setup import auth_service def authentication_required(function): def wrapper(request, *args, **kwargs): token = extract_bearer_token(request) if not token: - raise InvalidTokenError() - is_verify = verify_token(token) + return InvalidTokenError().django_response() + is_verify = auth_service.verify_token(token) if not is_verify: - raise InvalidTokenError() + return InvalidTokenError().django_response() return function(request, *args, **kwargs) return wrapper \ No newline at end of file diff --git a/docs/csr-repository-seperation.md b/docs/csr-repository-seperation.md new file mode 100644 index 0000000..abfd2df --- /dev/null +++ b/docs/csr-repository-seperation.md @@ -0,0 +1,170 @@ +# CSR Repository Separation Guide + +## Overview +This guide outlines the step-by-step process for refactoring Django ORM calls from service layers to repository layers, ensuring proper separation of concerns and maintainable code architecture. + +## Step-by-Step Refactoring Process + +### Step 1: Analyze Current Service Code +1. **Identify Django ORM Calls**: Search for patterns like `.objects.` +2. **Categorize ORM Operations**: Group by model types and operation types (CRUD) +3. **Map Dependencies**: Identify which models are being accessed and their relationships +4. **Document Current Methods**: List all service methods that contain direct ORM calls + +### Step 2: Determine Repository Responsibilities +1. **Model-Based Separation**: Each repository should handle only its specific model domain +2. **Permission Models**: All models containing "permission" go to `PermissionRepository` +3. **Group Models**: All models containing "group" (but not "permission") go to `GroupRepository` +4. **Core Models**: Each main model gets its own repository (e.g., `CollectionRepository`, `ProblemRepository`) + +### Step 3: Design Repository Methods +1. **Method Naming Convention**: + - Repository methods must NOT contain their repository name (โŒ `get_collection_by_id` โ†’ โœ… `get`) + - Methods that get items by ID should NOT have "_by_id" suffix (โŒ `get_by_id` โ†’ โœ… `get`) + - Use descriptive names for complex operations (โœ… `get_by_creator`, `get_manageable_collections`) + +2. **Parameter Guidelines**: + - Repository functions must NOT accept Django model objects as parameters + - Always use IDs or primitive types instead of model instances + - Example: โŒ `delete_permissions(collection: Collection)` โ†’ โœ… `delete_permissions(collection_id: str)` + +### Step 4: Create/Update Repository Files + +#### A. Import Required Models +```python +from api.models import ModelName1, ModelName2 +from typing import List +``` + +#### B. Implement Repository Methods +```python +class ExampleRepository: + def __init__(self): + pass + + def get(self, item_id: str): + return ModelName.objects.get(id=item_id) + + def list(self, filters: dict = {}): + queryset = ModelName.objects.all() + # Apply filters + return queryset + + def create(self, data: dict): + instance = ModelName.objects.create(**data) + return instance + + def delete(self, item_id: str): + ModelName.objects.filter(id=item_id).delete() +``` + +### Step 5: Update Service Layer + +#### A. Add Repository Dependencies +```python +def __init__(self, primary_repo: PrimaryRepository, permission_repo: PermissionRepository, group_repo: GroupRepository): + self.primary_repo = primary_repo + self.permission_repo = permission_repo + self.group_repo = group_repo +``` + +#### B. Replace ORM Calls +Replace direct ORM calls with repository method calls: +```python +# Before +collection = Collection.objects.get(collection_id=collection_id) +permissions = CollectionGroupPermission.objects.filter(collection=collection) + +# After +collection = self.collection_repo.get(collection_id) +permissions = self.permission_repo.get_collection_group_permissions(collection_id) +``` + +### Step 6: Handle Model Instantiation +1. **Direct Save Calls**: Always call `.save()` immediately when creating/updating instances +2. **No Wrapper Methods**: Don't create repository methods just to call `.save()` +3. **Bulk Operations**: Use repository methods for bulk create/update operations + +Example: +```python +# โœ… Correct approach +collection_problem = CollectionProblem( + problem=problem, + collection=collection, + order=index +) +collection_problem.save() + +# โŒ Wrong approach - unnecessary wrapper +def save_problem(self, collection_problem): + collection_problem.save() + return collection_problem +``` + +## Strict Rules to Follow + +### 1. Repository Naming Rules +- โœ… `get(id)` - Get single item by ID +- โœ… `list()` - Get multiple items +- โœ… `create(data)` - Create new item +- โœ… `update(id, data)` - Update existing item +- โœ… `delete(id)` - Delete item by ID +- โŒ `get_collection_by_id()` - Contains repository name +- โŒ `get_by_id()` - Unnecessary "_by_id" suffix + +### 2. Parameter Rules +- โœ… Use primitive types: `str`, `int`, `dict`, `List[str]` +- โœ… Use IDs instead of model objects: `collection_id: str` +- โŒ Never use Django model objects as parameters: `collection: Collection` + +### 3. Model Separation Rules +- โœ… Each repository handles only its domain models +- โœ… Permission models โ†’ `PermissionRepository` +- โœ… Group models (non-permission) โ†’ `GroupRepository` +- โŒ Cross-repository ORM calls +- โŒ Repository accessing models outside its domain + +### 4. Service Layer Rules +- โœ… No direct Django ORM calls (`ModelName.objects.*`) +- โœ… All data access through repository methods +- โœ… Call `.save()` directly on model instances +- โŒ Django ORM patterns in service methods +- โŒ Wrapper methods just for `.save()` calls + +### 5. Import Rules +- โœ… Import only necessary models in repositories +- โœ… Remove unused model imports after refactoring +- โŒ Importing models not used by the repository + +## Verification Checklist + +After refactoring, verify: + +1. **No ORM in Services**: Search for `\.objects\.` patterns in service files +2. **No "_by_id" Suffixes**: Search for `def get.*_by_id` patterns +3. **No Model Parameters**: Check repository method signatures +4. **Proper Separation**: Ensure each repository handles only its models +5. **No Linter Errors**: Run linter on all modified files +6. **Functionality Intact**: Test that all operations still work correctly + +## Benefits of Proper Separation + +1. **Maintainability**: Clear separation of concerns +2. **Testability**: Repository methods are easier to mock and test +3. **Consistency**: Uniform naming and structure across repositories +4. **Modularity**: Each repository handles its specific domain +5. **Scalability**: Easy to add new repositories and methods +6. **Type Safety**: Clear parameter types and return values + +## Common Pitfalls to Avoid + +1. **Repository Name in Methods**: Don't include repository name in method names +2. **Model Objects as Parameters**: Always use IDs instead of model instances +3. **Cross-Repository ORM**: Don't access other models directly +4. **Unnecessary Wrappers**: Don't create methods just to call `.save()` +5. **Mixed Responsibilities**: Keep each repository focused on its domain +6. **Inconsistent Naming**: Follow the naming conventions strictly + +--- + +*This guide should be followed strictly to ensure consistent and maintainable repository architecture across the entire codebase.* \ No newline at end of file diff --git a/docs/github-workflow-2.md b/docs/github-workflow-2.md new file mode 100644 index 0000000..ed1488f --- /dev/null +++ b/docs/github-workflow-2.md @@ -0,0 +1,185 @@ +# GitHub Workflows for ModelGrader-Backend + +This document explains the GitHub Actions workflows that automatically run tests when pull requests are made to the `main` and `dev` branches. + +## ๐Ÿš€ Quick Start + +The workflows will automatically run when you: +1. Create a pull request to `main` or `dev` branches +2. Push commits to `main` or `dev` branches + +## ๐Ÿ“ Available Workflows + +### 1. `run-tests.yml` (Recommended) +**Best for**: Most use cases +- โœ… Runs all tests using `run_all_tests.py` +- โœ… Tests individual services +- โœ… Includes coverage analysis +- โœ… Fast execution + +### 2. `simple-test.yml` (Minimal) +**Best for**: Quick testing +- โœ… Simple and fast +- โœ… Just runs the basic test suite +- โœ… Minimal dependencies + +### 3. `test.yml` (Basic) +**Best for**: Standard testing with coverage +- โœ… Runs tests with verbose output +- โœ… Includes coverage analysis +- โœ… Uploads coverage reports + +### 4. `ci.yml` (Comprehensive) +**Best for**: Full CI/CD pipeline +- โœ… Multiple test scenarios +- โœ… Code quality checks +- โœ… Matrix testing +- โœ… Artifact uploads + +## ๐Ÿ”ง How It Works + +### Automatic Triggers +```yaml +on: + pull_request: + branches: [ main, dev ] + push: + branches: [ main, dev ] +``` + +### Test Execution +1. **Checkout**: Gets your code +2. **Setup Python**: Installs Python 3.11 +3. **Install Dependencies**: Runs `pip install -r requirements.txt` +4. **Run Tests**: Executes `python run_all_tests.py --verbose` +5. **Report Results**: Shows โœ… pass or โŒ fail + +### Example Output +``` +๐Ÿš€ Starting ModelGrader-Backend Service Tests +============================================================ +๐Ÿ“ฆ Adding SubmissionService tests... + โœ… Unit tests added +๐Ÿ“ฆ Adding TopicService tests... + โœ… Unit tests added + +๐Ÿƒ Running tests... +------------------------------------------------------------ +.............................. +---------------------------------------------------------------------- +Ran 30 tests in 1.874s + +OK + +============================================================ +๐Ÿ“Š TEST EXECUTION SUMMARY +============================================================ +Total Tests Run: 30 +โœ… Passed: 30 +โŒ Failed: 0 +๐Ÿ’ฅ Errors: 0 +โญ๏ธ Skipped: 0 +๐Ÿ“ˆ Success Rate: 100.0% + +๐ŸŽฏ Overall Result: โœ… ALL TESTS PASSED! +============================================================ +``` + +## ๐Ÿ› ๏ธ Local Testing + +You can run the same tests locally: + +```bash +# Run all tests +python run_all_tests.py --verbose + +# Run with coverage +python run_all_tests.py --coverage --verbose + +# Run specific services +python run_all_tests.py --services account,auth,submission --verbose +``` + +## ๐Ÿ“Š Coverage Reports + +Coverage reports are generated and available: +- In GitHub Actions logs +- As HTML reports (artifacts) +- On Codecov (if configured) + +## ๐Ÿšจ Troubleshooting + +### Common Issues + +1. **Import Errors** + - Check that all test classes exist + - Verify imports in `run_all_tests.py` + +2. **Dependency Issues** + - Ensure `requirements.txt` is up to date + - Check for missing packages + +3. **Test Failures** + - Review test output in GitHub Actions logs + - Run tests locally to debug + +### Debugging Steps + +1. Go to your repository's "Actions" tab +2. Click on the failed workflow run +3. Review the logs for specific errors +4. Test locally using the same commands + +## โš™๏ธ Customization + +### Adding More Services +Edit `run_all_tests.py` to include new test classes: + +```python +# Add new service +if 'newservice' in self.services: + test_classes['NewService'] = { + 'unit': TestNewService + } +``` + +### Modifying Workflows +You can customize workflows by: +- Adding more Python versions +- Including additional test scenarios +- Adding code quality checks +- Configuring different triggers + +### Example: Adding Python 3.12 +```yaml +strategy: + matrix: + python-version: [3.11, 3.12] +``` + +## ๐Ÿ“‹ Requirements + +- Python 3.11+ +- All dependencies from `requirements.txt` +- Django project properly configured +- Test files in expected locations + +## ๐ŸŽฏ Best Practices + +1. **Use `run-tests.yml`** for most cases +2. **Test locally** before pushing +3. **Check the Actions tab** for results +4. **Review coverage reports** regularly +5. **Keep dependencies updated** + +## ๐Ÿ“ž Support + +If you encounter issues: +1. Check the GitHub Actions logs +2. Test locally with the same commands +3. Review this documentation +4. Check the `run_all_tests.py` script + +--- + +**Happy Testing! ๐Ÿงชโœจ** diff --git a/docs/github-workflow.md b/docs/github-workflow.md new file mode 100644 index 0000000..08055a3 --- /dev/null +++ b/docs/github-workflow.md @@ -0,0 +1,101 @@ +# GitHub Workflows + +This directory contains GitHub Actions workflows for the ModelGrader-Backend project. + +## Available Workflows + +### 1. `run-tests.yml` (Recommended) +**Purpose**: Simple and focused test runner for pull requests +**Triggers**: Pull requests and pushes to `main` and `dev` branches +**Features**: +- Runs all tests using `run_all_tests.py` +- Tests individual services +- Includes coverage analysis +- Fast execution + +### 2. `test.yml` +**Purpose**: Basic test runner with coverage +**Triggers**: Pull requests and pushes to `main` and `dev` branches +**Features**: +- Runs tests with verbose output +- Includes coverage analysis +- Uploads coverage reports to Codecov + +### 3. `ci.yml` +**Purpose**: Comprehensive CI/CD pipeline +**Triggers**: Pull requests and pushes to `main` and `dev` branches +**Features**: +- Multiple test scenarios (all, unit-only, integration-only) +- Code quality checks (flake8, black, isort) +- Matrix testing with different Python versions +- Artifact uploads + +## Usage + +### For Pull Requests +When you create a pull request to `main` or `dev` branches, the workflows will automatically: +1. Check out your code +2. Set up Python 3.11 +3. Install dependencies from `requirements.txt` +4. Run all tests using `run_all_tests.py` +5. Generate coverage reports +6. Report pass/fail status + +### Manual Testing +You can also run the same tests locally: + +```bash +# Run all tests +python run_all_tests.py --verbose + +# Run with coverage +python run_all_tests.py --coverage --verbose + +# Run specific services +python run_all_tests.py --services account,auth,submission --verbose +``` + +## Workflow Status + +The workflows will show: +- โœ… **Green checkmark**: All tests passed +- โŒ **Red X**: Some tests failed +- โš ๏ธ **Yellow circle**: Tests are running + +## Coverage Reports + +Coverage reports are generated and can be viewed: +- In the GitHub Actions logs +- As HTML reports (uploaded as artifacts) +- On Codecov (if configured) + +## Troubleshooting + +### Common Issues + +1. **Import Errors**: Make sure all test classes exist and are properly imported +2. **Dependency Issues**: Check that `requirements.txt` includes all necessary packages +3. **Test Failures**: Review the test output in the GitHub Actions logs + +### Debugging + +To debug workflow issues: +1. Check the "Actions" tab in your GitHub repository +2. Click on the failed workflow run +3. Review the logs for specific error messages +4. Test locally using the same commands + +## Customization + +You can modify the workflows to: +- Add more Python versions to test against +- Include additional test scenarios +- Add more code quality checks +- Configure different triggers + +## Requirements + +- Python 3.11 +- All dependencies from `requirements.txt` +- Django project properly configured +- Test files in the expected locations diff --git a/docs/refactor-step.md b/docs/refactor-step.md new file mode 100644 index 0000000..f2ddfe2 --- /dev/null +++ b/docs/refactor-step.md @@ -0,0 +1,412 @@ +# Django Module Refactoring Guide + +This document provides step-by-step instructions for refactoring any Django module from a views-based architecture to a clean service-controller architecture. + +## Overview + +The refactoring process transforms the codebase from: +- **Old Pattern**: Views handle both HTTP concerns and business logic +- **New Pattern**: Controllers handle HTTP concerns, Services handle business logic + +## Architecture Before vs After + +### Before Refactoring: +``` +api/ +โ”œโ”€โ”€ views/ +โ”‚ โ””โ”€โ”€ {module}.py (HTTP + Business Logic) +โ”œโ”€โ”€ controllers/ +โ”‚ โ””โ”€โ”€ {module}/ (Individual function files) +โ””โ”€โ”€ serializers.py (All serializers mixed together) +``` + +### After Refactoring: +``` +api/ +โ”œโ”€โ”€ controllers/ +โ”‚ โ””โ”€โ”€ {module}_controller.py (HTTP handling only) +โ”œโ”€โ”€ services/ +โ”‚ โ””โ”€โ”€ {module}/ +โ”‚ โ”œโ”€โ”€ {module}_service.py (Business logic only) +โ”‚ โ””โ”€โ”€ serializers.py (Module-specific serializers) +โ””โ”€โ”€ urls.py (Updated to use controllers) +``` + +**Example Modules**: `problem`, `account`, `auth`, `topic`, `collection`, `submission`, etc. + +## Step-by-Step Refactoring Process + +### Step 1: Consolidate Controller Functions into Service Layer + +#### 1.1 Identify All Controller Functions +First, examine the controller directory to identify all functions: +```bash +# List all files in the target module controller directory +ls api/controllers/{module}/ +``` + +Common function patterns: +- `create_{module}.py` +- `delete_{module}.py` +- `get_{module}.py` +- `get_all_{modules}.py` +- `update_{module}.py` +- Plus additional module-specific functions... + +#### 1.2 Read Each Controller Function +For each file, examine the function structure: +```python +# Example from create_{module}.py +def create_{module}(account_id: str, request): + # Business logic here + return Response(data, status=status.HTTP_201_CREATED) +``` + +#### 1.3 Create Service Module Structure +Create the service directory structure: +```bash +mkdir -p api/services/{module}/ +touch api/services/{module}/__init__.py +touch api/services/{module}/{module}_service.py +``` + +#### 1.4 Extract Functions to Service Layer +Copy all functions from individual controller files into `{module}_service.py`: + +```python +# api/services/{module}/{module}_service.py +from django.utils import timezone # If needed for timestamps +from ...models import * +from .serializers import * +from ...errors.common import * +# Add other imports specific to your module + +def create_{module}(account_id: str, request): + # Extract business logic from original controller + # Remove Response objects, return raw data + account = Account.objects.get(account_id=account_id) + # ... business logic ... + return serialized_data # Return raw data, not Response objects + +def delete_{module}(entity: ModelClass): + # Business logic only + related_objects = RelatedModel.objects.filter(entity=entity) + entity.delete() + related_objects.delete() + return None + +def get_{module}(entity: ModelClass): + # Business logic for retrieval + serialize = EntitySerializer(entity) + return serialize.data + +# ... Add all other functions from individual controller files +``` + +**Key Changes During Extraction:** +- Remove `Response()` objects - return raw data instead +- Remove HTTP status codes - let controllers handle them +- Replace generic error responses with proper exception classes +- Keep all business logic intact + +### Step 2: Create Module-Specific Serializers + +#### 2.1 Create Serializers Module +```bash +touch api/services/{module}/serializers.py +``` + +#### 2.2 Extract Module-Related Serializers +From the main `serializers.py`, identify and extract all module-related serializers: + +```python +# api/services/{module}/serializers.py +from rest_framework import serializers +from ...models import * + +# Core Module Serializers +class {Module}Serializer(serializers.ModelSerializer): + class Meta: + model = {Module} + fields = "__all__" + +class {Module}SecureSerializer(serializers.ModelSerializer): + class Meta: + model = {Module} + exclude = ['sensitive_field1', 'sensitive_field2'] # Exclude sensitive fields + +# Related Entity Serializers (if applicable) +class RelatedEntitySerializer(serializers.ModelSerializer): + class Meta: + model = RelatedEntity + fields = "__all__" + +# Complex Nested Serializers +class {Module}PopulateRelatedEntitiesSerializer(serializers.ModelSerializer): + related_field = RelatedEntitySerializer() + class Meta: + model = {Module} + fields = "__all__" + include = ['related_field'] +``` + +**How to Identify Module-Related Serializers:** +1. Search for `class.*{Module}.*Serializer` in main serializers.py +2. Look for serializers that reference your module's model +3. Include dependency serializers (Account, Group, etc.) if they're used +4. Move complex nested serializers that populate your module's relationships + +**Common Serializer Categories:** +- Core module serializers (basic CRUD) +- Secure versions (excluding sensitive fields) +- Related entity serializers +- Complex nested serializers (with relationships) +- Integration serializers (with other modules) + +#### 2.3 Update Service Import +```python +# In {module}_service.py +from .serializers import * # Use local serializers +``` + +### Step 3: Create Controller Layer + +#### 3.1 Create Module Controller +```bash +touch api/controllers/{module}_controller.py +``` + +#### 3.2 Implement Controller Pattern +Follow the same pattern as `account_controller.py`: + +```python +# api/controllers/{module}_controller.py +from rest_framework.response import Response +from rest_framework.decorators import api_view +from api.wrappers.auth_wrapper import authentication_required +from ..constant import GET, POST, PUT, DELETE +from ..models import * +from api.errors.common import InternalServerError +from api.errors.core.grader_exception import GraderException +import api.services.{module}.{module}_service as {module}_service + +@api_view([POST, GET]) +@authentication_required +def all_{modules}_creator_view(request, account_id): + try: + if request.method == POST: + result = {module}_service.create_{module}(account_id, request) + elif request.method == GET: + account = Account.objects.get(account_id=account_id) + result = {module}_service.get_all_{modules}_by_account(account, request) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() + +@api_view([GET, PUT, DELETE]) +@authentication_required +def one_{module}_view(request, {module}_id: str): + try: + entity = {Module}.objects.get({module}_id={module}_id) + if request.method == GET: + result = {module}_service.get_{module}(entity) + elif request.method == PUT: + result = {module}_service.update_{module}(entity, request) + elif request.method == DELETE: + {module}_service.delete_{module}(entity) + return Response(status=204) + return Response(result, status=200) + except GraderException as ge: + return ge.django_response() + except Exception as e: + return InternalServerError(e).django_response() + +# ... Add more controller functions as needed +``` + +**Controller Responsibilities:** +- HTTP method handling (`request.method == POST`) +- Model object retrieval (`{Module}.objects.get()`) +- Service layer calls (`{module}_service.function_name()`) +- Response formatting (`Response(result, status=200)`) +- Error handling (try/catch with proper error responses) +- Authentication decorators (`@authentication_required`) + +### Step 4: Update URL Configuration + +#### 4.1 Update Imports +```python +# api/urls.py +from django.urls import path +from .views import account, auth, script, submission, topic, collection, group # Remove '{module}' +from .controllers import {module}_controller # Add controller import +from api.controllers import account_controller, auth_controller +``` + +#### 4.2 Map URLs to New Controller +Replace all old view references with controller references: + +```python +urlpatterns = [ + # Old pattern: + # path('{modules}', {module}.all_{modules}_view), + + # New pattern: + path('{modules}', {module}_controller.all_{modules}_view), + path('{modules}/', {module}_controller.one_{module}_view), + path('accounts//{modules}', {module}_controller.all_{modules}_creator_view), + # ... Add more URL mappings as needed +] +``` + +### Step 5: Clean Up Unused Imports + +#### 5.1 Clean Service Layer Imports +Remove imports not used in business logic: + +```python +# COMMONLY REMOVED from {module}_service.py: +from django.forms.models import model_to_dict # Usually not needed +from api.utility import passwordEncryption # Usually not needed in services +from rest_framework.response import Response # Controllers handle responses +from rest_framework.decorators import api_view # Controllers handle decorators +from rest_framework import status # Controllers handle status codes +# Keep only imports used for business logic +``` + +#### 5.2 Clean Controller Layer Imports +Remove imports not used in HTTP handling: + +```python +# COMMONLY REMOVED from {module}_controller.py: +from api.utility import * # Usually not needed in controllers +from api.sandbox.grader import * # Service layer handles complex logic +from rest_framework import status # If using hardcoded status codes +from django.forms.models import model_to_dict # Usually not needed +from ..services.{module}.serializers import * # Service layer handles serialization +# Keep only imports used for HTTP handling +``` + +### Step 6: Error Handling Enhancement + +#### 6.1 Add Missing Error Classes +If needed, add error classes to `api/errors/common.py`: + +```python +class BadRequestError(GraderException): + def __init__(self, message: str = "Bad request"): + super().__init__(message, 400) +``` + +#### 6.2 Update Service Error Handling +Replace generic responses with proper exceptions: + +```python +# OLD (in original controllers): +return Response({'message': 'Error!'}, status=status.HTTP_400_BAD_REQUEST) + +# NEW (in services): +raise BadRequestError('Error!') +``` + +### Step 7: Verification & Testing + +#### 7.1 Check Linting +```bash +# Verify no linting errors +python manage.py check +``` + +#### 7.2 Verify URL Mappings +Ensure all endpoints still work: +- `GET /problems` +- `POST /accounts/{id}/problems` +- `PUT /problems/{id}` +- etc. + +#### 7.3 Test Service Independence +Verify services can be tested independently from HTTP layer. + +## Function Mapping Reference + +| Original View Function | New Controller Function | Service Function | +|------------------------|-------------------------|------------------| +| `{module}.all_{modules}_creator_view` | `{module}_controller.all_{modules}_creator_view` | `{module}_service.create_{module}` + `get_all_{modules}_by_account` | +| `{module}.one_{module}_creator_view` | `{module}_controller.one_{module}_creator_view` | `{module}_service.get_{module}` + `update_{module}` + `delete_{module}` | +| `{module}.{specific}_view` | `{module}_controller.{specific}_view` | `{module}_service.{specific}_function` | +| Add mappings for your specific module... | | | + +## Benefits Achieved + +### 1. **Separation of Concerns** +- Controllers: HTTP handling only +- Services: Business logic only +- Clear boundaries between layers + +### 2. **Maintainability** +- Single location for all problem logic +- Easier to test business logic +- Clear dependency management + +### 3. **Scalability** +- Easy to add new endpoints +- Service functions reusable across controllers +- Clear pattern for other modules + +### 4. **Code Quality** +- Eliminated duplicate code +- Consistent error handling +- Clean import dependencies + +## Next Steps + +Apply the same refactoring pattern to other modules in your project: + +### **Common Django Module Types:** +1. **Auth module** - Authentication and authorization +2. **Account/User module** - User management +3. **Content modules** - Main business entities (Topic, Collection, Problem, etc.) +4. **Transaction modules** - Actions and processes (Submission, Order, etc.) +5. **Configuration modules** - Settings and metadata + +### **Prioritization Strategy:** +1. Start with **core business modules** (most used entities) +2. **High-complexity modules** benefit most from this pattern +3. **Modules with many individual controller files** are good candidates +4. **Modules with mixed serializers** in main serializers.py + +### **Module Selection Criteria:** +- Has individual function files in `controllers/{module}/` +- Functions mixed in `views/{module}.py` +- Related serializers scattered in main `serializers.py` +- Business logic mixed with HTTP handling + +## Files Modified Summary + +### Created (Per Module): +- `api/services/{module}/{module}_service.py` (Contains all business logic) +- `api/services/{module}/serializers.py` (Module-specific serializers) +- `api/controllers/{module}_controller.py` (HTTP handling only) + +### Modified (Global): +- `api/urls.py` (Updated URL patterns to use controllers) +- `api/errors/common.py` (Add new error classes if needed) + +### Pattern Established: +This refactoring creates a **reusable pattern** that can be applied to any Django module for better architecture and maintainability. + +## Template Checklist for Each Module + +- [ ] Identify all controller functions in `controllers/{module}/` +- [ ] Create service directory structure +- [ ] Extract functions to service layer (remove HTTP concerns) +- [ ] Create module-specific serializers +- [ ] Create controller layer with proper error handling +- [ ] Update URL patterns +- [ ] Clean up unused imports +- [ ] Test endpoints still work +- [ ] Verify service functions work independently + +**Estimated Time Per Module**: 2-4 hours depending on complexity diff --git a/docs/test-all.md b/docs/test-all.md new file mode 100644 index 0000000..3cf0e30 --- /dev/null +++ b/docs/test-all.md @@ -0,0 +1,273 @@ +# All Tests Runner Guide + +This guide explains how to use the comprehensive test runner for all ModelGrader-Backend service unit tests. + +## ๐Ÿš€ Quick Start + +```bash +# Navigate to project directory +cd /Users/kanon.che/Documents/ModelGrader-Backend + +# Run all tests (simplest command) +python run_all_tests.py + +# Run with verbose output +python run_all_tests.py --verbose + +# Run with coverage analysis +python run_all_tests.py --coverage +``` + +## ๐Ÿ“‹ Available Options + +### Basic Usage +```bash +python run_all_tests.py [options] +``` + +### Options +- `--verbose, -v` : Run with detailed output showing each test +- `--coverage, -c` : Run with coverage analysis and HTML report +- `--services, -s` : Run specific services (comma-separated) +- `--help, -h` : Show help message + +## ๐ŸŽฏ Examples + +### Run All Tests +```bash +# Basic run +python run_all_tests.py + +# With verbose output +python run_all_tests.py --verbose +``` + +### Run Specific Services +```bash +# Run only AccountService tests +python run_all_tests.py --services account + +# Run AccountService and AuthService tests +python run_all_tests.py --services account,auth + +# Run ProblemService and AuthService tests +python run_all_tests.py --services problem,auth +``` + +### Run with Coverage +```bash +# Run all tests with coverage +python run_all_tests.py --coverage + +# Run specific services with coverage +python run_all_tests.py --coverage --services account,auth + +# Verbose output with coverage +python run_all_tests.py --verbose --coverage +``` + +## ๐Ÿ“Š What Gets Tested + +The test runner executes unit tests for: + +### AccountService +- โœ… Account creation, retrieval, and listing +- โœ… Error handling and edge cases +- โœ… Data validation and serialization + +### ProblemService +- โœ… Problem creation and deletion +- โœ… Program validation +- โœ… File import functionality +- โœ… Account-based problem retrieval + +### AuthService +- โœ… Token verification and management +- โœ… Login/logout functionality +- โœ… Authorization checks +- โœ… Security scenarios (expired tokens, wrong passwords) + +## ๐Ÿ“ˆ Output Examples + +### Basic Output +``` +๐Ÿš€ Starting ModelGrader-Backend Service Tests +============================================================ +๐Ÿ“ฆ Adding AccountService tests... + โœ… Unit tests added +๐Ÿ“ฆ Adding ProblemService tests... + โœ… Unit tests added +๐Ÿ“ฆ Adding AuthService tests... + โœ… Unit tests added + โœ… Module function tests added + +๐Ÿƒ Running tests... +------------------------------------------------------------ +..... +============================================================ +๐Ÿ“Š TEST EXECUTION SUMMARY +============================================================ +Total Tests Run: 45 +โœ… Passed: 45 +โŒ Failed: 0 +๐Ÿ’ฅ Errors: 0 +โญ๏ธ Skipped: 0 +๐Ÿ“ˆ Success Rate: 100.0% + +๐ŸŽฏ Overall Result: โœ… ALL TESTS PASSED! +============================================================ +``` + +### Verbose Output +``` +๐Ÿš€ Starting ModelGrader-Backend Service Tests +============================================================ +๐Ÿ“‹ Services to test: account, problem, auth +๐Ÿ”ง Verbose mode: ON +๐Ÿ“Š Coverage mode: OFF + +๐Ÿ“ฆ Adding AccountService tests... + โœ… Unit tests added +๐Ÿ“ฆ Adding ProblemService tests... + โœ… Unit tests added +๐Ÿ“ฆ Adding AuthService tests... + โœ… Unit tests added + โœ… Module function tests added + +๐Ÿƒ Running tests... +------------------------------------------------------------ +test_create_account_success (api.services.account.test_account_service.TestAccountService) ... ok +test_get_account_success (api.services.account.test_account_service.TestAccountService) ... ok +test_verify_token_success (api.services.auth.test_auth_service.TestAuthService) ... ok +... +``` + +### Coverage Output +``` +๐Ÿ” Running tests with coverage analysis... +๐Ÿš€ Starting ModelGrader-Backend Service Tests +============================================================ +... +๐Ÿ“Š Coverage Report: +================================================== +Name Stmts Miss Cover Missing +--------------------------------------------------------------------- +api/services/account/account_service.py 72 0 100% +api/services/auth/auth_service.py 137 0 100% +api/services/problem/problem_service.py 285 0 100% +--------------------------------------------------------------------- +TOTAL 494 0 100% + +๐Ÿ“ Generating HTML coverage report... +HTML report generated in 'htmlcov' directory +``` + +## ๐Ÿ› ๏ธ Troubleshooting + +### Common Issues + +**Import Errors:** +```bash +# Make sure you're in the project directory +cd /Users/kanon.che/Documents/ModelGrader-Backend + +# Check Django setup +python manage.py check +``` + +**Coverage Not Working:** +```bash +# Install coverage package +pip install coverage + +# Or use conda +conda install coverage +``` + +**Permission Errors:** +```bash +# Make script executable +chmod +x run_all_tests.py + +# Or run with python explicitly +python run_all_tests.py +``` + +### Exit Codes +- `0` : All tests passed +- `1` : Some tests failed or error occurred + +## ๐Ÿ”ง Advanced Usage + +### Custom Test Selection +```bash +# Run only unit tests (skip integration) +python run_all_tests.py --services account + +# Run with specific verbosity +python run_all_tests.py --verbose +``` + +### Integration with CI/CD +```bash +# For continuous integration +python run_all_tests.py --coverage + +# Check exit code +if [ $? -eq 0 ]; then + echo "All tests passed!" +else + echo "Tests failed!" + exit 1 +fi +``` + +## ๐Ÿ“ File Structure + +``` +ModelGrader-Backend/ +โ”œโ”€โ”€ run_all_tests.py # Main test runner +โ”œโ”€โ”€ ALL_TESTS_GUIDE.md # This guide +โ”œโ”€โ”€ TEST_COMMANDS.md # Individual service commands +โ”œโ”€โ”€ api/services/ +โ”‚ โ”œโ”€โ”€ account/ +โ”‚ โ”‚ โ”œโ”€โ”€ test_account_service.py +โ”‚ โ”‚ โ””โ”€โ”€ README_TESTS.md +โ”‚ โ”œโ”€โ”€ problem/ +โ”‚ โ”‚ โ”œโ”€โ”€ test_problem_service.py +โ”‚ โ”‚ โ””โ”€โ”€ README_TESTS.md +โ”‚ โ””โ”€โ”€ auth/ +โ”‚ โ”œโ”€โ”€ test_auth_service.py +โ”‚ โ””โ”€โ”€ README_TESTS.md +โ””โ”€โ”€ htmlcov/ # Coverage reports (generated) +``` + +## ๐ŸŽ‰ Benefits + +- **Single Command**: Run all service tests with one command +- **Flexible**: Choose which services to test +- **Comprehensive**: Includes coverage analysis +- **User-Friendly**: Clear output and error messages +- **CI/CD Ready**: Proper exit codes and structured output +- **Extensible**: Easy to add new services + +## ๐Ÿ”„ Regular Usage + +For daily development: +```bash +# Quick test run +python run_all_tests.py + +# Before committing +python run_all_tests.py --coverage --verbose +``` + +For debugging: +```bash +# Test specific service +python run_all_tests.py --services account --verbose + +# Check coverage +python run_all_tests.py --coverage +``` + diff --git a/docs/unit-test.md b/docs/unit-test.md new file mode 100644 index 0000000..d2ad542 --- /dev/null +++ b/docs/unit-test.md @@ -0,0 +1,120 @@ +# ProblemService Unit Tests + +This directory contains comprehensive unit tests for the `ProblemService` class. + +## Test Coverage + +The test suite covers the following scenarios: + +### `create_problem` method: +- โœ… Successful problem creation with grader integration +- โœ… Grader exception handling +- โœ… Account not found handling +- โœ… Repository integration + +### `delete_problem` method: +- โœ… Successful problem deletion +- โœ… Repository method verification + +### `validate_program` method: +- โœ… Successful program validation +- โœ… Invalid language handling +- โœ… Grader integration + +### `import_elabsheet_problem` method: +- โœ… Successful PDF import +- โœ… No file provided handling + +### `get_all_problems_by_account` method: +- โœ… Retrieval with query parameters +- โœ… Default parameter handling +- โœ… Empty results handling +- โœ… Repository exception handling +- โœ… Group and permission integration + +### General: +- โœ… Service initialization +- โœ… Repository dependency injection +- โœ… Integration test structure (requires database) + +## Running the Tests + +### Option 1: Using Django's test runner (Recommended) +```bash +cd /Users/kanon.che/Documents/ModelGrader-Backend +python manage.py test api.services.problem.test_problem_service +``` + +### Option 2: Using the test runner script +```bash +cd /Users/kanon.che/Documents/ModelGrader-Backend +python run_problem_service_tests.py +``` + +### Option 3: Using unittest directly +```bash +cd /Users/kanon.che/Documents/ModelGrader-Backend +python -m unittest api.services.problem.test_problem_service +``` + +## Test Structure + +### Unit Tests (`TestProblemService`) +- Uses mocked dependencies +- Fast execution +- No database required +- Tests business logic in isolation + +### Integration Tests (`TestProblemServiceIntegration`) +- Uses real repository implementation +- Requires database setup +- Tests end-to-end functionality +- Currently skipped in unit test runs + +## Mocking Strategy + +The tests use the following mocking approach: + +1. **Repository Mocking**: All repositories are mocked to isolate the service layer +2. **External Dependencies**: Grader classes are mocked +3. **Request Objects**: Django request objects are mocked +4. **Model Objects**: Problem, Account, and Testcase model instances are mocked +5. **Serializers**: Django REST Framework serializers are mocked + +## Test Data + +The tests use consistent test data: +- Sample problem data with realistic fields +- Various programming languages +- Different testcase scenarios +- Edge cases (empty data, missing fields) + +## Assertions + +Each test verifies: +- Correct method calls to dependencies +- Proper parameter passing +- Expected return value structure +- Exception handling +- Data integrity + +## Dependencies + +The ProblemService has the following dependencies: +- `ProblemRepository` - Core problem operations +- `AccountRepository` - Account management +- `PermissionRepository` - Permission handling +- `GroupRepository` - Group operations +- `TopicRepository` - Topic management +- `PythonGrader` - Code execution +- `Grader` - Language-specific graders + +## Future Enhancements + +Consider adding: +- Performance tests +- Security tests (code execution) +- Concurrent access tests +- Memory usage tests +- More edge case scenarios +- Integration with real graders diff --git a/requirements.txt b/requirements.txt index 271a2f4..27dfa66 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,10 +2,12 @@ asgiref==3.5.2 certifi==2022.9.24 charset-normalizer==2.1.1 cryptography==38.0.3 +coverage==7.10.7 Django==4.1.2 django-cors-headers==3.13.0 django-environ==0.9.0 django-filter==22.1 +django-types==0.22.0 djangorestframework==3.13.1 djangorestframework-jwt==1.11.0 djangorestframework-simplejwt==5.2.2 diff --git a/run_account_service_tests.py b/run_account_service_tests.py new file mode 100644 index 0000000..c6bb54d --- /dev/null +++ b/run_account_service_tests.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +""" +Test runner for AccountService unit tests +Usage: python run_account_service_tests.py +""" + +import os +import sys +import django +from django.conf import settings + +# Add the project root to Python path +project_root = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, project_root) + +# Setup Django +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Backend.settings') +django.setup() + +import unittest +from api.services.account.test_account_service import TestAccountService, TestAccountServiceIntegration + +def run_tests(): + """Run all AccountService tests""" + # Create test suite + suite = unittest.TestSuite() + + # Add unit tests + suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestAccountService)) + + # Add integration tests (optional - requires database) + # suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestAccountServiceIntegration)) + + # Run tests + runner = unittest.TextTestRunner(verbosity=2) + result = runner.run(suite) + + # Return exit code + return 0 if result.wasSuccessful() else 1 + +if __name__ == '__main__': + exit_code = run_tests() + sys.exit(exit_code) diff --git a/run_all_tests.py b/run_all_tests.py new file mode 100755 index 0000000..5932330 --- /dev/null +++ b/run_all_tests.py @@ -0,0 +1,328 @@ +#!/usr/bin/env python +""" +Comprehensive test runner for all ModelGrader-Backend service unit tests +Usage: python run_all_tests.py [options] + +This script runs unit tests for: +- AccountService +- ProblemService +- AuthService +- CollectionService +- GroupService +- SubmissionService +- TopicService + +Options: + --verbose, -v : Run with verbose output + --coverage, -c : Run with coverage analysis + --services, -s : Run specific services (comma-separated) + --help, -h : Show this help message + +Examples: + python run_all_tests.py + python run_all_tests.py --verbose + python run_all_tests.py --coverage + python run_all_tests.py --services account,auth,collection + python run_all_tests.py -v -c +""" + +import os +import sys +import argparse +import django +from django.conf import settings +import unittest +from io import StringIO + +# Add the project root to Python path +project_root = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, project_root) + +# Setup Django +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Backend.settings') +django.setup() + +# Import test classes +from api.services.account.test_account_service import TestAccountService, TestAccountServiceIntegration +from api.services.problem.test_problem_service import TestProblemService +from api.services.auth.test_auth_service import TestAuthService, TestAuthServiceIntegration, TestAuthServiceModuleFunctions +from api.services.collection.test_collection_service import TestCollectionService +from api.services.group.test_group_service import TestGroupService +from api.services.submission.test_submission_service import TestSubmissionService +from api.services.topic.test_topic_service import TestTopicService + + +class TestRunner: + """Main test runner class for all service tests""" + + def __init__(self, verbose=False, coverage=False, services=None): + self.verbose = verbose + self.coverage = coverage + self.services = services or ['account', 'problem', 'auth', 'collection', 'group', 'submission', 'topic'] + self.results = {} + self.total_tests = 0 + self.total_failures = 0 + self.total_errors = 0 + self.total_skipped = 0 + + def get_test_classes(self): + """Get test classes based on selected services""" + test_classes = {} + + if 'account' in self.services: + test_classes['AccountService'] = { + 'unit': TestAccountService, + 'integration': TestAccountServiceIntegration + } + + if 'problem' in self.services: + test_classes['ProblemService'] = { + 'unit': TestProblemService + } + + if 'auth' in self.services: + test_classes['AuthService'] = { + 'unit': TestAuthService, + 'integration': TestAuthServiceIntegration, + 'module_functions': TestAuthServiceModuleFunctions + } + + if 'collection' in self.services: + test_classes['CollectionService'] = { + 'unit': TestCollectionService + } + + if 'group' in self.services: + test_classes['GroupService'] = { + 'unit': TestGroupService + } + + if 'submission' in self.services: + test_classes['SubmissionService'] = { + 'unit': TestSubmissionService + } + + if 'topic' in self.services: + test_classes['TopicService'] = { + 'unit': TestTopicService + } + + return test_classes + + def run_coverage_analysis(self): + """Run tests with coverage analysis""" + try: + import coverage + print("๐Ÿ” Running tests with coverage analysis...") + + # Start coverage + cov = coverage.Coverage(source=['api.services']) + cov.start() + + # Run tests + self.run_tests() + + # Stop coverage and generate report + cov.stop() + cov.save() + + print("\n๐Ÿ“Š Coverage Report:") + print("=" * 50) + cov.report() + + # Generate HTML report + print("\n๐Ÿ“ Generating HTML coverage report...") + cov.html_report(directory='htmlcov') + print("HTML report generated in 'htmlcov' directory") + + return True + + except ImportError: + print("โŒ Coverage package not installed. Install with: pip install coverage") + return False + + def run_tests(self): + """Run all selected tests""" + test_classes = self.get_test_classes() + + print("๐Ÿš€ Starting ModelGrader-Backend Service Tests") + print("=" * 60) + + if self.verbose: + print(f"๐Ÿ“‹ Services to test: {', '.join(self.services)}") + print(f"๐Ÿ”ง Verbose mode: {'ON' if self.verbose else 'OFF'}") + print(f"๐Ÿ“Š Coverage mode: {'ON' if self.coverage else 'OFF'}") + print() + + # Create test suite + suite = unittest.TestSuite() + + # Add unit tests + for service_name, classes in test_classes.items(): + print(f"๐Ÿ“ฆ Adding {service_name} tests...") + + # Add unit tests + if 'unit' in classes: + suite.addTest(unittest.TestLoader().loadTestsFromTestCase(classes['unit'])) + print(f" โœ… Unit tests added") + + # Add module function tests (AuthService only) + if 'module_functions' in classes: + suite.addTest(unittest.TestLoader().loadTestsFromTestCase(classes['module_functions'])) + print(f" โœ… Module function tests added") + + # Skip integration tests for now (require database) + if 'integration' in classes: + print(f" โญ๏ธ Integration tests skipped (require database setup)") + + print() + + # Configure test runner + verbosity = 2 if self.verbose else 1 + runner = unittest.TextTestRunner( + verbosity=verbosity, + stream=sys.stdout, + descriptions=True, + failfast=False + ) + + # Run tests + print("๐Ÿƒ Running tests...") + print("-" * 60) + + result = runner.run(suite) + + # Store results + self.total_tests = result.testsRun + self.total_failures = len(result.failures) + self.total_errors = len(result.errors) + self.total_skipped = len(result.skipped) if hasattr(result, 'skipped') else 0 + + # Print summary + self.print_summary(result) + + return result.wasSuccessful() + + def print_summary(self, result): + """Print test execution summary""" + print("\n" + "=" * 60) + print("๐Ÿ“Š TEST EXECUTION SUMMARY") + print("=" * 60) + + # Test counts + print(f"Total Tests Run: {self.total_tests}") + print(f"โœ… Passed: {self.total_tests - self.total_failures - self.total_errors}") + print(f"โŒ Failed: {self.total_failures}") + print(f"๐Ÿ’ฅ Errors: {self.total_errors}") + print(f"โญ๏ธ Skipped: {self.total_skipped}") + + # Success rate + if self.total_tests > 0: + success_rate = ((self.total_tests - self.total_failures - self.total_errors) / self.total_tests) * 100 + print(f"๐Ÿ“ˆ Success Rate: {success_rate:.1f}%") + + # Overall result + print("\n๐ŸŽฏ Overall Result: ", end="") + if result.wasSuccessful(): + print("โœ… ALL TESTS PASSED!") + else: + print("โŒ SOME TESTS FAILED!") + + # Detailed failure/error info + if result.failures: + print(f"\nโŒ FAILURES ({len(result.failures)}):") + for test, traceback in result.failures: + if 'AssertionError:' in traceback: + error_msg = traceback.split('AssertionError: ')[-1].split('\n')[0] + else: + error_msg = 'Assertion failed' + print(f"{test}: {error_msg}") + + if result.errors: + print(f"\n๐Ÿ’ฅ ERRORS ({len(result.errors)}):") + for test, traceback in result.errors: + lines = traceback.split('\n') + error_msg = lines[-2] if lines else 'Unknown error' + print(f"{test}: {error_msg}") + + print("=" * 60) + + def run(self): + """Main entry point""" + if self.coverage: + return self.run_coverage_analysis() + else: + return self.run_tests() + + +def parse_arguments(): + """Parse command line arguments""" + parser = argparse.ArgumentParser( + description="Run all ModelGrader-Backend service unit tests", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python run_all_tests.py # Run all tests + python run_all_tests.py --verbose # Run with verbose output + python run_all_tests.py --coverage # Run with coverage analysis + python run_all_tests.py --services account,auth,collection,group # Run specific services + python run_all_tests.py -v -c # Verbose with coverage + """ + ) + + parser.add_argument( + '--verbose', '-v', + action='store_true', + help='Run with verbose output' + ) + + parser.add_argument( + '--coverage', '-c', + action='store_true', + help='Run with coverage analysis' + ) + + parser.add_argument( + '--services', '-s', + type=str, + help='Comma-separated list of services to test (account,problem,auth,collection,group,submission,topic)' + ) + + return parser.parse_args() + + +def main(): + """Main function""" + args = parse_arguments() + + # Parse services + services = None + if args.services: + services = [s.strip().lower() for s in args.services.split(',')] + valid_services = ['account', 'problem', 'auth', 'collection', 'group', 'submission', 'topic'] + invalid_services = [s for s in services if s not in valid_services] + if invalid_services: + print(f"โŒ Invalid services: {', '.join(invalid_services)}") + print(f"Valid services: {', '.join(valid_services)}") + sys.exit(1) + + # Create and run test runner + runner = TestRunner( + verbose=args.verbose, + coverage=args.coverage, + services=services + ) + + try: + success = runner.run() + sys.exit(0 if success else 1) + except KeyboardInterrupt: + print("\n\nโน๏ธ Tests interrupted by user") + sys.exit(1) + except Exception as e: + print(f"\nโŒ Unexpected error: {e}") + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/run_auth_service_tests.py b/run_auth_service_tests.py new file mode 100644 index 0000000..aa52054 --- /dev/null +++ b/run_auth_service_tests.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python +""" +Test runner for AuthService unit tests +Usage: python run_auth_service_tests.py +""" + +import os +import sys +import django +from django.conf import settings + +# Add the project root to Python path +project_root = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, project_root) + +# Setup Django +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Backend.settings') +django.setup() + +import unittest +from api.services.auth.test_auth_service import TestAuthService, TestAuthServiceIntegration, TestAuthServiceModuleFunctions + +def run_tests(): + """Run all AuthService tests""" + # Create test suite + suite = unittest.TestSuite() + + # Add unit tests + suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestAuthService)) + suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestAuthServiceModuleFunctions)) + + # Add integration tests (optional - requires database) + # suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestAuthServiceIntegration)) + + # Run tests + runner = unittest.TextTestRunner(verbosity=2) + result = runner.run(suite) + + # Return exit code + return 0 if result.wasSuccessful() else 1 + +if __name__ == '__main__': + exit_code = run_tests() + sys.exit(exit_code) diff --git a/run_problem_service_tests.py b/run_problem_service_tests.py new file mode 100644 index 0000000..653acb2 --- /dev/null +++ b/run_problem_service_tests.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +""" +Test runner for ProblemService unit tests +Usage: python run_problem_service_tests.py +""" + +import os +import sys +import django +from django.conf import settings + +# Add the project root to Python path +project_root = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, project_root) + +# Setup Django +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Backend.settings') +django.setup() + +import unittest +from api.services.problem.test_problem_service import TestProblemService, TestProblemServiceIntegration + +def run_tests(): + """Run all ProblemService tests""" + # Create test suite + suite = unittest.TestSuite() + + # Add unit tests + suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestProblemService)) + + # Add integration tests (optional - requires database) + # suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestProblemServiceIntegration)) + + # Run tests + runner = unittest.TextTestRunner(verbosity=2) + result = runner.run(suite) + + # Return exit code + return 0 if result.wasSuccessful() else 1 + +if __name__ == '__main__': + exit_code = run_tests() + sys.exit(exit_code)