Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
1826670
ci(pre-commit): update pre-commit hook
zzjc1234 Nov 15, 2025
c869563
chore(pyproject): add name for prj
zzjc1234 Nov 15, 2025
cbf6543
fix: revert uv lock
zzjc1234 Nov 15, 2025
a460966
fix: pyproject dep and rm deprecated pre-commit hook
A-lexisL Nov 15, 2025
7fab977
fix: rm include rule for ruff check and fix files to pass
A-lexisL Nov 15, 2025
0c07849
fix: rm include rule for ruff check and fix files to pass
A-lexisL Nov 15, 2025
2ecfd1d
chore: change python version back to 3.13
A-lexisL Nov 19, 2025
ed4a584
fix(apps): Delete empty django files
PACHAKUTlQ Nov 20, 2025
60c8920
chore: update python and deps to the latest version
A-lexisL Nov 20, 2025
656ad15
Merge pull request #32 from Tech-JI/ci/pre-commit
PACHAKUTlQ Nov 20, 2025
b2531ed
ci(bot): add feishu bot
zzjc1234 Dec 23, 2025
33b14e3
Merge pull request #35 from Tech-JI/ci/webhook
A-lexisL Dec 23, 2025
b276d91
chore: add logger for auth and web apps
A-lexisL Oct 7, 2025
6d5f8f1
refactor: coursedetail, courselist, review related to class based vie…
A-lexisL Oct 8, 2025
54a81d4
refactor: two hierarchy for reviews, course-relevant or user-releavan…
A-lexisL Oct 9, 2025
6bf01e8
refactor: combine review_form to ReviewSerializer and calculate revie…
A-lexisL Oct 13, 2025
b2a4d24
docs: add api doc for useful endpoint in apps.web
A-lexisL Oct 14, 2025
460b828
chore: update config and lock dep version
A-lexisL Nov 9, 2025
a237322
refactor: rearrange urls under their own apps
A-lexisL Nov 9, 2025
f4a7e42
fix: rebase conflict
A-lexisL Nov 20, 2025
047c2cf
fix: N+1 issue for review votes
A-lexisL Nov 30, 2025
3734879
style: queryset_raw to raw_queryset
A-lexisL Nov 30, 2025
9f877cd
fix: add csrf check for logout signup reset
A-lexisL Dec 4, 2025
79633ba
fix: use annotation for course scores to fix race condition and N+1
A-lexisL Dec 4, 2025
3414439
fix: ReviewManager.with_votes request_user param renamed to vote_user
A-lexisL Dec 4, 2025
92cffb2
chore: add celery dep
A-lexisL Dec 23, 2025
875949a
fix: use serializer for examining input of vote apis
A-lexisL Dec 23, 2025
53ec588
fix: N+1 issue in course offering
A-lexisL Dec 23, 2025
9e5d3fa
chore: rm celery dep
A-lexisL Dec 23, 2025
1d45ff2
chore: change pre-commit to prek
A-lexisL Dec 23, 2025
f3cf237
docs: update env example and docstrings for unused api
A-lexisL Dec 23, 2025
415af54
fix(auth)!: Login user after signup
PACHAKUTlQ Dec 23, 2025
54e3342
fix(auth): Make sure length_step is non-zero
PACHAKUTlQ Dec 23, 2025
68b3ed0
chore: Add ruff as dev dependency
PACHAKUTlQ Dec 23, 2025
e729301
fix(auth): Add CSRF protection for logout api
PACHAKUTlQ Dec 23, 2025
8f851fc
fix(auth): Add CSRF protection for login api
PACHAKUTlQ Dec 23, 2025
cd09a01
feat(auth): Add explicit `@permission_classes([AllowAny])` for clarity
PACHAKUTlQ Dec 23, 2025
20949e2
fix(auth): Use `user.has_usable_password()` to account for users with…
PACHAKUTlQ Dec 23, 2025
79f9c42
docs(auth): Make auth initiate API /api/auth/init, which is consisten…
PACHAKUTlQ Dec 23, 2025
4ba49c3
Revert "chore: Add ruff as dev dependency"
PACHAKUTlQ Dec 23, 2025
e9effe1
fix: rm csrf check for password login
A-lexisL Dec 26, 2025
2b31769
refactor: change reset to reset_password in action list
A-lexisL Dec 26, 2025
e5daa4a
refactor: web cq
A-lexisL Dec 26, 2025
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
10 changes: 7 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,17 @@ QUEST__LOGIN__API_KEY=dummy2
# QUEST__LOGIN__URL=
# QUEST__LOGIN__QUESTIONID=

