Compare commits

..

1 commit

Author SHA1 Message Date
dependabot[bot]
bab4888dee Bump django-environ from 0.11.2 to 0.12.0
Bumps [django-environ](https://github.com/joke2k/django-environ) from 0.11.2 to 0.12.0.
- [Release notes](https://github.com/joke2k/django-environ/releases)
- [Changelog](https://github.com/joke2k/django-environ/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/joke2k/django-environ/commits)

---
updated-dependencies:
- dependency-name: django-environ
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-14 02:26:58 +00:00
220 changed files with 1177 additions and 16678 deletions

View file

@ -12,5 +12,3 @@ 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

View file

@ -5,12 +5,12 @@ 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=.example.com
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=

View file

@ -277,5 +277,3 @@ lms/media/
.pytest_cache/ .pytest_cache/
.ipython/ .ipython/
.env .env
# Ignore Django migrations

21
LICENSE
View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2025 Ahmed Nagi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

151
README.md
View file

@ -1,8 +1,6 @@
# Learning Management System (LMS) API with Vue.js # Learning Management System (LMS) API
Welcome to the Learning Management System (LMS) API! This project is a robust, scalable backend solution built with Django and Django Rest Framework (DRF), coupled with a modern and responsive frontend using Vue.js. It is designed to manage courses and their associated modules, providing a structured and secure platform for educational content delivery. Welcome to the Learning Management System (LMS) API! This project is a robust and scalable backend solution built with Django and Django Rest Framework (DRF). It is designed to manage courses and their associated modules, providing a structured and secure platform for educational content delivery.
![Image](image.jpg)
## Features ## Features
@ -11,38 +9,42 @@ Welcome to the Learning Management System (LMS) API! This project is a robust, s
- **Authentication & Permissions**: Secure access using Token Authentication and IsAuthenticated permissions. - **Authentication & Permissions**: Secure access using Token Authentication and IsAuthenticated permissions.
- **RESTful API Design**: Follows REST principles with hyperlinked relationships for intuitive navigation. - **RESTful API Design**: Follows REST principles with hyperlinked relationships for intuitive navigation.
- **Custom Query Logic**: Retrieve modules filtered by course ID for efficient data access. - **Custom Query Logic**: Retrieve modules filtered by course ID for efficient data access.
- **Interactive Frontend**: A Vue.js-powered frontend for seamless interaction with the backend API, including real-time updates and dynamic views.
## Technologies Used ## Technologies Used
### Backend - **Backend**: Django, Django Rest Framework (DRF)
- **Framework**: Django, Django Rest Framework (DRF)
- **Authentication**: dj-rest-auth & django-alluth - **Authentication**: dj-rest-auth & django-alluth
- **Database**: PostgreSQL - **Database**: PostgreSQL
- **API Documentation**: Auto-generated using drf-spectacular browsable API. - **API Documentation**: Auto-generated using drf_yasg browsable API.
- **Project Scaffold**: Cookiecutter Django
### Frontend ## Getting Started
- **Framework**: Vue.js 3
- **Routing**: Vue Router
- **UI Framework**: Tailwind CSS, DaisyUI
### Integration # Prerequisites
1. **API Consumption**: Use Axios or Fetch API in Vue.js to interact with the backend API endpoints. - [Docker](https://docs.docker.com/docker-for-mac/install/)
2. **Authentication**: Implement login and token storage using Vuex/Pinia or localStorage.
3. **Components**: Create reusable Vue components for courses, modules, authentication, and navigation. ## Local Development
4. **Routing**: Use Vue Router to manage navigation between pages like course lists, module details, and user authentication.
Start the dev server for local development:
```bash
docker-compose up
```
Run a command inside the docker container:
```bash
docker-compose run --rm web [command]
```
## API Endpoints ## API Endpoints
This project includes a fully interactive API documentation powered by drf-spectacular, a library for generating Swagger and ReDoc documentation for Django REST Framework (DRF).
### Features This project includes a fully interactive API documentation powered by drf_yasg, a library for generating Swagger and ReDoc documentation for Django REST Framework (DRF).
Features
- **Interactive Swagger UI**: Test API endpoints directly within the browser. * Interactive Swagger UI: Test API endpoints directly within the browser.
- **MkDocs Material Interface**: A clean and customizable documentation tool with a modern Material Design theme. * ReDoc Interface: Professionally styled documentation for better readabi* lity.
- **Auto-generated**: No need to write documentation manually; drf-spectacularyasg extracts the information from DRF views and serializers. * Auto-generated: No need to write documentation manually; drf_yasg extracts t* he information from DRF views and serializers.
## Contributing ## Contributing
@ -57,7 +59,106 @@ Contributions are welcome! If youd like to contribute, please follow these st
This project is licensed under the MIT License. See the LICENSE file for details. This project is licensed under the MIT License. See the LICENSE file for details.
--- ## Acknowledgments
By combining the power of Django and Vue.js, this LMS API provides a full-stack solution for managing and delivering educational content effectively. Happy coding! - Built with ❤️ using Django and Django Rest Framework.
- Inspired by the need for scalable and secure e-learning solutions.
Feel free to explore the API and contribute to its development. For any questions or feedback, please open an issue or contact the maintainers. Happy coding! 🚀
This README is clear, concise, and provides all the necessary information for users and contributors.
[![Built with Cookiecutter Django](https://img.shields.io/badge/built%20with-Cookiecutter%20Django-ff69b4.svg?logo=cookiecutter)](https://github.com/cookiecutter/cookiecutter-django/)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
## Settings
Moved to [settings](https://cookiecutter-django.readthedocs.io/en/latest/1-getting-started/settings.html).
## Basic Commands
### Setting Up Your Users
- To create a **normal user account**, just go to Sign Up and fill out the form. Once you submit it, you'll see a "Verify Your E-mail Address" page. Go to your console to see a simulated email verification message. Copy the link into your browser. Now the user's email should be verified and ready to go.
- To create a **superuser account**, use this command:
$ python manage.py createsuperuser
For convenience, you can keep your normal user logged in on Chrome and your superuser logged in on Firefox (or similar), so that you can see how the site behaves for both kinds of users.
### Type checks
Running type checks with mypy:
$ mypy lms
### Test coverage
To run the tests, check your test coverage, and generate an HTML coverage report:
$ coverage run -m pytest
$ coverage html
$ open htmlcov/index.html
#### Running tests with pytest
$ pytest
### Live reloading and Sass CSS compilation
Moved to [Live reloading and SASS compilation](https://cookiecutter-django.readthedocs.io/en/latest/2-local-development/developing-locally.html#using-webpack-or-gulp).
### Celery
This app comes with Celery.
To run a celery worker:
```bash
cd lms
celery -A config.celery_app worker -l info
```
Please note: For Celery's import magic to work, it is important _where_ the celery commands are run. If you are in the same folder with _manage.py_, you should be right.
To run [periodic tasks](https://docs.celeryq.dev/en/stable/userguide/periodic-tasks.html), you'll need to start the celery beat scheduler service. You can start it as a standalone process:
```bash
cd lms
celery -A config.celery_app beat
```
or you can embed the beat service inside a worker with the `-B` option (not recommended for production use):
```bash
cd lms
celery -A config.celery_app worker -B -l info
```
### Email Server
In development, it is often nice to be able to see emails that are being sent from your application. For that reason local SMTP server [Mailpit](https://github.com/axllent/mailpit) with a web interface is available as docker container.
Container mailpit will start automatically when you will run all docker containers.
Please check [cookiecutter-django Docker documentation](https://cookiecutter-django.readthedocs.io/en/latest/2-local-development/developing-locally-docker.html) for more details how to start all containers.
With Mailpit running, to view messages that are sent by your application, open your browser and go to `http://127.0.0.1:8025`
### Sentry
Sentry is an error logging aggregator service. You can sign up for a free account at <https://sentry.io/signup/?code=cookiecutter> or download and host it yourself.
The system is set up with reasonable defaults, including 404 logging and integration with the WSGI application.
You must set the DSN url in production.
## Deployment
The following details how to deploy this application.
### Docker
See detailed [cookiecutter-django Docker documentation](https://cookiecutter-django.readthedocs.io/en/latest/3-deployment/deployment-with-docker.html).

View file

@ -1,57 +0,0 @@
{ pkgs, ... }: {
# Which nixpkgs channel to use.
channel = "stable-24.05"; # or "unstable"
# Use https://search.nixos.org/packages to find packages
packages = [
pkgs.docker
pkgs.docker-compose
pkgs.sudo
];
# Sets environment variables in the workspace
env = {
PORT = "6000";
};
services.docker.enable = true;
idx = {
# Search for the extensions you want on https://open-vsx.org/ and use "publisher.id"
extensions = [
"ms-azuretools.vscode-docker"
];
workspace = {
# Runs when a workspace is first created with this `dev.nix` file
onCreate = {
setup-docker-compose = ''
# Ensure Docker Compose is built only once
docker-compose -f docker-compose.local.yml build
'';
# Open editors for the following files by default, if they exist:
default.openFiles = ["docker-compose.local.yml"];
};
# To run something each time the workspace is (re)started, use the `onStart` hook
onStart = {
start-docker-compose = ''
docker-compose -f docker-compose.local.yml up
'';
};
};
# Enable previews and customize configuration
previews = {
enable = true;
previews = {
web = {
command = ["docker-compose" "-f" "docker-compose.local.yml" "up"];
env = {
PORT = "$PORT";
};
manager = "web";
};
};
};
};
}

View file

@ -1,4 +0,0 @@
{
"IDX.aI.enableInlineCompletion": true,
"IDX.aI.enableCodebaseIndexing": true
}

View file

@ -1,91 +0,0 @@
[![Built with Cookiecutter Django](https://img.shields.io/badge/built%20with-Cookiecutter%20Django-ff69b4.svg?logo=cookiecutter)](https://github.com/cookiecutter/cookiecutter-django/)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
## Settings
Moved to [settings](https://cookiecutter-django.readthedocs.io/en/latest/1-getting-started/settings.html).
## Basic Commands
### Setting Up Your Users
- To create a **normal user account**, just go to Sign Up and fill out the form. Once you submit it, you'll see a "Verify Your E-mail Address" page. Go to your console to see a simulated email verification message. Copy the link into your browser. Now the user's email should be verified and ready to go.
- To create a **superuser account**, use this command:
$ python manage.py createsuperuser
For convenience, you can keep your normal user logged in on Chrome and your superuser logged in on Firefox (or similar), so that you can see how the site behaves for both kinds of users.
### Type checks
Running type checks with mypy:
$ mypy lms
### Test coverage
To run the tests, check your test coverage, and generate an HTML coverage report:
$ coverage run -m pytest
$ coverage html
$ open htmlcov/index.html
#### Running tests with pytest
$ pytest
### Live reloading and Sass CSS compilation
Moved to [Live reloading and SASS compilation](https://cookiecutter-django.readthedocs.io/en/latest/2-local-development/developing-locally.html#using-webpack-or-gulp).
### Celery
This app comes with Celery.
To run a celery worker:
```bash
cd lms
celery -A config.celery_app worker -l info
```
Please note: For Celery's import magic to work, it is important _where_ the celery commands are run. If you are in the same folder with _manage.py_, you should be right.
To run [periodic tasks](https://docs.celeryq.dev/en/stable/userguide/periodic-tasks.html), you'll need to start the celery beat scheduler service. You can start it as a standalone process:
```bash
cd lms
celery -A config.celery_app beat
```
or you can embed the beat service inside a worker with the `-B` option (not recommended for production use):
```bash
cd lms
celery -A config.celery_app worker -B -l info
```
### Email Server
In development, it is often nice to be able to see emails that are being sent from your application. For that reason local SMTP server [Mailpit](https://github.com/axllent/mailpit) with a web interface is available as docker container.
Container mailpit will start automatically when you will run all docker containers.
Please check [cookiecutter-django Docker documentation](https://cookiecutter-django.readthedocs.io/en/latest/2-local-development/developing-locally-docker.html) for more details how to start all containers.
With Mailpit running, to view messages that are sent by your application, open your browser and go to `http://127.0.0.1:8025`
### Sentry
Sentry is an error logging aggregator service. You can sign up for a free account at <https://sentry.io/signup/?code=cookiecutter> or download and host it yourself.
The system is set up with reasonable defaults, including 404 logging and integration with the WSGI application.
You must set the DSN url in production.
## Deployment
The following details how to deploy this application.
### Docker
See detailed [cookiecutter-django Docker documentation](https://cookiecutter-django.readthedocs.io/en/latest/3-deployment/deployment-with-docker.html).

View file

@ -1,23 +0,0 @@
# Use a lightweight Python base image
FROM python:3.12.8-alpine AS python
# Python base stage
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# Install dependencies
RUN apk update && apk add --no-cache \
# Runtime dependencies
make \
gettext \
&& rm -rf /var/cache/apk/*
# Install MkDocs and required plugins
RUN pip install mkdocs==1.5.1 mkdocs-material==9.1.15 mkdocs-markdownextradata-plugin
# Copy the start script
COPY ./compose/local/docs/start /start-docs
RUN sed -i 's/\r$//g' /start-docs
RUN chmod +x /start-docs
WORKDIR /docs

View file

@ -1,8 +0,0 @@
#!/bin/bash
set -o errexit
set -o pipefail
set -o nounset
# Start MkDocs live development server
exec mkdocs serve -a 0.0.0.0:6000

View file

@ -1,67 +0,0 @@
# اسم الموقع
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

View file

@ -1,37 +0,0 @@
from rest_framework.permissions import IsAuthenticated, BasePermission, SAFE_METHODS
import logging
logger = logging.getLogger(__name__)
class IsOwnerOrReadOnly(BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in SAFE_METHODS:
return True
view_name = view.__class__.__name__
match view_name:
case "CourseViewSet":
return obj.owner == request.user
case "ModuleViewSet":
return obj.created_by == request.user
case "LessonViewSet":
return obj.created_by == request.user
case "EnrollmentViewSet":
return obj.student == request.user
class IsAdmin(BasePermission):
"""
Custom permission to allow access only to users with role 'instructor'.
"""
def has_permission(self, request, view):
# Ensure the user is authenticated and has a role of 'instructor'
return request.user.is_authenticated and request.user.role == 'admin'

View file

@ -1,89 +0,0 @@
from rest_framework import serializers
from .models import *
from dj_rest_auth.serializers import LoginSerializer
from django.contrib.auth import authenticate
from django.utils.translation import gettext_lazy as _
from allauth.account.models import EmailAddress
from dj_rest_auth.registration.serializers import RegisterSerializer
from lms.utils.exception_handler import CustomValidationError
class CourseSerializer(serializers.ModelSerializer):
owner_name = serializers.CharField(source='owner.full_name', read_only=True)
owner_image = serializers.SerializerMethodField()
students_in_course = serializers.SerializerMethodField()
class Meta:
model = Course
fields = ['id', 'title', 'description', 'is_paid', 'price', 'image', 'owner_name', 'owner_image', 'students_in_course', 'created_at', 'updated_at']
read_only_fields = ['created_at', 'updated_at']
def get_owner_image(self, obj):
request = self.context.get('request')
if obj.owner.image:
return request.build_absolute_uri(obj.owner.image.url)
return None
def get_students_in_course(self, obj):
return Enrollment.objects.filter(course=obj).values('student').distinct().count()
class LessonSerializer(serializers.ModelSerializer):
module = serializers.PrimaryKeyRelatedField(queryset=Module.objects.all())
class Meta:
model = Lesson
fields = ['id', 'title', 'description', 'content', 'module', 'file']
class ModuleSerializer(serializers.ModelSerializer):
lessons = serializers.SerializerMethodField()
class Meta:
model = Module
fields = ['id', 'title', 'description', 'lessons', 'course']
read_only_fields = ['course', 'lessons']
def get_lessons(self, obj):
return obj.lessons.values('id', 'title', 'description')
class EnrollmentSerializer(serializers.ModelSerializer):
course_details = serializers.SerializerMethodField()
class Meta:
model = Enrollment
fields = ['id', 'course', 'course_details', 'enrolled_at', 'completed']
read_only_fields = ['enrolled_at']
def get_course_details(self, obj):
course = obj.course
request = self.context.get('request')
image_url = course.image.url if course.image else None
if image_url and request:
image_url = request.build_absolute_uri(image_url)
return {
"id": course.id,
"title": course.title,
"description": course.description,
"image": image_url,
"is_paid": course.is_paid,
"price": course.price,
"rating": course.rating,
}
class CertificateSerializer(serializers.ModelSerializer):
class Meta:
model = Certificate
fields = ['student', 'course', 'issued_at', 'certificate_file']
class PrivateEnrollmentSerializer(serializers.ModelSerializer):
class Meta:
model = Enrollment
fields = ['id', 'course', 'student', 'enrolled_at']
read_only_fields = ['enrolled_at']

View file

@ -1,3 +0,0 @@
from django.db.models.signals import pre_save
from django.dispatch import receiver
from .model import Module

View file

@ -1,8 +0,0 @@
# tasks.py
from celery import shared_task
@shared_task
def print_message(message):
print(f"الرسالة هي: {message}")
return message

View file

@ -1,107 +0,0 @@
from django.test import TestCase
from django.contrib.auth import get_user_model
from rest_framework.test import APIClient
from rest_framework.exceptions import ValidationError
from lms.app.models import Course, Module, Lesson, Enrollment, Certificate, AD
from uuid import uuid4
User = get_user_model()
class ModelsTestCase(TestCase):
def setUp(self):
# Create test user
self.user = User.objects.create_user(email='testuser@email.com', password='password')
self.client = APIClient()
self.client.force_authenticate(user=self.user)
# Create a test course
self.course = Course.objects.create(
title="Test Course",
description="A test course description",
is_paid=True,
price=100.00,
owner=self.user
)
def test_course_creation(self):
"""Test creating a course"""
self.assertEqual(Course.objects.count(), 1)
self.assertEqual(self.course.title, "Test Course")
self.assertTrue(self.course.is_paid)
self.assertEqual(self.course.price, 100.00)
def test_course_validation(self):
"""Test course validation logic"""
course = Course(
title="Invalid Course",
description="Should fail validation",
is_paid=True,
price=None, # Invalid case
owner=self.user
)
with self.assertRaises(ValidationError):
course.clean()
def test_module_creation(self):
"""Test creating a module"""
module = Module.objects.create(
title="Test Module",
description="A test module description",
course=self.course,
created_by=self.user
)
self.assertEqual(Module.objects.count(), 1)
self.assertEqual(module.title, "Test Module")
def test_lesson_creation(self):
"""Test creating a lesson"""
module = Module.objects.create(
title="Test Module",
course=self.course,
created_by=self.user
)
lesson = Lesson.objects.create(
title="Test Lesson",
description="A test lesson description",
content="Lesson content here",
module=module,
created_by=self.user
)
self.assertEqual(Lesson.objects.count(), 1)
self.assertEqual(lesson.title, "Test Lesson")
self.assertEqual(lesson.module, module)
def test_enrollment_creation(self):
"""Test creating an enrollment"""
enrollment = Enrollment.objects.create(
student=self.user,
course=self.course,
completed=False
)
self.assertEqual(Enrollment.objects.count(), 1)
self.assertEqual(enrollment.student, self.user)
self.assertEqual(enrollment.course, self.course)
def test_certificate_creation(self):
"""Test creating a certificate"""
certificate = Certificate.objects.create(
student=self.user,
course=self.course,
certificate_file='path/to/certificate.pdf'
)
self.assertEqual(Certificate.objects.count(), 1)
self.assertEqual(certificate.student, self.user)
self.assertEqual(certificate.course, self.course)
def test_ad_creation(self):
"""Test creating an ad"""
ad = AD.objects.create(
title="Test Ad",
description="This is a test ad",
image=None
)
self.assertEqual(AD.objects.count(), 1)
self.assertEqual(ad.title, "Test Ad")
self.assertEqual(ad.description, "This is a test ad")

View file

@ -1,328 +0,0 @@
from django.shortcuts import render
from .serializers import *
from .models import *
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from rest_framework.response import Response
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated, BasePermission
from .permissions import IsOwnerOrReadOnly, IsAdmin
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from django.contrib.auth import get_user_model
from lms.utils.exception_handler import CustomValidationError
User = get_user_model()
class CourseViewSet(ModelViewSet):
"""
A ViewSet for viewing and editing Course instances.
"""
queryset = Course.objects.all()
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
serializer_class = CourseSerializer
def perform_create(self, serializer):
"""
Save the post data when creating a new course.
"""
serializer.save(owner=self.request.user)
@action(detail=False, methods=['get'], url_path='my-courses', url_name='my_courses')
def get_my_course(self, request):
my_courses = Course.objects.filter(owner=request.user).prefetch_related('enrollments__student')
total_students = Enrollment.objects.filter(course__in=my_courses).values('student').distinct().count()
serializer = self.get_serializer(my_courses, many=True)
response_data = {
"total_students": total_students,
"courses": serializer.data
}
return Response(response_data)
class ModuleViewSet(ModelViewSet):
"""
ViewSet for managing modules.
"""
serializer_class = ModuleSerializer
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
def get_queryset(self):
"""
Return modules only if the user is the course owner.
"""
course_id = self.request.query_params.get('pk')
if course_id:
course = Course.objects.filter(id=course_id).select_related('owner').first()
if course:
return Module.objects.filter(course=course).select_related('course')
return Module.objects.none()
def perform_create(self, serializer):
"""
Allow only the course owner to create a module.
"""
course_id = self.request.data.get('course')
course = Course.objects.filter(id=course_id, owner=self.request.user).first()
is_owner = course.owner == self.request.user
if not is_owner:
raise PermissionDenied("You do not have permission to create module.")
if not course:
raise CustomValidationError(
{"detail": "This course not found."},
status=status.HTTP_404_NOT_FOUND,
)
serializer.save(course=course, created_by=self.request.user)
class LessonViewSet(ModelViewSet):
"""
ViewSet for managing lessons.
"""
serializer_class = LessonSerializer
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
def get_queryset(self):
"""
Return a specific lesson within a specific module only if the user is authorized.
"""
lesson_id = self.request.query_params.get('lesson_id') # Get lesson ID from the request
module_id = self.request.query_params.get('module_id') # Get module ID from the request
# Check if both lesson_id and module_id are provided
if not lesson_id or not module_id:
return Lesson.objects.none() # Return no results if either is missing
# Verify that the module exist
module = Module.objects.filter(id=module_id).first()
if not module:
return Lesson.objects.none() # Return no results if the module does not exist
# Verify that the lesson exists and is associated with the module
lesson = Lesson.objects.filter(id=lesson_id, module=module).select_related('module__course__owner').first()
if not lesson:
return Lesson.objects.none() # Return no results if the lesson does not exist or is not linked to the module
# Check if the user has access (owner of the course or enrolled in the course)
is_owner = module.course.owner == self.request.user
is_enrolled = Enrollment.objects.filter(course=module.course, student=self.request.user).exists()
if is_owner or is_enrolled:
return Lesson.objects.filter(id=lesson_id) # Return the lesson if the user is authorized
return Lesson.objects.none() # Deny access if the user is not authorized
def perform_create(self, serializer):
"""
Customize the creation of a lesson to include the module and the user who created it.
"""
module_id = self.request.data.get('module') # Get the module ID from the request
module = Module.objects.filter(id=module_id).first() # Fetch the module
is_owner = module.course.owner == self.request.user
if not is_owner:
raise PermissionDenied("You do not have permission to create lessons in this module.")
if not module:
raise serializers.ValidationError({"module": "Module does not exist."})
# Save the lesson with the module and created_by user
serializer.save(module=module, created_by=self.request.user)
@action(detail=False, methods=['patch'], url_path='update-lesson')
def patch_lesson(self, request, *args, **kwargs):
"""
Custom PATCH method to update a lesson.
"""
lesson_id = self.request.query_params.get('lesson_id')
if not lesson_id:
raise CustomValidationError({"detail": "Lesson ID is required in the URL."}, status=status.HTTP_400_BAD_REQUEST)
lesson = Lesson.objects.filter(id=lesson_id).first()
if not lesson:
raise CustomValidationError({"detail": "Lesson not found."}, status=status.HTTP_404_NOT_FOUND)
is_owner = lesson.module.course.owner == request.user
if not is_owner:
raise PermissionDenied("You do not have permission to update this lesson.")
serializer = self.get_serializer(lesson, data=request.data, partial=True) # partial=True لتحديث الحقول المطلوبة فقط
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
raise CustomValidationError(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@action(detail=False, methods=['delete'], url_path='delete-lesson')
def delete_lesson(self, request, *args, **kwargs):
"""
Custom DELETE method to delete a lesson.
"""
# الحصول على معرف الكائن (lesson_id) من الـ URL
lesson_id = request.query_params.get('lesson_id')
if not lesson_id:
raise CustomValidationError(
{"detail": "Lesson ID is required in the URL."},
status=status.HTTP_400_BAD_REQUEST
)
try:
lesson = Lesson.objects.get(id=lesson_id)
except Lesson.DoesNotExist:
raise CustomValidationError(
{"detail": "Lesson not found."},
status=status.HTTP_404_NOT_FOUND
)
is_owner = lesson.module.course.owner == request.user
if not is_owner:
raise PermissionDenied("You do not have permission to delete this lesson.")
lesson.delete()
return Response(
{"detail": "Lesson deleted successfully."},
status=status.HTTP_204_NO_CONTENT
)
class EnrollmentViewSet(ModelViewSet):
queryset = Enrollment.objects.all()
serializer_class = EnrollmentSerializer
permission_classes = [IsAuthenticated]
http_method_names = ['get', 'post', 'delete']
def list(self, request, *args, **kwargs):
instance = Enrollment.objects.filter(student=request.user).select_related('course__owner')
serializer = self.get_serializer(instance, many=True)
return Response(serializer.data)
def create(self, request, *args, **kwargs):
course_id = request.data.get('course_id')
# Check if the student and course exist
try:
course = Course.objects.get(id=course_id)
except Course.DoesNotExist:
raise CustomValidationError({"detail": "Course not found"}, status=status.HTTP_404_NOT_FOUND)
if course.is_paid:
raise CustomValidationError({"detail": "This is paid"}, status=status.HTTP_404_NOT_FOUND)
if Enrollment.objects.filter(student=request.user, course=course).exists():
raise CustomValidationError({"detail": "You are already subscribed to this course."}, status=status.HTTP_404_NOT_FOUND)
if course.owner == request.user:
raise CustomValidationError({"detail": "You can't enroll in your course"}, status=status.HTTP_404_NOT_FOUND)
# Create a new enrollment
enrollment = Enrollment.objects.create(student=request.user, course=course)
serializer = self.get_serializer(enrollment)
return Response(serializer.data, status=status.HTTP_201_CREATED)
@action(detail=False, methods=['post'], url_path='private-enrollment')
def private_enrollment( self, request):
"""
Handles the private enrollment of a student into a specific course.
This custom action allows the owner of a paid course to manually enroll a student
using their email address. The course ID is provided in the URL, and the student's
email is received in the request body.
"""
course_id = request.data.get('course')
student_email = request.data.get('student_email').strip()
# Check if the course & student exists
course = Course.objects.filter(id=course_id).select_related('owner').first()
student = User.objects.filter(email=student_email).first()
if not student:
raise CustomValidationError("User not found", status=status.HTTP_404_NOT_FOUND)
if student_email == request.user.email:
raise CustomValidationError("You can't add yourself", status=status.HTTP_400_BAD_REQUEST)
if Enrollment.objects.filter(student__email=student_email, course=course).exists():
raise CustomValidationError("This user already exists", status=status.HTTP_400_BAD_REQUEST)
if not course.is_paid:
raise CustomValidationError("Course is not paid", status=status.HTTP_400_BAD_REQUEST)
# Allow only the course owner to enroll students
if course.owner != request.user:
raise CustomValidationError("You do not have permission to enroll students in this course",
status=status.HTTP_403_FORBIDDEN)
# Validate the data before saving
enrollment_data = {
'course': course.id,
'student': student.id
}
serializer = PrivateEnrollmentSerializer(data=enrollment_data)
if serializer.is_valid():
serializer.save()
return Response(f"Student {student.full_name} has been added",
status=status.HTTP_201_CREATED)
raise CustomValidationError(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@action(detail=False, methods=['get'], url_path='get-my-students')
def get_my_students(self, request):
"""
Fetch detailed information about my students in a specific course.
"""
course_id = request.query_params.get('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 = (
Enrollment.objects.filter(course=course)
.select_related('student')
.values('student__full_name', 'student__email')
.distinct()
)
return Response(list(my_students), status=status.HTTP_200_OK)

View file

@ -1,6 +0,0 @@
{% load i18n %}
{% block content %}{% autoescape off %}{% blocktrans %}You're receiving this em{% endblocktrans %}
{{ password_reset_url }}

View file

@ -1,39 +0,0 @@
from rest_framework.exceptions import APIException
from rest_framework import status
from rest_framework.views import exception_handler
from rest_framework.response import Response
class CustomValidationError(APIException):
default_status = status.HTTP_400_BAD_REQUEST
default_detail = 'A validation error occurred.'
def __init__(self, detail=None, status=None):
self.detail = detail if detail is not None else self.default_detail
self.status = status if status is not None else self.default_status
def __str__(self):
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):
# Call REST framework's default exception handler first
response = exception_handler(exc, context)
# Handle CustomValidationError
if isinstance(exc, CustomValidationError):
return Response(
{
'error': exc.detail,
'status_code': exc.status,
},
status=exc.status,
)
# Return the default response for other exceptions
return response

View file

@ -0,0 +1,62 @@
# define an alias for the specific python version used in this file.
FROM docker.io/python:3.12.8-slim-bookworm AS python
# Python build stage
FROM python AS python-build-stage
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 PYTHONDONTWRITEBYTECODE=1
RUN apt-get update && apt-get install --no-install-recommends -y \
# To run the Makefile
make \
# psycopg dependencies
libpq-dev \
# Translations dependencies
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
COPY --from=python-build-stage /usr/src/app/wheels /wheels
# use wheels to install python dependencies
RUN pip install --no-cache /wheels/* \
&& rm -rf /wheels
COPY ./compose/local/docs/start /start-docs
RUN sed -i 's/\r$//g' /start-docs
RUN chmod +x /start-docs
WORKDIR /docs

7
compose/local/docs/start Normal file
View file

@ -0,0 +1,7 @@
#!/bin/bash
set -o errexit
set -o pipefail
set -o nounset
exec make livehtml

View file

@ -75,7 +75,6 @@ THIRD_PARTY_APPS = [
"crispy_forms", "crispy_forms",
"crispy_bootstrap5", "crispy_bootstrap5",
"allauth", "allauth",
'allauth.headless',
"allauth.account", "allauth.account",
"allauth.mfa", "allauth.mfa",
"allauth.socialaccount", "allauth.socialaccount",
@ -234,7 +233,7 @@ EMAIL_TIMEOUT = 5
# Django Admin URL. # Django Admin URL.
ADMIN_URL = "admin/" ADMIN_URL = "admin/"
# https://docs.djangoproject.com/en/dev/ref/settings/#admins # https://docs.djangoproject.com/en/dev/ref/settings/#admins
ADMINS = [("""ص""", "e@gmail.com")] ADMINS = [("""Ahmed Nagi""", "ahmed10nagi@gmail.com")]
# https://docs.djangoproject.com/en/dev/ref/settings/#managers # https://docs.djangoproject.com/en/dev/ref/settings/#managers
MANAGERS = ADMINS MANAGERS = ADMINS
# https://cookiecutter-django.readthedocs.io/en/latest/settings.html#other-environment-settings # https://cookiecutter-django.readthedocs.io/en/latest/settings.html#other-environment-settings
@ -320,37 +319,19 @@ ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_USER_MODEL_USERNAME_FIELD = None ACCOUNT_USER_MODEL_USERNAME_FIELD = None
# https://docs.allauth.org/en/latest/account/configuration.html # https://docs.allauth.org/en/latest/account/configuration.html
ACCOUNT_EMAIL_VERIFICATION = "mandatory" ACCOUNT_EMAIL_VERIFICATION = "mandatory"
ACCOUNT_LOGIN_METHODS = {"email"}
ACCOUNT_LOGOUT_ON_GET = True ACCOUNT_LOGOUT_ON_GET = True
ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = False LOGOUT_ON_PASSWORD_CHANGE = False
ACCOUNT_CHANGE_EMAIL = True ACCOUNT_CHANGE_EMAIL = True
# ACCOUNT_EMAIL_VERIFICATION_BY_CODE_ENABLED = True
HEADLESS_SERVE_SPECIFICATION = True
ACCOUNT_EMAIL_CONFIRMATION_HMAC = True ACCOUNT_EMAIL_CONFIRMATION_HMAC = True
ACCOUNT_CONFIRM_EMAIL_ON_GET = True ACCOUNT_CONFIRM_EMAIL_ON_GET = True
ACCOUNT_MAX_EMAIL_ADDRESSES = 2 ACCOUNT_MAX_EMAIL_ADDRESSES = 2
ACCOUNT_EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL = None ACCOUNT_EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL = None
ACCOUNT_EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL = None ACCOUNT_EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL = None
# ACCOUNT_RATE_LIMITS = { # ACCOUNT_RATE_LIMITS = {
# "confirm_email": "1/4m", # 1 confirmation email every 4 minutes # "confirm_email": "1/4m", # 1 confirmation email every 4 minutes
# } # }
HEADLESS_FRONTEND_URLS = {
"account_signup":"http://localhost:3000/account/signup",
"account_confirm_email": "http://127.0.0.1:3000/account/email-confirmation/{key}/",
# "https://app.project.org/account/email/verify-email?token={key}",
"account_reset_password": "https://app.project.org/account/password/reset",
"account_reset_password_from_key": "https://app.project.org/account/password/reset/key/{key}",
# "account_signup": "https://app.project.org/account/signup",
# Fallback in case the state containing the `next` URL is lost and the handshake
# with the third-party provider fails.
# "socialaccount_login_error": "https://app.project.org/account/provider/callback",
}
HEADLESS_ONLY = True
# https://docs.allauth.org/en/latest/account/configuration.html # https://docs.allauth.org/en/latest/account/configuration.html
# ACCOUNT_ADAPTER = "lms.accounts.adapters.CustomAccountAdapter" ACCOUNT_ADAPTER = "lms.accounts.adapters.CustomAccountAdapter"
# https://docs.allauth.org/en/latest/account/forms.html # https://docs.allauth.org/en/latest/account/forms.html
# ACCOUNT_FORMS = {"signup": "lms.users.forms.UserSignupForm"} # ACCOUNT_FORMS = {"signup": "lms.users.forms.UserSignupForm"}
# https://docs.allauth.org/en/latest/socialaccount/configuration.html # https://docs.allauth.org/en/latest/socialaccount/configuration.html
@ -372,13 +353,10 @@ REST_FRAMEWORK = {
'dj_rest_auth.jwt_auth.JWTCookieAuthentication', 'dj_rest_auth.jwt_auth.JWTCookieAuthentication',
), ),
"DEFAULT_PERMISSION_CLASSES": ( "DEFAULT_PERMISSION_CLASSES": (
# 'rest_framework.permissions.AllowAny',
"rest_framework.permissions.IsAuthenticated", "rest_framework.permissions.IsAuthenticated",
), ),
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
'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',
@ -390,15 +368,15 @@ REST_AUTH = {
from datetime import timedelta from datetime import timedelta
SIMPLE_JWT = { SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(hours=5), 'ACCESS_TOKEN_LIFETIME': timedelta(hours=5),
'REFRESH_TOKEN_LIFETIME': timedelta(days=15), 'REFRESH_TOKEN_LIFETIME': timedelta(days=30),
'ROTATE_REFRESH_TOKENS': True, 'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True, 'BLACKLIST_AFTER_ROTATION': True,
'ALGORITHM': 'HS256', 'ALGORITHM': 'HS256',
'SIGNING_KEY': 'env("SIGNING_KEY")', 'SIGNING_KEY': 'SECRET_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
@ -406,9 +384,8 @@ SPECTACULAR_SETTINGS = {
"TITLE": "Learning Management System API", "TITLE": "Learning Management System API",
"DESCRIPTION": "Documentation of API endpoints of Learning Management System", "DESCRIPTION": "Documentation of API endpoints of Learning Management System",
"VERSION": "1.0.0", "VERSION": "1.0.0",
# "SERVE_PERMISSIONS": ["rest_framework.permissions.IsAdminUser"], "SERVE_PERMISSIONS": ["rest_framework.permissions.IsAdminUser"],
"SCHEMA_PATH_PREFIX": "/api/", "SCHEMA_PATH_PREFIX": "/api/",
'SERVE_INCLUDE_SCHEMA': False,
} }
# Your stuff... # Your stuff...
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -8,21 +8,17 @@ from .base import env
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#debug # https://docs.djangoproject.com/en/dev/ref/settings/#debug
DEBUG = True DEBUG = True
DEBUG = True
# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
SECRET_KEY = env( SECRET_KEY = env(
"DJANGO_SECRET_KEY", "DJANGO_SECRET_KEY",
default="DM837WrWz7KIfZM2eb4swzqGlIG0VhhAIFNXf9KgamMtT42DTkHIEXfpF4N9rh2Y", default="DM837WrWz7KIfZM2eb4swzqGlIG0VhhAIFNXf9KgamMtT42DTkHIEXfpF4N9rh2Y",
) )
# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts # https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
ALLOWED_HOSTS = ["127.0.0.1", "localhost", ALLOWED_HOSTS = ["127.0.0.1", "localhost"] # حدد المضيفين المسموح بهم
"8000-idx-learning-management-systemgit-1737467650700.cluster-y34ecccqenfhcuavp7vbnxv7zk.cloudworkstations.dev"
] # حدد المضيفين المسموح بهم
CSRF_TRUSTED_ORIGINS = [ CSRF_TRUSTED_ORIGINS = [
'http://localhost:3000', 'http://localhost:3000',
'http://127.0.0.1:3000', 'http://127.0.0.1:3000',
'https://8000-idx-learning-management-systemgit-1737467650700.cluster-y34ecccqenfhcuavp7vbnxv7zk.cloudworkstations.dev'
] ]
CORS_ORIGIN_ALLOW_ALL = True CORS_ORIGIN_ALLOW_ALL = True
CORS_ALLOW_CREDENTIALS = False CORS_ALLOW_CREDENTIALS = False
@ -45,7 +41,6 @@ CORS_ALLOW_HEADERS = [
"user-agent", "user-agent",
"x-csrftoken", "x-csrftoken",
"x-requested-with", "x-requested-with",
"x-session-token",
] ]

View file

@ -12,18 +12,13 @@ from drf_spectacular.views import SpectacularSwaggerView
from rest_framework.authtoken.views import obtain_auth_token from rest_framework.authtoken.views import obtain_auth_token
from lms.accounts.views import * from lms.accounts.views import *
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import AllowAny
urlpatterns = [ 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("authwed/", include("allauth.urls")), path("accounts/", include("allauth.urls")),
# Your stuff: custom urls includes go here # Your stuff: custom urls includes go here
# ... # ...
# Media files # Media files
@ -35,12 +30,18 @@ if settings.DEBUG:
# API URLS # API URLS
urlpatterns += [ urlpatterns += [
path("api/accounts/", include("allauth.urls")),
path('api/auth/', include('lms.accounts.urls')), path('authw/', include('dj_rest_auth.urls')),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
path('auth/', include('lms.accounts.urls')),
path('api/app/', include('lms.app.urls')), path('app/', include('lms.app.urls')),
# API base url
# path("api/", include("config.api_router")),
# DRF auth token
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/",

View file

@ -12,5 +12,5 @@ services:
- ./config:/app/config:z - ./config:/app/config:z
- ./lms:/app/lms:z - ./lms:/app/lms:z
ports: ports:
- '6000:6000' - '9000:9000'
command: mkdocs serve command: /start-docs

29
docs/Makefile Normal file
View file

@ -0,0 +1,29 @@
# 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
docs/__init__.py Normal file
View file

@ -0,0 +1 @@
# Included so that Django's startproject comment runs against the docs directory

63
docs/conf.py Normal file
View file

@ -0,0 +1,63 @@
# 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"]

38
docs/howto.rst Normal file
View file

@ -0,0 +1,38 @@
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

23
docs/index.rst Normal file
View file

@ -0,0 +1,23 @@
.. 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`

46
docs/make.bat Normal file
View file

@ -0,0 +1,46 @@
@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

15
docs/users.rst Normal file
View file

@ -0,0 +1,15 @@
.. _users:
Users
======================================================================
Starting a new project, its 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 youll be able to customize it in the future if the need arises.
.. automodule:: lms.accounts.models
:members:
:noindex:

View file

@ -1,12 +0,0 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = crlf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false

30
frontend/.gitignore vendored
View file

@ -1,30 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

View file

@ -1,6 +0,0 @@
{
"recommendations": [
"Vue.volar",
"vitest.explorer"
]
}

View file

@ -1,38 +0,0 @@
# Add Domain link
Open the src/api.js file and add your domain link to the apiLink variable.
# vue-project
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
pnpm install
```
### Compile and Hot-Reload for Development
```sh
pnpm dev
```
### Compile and Minify for Production
```sh
pnpm build
```
### Run Unit Tests with [Vitest](https://vitest.dev/)
```sh
pnpm test:unit
```

View file

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="" data-theme="dracula">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LMS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View file

@ -1,8 +0,0 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

View file

@ -1,34 +0,0 @@
{
"name": "vue-project",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test:unit": "vitest"
},
"dependencies": {
"axios": "^1.7.9",
"shaka-player": "^4.12.8",
"sweetalert2": "^11.15.10",
"vue": "^3.5.13",
"vue-easy-lightbox": "^1.19.0",
"vue-i18n": "^11.0.1",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"@vue/cli-plugin-pwa": "^5.0.8",
"@vue/test-utils": "^2.4.6",
"autoprefixer": "^10.4.20",
"daisyui": "^4.12.23",
"jsdom": "^25.0.1",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.17",
"vite": "^6.0.5",
"vite-plugin-vue-devtools": "^7.6.8",
"vitest": "^2.1.8"
}
}

10678
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -1,34 +0,0 @@
<script setup>
import { reactive, provide } from "vue";
import { setLoadingHandler } from "@/api";
import Loading from "./components/Loading.vue";
const state = reactive({
isLoading: false, // حالة شاشة الانتظار
});
// وظيفة لتغيير حالة شاشة الانتظار
const setLoading = (status) => {
state.isLoading = status;
};
// ربط شاشة الانتظار مع axios
setLoadingHandler(setLoading);
// توفير الحالة للمكونات الفرعية إذا لزم الأمر
provide("isLoading", state.isLoading);
</script>
<template>
<div id="app">
<Loading v-if="state.isLoading" />
<header class="bg-white dark:bg-gray-800 shadow-md"></header>
<main>
<RouterView />
</main>
</div>
</template>
<style scoped>
</style>

View file

@ -1,155 +0,0 @@
import axios from "axios";
const apiLink = "https://...";
const loginPage = "/account/login/";
let setLoadingCallback = null;
const api = axios.create({
baseURL: apiLink,
timeout: 20000,
});
let isRefreshing = false;
let failedQueue = [];
const processQueue = (error, token = null) => {
failedQueue.forEach((prom) => {
if (token) {
prom.resolve(token);
} else {
prom.reject(error);
}
});
failedQueue = [];
};
// Function to check the validity of the refresh token
const isRefreshTokenValid = (refreshToken) => {
if (!refreshToken) return false;
try {
const payload = JSON.parse(atob(refreshToken.split('.')[1]));
const expirationTime = payload.exp * 1000; // Convert expiration time to milliseconds
return Date.now() < expirationTime;
} catch (error) {
return false;
}
};
// Interceptor to add the Access Token to each request
api.interceptors.request.use((config) => {
if (setLoadingCallback && !config.noL) {
setLoadingCallback(true);
}
const accessToken = localStorage.getItem("accessToken");
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
});
// Interceptor to handle errors
api.interceptors.response.use(
(response) => {
if (setLoadingCallback && !response.config.noL) {
setLoadingCallback(false);
}
return response;
},
async (error) => {
const originalRequest = error.config;
if (setLoadingCallback && !originalRequest.noL) {
setLoadingCallback(false);
}
if (
error.response &&
(error.response.status === 401 || error.response.status === 403) &&
!originalRequest._retry
) {
originalRequest._retry = true;
const refreshToken = localStorage.getItem("refreshToken");
if (refreshToken && isRefreshTokenValid(refreshToken)) {
if (!isRefreshing) {
isRefreshing = true;
try {
// Send a request to refresh the Access Token
const response = await axios.post(
`${apiLink}/auth/token/refresh/`,
{ refresh: refreshToken }
);
const newAccessToken = response.data.access;
localStorage.setItem("accessToken", newAccessToken);
// Process failed requests
processQueue(null, newAccessToken);
isRefreshing = false;
// Resend the original request
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
return api(originalRequest);
} catch (refreshError) {
// If the refresh request fails, clear tokens and redirect
processQueue(refreshError, null);
isRefreshing = false;
localStorage.clear();
window.location.href = loginPage;
return Promise.reject(refreshError);
}
}
// Manage failed requests during refresh
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
})
.then((token) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
return api(originalRequest);
})
.catch((err) => {
return Promise.reject(err);
});
} else {
// If there's no refresh token or it's expired, clear tokens and redirect
localStorage.clear();
window.location.href = loginPage;
}
}
// Handle other errors
if (
error.response &&
error.response.data &&
(error.response.data.detail === "Authentication credentials were not provided." ||
error.response.data.code === "token_not_valid" ||
error.response.data.code === "user_not_found"
)
) {
const refreshToken = localStorage.getItem("refreshToken");
if (!refreshToken || !isRefreshTokenValid(refreshToken)) {
localStorage.clear();
window.location.href = loginPage;
}
}
return Promise.reject(error);
}
);
// Function to set the loading callback for managing loading screens
export const setLoadingHandler = (callback) => {
setLoadingCallback = callback;
};
export default api;

View file

@ -1,86 +0,0 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View file

@ -1,268 +0,0 @@
import { createI18n } from "vue-i18n";
const messages = {
en: {
settings: {
title: "Settings",
language: "Language",
english: "English",
arabic: "Arabic",
email: "Email",
changeEmail: "Change Email",
password: "Password",
changePassword: "Change Password",
logout: "Logout",
logoutConfirmTitle: "Are you sure?",
logoutConfirmText: "You will be logged out.",
logoutConfirmButton: "Yes, Logout",
logoutCancelButton: "Cancel",
info: "Your Info",
},
dashboard: {
title: "Dashboard",
userImageAlt: "User Image",
createNewCourse: "Create New Course",
createCourseDescription: "Click here to create a new training course.",
numberOfCourses: "Number of Courses",
totalRevenue: "Total Revenue",
numberOfStudents: "Number of Students",
myTrainingCourses: "My Training Courses",
free: "Free",
students: "Students",
},
enrolle: {
enrolledCourses: "Enrolled Courses",
confirmDeleteTitle: "Are you sure?",
confirmDeleteText: "You will not be able to recover this course after deletion!",
confirmButtonText: "Yes, Unsubscribe!",
cancelButtonText: "Cancel",
deleteSuccessTitle: "Deleted!",
deleteSuccessMessage: "The course has been successfully deleted.",
deleteErrorTitle: "Error!",
deleteErrorMessage: "Failed to delete the course.",
},
infoCourse: {
backToDashboard: "Back to Dashboard",
price: "Price",
free: "Free",
students: "Students",
rating: "Rating",
status: "Status",
published: "Published",
unpublished: "Unpublished",
description: "Description",
noDescription: "No detailed description available.",
subscribe: "Subscribe",
errorTitle: "Error",
errorText: "Failed to fetch course details. Please try again later.",
successTitle: "Success!",
successText: "You have successfully subscribed to the course!",
editCourse: "Edit Course",
deleteCourse: "Delete Course",
courseName: "Course Name",
courseImage: "Course Image",
isPaid: "Is Paid",
saveChanges: "Save Changes",
cancel: "Cancel",
viewChapters: "View Chapters",
addStudent: "Add Student",
getStudents: "Student List",
},
lessonDetail: {
error: "An error occurred while loading the lesson.",
description: "Lesson Description",
content: "Lesson Content",
attachment: "Attached File",
viewFile: "View File",
video: "Video",
videoNotSupported: "Your browser does not support the video tag.",
fetchError: "Failed to fetch lesson data. Please try again.",
},
createCourse: {
title: "Create a New Course",
courseTitle: "Course Title",
enterCourseTitle: "Enter course title",
courseDescription: "Course Description",
enterCourseDescription: "Enter course description",
courseImage: "Course Image",
uploadCourseImage: "Upload course image",
isPaid: "Is the course paid?",
coursePrice: "Course Price",
enterCoursePrice: "Enter course price",
createCourseButton: "Create Course",
successMessage: "The course has been created successfully.",
errorMessage: "An error occurred.",
},
module: {
title: "Course Chapters",
chapterTitle: "Chapter Title",
chapterDescription: "Chapter Description",
lessonTitle: "Lesson Title",
lessonDescription: "Lesson Description",
viewButton: "View",
errorMessage: "An error occurred while fetching chapters.",
},
sidebar: {
home: "Home",
dashboard: "Dashboard",
enrollCourses: "Enroll Courses",
settings: "Settings",
contactUs: "Contact Us",
},
studentsTable: {
title: "Students List",
name: "Name",
enrolledTime: "Enrolled Time",
email: "Email",
actions: "Actions",
delete: "Delete",
loading: "Loading data...",
error: "An error occurred",
defaultError: "An unexpected error occurred",
deleteConfirmationTitle: "Are you sure?",
deleteConfirmationText: "You won't be able to revert this!",
cancel: "Cancel",
deleteSuccessTitle: "Deleted!",
deleteSuccessText: "The student has been deleted successfully.",
deleteErrorTitle: "Error!",
deleteErrorText: "An error occurred while deleting the student.",
},
},
ar: {
settings: {
title: "الإعدادات",
language: "اللغة",
english: "الإنجليزية",
arabic: "العربية",
email: "البريد الإلكتروني",
changeEmail: "تغيير البريد الإلكتروني",
password: "كلمة المرور",
changePassword: "تغيير كلمة المرور",
logout: "تسجيل الخروج",
logoutConfirmTitle: "هل أنت متأكد؟",
logoutConfirmText: "سيتم تسجيل خروجك.",
logoutConfirmButton: "نعم، سجل الخروج",
logoutCancelButton: "إلغاء",
info: "معلوماتك الشخصية",
},
dashboard: {
title: "لوحة التحكم",
userImageAlt: "صورة المستخدم",
createNewCourse: "إنشاء دورة جديدة",
createCourseDescription: "اضغط هنا لإنشاء دورة تدريبية جديدة.",
numberOfCourses: "عدد الدورات",
totalRevenue: "إجمالي الإيرادات",
numberOfStudents: "عدد الطلاب",
myTrainingCourses: "دوراتي التدريبية",
free: "مجاني",
students: "طلاب",
},
enrolle: {
enrolledCourses: "الدورات المشترك فيها",
confirmDeleteTitle: "هل أنت متأكد؟",
confirmDeleteText: "لن تتمكن من استعادة هذه الدورة بعد الحذف!",
confirmButtonText: "نعم، إلغاء الإشتراك!",
cancelButtonText: "إلغاء",
deleteSuccessTitle: "تم الحذف!",
deleteSuccessMessage: "تم حذف الدورة بنجاح.",
deleteErrorTitle: "خطأ!",
deleteErrorMessage: "فشل في حذف الدورة.",
},
infoCourse: {
backToDashboard: "العودة إلى لوحة التحكم",
price: "السعر",
free: "مجاني",
students: "الطلاب",
rating: "التقييم",
status: "الحالة",
published: "منشور",
unpublished: "غير منشور",
description: "الوصف",
noDescription: "لا يوجد وصف تفصيلي.",
subscribe: "اشترك",
errorTitle: "خطأ",
errorText: "فشل في جلب تفاصيل الدورة. يرجى المحاولة لاحقاً.",
successTitle: "تم بنجاح!",
successText: "لقد اشتركت في الدورة بنجاح!",
editCourse: "تعديل الدورة",
deleteCourse: "حذف الدورة",
courseName: "اسم الدورة",
courseImage: "صورة الدورة",
isPaid: "مدفوعة",
saveChanges: "حفظ التعديلات",
cancel: "إلغاء",
viewChapters: "عرض الفصول",
addStudent: "إضافة طالب",
getStudents: "قائمة الطلاب"
},
lessonDetail: {
error: "حدث خطأ أثناء تحميل الدرس.",
description: "وصف الدرس",
content: "محتوى الدرس",
attachment: "الملف المرفق",
viewFile: "عرض الملف",
video: "الفيديو",
videoNotSupported: "متصفحك لا يدعم تشغيل الفيديو.",
fetchError: "فشل في جلب بيانات الدرس. يرجى المحاولة مرة أخرى.",
},
createCourse: {
title: "إنشاء دورة جديدة",
courseTitle: "عنوان الدورة",
enterCourseTitle: "أدخل عنوان الدورة",
courseDescription: "وصف الدورة",
enterCourseDescription: "أدخل وصف الدورة",
courseImage: "صورة الدورة",
uploadCourseImage: "ارفع صورة الدورة",
isPaid: "هل الدورة مدفوعة؟",
coursePrice: "سعر الدورة",
enterCoursePrice: "أدخل سعر الدورة",
createCourseButton: "إنشاء الدورة",
successMessage: "تم إنشاء الدورة بنجاح.",
errorMessage: "حدث خطأ.",
},
module: {
title: "فصول الدورة",
chapterTitle: "عنوان الفصل",
chapterDescription: "وصف الفصل",
lessonTitle: "عنوان الدرس",
lessonDescription: "وصف الدرس",
viewButton: "عرض",
errorMessage: "حدث خطأ أثناء جلب الفصول.",
},
sidebar: {
home: "الرئيسية",
dashboard: "لوحة التحكم",
enrollCourses: "الدورات المشتركة",
settings: "الإعدادات",
contactUs: "الاتصال بنا",
},
studentsTable: {
title: "قائمة الطلاب",
name: "الاسم",
enrolledTime: "وقت التسجيل",
email: "البريد الإلكتروني",
actions: "الإجراءات",
delete: "حذف",
loading: "جاري تحميل البيانات...",
error: "حدث خطأ",
defaultError: "حدث خطأ غير متوقع",
deleteConfirmationTitle: "هل أنت متأكد؟",
deleteConfirmationText: "لن تتمكن من التراجع عن هذا الإجراء!",
cancel: "إلغاء",
deleteSuccessTitle: "تم الحذف!",
deleteSuccessText: "تم حذف الطالب بنجاح.",
deleteErrorTitle: "خطأ!",
deleteErrorText: "حدث خطأ أثناء محاولة حذف الطالب.",
},
},
};
const savedLanguage = localStorage.getItem("selectedLanguage") || "en";
const i18n = createI18n({
legacy: false,
locale: savedLanguage,
fallbackLocale: "en",
messages,
});
export default i18n;

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

Before

Width:  |  Height:  |  Size: 276 B

View file

@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View file

@ -1,42 +0,0 @@
<template>
<div class="dropdown dropdown-end">
<label tabindex="0" class="btn">
<IconLanguage />
</label>
<ul
tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box w-52 p-2 shadow mt-2"
>
<li>
<a @click="changeLanguage('ar')">{{ $t('settings.arabic') }}</a>
</li>
<li>
<a @click="changeLanguage('en')">{{ $t('settings.english') }}</a>
</li>
</ul>
</div>
</template>
<script>
import IconLanguage from "./icons/IconLanguage.vue";
import { useI18n } from "vue-i18n";
export default {
components: {
IconLanguage,
},
setup() {
const { locale } = useI18n();
const changeLanguage = (lang) => {
locale.value = lang;
localStorage.setItem('selectedLanguage', lang); // حفظ اللغة في التخزين المحلي
};
return {
changeLanguage,
};
},
};
</script>

View file

@ -1,53 +0,0 @@
<template>
<div class="navbar bg-base-300">
<div class="flex gap-x-4 ml-auto">
<!-- Dropdown for Theme -->
<div class="dropdown dropdown-end">
<label tabindex="0" class="btn">
<IconTheme />
</label>
<ul
tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box w-52 p-2 shadow mt-2"
>
<!-- عرض الثيمات كخيارات -->
<li v-for="theme in themes" :key="theme">
<a @click="changeTheme(theme)">{{ theme }}</a>
</li>
</ul>
</div>
</div>
</div>
</template>
<script>
import IconLanguage from "./icons/IconLanguage.vue";
import IconTheme from "./icons/IconTheme.vue";
export default {
components: {
IconLanguage,
IconTheme,
},
data() {
return {
themes: ["dark", "light", "night", "cupcake"],
};
},
mounted() {
const savedTheme = localStorage.getItem("theme");
if (savedTheme) {
this.changeTheme(savedTheme);
}
},
methods: {
changeTheme(theme) {
// تغيير الثيم
document.documentElement.setAttribute("data-theme", theme);
// حفظ الثيم في localStorage
localStorage.setItem("theme", theme);
},
},
};
</script>

View file

@ -1,20 +0,0 @@
<template>
<div>
<Sidebar />
<Navbar />
<router-view />
</div>
</template>
<script>
import Sidebar from './Sidebar.vue';
import Navbar from './Navbar.vue';
export default {
components: {
Sidebar,
Navbar,
},
};
</script>

Some files were not shown because too many files have changed in this diff Show more