diff --git a/backend/.flake8 b/backend/.flake8 deleted file mode 100644 index 08c9b30..0000000 --- a/backend/.flake8 +++ /dev/null @@ -1,11 +0,0 @@ -[flake8] -exclude = - .pytest_cache - venv - aioredis - __init__.py - conftest.py - -ignore = - F811 -max-line-length = 120 diff --git a/backend/.test.env b/backend/.test.env index 4d87f0c..9499d74 100644 --- a/backend/.test.env +++ b/backend/.test.env @@ -11,10 +11,10 @@ SUPERUSER_USERNAME=admin SUPERUSER_PASSWORD=password SUPERUSER_EMAIL=admin@gmail.com -EMAIL_USER=suslanchikmopl@gmail.com -EMAIL_PASSWORD=12345678 -EMAIL_HOST=smtp.gmail.com EMAIL_PORT=465 +EMAIL_HOST=smtp.gmail.com +EMAIL_USER=suslanchikmopl@gmail.com +EMAIL_PASSWORD=fjyndurcxgzfgcjo ALGORITHM=HS256 SECRET_KEY=asdf0-aofjklasjmfg;algk diff --git a/backend/aioredis/client.py b/backend/aioredis/client.py index 3b5e091..87c9af7 100644 --- a/backend/aioredis/client.py +++ b/backend/aioredis/client.py @@ -515,7 +515,7 @@ def parse_georadius_generic(response, **options): # with other command arguments. return response - if type(response) != list: + if not isinstance(response, list): response_list = [response] else: response_list = response diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 0000000..5e38555 --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,70 @@ +[tool.ruff] +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", + "migrations", + "aioredis", + "tests", + +] + +# Same as Black. +line-length = 120 +indent-width = 4 + +# Assume Python 3.8 +target-version = "py311" + +[tool.ruff.lint] +# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. +select = ["E", "F", "W"] +ignore = [] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[tool.ruff.format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + + +[tool.mypy] +exclude = [ + "venv", + "src.aioredis" +] +explicit_package_bases = true +mypy_path = "src" diff --git a/backend/requirements.txt b/backend/requirements.txt index 18e411a..cad9231 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,66 +1,52 @@ aiosqlite==0.17.0 amqp==5.2.0 annotated-types==0.6.0 -anyio==4.2.0 -asgiref==3.7.2 +anyio==4.3.0 async-timeout==4.0.3 asyncpg==0.29.0 billiard==4.2.0 celery==5.3.6 -certifi==2024.2.2 -charset-normalizer==3.3.2 click==8.1.7 -click-didyoumean==0.3.0 +click-didyoumean==0.3.1 click-plugins==1.1.1 click-repl==0.3.0 -contourpy==1.2.0 +contourpy==1.2.1 cycler==0.12.1 -dnspython==2.5.0 -ecdsa==0.18.0 -email-validator==2.1.0.post1 -fastapi==0.109.2 +dnspython==2.6.1 +ecdsa==0.19.0 +email_validator==2.1.1 +fastapi==0.110.1 fastapi-authtools==0.6.2 -fonttools==4.48.1 -gunicorn==21.2.0 -h11==0.14.0 -httpcore==1.0.2 -httpx==0.26.0 +fonttools==4.51.0 idna==3.6 iso8601==1.1.0 kiwisolver==1.4.5 -kombu==5.3.5 -matplotlib==3.8.2 -motor==3.3.2 -mpmath==1.3.0 -nest-asyncio==1.6.0 +kombu==5.3.6 +matplotlib==3.8.4 numpy==1.26.4 -packaging==23.2 -pandas==2.2.0 +packaging==24.0 +pandas==2.2.1 passlib==1.7.4 -pillow==10.2.0 +pillow==10.3.0 prompt-toolkit==3.0.43 -pyasn1==0.5.1 -pydantic==2.6.1 -pydantic-settings==2.1.0 -pydantic_core==2.16.2 -pymongo==4.6.1 -pyparsing==3.1.1 +pyasn1==0.6.0 +pydantic==2.0.2 +pydantic-settings==2.2.1 +pydantic_core==2.1.2 +pyparsing==3.1.2 pypika-tortoise==0.1.6 -python-dateutil==2.8.2 +python-dateutil==2.9.0.post0 python-dotenv==1.0.1 python-jose==3.3.0 pytz==2024.1 -redis==5.0.1 -requests==2.31.0 +redis==5.0.3 rsa==4.9 +ruff==0.3.5 six==1.16.0 -sniffio==1.3.0 -starlette==0.36.3 -sympy==1.12 +sniffio==1.3.1 +starlette==0.37.2 tortoise-orm==0.20.0 -typing_extensions==4.9.0 +typing_extensions==4.11.0 tzdata==2024.1 -urllib3==2.2.0 -uvicorn==0.27.1 vine==5.1.0 wcwidth==0.2.13 diff --git a/backend/src/apps/cabinets/routes.py b/backend/src/apps/cabinets/routes.py index beb4be6..4f7b025 100644 --- a/backend/src/apps/cabinets/routes.py +++ b/backend/src/apps/cabinets/routes.py @@ -1,25 +1,33 @@ from fastapi import APIRouter, Request, Depends from fastapi.responses import FileResponse, RedirectResponse -from fastapi_authtools import login_required from .dependencies import HistoryParser from .models import History +from .schemas import HistoryListSchema from .services import delete_history +from ..users.permissions import login_required router = APIRouter(prefix="/cabinet", tags=["Cabinets"]) -@router.get('/history') +@router.get('/history', response_model=list[HistoryListSchema]) @login_required -async def history(request: Request): +async def history_list_view(request: Request): """History view.""" - history_list = await History.filter(user__id=request.user.id) - return history_list + history_list = await History.filter(user__id=request.user.id).select_related("formula", "formula__category") + return [ + { + **history.as_dict(), + "formula": history.formula.as_dict(), + "category": history.formula.category.as_dict(), + } + for history in history_list + ] @router.post('/history/download') @login_required -async def history(request: Request, filedata: str = Depends(HistoryParser())): +async def history_download_view(request: Request, filedata: str = Depends(HistoryParser())): if filedata is not None: filepath, filename = filedata return FileResponse(path=filepath, filename=filename) @@ -29,6 +37,6 @@ async def history(request: Request, filedata: str = Depends(HistoryParser())): @router.delete('/history') @login_required -async def history(request: Request): +async def history_delete_view(request: Request): await delete_history(request.user) return {"detail": "History deletes successfully"} diff --git a/backend/src/apps/cabinets/schemas.py b/backend/src/apps/cabinets/schemas.py index 374f010..8bfa2ce 100644 --- a/backend/src/apps/cabinets/schemas.py +++ b/backend/src/apps/cabinets/schemas.py @@ -1,6 +1,18 @@ +from datetime import datetime + from pydantic import BaseModel +from src.apps.sciences.schemas import FormulaListSchema, CategoryListSchema + class DownloadFile(BaseModel): filename: str extension: str + + +class HistoryListSchema(BaseModel): + id: str + result: float | str + date_time: datetime + formula: FormulaListSchema + category: CategoryListSchema diff --git a/backend/src/apps/main/routes.py b/backend/src/apps/main/routes.py index cb1b6c6..f2e7b8a 100644 --- a/backend/src/apps/main/routes.py +++ b/backend/src/apps/main/routes.py @@ -1,5 +1,4 @@ from fastapi import APIRouter, Request -from fastapi.responses import RedirectResponse router = APIRouter(prefix='', tags=["Main"]) @@ -9,12 +8,3 @@ async def homepage(request: Request): """Main page.""" return {"detail": "Application is started."} - - -@router.get("/github") -async def github_redirect(request: Request): - """Redirect to the GitHub project repository.""" - return RedirectResponse( - url="https://github.com/michael7nightingale/Calculations-FastAPI-Fullstack", - status_code=303, - ) diff --git a/backend/src/apps/sciences/dependencies.py b/backend/src/apps/sciences/dependencies.py index e7dd848..8e691a4 100644 --- a/backend/src/apps/sciences/dependencies.py +++ b/backend/src/apps/sciences/dependencies.py @@ -16,6 +16,7 @@ async def get_mongodb_db(request: Request): def get_mongodb_repository(repository_class): def inner(db=Depends(get_mongodb_db)): return repository_class(db) + return inner @@ -37,7 +38,7 @@ async def get_science_dependency(science_slug: str) -> Science: async def get_category_dependency(category_slug: str) -> Category: - category = await Formula.get_or_none(slug=category_slug) + category = await Category.get_or_none(slug=category_slug) if category is None: raise HTTPException(status_code=404, detail="Category is not found.") return category diff --git a/backend/src/apps/sciences/models.py b/backend/src/apps/sciences/models.py index 9337689..26d62e8 100644 --- a/backend/src/apps/sciences/models.py +++ b/backend/src/apps/sciences/models.py @@ -9,6 +9,9 @@ class Science(TortoiseModel): image_path = fields.CharField(max_length=255, null=True) slug = fields.CharField(max_length=40, unique=True, index=True) + class Meta: + ordering = ["title"] + def __str__(self): return self.title @@ -32,6 +35,9 @@ class Category(TortoiseModel): slug = fields.CharField(max_length=40, unique=True, index=True) is_special = fields.BooleanField(default=False) + class Meta: + ordering = ["title"] + def __str__(self): return self.title @@ -55,6 +61,7 @@ class Formula(TortoiseModel): related_name="formulas" ) slug = fields.CharField(max_length=40, unique=True, index=True) + data = fields.JSONField(null=True) def __str__(self): return self.title diff --git a/backend/src/apps/sciences/routes.py b/backend/src/apps/sciences/routes.py index 36b2de1..d4dd88f 100644 --- a/backend/src/apps/sciences/routes.py +++ b/backend/src/apps/sciences/routes.py @@ -1,15 +1,16 @@ from fastapi import APIRouter, Request, Body, Depends -from fastapi_authtools import login_required from fastapi.responses import FileResponse, JSONResponse import os from .models import Science, Category, Formula from ..cabinets.models import History +from ..users.permissions import login_required from ...services.formulas import counter, mathem_extra_counter from src.services.formulas.plots import Plot -from .schemas import RequestSchema, RequestData, DownloadPlot, PlotData, EquationsData +from .schemas import RequestSchema, RequestData, DownloadPlot, PlotData, EquationsData, \ + ScienceDetailSchema, CategoryDetailSchema, ScienceListSchema, FormulaDetailSchema from src.services.formulas.metadata import Formula as FormulaObject -from .dependencies import get_formula_mongo_repository, get_formula_dependency, get_science_dependency, \ +from .dependencies import get_formula_dependency, get_science_dependency, \ get_category_dependency router = APIRouter(prefix='/sciences', tags=['Sciences']) @@ -53,7 +54,7 @@ async def plots_view_post(request: Request, data: PlotData = Body()): message = "На ноль делить нет смысла." except ArithmeticError: message = "Вычислительно невозможное выражение" - except ValueError as e: # raises from Plot class + except ValueError as e: # raises from Plot class message = str(e) else: return {"plotPath": plot_path} @@ -64,14 +65,14 @@ async def plots_view_post(request: Request, data: PlotData = Body()): @router.post('/special-category/plots/download') @login_required -async def plots_view_post(request: Request, filedata: DownloadPlot = Body()): +async def plots_view_download(request: Request, filedata: DownloadPlot = Body()): """Plot file download view""" plot_path = PLOTS_DIR + f'/{request.user.id}.png' full_plot_path = request.app.state.STATIC_DIR + plot_path if os.path.exists(full_plot_path): return FileResponse(path=full_plot_path, filename=filedata.filename + ".png") else: - return JSONResponse({"detail": "Missing any plots."}, status_code=404) + return JSONResponse({"detail": "Missing any plots."}, 404) # ======================================= EQUATIONS ===================================== # @@ -100,63 +101,62 @@ async def equations_view_post(request: Request, data: EquationsData = Body()): return {"detail": message} -@router.get('/') -async def sciences_all(): +@router.get('/', response_model=list[ScienceListSchema]) +async def sciences_list_view(): """All sciences list endpoint.""" sciences = await Science.all() return sciences -@router.get('/science/{science_slug}') -async def science_get(science: Science = Depends(get_science_dependency)): +@router.get('/science/{science_slug}', response_model=ScienceDetailSchema) +async def science_detail_view(science: Science = Depends(get_science_dependency)): """Science detail endpoint.""" return { - "science": science.as_dict(), - "categories": [i.as_dict() for i in science.categories.all()] + **science.as_dict(), + "categories": (i.as_dict() for i in science.categories) } -@router.get('/category/{category_slug}') -async def category_get( +@router.get('/category/{category_slug}', response_model=CategoryDetailSchema) +async def category_detail_view( request: Request, category: Category = Depends(get_category_dependency), ): """Category GET view.""" return { - "category": category.as_dict(), + **category.as_dict(), "science": category.science.as_dict(), - "formulas": [f.as_dict() for f in category.formulas.all()] + "formulas": [f.as_dict() for f in category.formulas] } -@router.get('/formula/{formula_slug}') -async def formula_get( +@router.get('/formula/{formula_slug}', response_model=FormulaDetailSchema) +async def formula_detail_view( formula: Formula = Depends(get_formula_dependency), - formula_repository=Depends(get_formula_mongo_repository) + # formula_repository: FormulaRepository = Depends(get_formula_mongo_repository) ): """Science GET view.""" - formula_data = await formula_repository.get(slug=formula.slug) - formula_obj = FormulaObject.from_dict(formula_data) + # formula_data = await formula_repository.get(slug=formula.slug) + formula_obj = FormulaObject.from_dict(formula.data) if formula_obj is None: return JSONResponse({"detail": "Cannot find formula metadata."}, status_code=404) return { - "formula": formula.as_dict(), + **formula.as_dict(), "category": formula.category.as_dict(), + "science": (await Science.get(id=formula.category.science_id)).as_dict(), "info": formula_obj.as_dict() } @router.post('/formula/{formula_slug}') @login_required -async def formula_post( +async def formula_calculate_view( request: Request, formula: Formula = Depends(get_formula_dependency), request_data: RequestData = Body(), - formula_repository=Depends(get_formula_mongo_repository) ): """Request form to calculate.""" - formula_data = await formula_repository.get(slug=formula.slug) - formula_obj = FormulaObject.from_dict(formula_data) + formula_obj = FormulaObject.from_dict(formula.data) request_schema = RequestSchema( formula_id=str(formula.id), url=request.url.path, @@ -171,8 +171,8 @@ async def formula_post( if is_success: await History.create( formula_id=formula.id, - user_id=result.user_id, + user_id=request.user.id, result=result ) - return JSONResponse({"result": result}, status_code=200) - return JSONResponse({"detail": result}, status_code=400) + return JSONResponse({"result": result}, 200) + return JSONResponse({"detail": result}, 400) diff --git a/backend/src/apps/sciences/schemas.py b/backend/src/apps/sciences/schemas.py index e8cc21a..42358d3 100644 --- a/backend/src/apps/sciences/schemas.py +++ b/backend/src/apps/sciences/schemas.py @@ -1,5 +1,4 @@ from pydantic import BaseModel, Field -from enum import Enum class RequestSchema(BaseModel): @@ -43,6 +42,41 @@ class DownloadPlot(BaseModel): filename: str -class ScienceEnum(Enum): - physics = "physics" - mathem = "mathem" +class ScienceListSchema(BaseModel): + id: str + title: str + slug: str + image_path: str | None = None + content: str + + +class CategoryListSchema(BaseModel): + id: str + title: str + slug: str + content: str + image_path: str | None = None + is_special: bool + + +class ScienceDetailSchema(ScienceListSchema): + categories: list[CategoryListSchema] + + +class FormulaListSchema(BaseModel): + id: str + title: str + slug: str + formula: str + image_path: str | None = None + + +class CategoryDetailSchema(CategoryListSchema): + science: ScienceListSchema + formulas: list[FormulaListSchema] + + +class FormulaDetailSchema(FormulaListSchema): + info: dict + category: CategoryListSchema + science: ScienceListSchema diff --git a/backend/src/apps/users/models.py b/backend/src/apps/users/models.py index 2cdf2a5..7a12f9f 100644 --- a/backend/src/apps/users/models.py +++ b/backend/src/apps/users/models.py @@ -19,6 +19,9 @@ class User(TortoiseModel): time_registered = fields.DatetimeField(auto_now=True) last_login = fields.DatetimeField(auto_now_add=True, null=True) + def __str__(self): + return self.username + @classmethod async def login(cls, password: str, username: str | None = None, email: str | None = None): if username: @@ -27,7 +30,6 @@ async def login(cls, password: str, username: str | None = None, email: str | No user = await cls.get_or_none(email=email) else: return None, None - print(123123, user) if user is not None: return user, verify_password(password, user.password) return None, None @@ -38,7 +40,6 @@ async def register(cls, **kwargs): try: user = await cls.create(**kwargs, active=True) except IntegrityError: - raise return return user @@ -46,9 +47,6 @@ async def activate(self) -> None: self.is_active = True await self.save() - def __str__(self): - return self.username - def full_name(self) -> str: if self.first_name or self.last_name: return f"{self.first_name or ''} {self.last_name or ''}" diff --git a/backend/src/apps/users/permissions.py b/backend/src/apps/users/permissions.py new file mode 100644 index 0000000..ba11661 --- /dev/null +++ b/backend/src/apps/users/permissions.py @@ -0,0 +1,29 @@ +from functools import wraps +from typing import Callable + +from fastapi import Request, HTTPException, WebSocket +from starlette.authentication import UnauthenticatedUser + + +def login_required(view_func: Callable): + """Decorator for api view to check token header.""" + + @wraps(view_func) + async def inner(request: Request, *args, **kwargs): + if isinstance(request.user, UnauthenticatedUser): # raise exception if user is not found + raise HTTPException(403, "Authentication required.") + return await view_func(request, *args, **kwargs) + + return inner + + +def ws_login_required(view_func: Callable): + """Decorator for websocket view to check token header.""" + + @wraps(view_func) + async def inner(websocket: WebSocket, *args, **kwargs): + if isinstance(websocket.user, UnauthenticatedUser): # raise exception if user is not found + raise HTTPException(403, "Authentication required.") + return await view_func(websocket, *args, **kwargs) + + return inner diff --git a/backend/src/apps/users/routes.py b/backend/src/apps/users/routes.py index 6bfdc85..e731c28 100644 --- a/backend/src/apps/users/routes.py +++ b/backend/src/apps/users/routes.py @@ -1,16 +1,16 @@ from fastapi import APIRouter, Body, Request, Depends from fastapi.responses import JSONResponse -from fastapi_authtools import login_required -from fastapi_authtools.exceptions import raise_invalid_credentials from tortoise.exceptions import IntegrityError from .dependencies import get_oauth_provider from .models import User, ActivationCode from src.services.oauth import Providers +from .permissions import login_required from .schemas import UserRegister, UserCustomModel, UserLogin, ActivationCodeScheme from src.core.config import get_app_settings from .tasks import send_email_task from ...services.email import build_activation_email +from ...services.jwt import encode_jwt_token router = APIRouter(prefix='/auth', tags=["Authentication"]) @@ -32,7 +32,7 @@ async def provider_callback_view(request: Request, code: str, provider=Depends(g except IntegrityError: user = await User.get(email=user_data['email'], username=user_data['username']) user_model = UserCustomModel(**user.as_dict()) - access_token = request.app.state.auth_manager.create_token(user_model) + access_token = encode_jwt_token(user_id=user_model.id) return {"access_token": access_token} @@ -42,14 +42,21 @@ async def get_token_view(request: Request, user_token_data: UserLogin = Body()): user, password_correct = await User.login( **user_token_data.model_dump(exclude={"login"}) ) - print(await User.all().values_list("email", flat=True)) - print(user_token_data.model_dump()) - print(123123, await User.get_or_none(email=user_token_data.login)) + if user is None: + return JSONResponse( + {"detail": "User does not exists."}, + status_code=404 + ) + if not password_correct: + return JSONResponse( + {"detail": "Password is invalid."}, + status_code=400 + ) if not user.is_active: activation_code = await ActivationCode.create_activation_code(user=user) send_email_task.apply_async( kwargs={ - "body": build_activation_email(activation_code), + "body": build_activation_email(activation_code.code), "to_addrs": [user.email], "subject": "Activation", } @@ -58,13 +65,8 @@ async def get_token_view(request: Request, user_token_data: UserLogin = Body()): {"detail": "Activation required check you email."}, status_code=403 ) - if not password_correct: - return JSONResponse( - {"detail": "Password is invalid."}, - status_code=400 - ) user_model = UserCustomModel(**user.as_dict()) - token = request.app.state.auth_manager.create_token(user_model) + token = encode_jwt_token(user_id=user_model.id) return {"access_token": token} @@ -77,7 +79,7 @@ async def register_view(request: Request, user_data: UserRegister = Body()): activation_code = await ActivationCode.create_activation_code(user=new_user) send_email_task.apply_async( kwargs={ - "body": build_activation_email(activation_code), + "body": build_activation_email(activation_code.code), "to_addrs": [new_user.email], "subject": "Activation", } diff --git a/backend/src/apps/users/schemas.py b/backend/src/apps/users/schemas.py index ba8197b..ee84138 100644 --- a/backend/src/apps/users/schemas.py +++ b/backend/src/apps/users/schemas.py @@ -1,6 +1,6 @@ from datetime import datetime -from pydantic import BaseModel, EmailStr, Field +from pydantic import BaseModel, EmailStr class UserCustomModel(BaseModel): diff --git a/backend/src/apps/users/tasks.py b/backend/src/apps/users/tasks.py index 36a7607..ca6ed57 100644 --- a/backend/src/apps/users/tasks.py +++ b/backend/src/apps/users/tasks.py @@ -1,6 +1,6 @@ from email.message import EmailMessage -from src.core.celery import app +from src.core.worker import app from ...core.config import get_app_settings from ...services.email import EmailServer diff --git a/backend/src/core/app.py b/backend/src/core/app.py index a29c473..6f24e6c 100644 --- a/backend/src/core/app.py +++ b/backend/src/core/app.py @@ -1,16 +1,17 @@ +import logging +import sys + from fastapi import FastAPI -from fastapi_authtools import AuthManager +from starlette.middleware.authentication import AuthenticationMiddleware from starlette.staticfiles import StaticFiles from src.apps import __routers__ from src.core.config import get_app_settings -from src.apps.users.schemas import UserCustomModel from src.core.middleware.time import process_time_middleware from .middleware.cors import use_cors_middleware -from .middleware.authentication import use_authentication_middleware -from src.db.events import create_superuser, register_mongodb_db, register_db, authentication_user_getter +from .middleware.authentication import AuthenticationBackend +from src.db.events import create_superuser, register_db from src.db.redis import create_redis_client -from src.data.load_data import load_all_data, load_all_data_mongo class Application: @@ -41,28 +42,27 @@ def _configurate_app(self) -> None: self.app.add_event_handler(event_type="startup", func=self._on_startup_event) self.app.add_event_handler(event_type="shutdown", func=self._on_shutdown_event) self.app.state.SECRET_KEY = self.settings.SECRET_KEY - - # auth manager settings - self.app.state.auth_manager = AuthManager( - app=self.app, - use_cookies=False, - user_model=UserCustomModel, - algorithm=self.settings.ALGORITHM, - secret_key=self.settings.SECRET_KEY, - expire_minutes=self.settings.EXPIRE_MINUTES, - user_getter=authentication_user_getter, + fmt = logging.Formatter( + fmt="%(asctime)s - %(name)s:%(lineno)d - %(levelname)s - %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", ) + sh = logging.StreamHandler(sys.stdout) + sh.setLevel(getattr(logging, "DEBUG")) + sh.setFormatter(fmt) + logger_db_client = logging.getLogger("tortoise.db_client") + logger_db_client.setLevel(getattr(logging, "DEBUG")) + logger_db_client.addHandler(sh) + # auth manager settings self.app.mount("/static", StaticFiles(directory="src/public/static/"), name="static") self.app.state.STATIC_DIR = "src/public/static/" def _configurate_middleware(self) -> None: use_cors_middleware(self.app) - use_authentication_middleware(self.app) + self.app.add_middleware(AuthenticationMiddleware, backend=AuthenticationBackend()) self.app.middleware("http")(process_time_middleware) def _configurate_db(self) -> None: """Configurate database.""" - self.app.state.mongodb_db = register_mongodb_db(self.settings.MONGODB_URL, self.settings.MONGODB_NAME) self.app.state.redis = create_redis_client(self.settings.REDIS_URL) register_db( app=self.app, @@ -86,8 +86,7 @@ def _configure_services(self): async def _load_data(self): """Data loading function.""" await create_superuser(settings=self.settings) - await load_all_data() - await load_all_data_mongo(self.app.state.mongodb_db) + # await load_all_data() async def _on_startup_event(self): """Startup handler.""" diff --git a/backend/src/core/middleware/authentication.py b/backend/src/core/middleware/authentication.py index fb02307..7f75c60 100644 --- a/backend/src/core/middleware/authentication.py +++ b/backend/src/core/middleware/authentication.py @@ -1,6 +1,4 @@ -from fastapi import FastAPI from starlette.middleware import authentication -from starlette.middleware.authentication import AuthenticationMiddleware from starlette.requests import HTTPConnection from src.apps.users.models import User @@ -31,7 +29,7 @@ async def verify_token(token: str | None) -> tuple[list, RequestUser]: return scopes, user @staticmethod - def get_token_ws(conn: HTTPConnection) -> str: + def get_token_ws(conn: HTTPConnection) -> str | None: """Get token from current http connection.""" query_token = conn.query_params.get("authorization") if query_token is None: @@ -58,7 +56,3 @@ async def authenticate( response = await self.verify_token(token) scopes, user = response return authentication.AuthCredentials(scopes=scopes), user - - -def use_authentication_middleware(app: FastAPI) -> None: - app.add_middleware(AuthenticationMiddleware, backend=AuthenticationBackend()) diff --git a/backend/src/core/middleware/time.py b/backend/src/core/middleware/time.py index ed19564..a9089f2 100644 --- a/backend/src/core/middleware/time.py +++ b/backend/src/core/middleware/time.py @@ -5,6 +5,5 @@ async def process_time_middleware(request: Request, call_next) -> Response: start_time = time.time() response = await call_next(request) - finished_time = time.time() - response.headers['X-Process-Time'] = str(finished_time - start_time) + response.headers['X-Process-Time'] = str(time.time() - start_time) return response diff --git a/backend/src/core/settings/base.py b/backend/src/core/settings/base.py index 9fb1fe3..f8d0be1 100644 --- a/backend/src/core/settings/base.py +++ b/backend/src/core/settings/base.py @@ -1,8 +1,8 @@ from pydantic_settings import BaseSettings -from enum import StrEnum +from enum import Enum -class AppEnvTypes(StrEnum): +class AppEnvTypes(Enum): prod = "prod" dev = "dev" test = "test" @@ -37,6 +37,8 @@ class BaseAppSettings(BaseSettings): MONGODB_URL: str MONGODB_NAME: str + BASE_URL: str + @property def REDIS_URL(self) -> str: return f"redis://{self.REDIS_HOST}:{self.REDIS_PORT}" diff --git a/backend/src/core/celery.py b/backend/src/core/worker.py similarity index 100% rename from backend/src/core/celery.py rename to backend/src/core/worker.py diff --git a/backend/src/db/events.py b/backend/src/db/events.py index 6bfa623..992b755 100644 --- a/backend/src/db/events.py +++ b/backend/src/db/events.py @@ -1,7 +1,7 @@ from fastapi import FastAPI from tortoise.contrib.fastapi import register_tortoise from tortoise.exceptions import IntegrityError -from motor.motor_asyncio import AsyncIOMotorClient +# from motor.motor_asyncio import AsyncIOMotorClient from src.apps.users.models import User from src.services.password import hash_password @@ -42,7 +42,6 @@ async def create_superuser(settings) -> None: except IntegrityError: pass - -def register_mongodb_db(db_url: str, db_name: str): - client = AsyncIOMotorClient(db_url) - return getattr(client, db_name) +# def register_mongodb_db(db_url: str, db_name: str): +# client = AsyncIOMotorClient(db_url) +# return getattr(client, db_name) diff --git a/backend/src/db/migrations/README b/backend/src/db/migrations/README deleted file mode 100644 index e0d0858..0000000 --- a/backend/src/db/migrations/README +++ /dev/null @@ -1 +0,0 @@ -Generic single-database configuration with an async dbapi. \ No newline at end of file diff --git a/backend/src/db/migrations/__init__.py b/backend/src/db/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/src/db/migrations/env.py b/backend/src/db/migrations/env.py deleted file mode 100644 index 9c4999e..0000000 --- a/backend/src/db/migrations/env.py +++ /dev/null @@ -1,95 +0,0 @@ -import asyncio -from logging.config import fileConfig - -from sqlalchemy import pool -from sqlalchemy.engine import Connection -from sqlalchemy.ext.asyncio import async_engine_from_config - -from alembic import context - -from app.core.config import get_app_settings -from app.db import Base -from app.db.models import User, Science, Category, Formula, History # noqa: F401 - - -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. -config = context.config -config.set_main_option("db_uri", get_app_settings().db_uri) - -# Interpret the config file for Python logging. -# This line sets up loggers basically. -if config.config_file_name is not None: - fileConfig(config.config_file_name) - -# add your model's MetaData object here -# for 'autogenerate' support -# from myapp import mymodel -# target_metadata = mymodel.Base.metadata -target_metadata = Base.metadata - -# other values from the config, defined by the needs of env.py, -# can be acquired: -# my_important_option = config.get_main_option("my_important_option") -# ... etc. - - -def run_migrations_offline() -> None: - """Run migrations in 'offline' mode. - - This configures the context with just a URL - and not an Engine, though an Engine is acceptable - here as well. By skipping the Engine creation - we don't even need a DBAPI to be available. - - Calls to context.execute() here emit the given string to the - script output. - - """ - url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, - target_metadata=target_metadata, - literal_binds=True, - dialect_opts={"paramstyle": "named"}, - ) - - with context.begin_transaction(): - context.run_migrations() - - -def do_run_migrations(connection: Connection) -> None: - context.configure(connection=connection, target_metadata=target_metadata) - - with context.begin_transaction(): - context.run_migrations() - - -async def run_async_migrations() -> None: - """In this scenario we need to create an Engine - and associate a connection with the context. - - """ - - connectable = async_engine_from_config( - config.get_section(config.config_ini_section, {}), - prefix="sqlalchemy.", - poolclass=pool.NullPool, - ) - - async with connectable.connect() as connection: - await connection.run_sync(do_run_migrations) - - await connectable.dispose() - - -def run_migrations_online() -> None: - """Run migrations in 'online' mode.""" - - asyncio.run(run_async_migrations()) - - -if context.is_offline_mode(): - run_migrations_offline() -else: - run_migrations_online() diff --git a/backend/src/db/migrations/script.py.mako b/backend/src/db/migrations/script.py.mako deleted file mode 100644 index 55df286..0000000 --- a/backend/src/db/migrations/script.py.mako +++ /dev/null @@ -1,24 +0,0 @@ -"""${message} - -Revision ID: ${up_revision} -Revises: ${down_revision | comma,n} -Create Date: ${create_date} - -""" -from alembic import op -import sqlalchemy as sa -${imports if imports else ""} - -# revision identifiers, used by Alembic. -revision = ${repr(up_revision)} -down_revision = ${repr(down_revision)} -branch_labels = ${repr(branch_labels)} -depends_on = ${repr(depends_on)} - - -def upgrade() -> None: - ${upgrades if upgrades else "pass"} - - -def downgrade() -> None: - ${downgrades if downgrades else "pass"} diff --git a/backend/src/db/migrations/versions/273df632e57f_.py b/backend/src/db/migrations/versions/273df632e57f_.py deleted file mode 100644 index df3c280..0000000 --- a/backend/src/db/migrations/versions/273df632e57f_.py +++ /dev/null @@ -1,28 +0,0 @@ -"""empty message - -Revision ID: 273df632e57f -Revises: f33e63753166 -Create Date: 2023-07-23 13:36:53.898680 - -""" -from alembic import op -import sqlalchemy as sa # noqa: F401 - - -# revision identifiers, used by Alembic. -revision = '273df632e57f' -down_revision = 'f33e63753166' -branch_labels = None -depends_on = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.create_foreign_key(None, 'problems', 'solutions', ['solution_id'], ['id'], ondelete='SET NULL') - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint(None, 'problems', type_='foreignkey') - # ### end Alembic commands ### diff --git a/backend/src/db/migrations/versions/5256d9890caa_.py b/backend/src/db/migrations/versions/5256d9890caa_.py deleted file mode 100644 index bc1b3e0..0000000 --- a/backend/src/db/migrations/versions/5256d9890caa_.py +++ /dev/null @@ -1,28 +0,0 @@ -"""empty message - -Revision ID: 5256d9890caa -Revises: 273df632e57f -Create Date: 2023-07-27 23:28:38.867470 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '5256d9890caa' -down_revision = '273df632e57f' -branch_labels = None -depends_on = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('users', sa.Column('is_active', sa.Boolean(), nullable=True)) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('users', 'is_active') - # ### end Alembic commands ### diff --git a/backend/src/db/migrations/versions/__init__.py b/backend/src/db/migrations/versions/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/src/db/migrations/versions/f33e63753166_.py b/backend/src/db/migrations/versions/f33e63753166_.py deleted file mode 100644 index 253299e..0000000 --- a/backend/src/db/migrations/versions/f33e63753166_.py +++ /dev/null @@ -1,147 +0,0 @@ -"""empty message - -Revision ID: f33e63753166 -Revises: -Create Date: 2023-07-23 13:23:03.953111 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = 'f33e63753166' -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.create_table( - 'users', - sa.Column('username', sa.String(length=40), nullable=True), - sa.Column('email', sa.String(length=40), nullable=True), - sa.Column('password', sa.String(length=100), nullable=True), - sa.Column('last_login', sa.String(length=50), nullable=True), - sa.Column('joined', sa.String(length=50), nullable=True), - sa.Column('is_stuff', sa.Boolean(), nullable=True), - sa.Column('is_superuser', sa.Boolean(), nullable=True), - sa.Column('id', sa.String(length=100), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('email'), - sa.UniqueConstraint('username') - ) - op.create_table( - 'sciences', - sa.Column('title', sa.String(length=40), nullable=True), - sa.Column('content', sa.Text(), nullable=True), - sa.Column('image_path', sa.String(length=100), nullable=True), - sa.Column('slug', sa.String(length=40), nullable=True), - sa.Column('id', sa.String(length=100), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('slug'), - sa.UniqueConstraint('title') - ) - op.create_table( - 'categories', - sa.Column('title', sa.String(length=40), nullable=True), - sa.Column('content', sa.Text(), nullable=True), - sa.Column('image_path', sa.String(length=100), nullable=True), - sa.Column('science_id', sa.String(length=100), nullable=True), - sa.Column('slug', sa.String(length=40), nullable=True), - sa.Column('id', sa.String(length=100), nullable=False), - sa.ForeignKeyConstraint(['science_id'], ['sciences.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('slug'), - sa.UniqueConstraint('title') - ) - op.create_table( - 'formulas', - sa.Column('title', sa.String(length=40), nullable=True), - sa.Column('formula', sa.String(length=40), nullable=True), - sa.Column('content', sa.Text(), nullable=True), - sa.Column('image_path', sa.String(length=100), nullable=True), - sa.Column('category_id', sa.String(length=100), nullable=True), - sa.Column('slug', sa.String(length=40), nullable=True), - sa.Column('id', sa.String(length=100), nullable=False), - sa.ForeignKeyConstraint(['category_id'], ['categories.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('slug'), - sa.UniqueConstraint('title') - ) - op.create_table( - 'problems', - sa.Column('title', sa.String(length=100), nullable=True), - sa.Column('text', sa.Text(), nullable=True), - sa.Column('time_asked', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.Column('time_answered', sa.DateTime(timezone=True), nullable=True), - sa.Column('is_solved', sa.Boolean(), nullable=True), - # sa.Column('solution_id', sa.String(length=100), nullable=True), - sa.Column('science_id', sa.String(length=100), nullable=True), - sa.Column('user_id', sa.String(length=100), nullable=True), - sa.Column('id', sa.String(length=100), nullable=False), - sa.ForeignKeyConstraint(['science_id'], ['sciences.id'], ondelete='CASCADE'), - # sa.ForeignKeyConstraint(['solution_id'], ['solutions.id'], ondelete='SET NULL'), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), - sa.PrimaryKeyConstraint('id') - ) - op.create_table( - 'problemmedias', - sa.Column('problem_id', sa.String(length=100), nullable=True), - sa.Column('media_path', sa.String(length=255), nullable=True), - sa.Column('id', sa.String(length=100), nullable=False), - sa.ForeignKeyConstraint(['problem_id'], ['problems.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_table( - 'solutions', - sa.Column('author_id', sa.String(length=100), nullable=True), - sa.Column('problem_id', sa.String(length=100), nullable=True), - sa.Column('text', sa.Text(), nullable=True), - sa.Column('time_created', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.Column('id', sa.String(length=100), nullable=False), - sa.ForeignKeyConstraint(['author_id'], ['users.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['problem_id'], ['problems.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_table( - 'solutionmedias', - sa.Column('solution_id', sa.String(length=100), nullable=True), - sa.Column('media_path', sa.String(length=255), nullable=True), - sa.Column('id', sa.String(length=100), nullable=False), - sa.ForeignKeyConstraint(['solution_id'], ['solutions.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_table( - 'history', - sa.Column('formula_id', sa.String(length=100), nullable=True), - sa.Column('result', sa.String(length=100), nullable=True), - sa.Column('formula_url', sa.String(length=50), nullable=True), - sa.Column('date_time', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.Column('user_id', sa.String(length=100), nullable=True), - sa.Column('id', sa.String(length=100), nullable=False), - sa.ForeignKeyConstraint(['formula_id'], ['formulas.id'], ), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), - sa.PrimaryKeyConstraint('id') - ) - op.add_column( - 'problems', - sa.Column("solution_id", sa.String(200), nullable=True) - ) - - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('history') - op.drop_table('formulas') - op.drop_table('solutionmedias') - op.drop_table('problemmedias') - op.drop_table('categories') - op.drop_table('users') - op.drop_table('solutions') - op.drop_table('sciences') - op.drop_table('problems') - # ### end Alembic commands ### diff --git a/backend/src/public/static/files/plots/122199262.png b/backend/src/public/static/files/plots/122199262.png new file mode 100644 index 0000000..9d7b736 Binary files /dev/null and b/backend/src/public/static/files/plots/122199262.png differ diff --git a/backend/src/public/static/files/plots/e1632a02-948b-4fff-909c-1280395bf5e8.png b/backend/src/public/static/files/plots/e1632a02-948b-4fff-909c-1280395bf5e8.png new file mode 100644 index 0000000..deb2972 Binary files /dev/null and b/backend/src/public/static/files/plots/e1632a02-948b-4fff-909c-1280395bf5e8.png differ diff --git a/backend/src/public/static/sciences/images/categories/123.webp b/backend/src/public/static/sciences/images/categories/123.webp new file mode 100644 index 0000000..bcdb161 Binary files /dev/null and b/backend/src/public/static/sciences/images/categories/123.webp differ diff --git a/backend/src/public/static/sciences/images/categories/51588.jpg b/backend/src/public/static/sciences/images/categories/51588.jpg new file mode 100644 index 0000000..3cc75d4 Binary files /dev/null and b/backend/src/public/static/sciences/images/categories/51588.jpg differ diff --git a/backend/src/public/static/sciences/images/categories/graphics.png b/backend/src/public/static/sciences/images/categories/graphics.png new file mode 100644 index 0000000..3c536bc Binary files /dev/null and b/backend/src/public/static/sciences/images/categories/graphics.png differ diff --git a/backend/src/public/static/sciences/images/categories/hydrostatics.png b/backend/src/public/static/sciences/images/categories/hydrostatics.png new file mode 100644 index 0000000..2762a44 Binary files /dev/null and b/backend/src/public/static/sciences/images/categories/hydrostatics.png differ diff --git a/backend/src/public/static/sciences/images/categories/kinematics.png b/backend/src/public/static/sciences/images/categories/kinematics.png new file mode 100644 index 0000000..7676c2e Binary files /dev/null and b/backend/src/public/static/sciences/images/categories/kinematics.png differ diff --git a/backend/src/public/static/sciences/images/categories/magnethism.webp b/backend/src/public/static/sciences/images/categories/magnethism.webp new file mode 100644 index 0000000..e9a8ca7 Binary files /dev/null and b/backend/src/public/static/sciences/images/categories/magnethism.webp differ diff --git a/backend/src/public/static/sciences/images/categories/mechanics.png b/backend/src/public/static/sciences/images/categories/mechanics.png new file mode 100644 index 0000000..10f1524 Binary files /dev/null and b/backend/src/public/static/sciences/images/categories/mechanics.png differ diff --git a/backend/src/public/static/sciences/images/categories/molecular.webp b/backend/src/public/static/sciences/images/categories/molecular.webp new file mode 100644 index 0000000..fff570b Binary files /dev/null and b/backend/src/public/static/sciences/images/categories/molecular.webp differ diff --git a/backend/src/public/static/sciences/images/categories/nuclear.png b/backend/src/public/static/sciences/images/categories/nuclear.png new file mode 100644 index 0000000..66c947e Binary files /dev/null and b/backend/src/public/static/sciences/images/categories/nuclear.png differ diff --git a/backend/src/public/static/sciences/images/categories/optics.webp b/backend/src/public/static/sciences/images/categories/optics.webp new file mode 100644 index 0000000..bdc7b9d Binary files /dev/null and b/backend/src/public/static/sciences/images/categories/optics.webp differ diff --git a/backend/src/public/static/sciences/images/categories/oscillation.png b/backend/src/public/static/sciences/images/categories/oscillation.png new file mode 100644 index 0000000..22492ec Binary files /dev/null and b/backend/src/public/static/sciences/images/categories/oscillation.png differ diff --git a/backend/src/public/static/sciences/images/categories/statics.webp b/backend/src/public/static/sciences/images/categories/statics.webp new file mode 100644 index 0000000..a89e32e Binary files /dev/null and b/backend/src/public/static/sciences/images/categories/statics.webp differ diff --git a/backend/src/public/static/sciences/images/categories/thermodynamics.png b/backend/src/public/static/sciences/images/categories/thermodynamics.png new file mode 100644 index 0000000..090c205 Binary files /dev/null and b/backend/src/public/static/sciences/images/categories/thermodynamics.png differ diff --git a/backend/src/public/static/sciences/images/categories/triangles.webp b/backend/src/public/static/sciences/images/categories/triangles.webp new file mode 100644 index 0000000..481121c Binary files /dev/null and b/backend/src/public/static/sciences/images/categories/triangles.webp differ diff --git a/backend/src/public/static/sciences/images/sciences/mathem.png b/backend/src/public/static/sciences/images/sciences/mathem.png index e69de29..5448b26 100644 Binary files a/backend/src/public/static/sciences/images/sciences/mathem.png and b/backend/src/public/static/sciences/images/sciences/mathem.png differ diff --git a/backend/src/public/static/sciences/images/sciences/physics.jpg b/backend/src/public/static/sciences/images/sciences/physics.jpg deleted file mode 100644 index e69de29..0000000 diff --git a/backend/src/public/static/sciences/images/sciences/physics.png b/backend/src/public/static/sciences/images/sciences/physics.png new file mode 100644 index 0000000..8e4944d Binary files /dev/null and b/backend/src/public/static/sciences/images/sciences/physics.png differ diff --git a/backend/src/services/formulas/counter.py b/backend/src/services/formulas/counter.py index 4392c7f..5bdff78 100644 --- a/backend/src/services/formulas/counter.py +++ b/backend/src/services/formulas/counter.py @@ -176,6 +176,7 @@ def count_result(request: RequestSchema, formula_obj: Formula): result = formula_obj.match( **dict(zip(find_args, nums * si)) )[0] + print(result, nums_comma) result = round(float(result), nums_comma) except (SyntaxError, NameError): diff --git a/backend/src/services/tables/__init__.py b/backend/src/services/tables/__init__.py index 2f0919d..6c81366 100644 --- a/backend/src/services/tables/__init__.py +++ b/backend/src/services/tables/__init__.py @@ -1 +1,3 @@ from .tables import CsvTableManager, PandasTableManager + +__all__ = ("CsvTableManager", "PandasTableManager") diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c74db02..9980b54 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -3253,8 +3253,7 @@ "commander": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "dev": true + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==" }, "commondir": { "version": "1.0.1", @@ -5508,6 +5507,14 @@ "universalify": "^2.0.0" } }, + "katex": { + "version": "0.16.9", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.9.tgz", + "integrity": "sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ==", + "requires": { + "commander": "^8.3.0" + } + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -5867,9 +5874,9 @@ } }, "mathjax": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/mathjax/-/mathjax-3.2.2.tgz", - "integrity": "sha512-Bt+SSVU8eBG27zChVewOicYs7Xsdt40qm4+UpHyX7k0/O9NliPc+x77k1/FEsPsjKPZGJvtRZM1vO+geW0OhGw==" + "version": "2.7.9", + "resolved": "https://registry.npmjs.org/mathjax/-/mathjax-2.7.9.tgz", + "integrity": "sha512-NOGEDTIM9+MrsqnjPEjVGNx4q0GQxqm61yQwSK+/5S59i26wId5IC5gNu9/bu8+CCVl5p9G2IHcAl/wJa+5+BQ==" }, "mdn-data": { "version": "2.0.14", @@ -7001,7 +7008,6 @@ "version": "2.8.8", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", - "dev": true, "optional": true }, "pretty-error": { @@ -8380,6 +8386,21 @@ "integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==", "dev": true }, + "vue-katex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/vue-katex/-/vue-katex-0.5.0.tgz", + "integrity": "sha512-KsjSK4ftpw9q8SP1OJbigPOozdthOS46+6GmqkToXZVmmPejBHGGmDUxJ/2UtkyAuHf5dHL+2RvOXi/RV77YOA==", + "requires": { + "deepmerge": "^4.2.2" + }, + "dependencies": { + "deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==" + } + } + }, "vue-loader": { "version": "17.2.2", "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-17.2.2.tgz", @@ -8451,27 +8472,33 @@ "vue": "^2.6.11" }, "dependencies": { + "@babel/parser": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz", + "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==" + }, "@vue/compiler-sfc": { - "version": "2.7.14", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.14.tgz", - "integrity": "sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==", + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.16.tgz", + "integrity": "sha512-KWhJ9k5nXuNtygPU7+t1rX6baZeqOYLEforUPjgNDBnLicfHCoi48H87Q8XyLZOrNNsmhuwKqtpDQWjEFe6Ekg==", "requires": { - "@babel/parser": "^7.18.4", + "@babel/parser": "^7.23.5", "postcss": "^8.4.14", + "prettier": "^1.18.2 || ^2.0.0", "source-map": "^0.6.1" } }, "csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "vue": { - "version": "2.7.14", - "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.14.tgz", - "integrity": "sha512-b2qkFyOM0kwqWFuQmgd4o+uHGU7T+2z3T+WQp8UBjADfEv2n4FEMffzBmCKNP0IGzOEEfYjvtcC62xaSKeQDrQ==", + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.16.tgz", + "integrity": "sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==", "requires": { - "@vue/compiler-sfc": "2.7.14", + "@vue/compiler-sfc": "2.7.16", "csstype": "^3.1.0" } } diff --git a/frontend/package.json b/frontend/package.json index 1b8b246..f570287 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,9 +11,11 @@ "@tailwindcss/forms": "^0.5.7", "axios": "^1.4.0", "core-js": "^3.8.3", - "mathjax": "^3.2.2", + "katex": "^0.16.9", + "mathjax": "^2.7.9", "tailwindcss": "^3.4.1", "vue": "^3.2.26", + "vue-katex": "^0.5.0", "vue-mathjax": "^0.1.1", "vue-router": "^4.0.3", "vue-tailwind": "^2.5.1" diff --git a/frontend/public/index.html b/frontend/public/index.html index e9173bd..e4120d9 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -8,16 +8,15 @@ Сайт для вычислений - - + + - +
- diff --git a/frontend/src/App.vue b/frontend/src/App.vue index d5208be..d81e786 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -6,14 +6,13 @@ export default { name: "AppItem", components: { Footer, - }, data() { return { publicPath: process.env.BASE_URL, - editMeUrl: 'https://github.com/michael7nightingale/FastAPI-Science', - + githubUrl: 'https://github.com/michael7nightingale/FastAPI-Science', + menuOpened: false } }, @@ -28,18 +27,32 @@ export default { logoutUser() window.location.reload(); }, - githubOpen() { - window.open(this.editMeUrl, "_blank"); - }, openMobileMenu() { - const menuElement = document.getElementById("mobile-nav"); - if (menuElement.style.display === 'none') { - menuElement.style.display = 'block'; - } else { - menuElement.style.display = 'none'; + this.menuOpened = !this.menuOpened; + }, + checkLogin(){ + let user = getUser(); + if (!user){ + window.location = this.$router.resolve({name: "login"}).fullPath; } } }, + watch: { + // eslint-disable-next-line no-unused-vars + $route(to, from) { + this.menuOpened = false; + document.title = to.meta.title ? `${to.meta.title} | Сайт для вычислений` : 'Сайт для вычислений' + const description = document.querySelector('meta[name="description"]') + if (description) { + description.setAttribute("content", to.meta.description ? `${to.meta.description} | Сайт для вычислений` : 'Сайт для вычислений') + } + let loginRequired = to.loginRequired || false; + if (loginRequired){ + this.checkLogin(); + } + } + } + } @@ -47,7 +60,7 @@ export default {