QUEST__RESET__API_KEY=dummy3
# QUEST__RESET__URL=
# QUEST__RESET__QUESTIONID=
QUEST__RESET_PASSWORD__API_KEY=dummy3
# QUEST__RESET_PASSWORD__URL=
# QUEST__RESET_PASSWORD__QUESTIONID=

# --- Other Overrides (Optional) ---
# Example of overriding a nested value in the AUTH dictionary
# AUTH__OTP_TIMEOUT=60
# Example of overridng web size constraints
# WEB__COURSE__PAGE_SIZE=5
# WEB__REVIEW__PAGE_SIZE=10
# WEB__REVIEW__COMMENT_MIN_LENGTH=30

# Example of overriding a list with a comma-separated string
# ALLOWED_HOSTS=localhost,127.0.0.1,dev.my-app.com
63 changes: 63 additions & 0 deletions .github/workflows/bot.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: feishu bot

on:
branch_protection_rule:
types: [created, deleted]
check_run:
types: [rerequested, completed]
check_suite:
types: [completed]
create:
delete:
deployment_status:
discussion:
types: [created, edited, answered]
discussion_comment:
types: [created, deleted]
fork:
gollum:
issues:
types: [opened, edited, milestoned, pinned, reopened]
issue_comment:
types: [created, deleted]
label:
types: [created, deleted]
merge_group:
types: [checks_requested]
milestone:
types: [opened, deleted]
page_build:
project:
types: [created, deleted, reopened]
project_card:
types: [created, deleted]
project_column:
types: [created, deleted]
public:
pull_request:
branches:
- '*'
types: [opened, reopened]
pull_request_review:
types: [edited, dismissed, submitted]
pull_request_review_comment:
types: [created, edited, deleted]
pull_request_target:
types: [assigned, opened, synchronize, reopened]
push:
branches:
- '*'
registry_package:
types: [published]
release:
types: [published]

jobs:
send-event:
name: Webhook
runs-on: ubuntu-latest
steps:
- uses: junka/feishu-bot-webhook-action@main
with:
webhook: ${{ secrets.FEISHU_BOT_WEBHOOK }}
signkey: ${{ secrets.FEISHU_BOT_SIGNKEY }}
5 changes: 2 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -198,9 +198,9 @@ cython_debug/
.abstra/

# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/

Expand All @@ -217,4 +217,3 @@ __marimo__/

# Streamlit
.streamlit/secrets.toml

23 changes: 17 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
repos:
- repo: local

- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: format-and-add
name: Format code and stage changes
entry: uv run scripts/pre-commit-format.py
language: system
pass_filenames: false
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.5
hooks:
- id: ruff-check
types_or: [python, pyi]
always_run: true
args: ["--fix"]
- id: ruff-format
types_or: [python, pyi]
always_run: true
9 changes: 5 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ help:
@echo " collect - Collects Django static files"
@echo " install-frontend - Installs frontend dependencies using bun"
@echo " format - Formats both backend (Python) and frontend (JS/TS/CSS) code"
@echo " format-backend - Formats Python code using isort and black"
@echo " format-backend - Formats Python code using ruff check and format"
@echo " format-frontend - Formats frontend code using prettier"
@echo " lint - Lints both backend (Python) and frontend (JS/TS/CSS) code"
@echo " lint-backend - Lints Python code using ruff"
Expand Down Expand Up @@ -45,8 +45,9 @@ format: format-backend format-frontend
@echo "All code formatted successfully!"

format-backend:
@echo "Formatting backend (Python) code with isort and black..."
uvx ruff format
@echo "Formatting backend (Python) code with ruff check and format..."
uv run ruff check --select I . --fix && \
uv run ruff format

format-frontend:
@echo "Formatting frontend code with prettier..."
Expand All @@ -57,7 +58,7 @@ lint: lint-backend lint-frontend

lint-backend: format-backend
@echo "Linting backend (Python) code with ruff..."
uvx ruff check
uv run ruff check

lint-frontend: format-frontend
@echo "Linting frontend code with eslint..."
Expand Down
3 changes: 0 additions & 3 deletions apps/auth/admin.py

This file was deleted.

3 changes: 0 additions & 3 deletions apps/auth/models.py

This file was deleted.

3 changes: 0 additions & 3 deletions apps/auth/tests.py

This file was deleted.

16 changes: 16 additions & 0 deletions apps/auth/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django.urls import re_path

from apps.auth import views as auth_views

