Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e4d429a
Merge pull request #10 from Project-CyberMaster/dev
Mehloul-Mohamed Apr 19, 2025
984af49
Feat: unfinished exams
Mehloul-Mohamed Apr 23, 2025
c1a3909
idk what is going on at this point but we move onwards
Mehloul-Mohamed Apr 23, 2025
014d500
oh no, anyway
Mehloul-Mohamed Apr 23, 2025
bfb03c0
oh no, anyway^2
Mehloul-Mohamed Apr 23, 2025
0c4fed7
jkjkjkafvjavavji
Mehloul-Mohamed Apr 23, 2025
518aafc
jiojefe;jioawefjo wooooooooooooo
Mehloul-Mohamed Apr 23, 2025
2b53b0a
jiojefe;jioawefjo wooooooooooooo^2
Mehloul-Mohamed Apr 23, 2025
5e8d4fa
I should really go to sleep(nah)
Mehloul-Mohamed Apr 23, 2025
a9fd26f
I should really really REALLY go to sleep(nah)
Mehloul-Mohamed Apr 23, 2025
99bdf74
No signs of sleep just yet
Mehloul-Mohamed Apr 23, 2025
193ee6a
gamble, but let's see
Mehloul-Mohamed Apr 23, 2025
1cbc55e
IT WORKS
Mehloul-Mohamed Apr 23, 2025
c1fbf8c
untested code
Mehloul-Mohamed Apr 23, 2025
30dcabc
more untested code
Mehloul-Mohamed Apr 23, 2025
117b14e
pod lifetimes
Mehloul-Mohamed Apr 23, 2025
ae1079e
tiny fix
Mehloul-Mohamed Apr 23, 2025
0d49d1a
goof
Mehloul-Mohamed Apr 23, 2025
37adf07
naming conventions strike again
Mehloul-Mohamed Apr 23, 2025
98adeb9
done done annnnnnnnnnd done
Mehloul-Mohamed Apr 23, 2025
dce02f0
radom
Mehloul-Mohamed Apr 24, 2025
a222a08
thingie mc doodle
Mehloul-Mohamed Apr 24, 2025
61d4123
something
Mehloul-Mohamed Apr 24, 2025
ffcdcc7
things
Mehloul-Mohamed Apr 25, 2025
cea7819
more
Mehloul-Mohamed Apr 25, 2025
27b01f8
orisdfgvnios
Mehloul-Mohamed Apr 25, 2025
6eeeffa
Should be working
Mehloul-Mohamed Apr 26, 2025
21e5c92
things
Mehloul-Mohamed Apr 26, 2025
cf74e39
Machine feature finished
Mehloul-Mohamed Apr 26, 2025
b54bacc
Features ready!
Mehloul-Mohamed Apr 26, 2025
0a6378a
Feat: Add swagger docs
Mehloul-Mohamed Apr 26, 2025
15db0fb
small fix
Mehloul-Mohamed Apr 26, 2025
860f01a
Final touch
Mehloul-Mohamed Apr 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
4 changes: 2 additions & 2 deletions .github/workflows/build-container.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ name: CI
on:
# Triggers the workflow on push or pull request events but only for the "main" branch
push:
branches: [ "experimental" ]
branches: [ "machines-exp" ]
pull_request:
branches: [ "experimental" ]
branches: [ "machines-exp" ]

# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
Expand Down
10 changes: 8 additions & 2 deletions Backend/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@
'chat',
'categories',
'certs',
'exams',
'drf_yasg',
'corsheaders',
'django_rest_passwordreset', # Swagger stuff for docs (TODO: comment out later)
]

Expand All @@ -78,6 +80,7 @@

MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'corsheaders.middleware.CorsMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
Expand Down Expand Up @@ -118,6 +121,7 @@
'USER': os.environ.get('DB_USER','dbuser'),
'PASSWORD': os.environ.get('DB_PASSWORD','1234'),
'HOST':os.environ.get('DB_HOST','cybermaster-postgres.default.svc.cluster.local'),
# 'HOST':os.environ.get('DB_HOST','127.0.0.1'),
'PORT': os.environ.get('DB_PORT','5432'),
}
}
Expand Down Expand Up @@ -178,12 +182,14 @@
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

