diff --git a/.env.sample b/.env.sample index 0572197..023f28f 100644 --- a/.env.sample +++ b/.env.sample @@ -1,3 +1,4 @@ -AMMENTOR_DB_URL= +PRAVESHAN_DB_URL= SMTP_EMAIL= SMTP_PASSWORD= +GOOGLE_SHEET_ID= \ No newline at end of file diff --git a/.github/workflows/ghcr-deploy.yml b/.github/workflows/ghcr-deploy.yml index c305bfd..e3ec95f 100644 --- a/.github/workflows/ghcr-deploy.yml +++ b/.github/workflows/ghcr-deploy.yml @@ -1,55 +1,71 @@ -# Inspired from: https://docs.github.com/en/actions/use-cases-and-examples/publishing-packages/publishing-docker-images -name: Create and publish Docker image to GHCR +# Inspired the workflow from https://faun.pub/full-ci-cd-with-docker-github-actions-digitalocean-droplets-container-registry-db2938db8246 +name: CI -# Configures this workflow to run every time a change is pushed to the branch called `release`. +# 1 +# Controls when the workflow will run on: - workflow_dispatch: + # Triggers the workflow on push events but only for the master branch push: - branches: ['production'] + branches: [ praveshan ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + inputs: + version: + description: 'Image version' + required: true +#2 +env: + REGISTRY: "registry.digitalocean.com/praveshan" + IMAGE_NAME: "ammentor-backend-praveshan" +#3 jobs: - build-and-push-image: + build_and_push: runs-on: ubuntu-latest - # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. - permissions: - contents: read - packages: write - id-token: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 + - name: Checkout the repo + uses: actions/checkout@v2 + + - name: Build container image + run: docker build -t $(echo $REGISTRY)/$(echo $IMAGE_NAME):$(echo $GITHUB_SHA | head -c7) . - # Uses the `docker/login-action` action to log in to the Github Container Registry - - name: Log in to the Container registry - uses: docker/login-action@v3 + - name: Install doctl + uses: digitalocean/action-doctl@v2 with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} + + - name: Log in to DigitalOcean Container Registry with short-lived credentials + run: doctl registry login --expiry-seconds 600 + + - name: Remove all old images + run: if [ ! -z "$(doctl registry repository list | grep "$(echo $IMAGE_NAME)")" ]; then doctl registry repository delete-manifest $(echo $IMAGE_NAME) $(doctl registry repository list-tags $(echo $IMAGE_NAME) | grep -o "sha.*") --force; else echo "No repository"; fi - # This step uses `docker/metadata-action` to extract tags and labels that will be applied to the specified image. - # The `id` "meta" allows the output of this step to be referenced in a subsequent step. - # The `images` value provides the base name for the tags and labels. - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: ghcr.io/amfoss/amMentor-backend - tags: | - # set latest tag for master branch - type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'production') }},priority=2000 - type=schedule,pattern={{date 'YYYYMMDD'}} - type=ref,event=tag - type=ref,event=pr - type=sha - - # This step uses the `docker/build-push-action` action to build the image. If the build succeeds, it pushes the image to GitHub Packages. - - name: Build and push Docker image - id: push - uses: docker/build-push-action@v6 + - name: Push image to DigitalOcean Container Registry + run: docker push $(echo $REGISTRY)/$(echo $IMAGE_NAME):$(echo $GITHUB_SHA | head -c7) + + deploy: + runs-on: ubuntu-latest + needs: build_and_push + + steps: + - name: Deploy to Digital Ocean droplet via SSH action + uses: appleboy/ssh-action@master with: - context: . - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} + host: ${{ secrets.HOST }} + username: ${{ secrets.USERNAME }} + key: ${{ secrets.SSHKEY }} + passphrase: ${{ secrets.PASSPHRASE }} + envs: IMAGE_NAME,REGISTRY,{{ secrets.DIGITALOCEAN_ACCESS_TOKEN }},GITHUB_SHA + script: | + # Login to registry + docker login -u ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} -p ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} registry.digitalocean.com + # Stop running container + docker stop $(echo $IMAGE_NAME) + # Remove old container + docker rm $(echo $IMAGE_NAME) + # Run a new container from a new image + docker run -d \ + --restart always \ + --name $(echo $IMAGE_NAME) \ + $(echo $REGISTRY)/$(echo $IMAGE_NAME):$(echo $GITHUB_SHA | head -c7) \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7c198e3..54b4806 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ dist/ *.bak *.tmp txt.txt +credentials.json diff --git a/alembic/versions/398274d89756_add_start_date_to_submissions.py b/alembic/versions/12599f674ee9_nullable_true_for_group_name.py similarity index 55% rename from alembic/versions/398274d89756_add_start_date_to_submissions.py rename to alembic/versions/12599f674ee9_nullable_true_for_group_name.py index e0801e9..b782dfe 100644 --- a/alembic/versions/398274d89756_add_start_date_to_submissions.py +++ b/alembic/versions/12599f674ee9_nullable_true_for_group_name.py @@ -1,8 +1,8 @@ -"""Add start_date to submissions +"""nullable = true for group name -Revision ID: 398274d89756 -Revises: e0d610d30201 -Create Date: 2025-06-02 18:41:29.709189 +Revision ID: 12599f674ee9 +Revises: 1d2a6bece126 +Create Date: 2025-07-23 01:16:53.822252 """ from typing import Sequence, Union @@ -12,8 +12,8 @@ # revision identifiers, used by Alembic. -revision: str = '398274d89756' -down_revision: Union[str, None] = 'e0d610d30201' +revision: str = '12599f674ee9' +down_revision: Union[str, None] = '1d2a6bece126' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -21,12 +21,16 @@ def upgrade() -> None: """Upgrade schema.""" # ### commands auto generated by Alembic - please adjust! ### - op.add_column('submissions', sa.Column('start_date', sa.DateTime(), nullable=False)) + op.alter_column('users', 'group_name', + existing_type=sa.VARCHAR(), + nullable=True) # ### end Alembic commands ### def downgrade() -> None: """Downgrade schema.""" # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('submissions', 'start_date') + op.alter_column('users', 'group_name', + existing_type=sa.VARCHAR(), + nullable=False) # ### end Alembic commands ### diff --git a/alembic/versions/1d2a6bece126_initial_schema_for_new_branch.py b/alembic/versions/1d2a6bece126_initial_schema_for_new_branch.py new file mode 100644 index 0000000..c408493 --- /dev/null +++ b/alembic/versions/1d2a6bece126_initial_schema_for_new_branch.py @@ -0,0 +1,123 @@ +"""Initial schema for new branch + +Revision ID: 1d2a6bece126 +Revises: +Create Date: 2025-07-23 00:20:39.735170 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '1d2a6bece126' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('otp', + sa.Column('email', sa.String(), nullable=False), + sa.Column('otp', sa.String(), nullable=False), + sa.Column('expires_at', sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint('email') + ) + op.create_index(op.f('ix_otp_email'), 'otp', ['email'], unique=False) + op.create_table('tracks', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('title', sa.String(), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('title') + ) + op.create_index(op.f('ix_tracks_id'), 'tracks', ['id'], unique=False) + op.create_table('users', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('email', sa.String(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('role', sa.String(), nullable=False), + sa.Column('group_name', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True) + op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False) + op.create_table('leaderboard', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('track_id', sa.Integer(), nullable=True), + sa.Column('mentee_id', sa.Integer(), nullable=True), + sa.Column('total_points', sa.Integer(), nullable=True), + sa.Column('tasks_completed', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['mentee_id'], ['users.id'], ), + sa.ForeignKeyConstraint(['track_id'], ['tracks.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_leaderboard_id'), 'leaderboard', ['id'], unique=False) + op.create_table('mentor_mentee_map', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('mentor_id', sa.Integer(), nullable=False), + sa.Column('mentee_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['mentee_id'], ['users.id'], ), + sa.ForeignKeyConstraint(['mentor_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('mentor_id', 'mentee_id', name='unique_mentor_mentee') + ) + op.create_index(op.f('ix_mentor_mentee_map_id'), 'mentor_mentee_map', ['id'], unique=False) + op.create_table('tasks', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('track_id', sa.Integer(), nullable=False), + sa.Column('task_no', sa.Integer(), nullable=False), + sa.Column('title', sa.String(), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('points', sa.Integer(), nullable=True), + sa.Column('deadline_days', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['track_id'], ['tracks.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('track_id', 'task_no', name='unique_track_task') + ) + op.create_index(op.f('ix_tasks_id'), 'tasks', ['id'], unique=False) + op.create_table('submissions', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('mentee_id', sa.Integer(), nullable=False), + sa.Column('task_id', sa.Integer(), nullable=False), + sa.Column('task_name', sa.String(), nullable=False), + sa.Column('task_no', sa.Integer(), nullable=False), + sa.Column('reference_link', sa.Text(), nullable=False), + sa.Column('status', sa.String(), nullable=True), + sa.Column('submitted_at', sa.DateTime(), nullable=True), + sa.Column('start_date', sa.DateTime(), nullable=False), + sa.Column('approved_at', sa.DateTime(), nullable=True), + sa.Column('mentor_feedback', sa.Text(), nullable=True), + sa.Column('submitted_late', sa.Boolean(), nullable=True), + sa.Column('commit_hash', sa.String(), nullable=False), + sa.ForeignKeyConstraint(['mentee_id'], ['users.id'], ), + sa.ForeignKeyConstraint(['task_id'], ['tasks.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_submissions_id'), 'submissions', ['id'], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_submissions_id'), table_name='submissions') + op.drop_table('submissions') + op.drop_index(op.f('ix_tasks_id'), table_name='tasks') + op.drop_table('tasks') + op.drop_index(op.f('ix_mentor_mentee_map_id'), table_name='mentor_mentee_map') + op.drop_table('mentor_mentee_map') + op.drop_index(op.f('ix_leaderboard_id'), table_name='leaderboard') + op.drop_table('leaderboard') + op.drop_index(op.f('ix_users_id'), table_name='users') + op.drop_index(op.f('ix_users_email'), table_name='users') + op.drop_table('users') + op.drop_index(op.f('ix_tracks_id'), table_name='tracks') + op.drop_table('tracks') + op.drop_index(op.f('ix_otp_email'), table_name='otp') + op.drop_table('otp') + # ### end Alembic commands ### diff --git a/alembic/versions/71c51601b989_add_testing_column_to_tracks.py b/alembic/versions/71c51601b989_add_testing_column_to_tracks.py deleted file mode 100644 index d279e76..0000000 --- a/alembic/versions/71c51601b989_add_testing_column_to_tracks.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Add testing column to tracks - -Revision ID: 71c51601b989 -Revises: -Create Date: 2025-05-28 21:36:08.247870 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '71c51601b989' -down_revision: Union[str, None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('tracks', sa.Column('testing', sa.Text(), nullable=True)) - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('tracks', 'testing') - # ### end Alembic commands ### diff --git a/alembic/versions/786378e6e511_add_task_name_and_task_no_to_submissions.py b/alembic/versions/786378e6e511_add_task_name_and_task_no_to_submissions.py deleted file mode 100644 index db2b622..0000000 --- a/alembic/versions/786378e6e511_add_task_name_and_task_no_to_submissions.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Add task_name and task_no to submissions - -Revision ID: 786378e6e511 -Revises: 398274d89756 -Create Date: 2025-06-12 00:37:23.509418 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '786378e6e511' -down_revision: Union[str, None] = '398274d89756' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('submissions', sa.Column('task_name', sa.String(), nullable=False)) - op.add_column('submissions', sa.Column('task_no', sa.Integer(), nullable=False)) - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('submissions', 'task_no') - op.drop_column('submissions', 'task_name') - # ### end Alembic commands ### diff --git a/alembic/versions/e0d610d30201_removed_testing_column_to_tracks.py b/alembic/versions/e0d610d30201_removed_testing_column_to_tracks.py deleted file mode 100644 index 10b5b6c..0000000 --- a/alembic/versions/e0d610d30201_removed_testing_column_to_tracks.py +++ /dev/null @@ -1,32 +0,0 @@ -"""removed testing column to tracks - -Revision ID: e0d610d30201 -Revises: 71c51601b989 -Create Date: 2025-05-28 21:36:49.920817 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'e0d610d30201' -down_revision: Union[str, None] = '71c51601b989' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('tracks', 'testing') - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('tracks', sa.Column('testing', sa.TEXT(), autoincrement=False, nullable=True)) - # ### end Alembic commands ### diff --git a/app/db/crud.py b/app/db/crud.py index f520c2f..1d488a3 100644 --- a/app/db/crud.py +++ b/app/db/crud.py @@ -1,9 +1,22 @@ +import os +import json +import gspread from typing import Optional from app.schemas.submission import SubmissionOut -from sqlalchemy.orm import Session,joinedload +from sqlalchemy.orm import Session from app.db import models -from datetime import datetime, date +from datetime import datetime, date, timedelta from sqlalchemy import func +from app.db.db import SessionLocal + +def _get_gspread_client(): + creds_json_str = os.getenv("GOOGLE_CREDENTIALS_JSON") + if not creds_json_str: + raise ValueError("GOOGLE_CREDENTIALS_JSON environment variable is not set.") + + creds_info = json.loads(creds_json_str) + client = gspread.service_account_from_dict(creds_info) + return client def get_user_by_email(db: Session, email: str): return db.query(models.User).filter(models.User.email == email).first() @@ -11,14 +24,26 @@ def get_user_by_email(db: Session, email: str): def get_task(db: Session, track_id: int, task_no: int): return db.query(models.Task).filter_by(track_id=track_id, task_no=task_no).first() -def submit_task(db: Session, mentee_id: int, task_id: int, reference_link: str, start_date: date): +def submit_task(db: Session, mentee_id: int, task_id: int, reference_link: str, start_date: date, commit_hash: str): existing = db.query(models.Submission).filter_by(mentee_id=mentee_id, task_id=task_id).first() if existing: - return None # Already submitted - + return None + + mentee = db.query(models.User).filter(models.User.id == mentee_id).first() task = db.query(models.Task).filter(models.Task.id == task_id).first() if not task: raise Exception("Task not found") + + start_date = datetime.combine(start_date, datetime.min.time()) + deadline = start_date + timedelta(days=task.deadline_days) + submitted_at = datetime.now() + + if deadline >= submitted_at: + submitted_late = False + elif deadline + timedelta(hours=12) >= submitted_at: + submitted_late = True + else: + return "late submission not allowed" submission = models.Submission( mentee_id=mentee_id, @@ -29,11 +54,35 @@ def submit_task(db: Session, mentee_id: int, task_id: int, reference_link: str, submitted_at=date.today(), status="submitted", start_date=start_date, + submitted_late=submitted_late, + commit_hash=commit_hash ) + client = _get_gspread_client() + sheet_name = "Copy of Praveshan 2025 Master DB" + + if task.track_id == 1: + worksheet_name = "S1 Submissions" + elif task.track_id == 2: + worksheet_name = "S2 Submissions" + else: + worksheet_name = None + + if worksheet_name: + sheet = client.open(sheet_name).worksheet(worksheet_name) + cell = sheet.find(mentee.name) + if not cell: + row = len(sheet.col_values(1)) + 1 + sheet.update_cell(row, 1, mentee.name) + sheet.update_cell(row, task.task_no + 2, commit_hash) + else: + row = cell.row + sheet.update_cell(row, task.task_no + 2, commit_hash) + db.add(submission) db.commit() db.refresh(submission) + return submission def approve_submission(db: Session, submission_id: int, mentor_feedback: str, status: str): @@ -54,7 +103,6 @@ def is_mentor_of(db: Session, mentor_id: int, mentee_id: int): return db.query(models.MentorMenteeMap).filter_by(mentor_id=mentor_id, mentee_id=mentee_id).first() is not None def get_leaderboard_data(db: Session, track_id: int): - return ( db.query( models.User.name, @@ -69,6 +117,7 @@ def get_leaderboard_data(db: Session, track_id: int): .order_by(func.sum(models.Task.points).desc()) .all() ) + def get_otp_by_email(db, email): return db.query(models.OTP).filter(models.OTP.email == email).first() @@ -82,7 +131,6 @@ def create_or_update_otp(db, email, otp, expires_at): db.add(entry) db.commit() - def get_submissions_for_user(db: Session, email: str, track_id: Optional[int] = None) -> list[SubmissionOut]: user = db.query(models.User).filter(models.User.email == email).first() if not user: @@ -112,4 +160,35 @@ def get_submissions_for_user(db: Session, email: str, track_id: Optional[int] = start_date=sub.start_date.date() if sub.start_date else None ) for sub in submissions - ] \ No newline at end of file + ] + +def get_sheet_data(): + client = _get_gspread_client() + sheet_id = os.getenv("GOOGLE_SHEET_ID") + if not sheet_id: + raise ValueError("GOOGLE_SHEET_ID environment variable not set.") + + worksheet = client.open_by_key(sheet_id).worksheet("Form Responses 1") + expected_headers = ["Name", "Email Address"] + data = worksheet.get_all_records(expected_headers=expected_headers) + return data + +def sync_users_from_sheet(): + db: Session = SessionLocal() + try: + rows = get_sheet_data() + inserted_count = 0 + for row in rows: + email = row.get("Email Address", "").strip() + name = row.get("Name", "").strip() + if not email or not name: + continue + if get_user_by_email(db, email): + continue + user = models.User(name=name, email=email, role="mentee") + db.add(user) + inserted_count += 1 + + db.commit() + finally: + db.close() \ No newline at end of file diff --git a/app/db/models.py b/app/db/models.py index a755358..3749eb6 100644 --- a/app/db/models.py +++ b/app/db/models.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, UniqueConstraint +from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, UniqueConstraint, Boolean from sqlalchemy.orm import relationship from datetime import datetime from app.db.db import Base @@ -9,6 +9,7 @@ class User(Base): email = Column(String, unique=True, index=True, nullable=False) name = Column(String, nullable=False) role = Column(String, nullable=False) # "mentor" or "mentee" + group_name = Column(String, nullable=True) class Track(Base): __tablename__ = "tracks" @@ -45,8 +46,10 @@ class Submission(Base): start_date = Column(DateTime, nullable=False) approved_at = Column(DateTime, nullable=True) mentor_feedback = Column(Text, nullable=True) + submitted_late = Column(Boolean, default=False) mentee = relationship("User") task = relationship("Task") + commit_hash = Column(String, nullable=False) class MentorMenteeMap(Base): __tablename__ = "mentor_mentee_map" diff --git a/app/main.py b/app/main.py index 8596063..8bf7b5d 100644 --- a/app/main.py +++ b/app/main.py @@ -1,8 +1,9 @@ from fastapi import FastAPI +from app.db.crud import sync_users_from_sheet from app.db.db import Base, engine from app.routes import auth, progress, tracks, leaderboard, mentors , submissions from fastapi.middleware.cors import CORSMiddleware - +import asyncio app = FastAPI(title="amMentor API") app.add_middleware( @@ -14,9 +15,12 @@ ) @app.on_event("startup") -def on_startup(): - Base.metadata.create_all(bind=engine) - +async def start_background_sync(): + async def loop(): + while True: + sync_users_from_sheet() + await asyncio.sleep(60) + asyncio.create_task(loop()) app.include_router(auth.router, prefix="/auth", tags=["Auth"]) app.include_router(progress.router, prefix="/progress", tags=["Progress"]) diff --git a/app/routes/auth.py b/app/routes/auth.py index 53b97f9..b9c5eba 100644 --- a/app/routes/auth.py +++ b/app/routes/auth.py @@ -77,4 +77,4 @@ def get_user_by_email(email: str, db: Session = Depends(get_db)): "name": user.name, "role": user.role, "total_points": total_points - } \ No newline at end of file + } diff --git a/app/routes/progress.py b/app/routes/progress.py index 0130b93..2411af5 100644 --- a/app/routes/progress.py +++ b/app/routes/progress.py @@ -19,10 +19,11 @@ def submit_task(data: SubmissionCreate, db: Session = Depends(get_db)): raise HTTPException(status_code=404, detail="Task not found") # 3. Submit - submission = crud.submit_task(db, mentee_id=mentee.id, task_id=task.id, reference_link=data.reference_link, start_date=data.start_date) + submission = crud.submit_task(db, mentee_id=mentee.id, task_id=task.id, reference_link=data.reference_link, start_date=data.start_date, commit_hash=data.commit_hash) if not submission: raise HTTPException(status_code=400, detail="Task already submitted") - + if submission == "late submission not allowed": + raise HTTPException(status_code=400, detail="You cannot submit this task as the deadline has passed") return submission @router.patch("/approve-task", response_model=SubmissionOut) diff --git a/app/schemas/submission.py b/app/schemas/submission.py index 53f9472..39999ed 100644 --- a/app/schemas/submission.py +++ b/app/schemas/submission.py @@ -9,6 +9,7 @@ class SubmissionBase(BaseModel): start_date: date class SubmissionCreate(SubmissionBase): mentee_email: str + commit_hash : str class SubmissionOut(BaseModel): @@ -23,6 +24,8 @@ class SubmissionOut(BaseModel): approved_at: Optional[date] = None mentor_feedback: Optional[str] = None start_date: date + submitted_late: bool + commit_hash: str class Config: orm_mode = True diff --git a/docker-compose.yml b/docker-compose.yml index eee26ef..f038fda 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ version: '3.9' services: backend: build: . - container_name: ammentor-backend + container_name: praveshan-ammentor-backend ports: - "8000:8000" env_file: @@ -11,13 +11,15 @@ services: depends_on: - db command: uvicorn app.main:app --host 0.0.0.0 --port 8000 + volumes: + - ./alembic/versions:/app/alembic/versions db: image: postgres:14 - container_name: ammentor-db + container_name: praveshan-ammentor-db restart: always environment: - POSTGRES_DB: ammentor + POSTGRES_DB: praveshan-ammentor POSTGRES_USER: ammentor_user POSTGRES_PASSWORD: strongpassword123 diff --git a/requirements.txt b/requirements.txt index 716f2cf..f170c7c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,4 +21,6 @@ uvloop==0.21.0 watchfiles==1.0.5 websockets==15.0.1 pydantic[email] -alembic \ No newline at end of file +alembic +gspread +oauth2client \ No newline at end of file