urlpatterns = [
re_path(r"^init/$", auth_views.auth_initiate_api, name="auth_initiate_api"),
re_path(r"^verify/$", auth_views.verify_callback_api, name="verify_callback_api"),
re_path(
r"^password/$",
auth_views.auth_reset_password_api,
name="auth_reset_password_api",
),
re_path(r"^signup/$", auth_views.auth_signup_api, name="auth_signup_api"),
re_path(r"^login/$", auth_views.auth_login_api, name="auth_login_api"),
re_path(r"^logout/?$", auth_views.auth_logout_api, name="auth_logout_api"),
]
40 changes: 25 additions & 15 deletions apps/auth/utils.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import json
import logging
import re
from typing import Any

import httpx
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError
from rest_framework.authentication import SessionAuthentication
from rest_framework.response import Response
from typing import Any

from apps.web.models import Student

logger = logging.getLogger(__name__)

AUTH_SETTINGS = settings.AUTH
PASSWORD_LENGTH_MIN = AUTH_SETTINGS["PASSWORD_LENGTH_MIN"]
PASSWORD_LENGTH_MAX = AUTH_SETTINGS["PASSWORD_LENGTH_MAX"]
Expand All @@ -23,22 +26,28 @@
QUEST_BASE_URL = QUEST_SETTINGS["BASE_URL"]


class CSRFCheckSessionAuthentication(SessionAuthentication):
def authenticate(self, request):
super().enforce_csrf(request)
return super().authenticate(request)


def get_survey_details(action: str) -> dict[str, Any] | None:
"""
A single, clean function to get all survey details for a given action.
Valid actions: "signup", "login", "reset".
Valid actions: "signup", "login", "reset_password".
"""

action_details = QUEST_SETTINGS.get(action.upper())

if not action_details:
logging.error("Invalid quest action requested: %s", action)
logger.error("Invalid quest action requested: %s", action)
return None

try:
question_id = int(action_details.get("QUESTIONID"))
except (ValueError, TypeError):
logging.error(
logger.error(
"Could not parse 'QUESTIONID' for action '%s'. Check your settings.", action
)
return None
Expand Down Expand Up @@ -66,18 +75,18 @@ async def verify_turnstile_token(
},
)
if not response.json().get("success"):
logging.warning("Turnstile verification failed: %s", response.json())
logger.warning("Turnstile verification failed: %s", response.json())
return False, Response(
{"error": "Turnstile verification failed"}, status=403
)
return True, None
except httpx.TimeoutException:
logging.error("Turnstile verification timed out")
logger.error("Turnstile verification timed out")
return False, Response(
{"error": "Turnstile verification timed out"}, status=504
)
except Exception as e:
logging.error(f"Error verifying Turnstile token: {e}")
except Exception:
logger.error("Turnstile verification error")
return False, Response({"error": "Turnstile verification error"}, status=500)


Expand Down Expand Up @@ -132,19 +141,19 @@ async def get_latest_answer(
response.raise_for_status() # Raise an exception for bad status codes
full_data = response.json()
except httpx.TimeoutException:
logging.exception("Questionnaire API query timed out")
logger.error("Questionnaire API query timed out")
return None, Response(
{"error": "Questionnaire API query timed out"},
status=504,
)
except httpx.RequestError as e:
logging.exception(f"Error querying questionnaire API: {e}")
except httpx.RequestError:
logger.error("Error querying questionnaire API")
return None, Response(
{"error": "Failed to query questionnaire API"},
status=500,
)
except Exception as e:
logging.exception(f"An unexpected error occurred: {e}")
except Exception:
logger.error("An unexpected error occurred")
return None, Response({"error": "An unexpected error occurred"}, status=500)

# Filter and return only the required fields from the first row
Expand Down Expand Up @@ -180,7 +189,7 @@ async def get_latest_answer(
key in filtered_data and filtered_data[key] is not None
for key in ["id", "submitted_at", "account", "otp"]
):
logging.warning("Missing required field(s) in questionnaire response")
logger.warning("Missing required field(s) in questionnaire response")
return None, Response(
{"error": "Missing required field(s) in questionnaire response"},
status=400,
Expand Down Expand Up @@ -211,7 +220,8 @@ def rate_password_strength(password: str) -> int:
if re.search(r"[^a-zA-Z0-9\s]", password):
score += 1

length_step = (PASSWORD_LENGTH_MAX - PASSWORD_LENGTH_MIN) // 10
length_range = max(1, PASSWORD_LENGTH_MAX - PASSWORD_LENGTH_MIN)
length_step = max(1, length_range // 10)

score += (len(password) - PASSWORD_LENGTH_MIN) // length_step

Expand Down
Loading
Loading