GOOGLE_API_KEY = 'AIzaSyB-TzEi633vh6CQy73MRi-_LS4v7mjoYVc'

SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME':timedelta(minutes=90)
}



CORS_ALLOWED_ORIGINS=[
'http://127.0.0.1'
]

EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "smtp.gmail.com"
Expand Down
1 change: 1 addition & 0 deletions Backend/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
path('api/v1/chat/',include('chat.urls')),
path('api/v1/certify/',include('certs.urls')),
path('api/v1/category/',include('categories.urls')),
path('api/v1/exams/',include('exams.urls')),

# Swagger stuff for docs (TODO: comment out in prod)
path('swagger<format>/', schema_view.without_ui(cache_timeout=0), name='schema-json'),
Expand Down
8 changes: 6 additions & 2 deletions categories/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ class Meta:

def get_labs(self,obj):
request = self.context.get('request')
expand = request.query_params.get('expand','').split(',')
expand=[]
if request:
expand = request.query_params.get('expand','').split(',')

if 'labs' in expand:
return LabSerializer(obj.labs.all(),many=True, read_only=True,context={'request':request}).data
Expand All @@ -22,7 +24,9 @@ def get_labs(self,obj):

def get_courses(self,obj):
request = self.context.get('request')
expand = request.query_params.get('expand','').split(',')
expand=[]
if request:
expand = request.query_params.get('expand','').split(',')

if 'courses' in expand:
return CourseSerializer(obj.courses.all(),many=True, read_only=True,context={'request':request}).data
Expand Down
20 changes: 20 additions & 0 deletions categories/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,23 @@
from .models import *
from .serializers import *
from django.shortcuts import get_object_or_404
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi

class CategoryListCreate(APIView):
@swagger_auto_schema(
operation_description="List Categories",
manual_parameters=[
openapi.Parameter(
'name',
openapi.IN_QUERY,
description="Filter categories by name (optional)",
type=openapi.TYPE_STRING
)
],
responses={200: CategorySerializer(many=True)}
)

def get(self, request, format=None):
name = request.query_params.get('name', None)
categories = Category.objects.all()
Expand All @@ -16,6 +31,11 @@ def get(self, request, format=None):
return Response(serializer.data)

class CategoryDetail(APIView):
@swagger_auto_schema(
operation_description="View Category Details",
responses={200: CategorySerializer}
)

def get(self, request, pk, format=None):
category = get_object_or_404(Category,pk=pk)
serializer = CategorySerializer(category, context={'request': request})
Expand Down
53 changes: 52 additions & 1 deletion certs/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@
from certs.serializers import CertificationSerializer
from .models import *
from courses.models import Enrollment
from exams.models import *
import segno
from playwright.sync_api import sync_playwright
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi

def render_pdf(html, buffer):
with sync_playwright() as p:
Expand All @@ -31,11 +34,33 @@ def render_pdf(html, buffer):
class GetCert(APIView):
permission_classes=[IsAuthenticated]

@swagger_auto_schema(
operation_description="Get or create a certification PDF for a user who passed the exam",
responses={
200: CertificationSerializer,
400: 'Certification not ready'
},
manual_parameters=[
openapi.Parameter(
'pk',
openapi.IN_PATH,
description="Primary key of the course",
type=openapi.TYPE_INTEGER
)
]
)

def get(self,request,pk):
course = get_object_or_404(Course,pk=pk)
enrollment=get_object_or_404(Enrollment,user=request.user,course=course)
exam=get_object_or_404(Exam,course=course)
passing_attempt=ExamAttempt.objects.filter(
user=request.user,
exam=exam,
cert_ready=True
).first()

if enrollment.cert_ready:
if passing_attempt:
cert, _ = Certification.objects.get_or_create(
user=request.user,
course = course,
Expand Down Expand Up @@ -75,6 +100,32 @@ def get(self,request,pk):
}, status=400)

