UPDATE
This commit is contained in:
parent
060f4301e9
commit
aae1451eeb
29 changed files with 234 additions and 349 deletions
|
|
@ -12,3 +12,5 @@ REDIS_URL=redis://redis:6379/0
|
||||||
# Flower
|
# Flower
|
||||||
CELERY_FLOWER_USER=debug
|
CELERY_FLOWER_USER=debug
|
||||||
CELERY_FLOWER_PASSWORD=debug
|
CELERY_FLOWER_PASSWORD=debug
|
||||||
|
|
||||||
|
SIGNING_KEY=ebd0c2f345ede5we3244t5r34a0dc1b994e33e729e
|
||||||
|
|
@ -4,13 +4,13 @@
|
||||||
DJANGO_SETTINGS_MODULE=config.settings.production
|
DJANGO_SETTINGS_MODULE=config.settings.production
|
||||||
DJANGO_SECRET_KEY=CQHQz4M3wN1VL2TT53Gl8yupKOjQ5m01js4jPw6bQsUexzkdy9JGXhQg9h6H24M5
|
DJANGO_SECRET_KEY=CQHQz4M3wN1VL2TT53Gl8yupKOjQ5m01js4jPw6bQsUexzkdy9JGXhQg9h6H24M5
|
||||||
DJANGO_ADMIN_URL=6XfjlokEGlPf6SpVfGh7wBvs7t5ZFMDs/
|
DJANGO_ADMIN_URL=6XfjlokEGlPf6SpVfGh7wBvs7t5ZFMDs/
|
||||||
DJANGO_ALLOWED_HOSTS=.example.com
|
DJANGO_ALLOWED_HOSTS=.learngpt.tech
|
||||||
|
SIGNING_KEY=HQz4M3wN1ebd0c2f345ede5we324@#$%$#@#R$Q#Zaexsredg/*43/54333e729e
|
||||||
|
|
||||||
# Security
|
# Security
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# TIP: better off using DNS, however, redirect is OK too
|
# TIP: better off using DNS, however, redirect is OK too
|
||||||
DJANGO_SECURE_SSL_REDIRECT=False
|
DJANGO_SECURE_SSL_REDIRECT=False
|
||||||
|
|
||||||
# Email
|
# Email
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
DJANGO_SERVER_EMAIL=
|
DJANGO_SERVER_EMAIL=
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,16 @@
|
||||||
packages = [
|
packages = [
|
||||||
pkgs.docker
|
pkgs.docker
|
||||||
pkgs.docker-compose
|
pkgs.docker-compose
|
||||||
|
pkgs.sudo
|
||||||
];
|
];
|
||||||
|
|
||||||
# Sets environment variables in the workspace
|
# Sets environment variables in the workspace
|
||||||
env = {};
|
env = {
|
||||||
|
PORT = "6000";
|
||||||
|
};
|
||||||
services.docker.enable = true;
|
services.docker.enable = true;
|
||||||
|
|
||||||
|
|
||||||
idx = {
|
idx = {
|
||||||
# Search for the extensions you want on https://open-vsx.org/ and use "publisher.id"
|
# Search for the extensions you want on https://open-vsx.org/ and use "publisher.id"
|
||||||
extensions = [
|
extensions = [
|
||||||
|
|
|
||||||
|
|
@ -1,60 +1,21 @@
|
||||||
# define an alias for the specific python version used in this file.
|
# Use a lightweight Python base image
|
||||||
FROM docker.io/python:3.12.8-slim-bookworm AS python
|
FROM python:3.12.8-alpine AS python
|
||||||
|
|
||||||
|
|
||||||
# Python build stage
|
|
||||||
FROM python AS python-build-stage
|
|
||||||
|
|
||||||
|
# Python base stage
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
|
||||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
|
||||||
# dependencies for building Python packages
|
|
||||||
build-essential \
|
|
||||||
# psycopg dependencies
|
|
||||||
libpq-dev \
|
|
||||||
# cleaning up unused files
|
|
||||||
&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Requirements are installed here to ensure they will be cached.
|
|
||||||
COPY ./requirements /requirements
|
|
||||||
|
|
||||||
# create python dependency wheels
|
|
||||||
RUN pip wheel --no-cache-dir --wheel-dir /usr/src/app/wheels \
|
|
||||||
-r /requirements/local.txt -r /requirements/production.txt \
|
|
||||||
&& rm -rf /requirements
|
|
||||||
|
|
||||||
|
|
||||||
# Python 'run' stage
|
|
||||||
FROM python AS python-run-stage
|
|
||||||
|
|
||||||
ARG BUILD_ENVIRONMENT
|
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
# Install dependencies
|
||||||
# To run the Makefile
|
RUN apk update && apk add --no-cache \
|
||||||
make \
|
# Runtime dependencies
|
||||||
# psycopg dependencies
|
make \
|
||||||
libpq-dev \
|
gettext \
|
||||||
# Translations dependencies
|
&& rm -rf /var/cache/apk/*
|
||||||
gettext \
|
|
||||||
# Uncomment below lines to enable Sphinx output to latex and pdf
|
|
||||||
# texlive-latex-recommended \
|
|
||||||
# texlive-fonts-recommended \
|
|
||||||
# texlive-latex-extra \
|
|
||||||
# latexmk \
|
|
||||||
# cleaning up unused files
|
|
||||||
&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# copy python dependency wheels from python-build-stage
|
# Install MkDocs and required plugins
|
||||||
COPY --from=python-build-stage /usr/src/app/wheels /wheels
|
RUN pip install mkdocs==1.5.1 mkdocs-material==9.1.15 mkdocs-markdownextradata-plugin
|
||||||
|
|
||||||
# use wheels to install python dependencies
|
|
||||||
RUN pip install --no-cache /wheels/* \
|
|
||||||
&& rm -rf /wheels
|
|
||||||
|
|
||||||
|
# Copy the start script
|
||||||
COPY ./compose/local/docs/start /start-docs
|
COPY ./compose/local/docs/start /start-docs
|
||||||
RUN sed -i 's/\r$//g' /start-docs
|
RUN sed -i 's/\r$//g' /start-docs
|
||||||
RUN chmod +x /start-docs
|
RUN chmod +x /start-docs
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,5 @@ set -o errexit
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
set -o nounset
|
set -o nounset
|
||||||
|
|
||||||
exec make livehtml
|
# Start MkDocs live development server
|
||||||
|
exec mkdocs serve -a 0.0.0.0:6000
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ certificatesResolvers:
|
||||||
http:
|
http:
|
||||||
routers:
|
routers:
|
||||||
web-secure-router:
|
web-secure-router:
|
||||||
rule: 'Host(`example.com`) || Host(`www.example.com`)'
|
rule: 'Host(`learngpt.tech`) || Host(`www.learngpt.tech`)'
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- web-secure
|
- web-secure
|
||||||
middlewares:
|
middlewares:
|
||||||
|
|
@ -42,7 +42,7 @@ http:
|
||||||
certResolver: letsencrypt
|
certResolver: letsencrypt
|
||||||
|
|
||||||
flower-secure-router:
|
flower-secure-router:
|
||||||
rule: 'Host(`example.com`)'
|
rule: 'Host(`learngpt.tech`)'
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- flower
|
- flower
|
||||||
service: flower
|
service: flower
|
||||||
|
|
@ -51,7 +51,7 @@ http:
|
||||||
certResolver: letsencrypt
|
certResolver: letsencrypt
|
||||||
|
|
||||||
web-media-router:
|
web-media-router:
|
||||||
rule: '(Host(`example.com`) || Host(`www.example.com`)) && PathPrefix(`/media/`)'
|
rule: '(Host(`learngpt.tech`) || Host(`www.learngpt.tech`)) && PathPrefix(`/media/`)'
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- web-secure
|
- web-secure
|
||||||
middlewares:
|
middlewares:
|
||||||
|
|
|
||||||
|
|
@ -380,6 +380,7 @@ REST_FRAMEWORK = {
|
||||||
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||||
'EXCEPTION_HANDLER': 'utils.exception_handler.custom_exception_handler',
|
'EXCEPTION_HANDLER': 'utils.exception_handler.custom_exception_handler',
|
||||||
}
|
}
|
||||||
|
|
||||||
REST_AUTH = {
|
REST_AUTH = {
|
||||||
'LOGIN_SERIALIZER': 'lms.accounts.serializers.CustomLoginSerializer',
|
'LOGIN_SERIALIZER': 'lms.accounts.serializers.CustomLoginSerializer',
|
||||||
'REGISTER_SERIALIZER': 'lms.accounts.serializers.CustomRegisterSerializer',
|
'REGISTER_SERIALIZER': 'lms.accounts.serializers.CustomRegisterSerializer',
|
||||||
|
|
@ -395,11 +396,11 @@ SIMPLE_JWT = {
|
||||||
'ROTATE_REFRESH_TOKENS': True,
|
'ROTATE_REFRESH_TOKENS': True,
|
||||||
'BLACKLIST_AFTER_ROTATION': True,
|
'BLACKLIST_AFTER_ROTATION': True,
|
||||||
'ALGORITHM': 'HS256',
|
'ALGORITHM': 'HS256',
|
||||||
'SIGNING_KEY': 'SECRET_KEY',
|
'SIGNING_KEY': 'env("SIGNING_KEY")',
|
||||||
}
|
}
|
||||||
|
|
||||||
# django-cors-headers - https://github.com/adamchainz/django-cors-headers#setup
|
# django-cors-headers - https://github.com/adamchainz/django-cors-headers#setup
|
||||||
# CORS_URLS_REGEX = r"^/api/.*$"
|
CORS_URLS_REGEX = r"^/api/.*$"
|
||||||
|
|
||||||
# By Default swagger ui is available only to admin user(s). You can change permission classes to change that
|
# By Default swagger ui is available only to admin user(s). You can change permission classes to change that
|
||||||
# See more configuration options at https://drf-spectacular.readthedocs.io/en/latest/settings.html#settings
|
# See more configuration options at https://drf-spectacular.readthedocs.io/en/latest/settings.html#settings
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ from .base import env
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
|
# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
|
||||||
SECRET_KEY = env("DJANGO_SECRET_KEY")
|
SECRET_KEY = env("DJANGO_SECRET_KEY")
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
|
# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
|
||||||
ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["example.com"])
|
ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["learngpt.tech"])
|
||||||
|
|
||||||
# DATABASES
|
# DATABASES
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
@ -87,7 +87,7 @@ STORAGES = {
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#default-from-email
|
# https://docs.djangoproject.com/en/dev/ref/settings/#default-from-email
|
||||||
DEFAULT_FROM_EMAIL = env(
|
DEFAULT_FROM_EMAIL = env(
|
||||||
"DJANGO_DEFAULT_FROM_EMAIL",
|
"DJANGO_DEFAULT_FROM_EMAIL",
|
||||||
default="Learning Management System <noreply@example.com>",
|
default="Learning Management System <noreply@learngpt.tech>",
|
||||||
)
|
)
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#server-email
|
# https://docs.djangoproject.com/en/dev/ref/settings/#server-email
|
||||||
SERVER_EMAIL = env("DJANGO_SERVER_EMAIL", default=DEFAULT_FROM_EMAIL)
|
SERVER_EMAIL = env("DJANGO_SERVER_EMAIL", default=DEFAULT_FROM_EMAIL)
|
||||||
|
|
@ -200,7 +200,7 @@ sentry_sdk.init(
|
||||||
# -------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------
|
||||||
# Tools that generate code samples can use SERVERS to point to the correct domain
|
# Tools that generate code samples can use SERVERS to point to the correct domain
|
||||||
SPECTACULAR_SETTINGS["SERVERS"] = [
|
SPECTACULAR_SETTINGS["SERVERS"] = [
|
||||||
{"url": "https://example.com", "description": "Production server"},
|
{"url": "https://learngpt.tech", "description": "Production server"},
|
||||||
]
|
]
|
||||||
# Your stuff...
|
# Your stuff...
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,7 @@ urlpatterns = [
|
||||||
path("", TemplateView.as_view(template_name="pages/home.html"), name="home"),
|
path("", TemplateView.as_view(template_name="pages/home.html"), name="home"),
|
||||||
# Django Admin, use {% url 'admin:index' %}
|
# Django Admin, use {% url 'admin:index' %}
|
||||||
path(settings.ADMIN_URL, admin.site.urls),
|
path(settings.ADMIN_URL, admin.site.urls),
|
||||||
# User management
|
|
||||||
# path("users/", include("lms.users.urls", namespace="users")),
|
|
||||||
path("accounts/", include("allauth.urls")),
|
|
||||||
# path("auth/", include("allauth.headless.urls")),
|
# path("auth/", include("allauth.headless.urls")),
|
||||||
# Your stuff: custom urls includes go here
|
# Your stuff: custom urls includes go here
|
||||||
# ...
|
# ...
|
||||||
|
|
@ -37,16 +35,12 @@ if settings.DEBUG:
|
||||||
|
|
||||||
# API URLS
|
# API URLS
|
||||||
urlpatterns += [
|
urlpatterns += [
|
||||||
|
path("api/accounts/", include("allauth.urls")),
|
||||||
|
|
||||||
# path('authw/', include('dj_rest_auth.urls')),
|
path('api/auth/', include('lms.accounts.urls')),
|
||||||
# path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
|
||||||
path('auth/', include('lms.accounts.urls')),
|
|
||||||
|
|
||||||
path('app/', include('lms.app.urls')),
|
path('api/app/', include('lms.app.urls')),
|
||||||
|
|
||||||
|
|
||||||
# API base url
|
|
||||||
# path("api/auth-token/", obtain_auth_token),
|
|
||||||
path("api/schema/", SpectacularAPIView.as_view(), name="api-schema"),
|
path("api/schema/", SpectacularAPIView.as_view(), name="api-schema"),
|
||||||
path(
|
path(
|
||||||
"api/docs/",
|
"api/docs/",
|
||||||
|
|
|
||||||
|
|
@ -12,5 +12,5 @@ services:
|
||||||
- ./config:/app/config:z
|
- ./config:/app/config:z
|
||||||
- ./lms:/app/lms:z
|
- ./lms:/app/lms:z
|
||||||
ports:
|
ports:
|
||||||
- '9000:9000'
|
- '6000:6000'
|
||||||
command: /start-docs
|
command: mkdocs serve
|
||||||
|
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
# Minimal makefile for Sphinx documentation
|
|
||||||
#
|
|
||||||
|
|
||||||
# You can set these variables from the command line, and also
|
|
||||||
# from the environment for the first two.
|
|
||||||
SPHINXOPTS ?=
|
|
||||||
SPHINXBUILD ?= sphinx-build
|
|
||||||
SOURCEDIR = .
|
|
||||||
BUILDDIR = ./_build
|
|
||||||
APP = /app
|
|
||||||
|
|
||||||
.PHONY: html livehtml apidocs Makefile
|
|
||||||
|
|
||||||
# Put it first so that "make" without argument is like "make html".
|
|
||||||
html:
|
|
||||||
@$(SPHINXBUILD) -M html "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -c .
|
|
||||||
|
|
||||||
# Build, watch and serve docs with live reload
|
|
||||||
livehtml:
|
|
||||||
sphinx-autobuild -b html --host 0.0.0.0 --port 9000 --watch $(APP) -c . $(SOURCEDIR) $(BUILDDIR)/html
|
|
||||||
|
|
||||||
# Outputs rst files from django application code
|
|
||||||
apidocs:
|
|
||||||
sphinx-apidoc -o $(SOURCEDIR)/api $(APP)
|
|
||||||
|
|
||||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
|
||||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
|
||||||
%: Makefile
|
|
||||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -c .
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
# Included so that Django's startproject comment runs against the docs directory
|
|
||||||
63
docs/conf.py
63
docs/conf.py
|
|
@ -1,63 +0,0 @@
|
||||||
# ruff: noqa
|
|
||||||
# Configuration file for the Sphinx documentation builder.
|
|
||||||
#
|
|
||||||
# This file only contains a selection of the most common options. For a full
|
|
||||||
# list see the documentation:
|
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
|
||||||
|
|
||||||
# -- Path setup --------------------------------------------------------------
|
|
||||||
|
|
||||||
# If extensions (or modules to document with autodoc) are in another directory,
|
|
||||||
# add these directories to sys.path here. If the directory is relative to the
|
|
||||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import django
|
|
||||||
|
|
||||||
if os.getenv("READTHEDOCS", default=False) == "True":
|
|
||||||
sys.path.insert(0, os.path.abspath(".."))
|
|
||||||
os.environ["DJANGO_READ_DOT_ENV_FILE"] = "True"
|
|
||||||
os.environ["USE_DOCKER"] = "no"
|
|
||||||
else:
|
|
||||||
sys.path.insert(0, os.path.abspath("/app"))
|
|
||||||
os.environ["DATABASE_URL"] = "sqlite:///readthedocs.db"
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
|
|
||||||
django.setup()
|
|
||||||
|
|
||||||
# -- Project information -----------------------------------------------------
|
|
||||||
|
|
||||||
project = "Learning Management System"
|
|
||||||
copyright = """2025, Ahmed Nagi"""
|
|
||||||
author = "Ahmed Nagi"
|
|
||||||
|
|
||||||
|
|
||||||
# -- General configuration ---------------------------------------------------
|
|
||||||
|
|
||||||
# Add any Sphinx extension module names here, as strings. They can be
|
|
||||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
|
||||||
# ones.
|
|
||||||
extensions = [
|
|
||||||
"sphinx.ext.autodoc",
|
|
||||||
"sphinx.ext.napoleon",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
|
||||||
# templates_path = ["_templates"]
|
|
||||||
|
|
||||||
# List of patterns, relative to source directory, that match files and
|
|
||||||
# directories to ignore when looking for source files.
|
|
||||||
# This pattern also affects html_static_path and html_extra_path.
|
|
||||||
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
|
||||||
|
|
||||||
# -- Options for HTML output -------------------------------------------------
|
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
|
||||||
# a list of builtin themes.
|
|
||||||
#
|
|
||||||
html_theme = "alabaster"
|
|
||||||
|
|
||||||
# Add any paths that contain custom static files (such as style sheets) here,
|
|
||||||
# relative to this directory. They are copied after the builtin static files,
|
|
||||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
|
||||||
# html_static_path = ["_static"]
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
How To - Project Documentation
|
|
||||||
======================================================================
|
|
||||||
|
|
||||||
Get Started
|
|
||||||
----------------------------------------------------------------------
|
|
||||||
|
|
||||||
Documentation can be written as rst files in `lms/docs`.
|
|
||||||
|
|
||||||
|
|
||||||
To build and serve docs, use the commands::
|
|
||||||
|
|
||||||
docker compose -f docker-compose.local.yml up docs
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Changes to files in `docs/_source` will be picked up and reloaded automatically.
|
|
||||||
|
|
||||||
`Sphinx <https://www.sphinx-doc.org/>`_ is the tool used to build documentation.
|
|
||||||
|
|
||||||
Docstrings to Documentation
|
|
||||||
----------------------------------------------------------------------
|
|
||||||
|
|
||||||
The sphinx extension `apidoc <https://www.sphinx-doc.org/en/master/man/sphinx-apidoc.html>`_ is used to automatically document code using signatures and docstrings.
|
|
||||||
|
|
||||||
Numpy or Google style docstrings will be picked up from project files and available for documentation. See the `Napoleon <https://sphinxcontrib-napoleon.readthedocs.io/en/latest/>`_ extension for details.
|
|
||||||
|
|
||||||
For an in-use example, see the `page source <_sources/users.rst.txt>`_ for :ref:`users`.
|
|
||||||
|
|
||||||
To compile all docstrings automatically into documentation source files, use the command:
|
|
||||||
::
|
|
||||||
|
|
||||||
make apidocs
|
|
||||||
|
|
||||||
|
|
||||||
This can be done in the docker container:
|
|
||||||
::
|
|
||||||
|
|
||||||
docker run --rm docs make apidocs
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
.. Learning Management System documentation master file, created by
|
|
||||||
sphinx-quickstart.
|
|
||||||
You can adapt this file completely to your liking, but it should at least
|
|
||||||
contain the root `toctree` directive.
|
|
||||||
|
|
||||||
Welcome to Learning Management System's documentation!
|
|
||||||
======================================================================
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 2
|
|
||||||
:caption: Contents:
|
|
||||||
|
|
||||||
howto
|
|
||||||
users
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Indices and tables
|
|
||||||
==================
|
|
||||||
|
|
||||||
* :ref:`genindex`
|
|
||||||
* :ref:`modindex`
|
|
||||||
* :ref:`search`
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
@ECHO OFF
|
|
||||||
|
|
||||||
pushd %~dp0
|
|
||||||
|
|
||||||
REM Command file for Sphinx documentation
|
|
||||||
|
|
||||||
|
|
||||||
if "%SPHINXBUILD%" == "" (
|
|
||||||
set SPHINXBUILD=sphinx-build -c .
|
|
||||||
)
|
|
||||||
set SOURCEDIR=_source
|
|
||||||
set BUILDDIR=_build
|
|
||||||
set APP=..\lms
|
|
||||||
|
|
||||||
if "%1" == "" goto html
|
|
||||||
|
|
||||||
%SPHINXBUILD% >NUL 2>NUL
|
|
||||||
if errorlevel 9009 (
|
|
||||||
echo.
|
|
||||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
|
||||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
|
||||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
|
||||||
echo.may add the Sphinx directory to PATH.
|
|
||||||
echo.
|
|
||||||
echo.Install sphinx-autobuild for live serving.
|
|
||||||
echo.If you don't have Sphinx installed, grab it from
|
|
||||||
echo.http://sphinx-doc.org/
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
%SPHINXBUILD% -b %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
|
||||||
goto end
|
|
||||||
|
|
||||||
:livehtml
|
|
||||||
sphinx-autobuild -b html --open-browser -p 9000 --watch %APP% -c . %SOURCEDIR% %BUILDDIR%/html
|
|
||||||
GOTO :EOF
|
|
||||||
|
|
||||||
:apidocs
|
|
||||||
sphinx-apidoc -o %SOURCEDIR%/api %APP%
|
|
||||||
GOTO :EOF
|
|
||||||
|
|
||||||
:html
|
|
||||||
%SPHINXBUILD% -b html %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
|
||||||
|
|
||||||
:end
|
|
||||||
popd
|
|
||||||
67
docs/mkdocs.yml
Normal file
67
docs/mkdocs.yml
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
# اسم الموقع
|
||||||
|
site_name: My Project
|
||||||
|
|
||||||
|
|
||||||
|
site_description: Learning Management System
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
theme:
|
||||||
|
name: material
|
||||||
|
custom_dir: overrides # تخصيص السمات (اختياري)
|
||||||
|
palette:
|
||||||
|
- scheme: default # الوضع الافتراضي
|
||||||
|
primary: indigo # اللون الأساسي
|
||||||
|
accent: pink # اللون الثانوي
|
||||||
|
- scheme: slate # وضع داكن
|
||||||
|
primary: deep purple
|
||||||
|
accent: amber
|
||||||
|
# features:
|
||||||
|
# - navigation.tabs # استخدام التبويبات للتنقل
|
||||||
|
# - navigation.expand # توسيع القوائم تلقائيًا
|
||||||
|
# - toc.integrate # دمج قائمة المحتويات (Table of Contents) مع التنقل
|
||||||
|
# logo: images/logo.png # شعار الموقع (اختياري)
|
||||||
|
# favicon: images/favicon.ico # أيقونة الموقع (اختياري)
|
||||||
|
|
||||||
|
# التنقل (Navigation)
|
||||||
|
nav:
|
||||||
|
- Home: index.md
|
||||||
|
- Getting Started:
|
||||||
|
- Introduction: getting-started/introduction.md
|
||||||
|
- Installation: getting-started/installation.md
|
||||||
|
- Reference:
|
||||||
|
- API Documentation: reference/api.md
|
||||||
|
- CLI: reference/cli.md
|
||||||
|
- About: about.md
|
||||||
|
|
||||||
|
# الإضافات (Plugins)
|
||||||
|
plugins:
|
||||||
|
- search # محرك البحث
|
||||||
|
- markdownextradata # إدراج البيانات الإضافية (اختياري)
|
||||||
|
|
||||||
|
# ملحقات Markdown
|
||||||
|
markdown_extensions:
|
||||||
|
- admonition # الملاحظات (تحذير، نصيحة، إلخ)
|
||||||
|
- codehilite # تمييز الأكواد
|
||||||
|
- toc # قائمة المحتويات
|
||||||
|
- tables # دعم الجداول
|
||||||
|
- pymdownx.arithmatex # دعم LaTeX (للمعادلات الرياضية)
|
||||||
|
- pymdownx.superfences # تحسين تداخل الأكواد والجداول
|
||||||
|
|
||||||
|
# إعدادات البحث
|
||||||
|
extra:
|
||||||
|
search:
|
||||||
|
lang: en # لغة البحث (يدعم الإنجليزية، الفرنسية، إلخ)
|
||||||
|
separator: "[\\s\\-]+" # الفاصل للبحث
|
||||||
|
|
||||||
|
# بيانات إضافية (اختيارية)
|
||||||
|
extra_css:
|
||||||
|
- styles/custom.css # ملف CSS مخصص
|
||||||
|
extra_javascript:
|
||||||
|
- scripts/custom.js # ملف JavaScript مخصص
|
||||||
|
|
||||||
|
# إعدادات مخرجات البناء
|
||||||
|
# site_dir: site # مسار مجلد الإخراج
|
||||||
|
docs_dir: docs # مسار مجلد الوثائق
|
||||||
|
|
||||||
|
dev_addr: 0.0.0.0:6000
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
.. _users:
|
|
||||||
|
|
||||||
Users
|
|
||||||
======================================================================
|
|
||||||
|
|
||||||
Starting a new project, it’s highly recommended to set up a custom user model,
|
|
||||||
even if the default User model is sufficient for you.
|
|
||||||
|
|
||||||
This model behaves identically to the default user model,
|
|
||||||
but you’ll be able to customize it in the future if the need arises.
|
|
||||||
|
|
||||||
.. automodule:: lms.accounts.models
|
|
||||||
:members:
|
|
||||||
:noindex:
|
|
||||||
|
|
||||||
|
|
@ -78,10 +78,10 @@ class CustomRegisterSerializer(RegisterSerializer):
|
||||||
email_address = EmailAddress.objects.filter(email=email).first()
|
email_address = EmailAddress.objects.filter(email=email).first()
|
||||||
if email_address:
|
if email_address:
|
||||||
if email_address.verified:
|
if email_address.verified:
|
||||||
CustomValidationError({'email': 'This email is already.'})
|
raise CustomValidationError({'email': 'This email is already.'})
|
||||||
else:
|
else:
|
||||||
send_email_confirmation(request, email_address.user)
|
send_email_confirmation(request, email_address.user)
|
||||||
CustomValidationError({'email': 'A confirmation email has been sent. Please confirm your email.'})
|
raise CustomValidationError({'email': 'A confirmation email has been sent. Please confirm your email.'})
|
||||||
|
|
||||||
user = super().save(request)
|
user = super().save(request)
|
||||||
user.full_name = self.data.get('full_name', '')
|
user.full_name = self.data.get('full_name', '')
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
from .serializers import ChangeEmailSerializer
|
from .serializers import ChangeEmailSerializer
|
||||||
from asgiref.sync import sync_to_async
|
from asgiref.sync import sync_to_async
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
from lms.utils.exception_handler import CustomValidationError
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
class UserView(APIView):
|
class UserView(APIView):
|
||||||
|
|
@ -53,7 +53,7 @@ class ChangeEmailView(APIView):
|
||||||
"message": "Confirmation email has been sent to the new address.",
|
"message": "Confirmation email has been sent to the new address.",
|
||||||
}, status=status.HTTP_200_OK)
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
raise CustomValidationError(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -63,7 +63,7 @@ class ConfirmEmailAPIView(APIView):
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
key = request.data.get("key")
|
key = request.data.get("key")
|
||||||
if not key:
|
if not key:
|
||||||
return Response({"detail": _("Key is required.")}, status=status.HTTP_400_BAD_REQUEST)
|
raise CustomValidationError({"detail": _("Key is required.")}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Attempt to retrieve the email confirmation using HMAC key
|
# Attempt to retrieve the email confirmation using HMAC key
|
||||||
|
|
@ -72,7 +72,7 @@ class ConfirmEmailAPIView(APIView):
|
||||||
# If HMAC fails, fallback to database key
|
# If HMAC fails, fallback to database key
|
||||||
email_confirmation = EmailConfirmation.objects.get(key=key)
|
email_confirmation = EmailConfirmation.objects.get(key=key)
|
||||||
except EmailConfirmation.DoesNotExist:
|
except EmailConfirmation.DoesNotExist:
|
||||||
return Response({"detail": _("Invalid or expired key.")}, status=status.HTTP_400_BAD_REQUEST)
|
raise CustomValidationError({"detail": _("Invalid or expired key.")}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
if email_confirmation.email_address.verified:
|
if email_confirmation.email_address.verified:
|
||||||
return Response({"detail": _("Email is already verified.")}, status=status.HTTP_200_OK)
|
return Response({"detail": _("Email is already verified.")}, status=status.HTTP_200_OK)
|
||||||
|
|
|
||||||
31
lms/app/migrations/0003_advertisement_alter_course_owner.py
Normal file
31
lms/app/migrations/0003_advertisement_alter_course_owner.py
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Generated by Django 5.0.10 on 2025-01-21 09:33
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('app', '0002_lesson_quiz_alter_enrollment_student_delete_quiz'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Advertisement',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.CharField(max_length=255)),
|
||||||
|
('description', models.TextField()),
|
||||||
|
('image', models.ImageField(blank=True, null=True, upload_to='ads_images/')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='course',
|
||||||
|
name='owner',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owner', to=settings.AUTH_USER_MODEL, verbose_name='Owner'),
|
||||||
|
),
|
||||||
|
]
|
||||||
17
lms/app/migrations/0004_rename_advertisement_ad.py
Normal file
17
lms/app/migrations/0004_rename_advertisement_ad.py
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Django 5.0.10 on 2025-01-21 10:43
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('app', '0003_advertisement_alter_course_owner'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameModel(
|
||||||
|
old_name='Advertisement',
|
||||||
|
new_name='AD',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -5,7 +5,6 @@ from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
# Table for courses (Course)
|
|
||||||
class Course(models.Model):
|
class Course(models.Model):
|
||||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||||
title = models.CharField(max_length=255, verbose_name="Course Title")
|
title = models.CharField(max_length=255, verbose_name="Course Title")
|
||||||
|
|
@ -13,7 +12,7 @@ class Course(models.Model):
|
||||||
image = models.ImageField(upload_to="courses/image", null=True)
|
image = models.ImageField(upload_to="courses/image", null=True)
|
||||||
is_paid = models.BooleanField(default=False)
|
is_paid = models.BooleanField(default=False)
|
||||||
price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
|
price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
|
||||||
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='courses_taught', verbose_name="Instructor")
|
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='owner', verbose_name="Owner")
|
||||||
rating = models.PositiveSmallIntegerField(null=True, blank=True)
|
rating = models.PositiveSmallIntegerField(null=True, blank=True)
|
||||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Created At")
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Created At")
|
||||||
updated_at = models.DateTimeField(auto_now=True, verbose_name="Updated At")
|
updated_at = models.DateTimeField(auto_now=True, verbose_name="Updated At")
|
||||||
|
|
@ -29,7 +28,6 @@ class Course(models.Model):
|
||||||
raise ValidationError({'price': 'Price must be empty for free products.'})
|
raise ValidationError({'price': 'Price must be empty for free products.'})
|
||||||
|
|
||||||
|
|
||||||
# Table for modules (Module)
|
|
||||||
class Module(models.Model):
|
class Module(models.Model):
|
||||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||||
title = models.CharField(max_length=255, verbose_name="Module Title")
|
title = models.CharField(max_length=255, verbose_name="Module Title")
|
||||||
|
|
@ -40,7 +38,6 @@ class Module(models.Model):
|
||||||
def str(self):
|
def str(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
# Table for lessons (Lesson)
|
|
||||||
class Lesson(models.Model):
|
class Lesson(models.Model):
|
||||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||||
title = models.CharField(max_length=255, verbose_name="Lesson Title")
|
title = models.CharField(max_length=255, verbose_name="Lesson Title")
|
||||||
|
|
@ -54,7 +51,6 @@ class Lesson(models.Model):
|
||||||
def str(self):
|
def str(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
# Table for enrollments (Enrollment)
|
|
||||||
class Enrollment(models.Model):
|
class Enrollment(models.Model):
|
||||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||||
student = models.ForeignKey(User, on_delete=models.CASCADE, related_name='students_enrollments', verbose_name="Student")
|
student = models.ForeignKey(User, on_delete=models.CASCADE, related_name='students_enrollments', verbose_name="Student")
|
||||||
|
|
@ -65,7 +61,6 @@ class Enrollment(models.Model):
|
||||||
def str(self):
|
def str(self):
|
||||||
return f"{self.student.username} - {self.course.title}"
|
return f"{self.student.username} - {self.course.title}"
|
||||||
|
|
||||||
# Table for certificates (Certificate)
|
|
||||||
class Certificate(models.Model):
|
class Certificate(models.Model):
|
||||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||||
student = models.ForeignKey(User, on_delete=models.CASCADE, related_name='certificates', verbose_name="Student")
|
student = models.ForeignKey(User, on_delete=models.CASCADE, related_name='certificates', verbose_name="Student")
|
||||||
|
|
@ -75,3 +70,16 @@ class Certificate(models.Model):
|
||||||
|
|
||||||
def str(self):
|
def str(self):
|
||||||
return f"{self.student.username} - {self.course.title}"
|
return f"{self.student.username} - {self.course.title}"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class AD(models.Model):
|
||||||
|
title = models.CharField(max_length=255)
|
||||||
|
description = models.TextField()
|
||||||
|
image = models.ImageField(upload_to='ads_images/', blank=True, null=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
|
|
@ -27,10 +27,6 @@ class IsOwnerOrReadOnly(BasePermission):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class IsAdmin(BasePermission):
|
class IsAdmin(BasePermission):
|
||||||
"""
|
"""
|
||||||
Custom permission to allow access only to users with role 'instructor'.
|
Custom permission to allow access only to users with role 'instructor'.
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from django.contrib.auth import authenticate
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from allauth.account.models import EmailAddress
|
from allauth.account.models import EmailAddress
|
||||||
from dj_rest_auth.registration.serializers import RegisterSerializer
|
from dj_rest_auth.registration.serializers import RegisterSerializer
|
||||||
|
from lms.utils.exception_handler import CustomValidationError
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
8
lms/app/tasks.py
Normal file
8
lms/app/tasks.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# tasks.py
|
||||||
|
|
||||||
|
from celery import shared_task
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def print_message(message):
|
||||||
|
print(f"الرسالة هي: {message}")
|
||||||
|
return message
|
||||||
|
|
@ -13,6 +13,7 @@ from django.contrib.auth import get_user_model
|
||||||
from lms.utils.exception_handler import CustomValidationError
|
from lms.utils.exception_handler import CustomValidationError
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
class CourseViewSet(ModelViewSet):
|
class CourseViewSet(ModelViewSet):
|
||||||
|
|
@ -32,21 +33,14 @@ class CourseViewSet(ModelViewSet):
|
||||||
|
|
||||||
@action(detail=False, methods=['get'], url_path='my-courses', url_name='my_courses')
|
@action(detail=False, methods=['get'], url_path='my-courses', url_name='my_courses')
|
||||||
def get_my_course(self, request):
|
def get_my_course(self, request):
|
||||||
"""
|
my_courses = Course.objects.filter(owner=request.user).prefetch_related('enrollments__student')
|
||||||
Custom GET method to fetch detailed information about my courses.
|
|
||||||
"""
|
|
||||||
|
|
||||||
my_courses = Course.objects.filter(owner=request.user)
|
|
||||||
|
|
||||||
total_students = Enrollment.objects.filter(course__in=my_courses).values('student').distinct().count()
|
total_students = Enrollment.objects.filter(course__in=my_courses).values('student').distinct().count()
|
||||||
|
|
||||||
# Serialize the data
|
|
||||||
serializer = self.get_serializer(my_courses, many=True)
|
serializer = self.get_serializer(my_courses, many=True)
|
||||||
response_data = {
|
response_data = {
|
||||||
"total_students": total_students, # Add the total count of students
|
"total_students": total_students,
|
||||||
"courses": serializer.data # Include detailed courses data
|
"courses": serializer.data
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response(response_data)
|
return Response(response_data)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -65,9 +59,9 @@ class ModuleViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
course_id = self.request.query_params.get('pk')
|
course_id = self.request.query_params.get('pk')
|
||||||
if course_id:
|
if course_id:
|
||||||
course = Course.objects.filter(id=course_id).first()
|
course = Course.objects.filter(id=course_id).select_related('owner').first()
|
||||||
if course:
|
if course:
|
||||||
return Module.objects.filter(course=course)
|
return Module.objects.filter(course=course).select_related('course')
|
||||||
return Module.objects.none()
|
return Module.objects.none()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -82,7 +76,7 @@ class ModuleViewSet(ModelViewSet):
|
||||||
raise PermissionDenied("You do not have permission to create module.")
|
raise PermissionDenied("You do not have permission to create module.")
|
||||||
|
|
||||||
if not course:
|
if not course:
|
||||||
return Response(
|
raise CustomValidationError(
|
||||||
{"detail": "This course not found."},
|
{"detail": "This course not found."},
|
||||||
status=status.HTTP_404_NOT_FOUND,
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
)
|
)
|
||||||
|
|
@ -116,7 +110,7 @@ class LessonViewSet(ModelViewSet):
|
||||||
return Lesson.objects.none() # Return no results if the module does not exist
|
return Lesson.objects.none() # Return no results if the module does not exist
|
||||||
|
|
||||||
# Verify that the lesson exists and is associated with the module
|
# Verify that the lesson exists and is associated with the module
|
||||||
lesson = Lesson.objects.filter(id=lesson_id, module=module).first()
|
lesson = Lesson.objects.filter(id=lesson_id, module=module).select_related('module__course__owner').first()
|
||||||
if not lesson:
|
if not lesson:
|
||||||
return Lesson.objects.none() # Return no results if the lesson does not exist or is not linked to the module
|
return Lesson.objects.none() # Return no results if the lesson does not exist or is not linked to the module
|
||||||
|
|
||||||
|
|
@ -154,12 +148,12 @@ class LessonViewSet(ModelViewSet):
|
||||||
# الحصول على معرف الكائن (lesson_id) من الـ URL
|
# الحصول على معرف الكائن (lesson_id) من الـ URL
|
||||||
lesson_id = self.request.query_params.get('lesson_id')
|
lesson_id = self.request.query_params.get('lesson_id')
|
||||||
if not lesson_id:
|
if not lesson_id:
|
||||||
return Response({"detail": "Lesson ID is required in the URL."}, status=status.HTTP_400_BAD_REQUEST)
|
raise CustomValidationError({"detail": "Lesson ID is required in the URL."}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# البحث عن الدرس
|
# البحث عن الدرس
|
||||||
lesson = Lesson.objects.filter(id=lesson_id).first()
|
lesson = Lesson.objects.filter(id=lesson_id).first()
|
||||||
if not lesson:
|
if not lesson:
|
||||||
return Response({"detail": "Lesson not found."}, status=status.HTTP_404_NOT_FOUND)
|
raise CustomValidationError({"detail": "Lesson not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
# التحقق من الصلاحيات (مالك الكورس أو مسجل فيه)
|
# التحقق من الصلاحيات (مالك الكورس أو مسجل فيه)
|
||||||
is_owner = lesson.module.course.owner == request.user
|
is_owner = lesson.module.course.owner == request.user
|
||||||
|
|
@ -173,7 +167,7 @@ class LessonViewSet(ModelViewSet):
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
raise CustomValidationError(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -186,7 +180,7 @@ class LessonViewSet(ModelViewSet):
|
||||||
# الحصول على معرف الكائن (lesson_id) من الـ URL
|
# الحصول على معرف الكائن (lesson_id) من الـ URL
|
||||||
lesson_id = request.query_params.get('lesson_id')
|
lesson_id = request.query_params.get('lesson_id')
|
||||||
if not lesson_id:
|
if not lesson_id:
|
||||||
return Response(
|
raise CustomValidationError(
|
||||||
{"detail": "Lesson ID is required in the URL."},
|
{"detail": "Lesson ID is required in the URL."},
|
||||||
status=status.HTTP_400_BAD_REQUEST
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
)
|
)
|
||||||
|
|
@ -195,7 +189,7 @@ class LessonViewSet(ModelViewSet):
|
||||||
try:
|
try:
|
||||||
lesson = Lesson.objects.get(id=lesson_id)
|
lesson = Lesson.objects.get(id=lesson_id)
|
||||||
except Lesson.DoesNotExist:
|
except Lesson.DoesNotExist:
|
||||||
return Response(
|
raise CustomValidationError(
|
||||||
{"detail": "Lesson not found."},
|
{"detail": "Lesson not found."},
|
||||||
status=status.HTTP_404_NOT_FOUND
|
status=status.HTTP_404_NOT_FOUND
|
||||||
)
|
)
|
||||||
|
|
@ -223,7 +217,7 @@ class EnrollmentViewSet(ModelViewSet):
|
||||||
http_method_names = ['get', 'post', 'delete']
|
http_method_names = ['get', 'post', 'delete']
|
||||||
|
|
||||||
def list(self, request, *args, **kwargs):
|
def list(self, request, *args, **kwargs):
|
||||||
instance = Enrollment.objects.filter(student=request.user)
|
instance = Enrollment.objects.filter(student=request.user).select_related('course__owner')
|
||||||
|
|
||||||
serializer = self.get_serializer(instance, many=True)
|
serializer = self.get_serializer(instance, many=True)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
@ -237,16 +231,16 @@ class EnrollmentViewSet(ModelViewSet):
|
||||||
try:
|
try:
|
||||||
course = Course.objects.get(id=course_id)
|
course = Course.objects.get(id=course_id)
|
||||||
except Course.DoesNotExist:
|
except Course.DoesNotExist:
|
||||||
return Response({"detail": "Course not found"}, status=status.HTTP_404_NOT_FOUND)
|
raise CustomValidationError({"detail": "Course not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
if course.is_paid:
|
if course.is_paid:
|
||||||
return Response({"detail": "This is paid"}, status=status.HTTP_404_NOT_FOUND)
|
raise CustomValidationError({"detail": "This is paid"}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
if Enrollment.objects.filter(student=request.user, course=course).exists():
|
if Enrollment.objects.filter(student=request.user, course=course).exists():
|
||||||
return Response({"detail": "You are already subscribed to this course."}, status=status.HTTP_404_NOT_FOUND)
|
raise CustomValidationError({"detail": "You are already subscribed to this course."}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
if course.owner == request.user:
|
if course.owner == request.user:
|
||||||
return Response({"detail": "You can't enroll in your course"}, status=status.HTTP_404_NOT_FOUND)
|
raise CustomValidationError({"detail": "You can't enroll in your course"}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
# Create a new enrollment
|
# Create a new enrollment
|
||||||
enrollment = Enrollment.objects.create(student=request.user, course=course)
|
enrollment = Enrollment.objects.create(student=request.user, course=course)
|
||||||
|
|
@ -267,7 +261,7 @@ class EnrollmentViewSet(ModelViewSet):
|
||||||
student_email = request.data.get('student_email').strip()
|
student_email = request.data.get('student_email').strip()
|
||||||
|
|
||||||
# Check if the course & student exists
|
# Check if the course & student exists
|
||||||
course = Course.objects.filter(id=course_id).first()
|
course = Course.objects.filter(id=course_id).select_related('owner').first()
|
||||||
student = User.objects.filter(email=student_email).first()
|
student = User.objects.filter(email=student_email).first()
|
||||||
|
|
||||||
if not student:
|
if not student:
|
||||||
|
|
@ -304,17 +298,31 @@ class EnrollmentViewSet(ModelViewSet):
|
||||||
@action(detail=False, methods=['get'], url_path='get-my-students')
|
@action(detail=False, methods=['get'], url_path='get-my-students')
|
||||||
def get_my_students(self, request):
|
def get_my_students(self, request):
|
||||||
"""
|
"""
|
||||||
fetch detailed information about my students in my courses.
|
Fetch detailed information about my students in a specific course.
|
||||||
"""
|
"""
|
||||||
course = request.query_params.get('course')
|
course_id = request.query_params.get('course')
|
||||||
my_courses = Course.objects.filter(owner=request.user, id=course)
|
if not course_id:
|
||||||
|
raise CustomValidationError(
|
||||||
|
{"detail": "Course ID is required in the query parameters."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
course = Course.objects.get(id=course_id, owner=request.user)
|
||||||
|
except Course.DoesNotExist:
|
||||||
|
raise CustomValidationError(
|
||||||
|
{"detail": "Course not found or you do not have permission to access it."},
|
||||||
|
status=status.HTTP_404_NOT_FOUND
|
||||||
|
)
|
||||||
|
|
||||||
my_students = (
|
my_students = (
|
||||||
Enrollment.objects.filter(course__in=my_courses)
|
Enrollment.objects.filter(course=course)
|
||||||
.values('student__full_name', 'student__email')
|
.select_related('student')
|
||||||
.distinct()
|
.values('student__full_name', 'student__email')
|
||||||
)
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
return Response(list(my_students), status=200)
|
return Response(list(my_students), status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-djan
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
|
||||||
domain_name = "example.com" # Front end domain
|
domain_name = "learngpt.tech" # Front end domain
|
||||||
|
|
||||||
def _update_or_create_site_with_sequence(site_model, connection, domain, name):
|
def _update_or_create_site_with_sequence(site_model, connection, domain, name):
|
||||||
"""Update or create the site with default ID and keep the DB sequence in sync."""
|
"""Update or create the site with default ID and keep the DB sequence in sync."""
|
||||||
|
|
|
||||||
|
|
@ -5,18 +5,20 @@ from rest_framework.response import Response
|
||||||
|
|
||||||
|
|
||||||
class CustomValidationError(APIException):
|
class CustomValidationError(APIException):
|
||||||
status_code = status.HTTP_400_BAD_REQUEST
|
default_status = status.HTTP_400_BAD_REQUEST
|
||||||
default_detail = 'A validation error occurred.'
|
default_detail = 'A validation error occurred.'
|
||||||
|
|
||||||
def __init__(self, detail=None, status=None):
|
def __init__(self, detail=None, status=None):
|
||||||
self.detail = detail if detail is not None else self.default_detail
|
self.detail = detail if detail is not None else self.default_detail
|
||||||
self.status = status if status is not None else self.status
|
self.status = status if status is not None else self.default_status
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.detail} (Status: {self.status})"
|
return f"{self.detail} (Status: {self.status})"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status_code(self):
|
||||||
|
# Map `status` to `status_code` to ensure compatibility with DRF
|
||||||
|
return self.status
|
||||||
|
|
||||||
|
|
||||||
def custom_exception_handler(exc, context):
|
def custom_exception_handler(exc, context):
|
||||||
|
|
@ -28,9 +30,9 @@ def custom_exception_handler(exc, context):
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
'error': exc.detail,
|
'error': exc.detail,
|
||||||
'status_code': exc.status_code,
|
'status_code': exc.status,
|
||||||
},
|
},
|
||||||
status=exc.status_code,
|
status=exc.status,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Return the default response for other exceptions
|
# Return the default response for other exceptions
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue