Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Version control and local editor state
.git
.github
.vscode

# Local environment files (examples remain available to the build context)
.env
.env.*
!.env.example
!**/.env.example

# JavaScript dependencies and build output
**/node_modules
**/dist
.pnpm-store
test-results
playwright-report
blob-report
**/playwright/.cache

# Python environments and caches
.venv
**/__pycache__
**/*.pyc
**/.mypy_cache
**/.ruff_cache
**/.pytest_cache
**/.coverage
**/htmlcov
**/*.egg-info

# Local tooling and generated caches
.agents
.claude
.codex
.tanstack
.uv-cache
42 changes: 42 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Copy this file to .env for local development.
# Replace every "changethis" value with a strong, unique secret.

# Domain
DOMAIN=localhost

# Frontend
FRONTEND_HOST=http://localhost:5173

# Environment: local, staging, production
ENVIRONMENT=local

PROJECT_NAME="Bavly"
STACK_NAME=bavly

# Backend
BACKEND_CORS_ORIGINS="http://localhost,http://localhost:5173,https://localhost,https://localhost:5173,http://localhost.tiangolo.com"
SECRET_KEY=changethis
FIRST_SUPERUSER=admin@example.com
FIRST_SUPERUSER_PASSWORD=changethis

# Emails
SMTP_HOST=
SMTP_USER=
SMTP_PASSWORD=
EMAILS_FROM_EMAIL=info@example.com
SMTP_TLS=True
SMTP_SSL=False
SMTP_PORT=587

# Postgres
POSTGRES_SERVER=localhost
POSTGRES_PORT=5432
POSTGRES_DB=app
POSTGRES_USER=postgres
POSTGRES_PASSWORD=changethis

SENTRY_DSN=

# Local Docker images
DOCKER_IMAGE_BACKEND=backend
DOCKER_IMAGE_FRONTEND=frontend
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Replace items with lessons

Revision ID: a7f3c9d2e841
Revises: fe56fa70289e
"""

from collections.abc import Sequence

import sqlalchemy as sa
from alembic import op

revision: str = "a7f3c9d2e841"
down_revision: str | None = "fe56fa70289e"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
op.rename_table("item", "lesson")
# Template items cannot be meaningfully mapped to lesson language settings.
op.execute("DELETE FROM lesson")
op.drop_column("lesson", "title")
op.drop_column("lesson", "description")
op.add_column(
"lesson", sa.Column("target_language", sa.String(length=255), nullable=False)
)
op.add_column(
"lesson", sa.Column("native_language", sa.String(length=255), nullable=False)
)
op.add_column("lesson", sa.Column("prompt", sa.String(length=4000), nullable=False))


def downgrade() -> None:
op.drop_column("lesson", "prompt")
op.drop_column("lesson", "native_language")
op.drop_column("lesson", "target_language")
op.add_column(
"lesson", sa.Column("description", sa.String(length=255), nullable=True)
)
op.add_column(
"lesson",
sa.Column("title", sa.String(length=255), nullable=False, server_default="Lesson"),
)
op.alter_column("lesson", "title", server_default=None)
op.rename_table("lesson", "item")
4 changes: 2 additions & 2 deletions backend/app/api/main.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from fastapi import APIRouter

from app.api.routes import items, login, private, users, utils
from app.api.routes import lessons, login, private, users, utils
from app.core.config import settings

api_router = APIRouter()
api_router.include_router(login.router)
api_router.include_router(users.router)
api_router.include_router(utils.router)
api_router.include_router(items.router)
api_router.include_router(lessons.router)


if settings.ENVIRONMENT == "local":
Expand Down
113 changes: 0 additions & 113 deletions backend/app/api/routes/items.py

This file was deleted.

123 changes: 123 additions & 0 deletions backend/app/api/routes/lessons.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import uuid
from typing import Any

from fastapi import APIRouter, HTTPException
from sqlmodel import col, func, select

from app.api.deps import CurrentUser, SessionDep
from app.models import (
Lesson,
LessonCreate,
LessonPublic,
LessonsPublic,
LessonUpdate,
Message,
)

router = APIRouter(prefix="/lessons", tags=["lessons"])


@router.get("/", response_model=LessonsPublic)
def read_lessons(
session: SessionDep, current_user: CurrentUser, skip: int = 0, limit: int = 100
) -> Any:
"""
Retrieve lessons.
"""

if current_user.is_superuser:
count_statement = select(func.count()).select_from(Lesson)
count = session.exec(count_statement).one()
statement = (
select(Lesson)
.order_by(col(Lesson.created_at).desc())
.offset(skip)
.limit(limit)
)
lessons = session.exec(statement).all()
else:
count_statement = (
select(func.count())
.select_from(Lesson)
.where(Lesson.owner_id == current_user.id)
)
count = session.exec(count_statement).one()
statement = (
select(Lesson)
.where(Lesson.owner_id == current_user.id)
.order_by(col(Lesson.created_at).desc())
.offset(skip)
.limit(limit)
)
lessons = session.exec(statement).all()

lessons_public = [LessonPublic.model_validate(lesson) for lesson in lessons]
return LessonsPublic(data=lessons_public, count=count)


@router.get("/{id}", response_model=LessonPublic)
def read_lesson(session: SessionDep, current_user: CurrentUser, id: uuid.UUID) -> Any:
"""
Get lesson by ID.
"""
lesson = session.get(Lesson, id)
if not lesson:
raise HTTPException(status_code=404, detail="Lesson not found")
if not current_user.is_superuser and (lesson.owner_id != current_user.id):
raise HTTPException(status_code=403, detail="Not enough permissions")
return lesson


@router.post("/", response_model=LessonPublic)
def create_lesson(
*, session: SessionDep, current_user: CurrentUser, lesson_in: LessonCreate
) -> Any:
"""
Create new lesson.
"""
lesson = Lesson.model_validate(lesson_in, update={"owner_id": current_user.id})
session.add(lesson)
session.commit()
session.refresh(lesson)
return lesson


@router.put("/{id}", response_model=LessonPublic)
def update_lesson(
*,
session: SessionDep,
current_user: CurrentUser,
id: uuid.UUID,
lesson_in: LessonUpdate,
) -> Any:
"""
Update a lesson.
"""
lesson = session.get(Lesson, id)
if not lesson:
raise HTTPException(status_code=404, detail="Lesson not found")
if not current_user.is_superuser and (lesson.owner_id != current_user.id):
raise HTTPException(status_code=403, detail="Not enough permissions")
update_dict = lesson_in.model_dump(exclude_unset=True)
lesson.sqlmodel_update(update_dict)
session.add(lesson)
session.commit()
session.refresh(lesson)
return lesson


@router.delete("/{id}")
def delete_lesson(
session: SessionDep, current_user: CurrentUser, id: uuid.UUID
) -> Message:
"""
Delete a lesson.
"""
lesson = session.get(Lesson, id)
if not lesson:
raise HTTPException(status_code=404, detail="Lesson not found")
if not current_user.is_superuser and (lesson.owner_id != current_user.id):
raise HTTPException(status_code=403, detail="Not enough permissions")
session.delete(lesson)
session.commit()
return Message(message="Lesson deleted successfully")
4 changes: 2 additions & 2 deletions backend/app/api/routes/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from app.core.config import settings
from app.core.security import get_password_hash, verify_password
from app.models import (
Item,
Lesson,
Message,
UpdatePassword,
User,
Expand Down Expand Up @@ -225,7 +225,7 @@ def delete_user(
raise HTTPException(
status_code=403, detail="Super users are not allowed to delete themselves"
)
statement = delete(Item).where(col(Item.owner_id) == user_id)
statement = delete(Lesson).where(col(Lesson.owner_id) == user_id)
session.exec(statement)
session.delete(user)
session.commit()
Expand Down
Loading
Loading