class Validate(APIView):
@swagger_auto_schema(
operation_description="Validate a certification by its certificate ID",
responses={
200: openapi.Response(
description="Validation result",
examples={
"application/json": {
"status": "Valid",
"cert_id": "123456",
"username": "user1",
"course": "Python Basics",
"date": "2025-04-20"
}
}
),
404: 'Certificate not found'
},
manual_parameters=[
openapi.Parameter(
'id',
openapi.IN_PATH,
description="Certificate ID to validate",
type=openapi.TYPE_STRING
)
]
)
def get(self,request,id):
cert = Certification.objects.filter(cert_id=id).first()
if not cert:
Expand Down
41 changes: 39 additions & 2 deletions chat/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,19 @@
from .models import Conversation, Message
from .serializers import ConversationSerializer, MessageSerializer
from .utils.gemini import generate_with_gemini
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi

User = get_user_model()

class GeminiAssistantAPI(APIView):
permission_classes = [IsAuthenticated]

@swagger_auto_schema(
operation_summary="Get or Create Active Conversation",
operation_description="Retrieves the user's active conversation or creates one if none exists.",
responses={200: ConversationSerializer()}
)

def get(self, request):
# Get or create active conversation
conversation, created = Conversation.objects.get_or_create(
Expand All @@ -27,6 +34,25 @@ def get(self, request):
serializer = ConversationSerializer(conversation)
return Response(serializer.data)

@swagger_auto_schema(
operation_summary="Send Message to Gemini Assistant",
operation_description="Sends a user message to the Gemini assistant and receives a generated response.",
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'message': openapi.Schema(type=openapi.TYPE_STRING, description='User message to the assistant'),
'max_tokens': openapi.Schema(type=openapi.TYPE_INTEGER, description='Maximum tokens for the model response', default=300),
},
required=['message']
),
responses={200: openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'response': openapi.Schema(type=openapi.TYPE_STRING, description='Assistant\'s reply'),
'conversation_id': openapi.Schema(type=openapi.TYPE_INTEGER, description='ID of the active conversation'),
}
)}
)
def post(self, request):
user_message = request.data.get('message', '')
max_tokens = request.data.get('max_tokens', 300)
Expand Down Expand Up @@ -77,7 +103,18 @@ def post(self, request):
})
class ResetConversationAPI(APIView):
permission_classes = [IsAuthenticated]

@swagger_auto_schema(
operation_summary="Reset Conversation",
operation_description="Deletes all active conversations and associated messages for the authenticated user.",
responses={200: openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'status': openapi.Schema(type=openapi.TYPE_STRING, example='success'),
'message': openapi.Schema(type=openapi.TYPE_STRING, example='Conversation and messages deleted successfully'),
}
)}
)

def post(self, request):

conversations = Conversation.objects.filter(user=request.user, is_active=True)
Expand Down
16 changes: 12 additions & 4 deletions courses/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ class Meta:

def get_fields(self):
request=self.context.get('request')
expand=request.query_params.get('expand','').split(',')
expand=[]
if request:
expand=request.query_params.get('expand','').split(',')
fields=super().get_fields()

if 'link' not in expand:
Expand Down Expand Up @@ -57,7 +59,9 @@ def get_markdown(self,obj):

def get_labs(self,obj):
request=self.context.get('request')
expand=request.query_params.get('expand','').split(',')
expand=[]
if request:
expand=request.query_params.get('expand','').split(',')

if 'labs' in expand:
return LabSerializer(obj.labs.all(),many=True,read_only=True,context={'request':request}).data
Expand All @@ -79,7 +83,9 @@ class Meta:

def get_lessons(self,obj):
request=self.context.get('request')
expand=request.query_params.get('expand','').split(',')
expand=[]
if request:
expand=request.query_params.get('expand','').split(',')

if 'lessons' in expand:
return LessonSerializer(obj.lessons.all(),many=True,read_only=True,context={'request':request}).data
Expand All @@ -97,7 +103,9 @@ class Meta:

def get_chapters(self,obj):
request=self.context.get('request')
expand=request.query_params.get('expand','').split(',')
expand=[]
if request:
expand=request.query_params.get('expand','').split(',')

if 'chapters' in expand:
return ChapterSerializer(obj.chapters.all(),many=True,read_only=True,context={'request':request}).data
Expand Down
Loading