Compare commits
1 commit
main
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79049dbd6c |
207 changed files with 672 additions and 16292 deletions
|
|
@ -12,5 +12,3 @@ REDIS_URL=redis://redis:6379/0
|
|||
# Flower
|
||||
CELERY_FLOWER_USER=debug
|
||||
CELERY_FLOWER_PASSWORD=debug
|
||||
|
||||
SIGNING_KEY=ebd0c2f345ede5we3244t5r34a0dc1b994e33e729e
|
||||
|
|
@ -5,12 +5,12 @@ DJANGO_SETTINGS_MODULE=config.settings.production
|
|||
DJANGO_SECRET_KEY=CQHQz4M3wN1VL2TT53Gl8yupKOjQ5m01js4jPw6bQsUexzkdy9JGXhQg9h6H24M5
|
||||
DJANGO_ADMIN_URL=6XfjlokEGlPf6SpVfGh7wBvs7t5ZFMDs/
|
||||
DJANGO_ALLOWED_HOSTS=.example.com
|
||||
SIGNING_KEY=HQz4M3wN1ebd0c2f345ede5we324@#$%$#@#R$Q#Zaexsredg/*43/54333e729e
|
||||
|
||||
# Security
|
||||
# ------------------------------------------------------------------------------
|
||||
# TIP: better off using DNS, however, redirect is OK too
|
||||
DJANGO_SECURE_SSL_REDIRECT=False
|
||||
|
||||
# Email
|
||||
# ------------------------------------------------------------------------------
|
||||
DJANGO_SERVER_EMAIL=
|
||||
0
backend/.gitattributes → .gitattributes
vendored
0
backend/.gitattributes → .gitattributes
vendored
0
backend/.gitignore → .gitignore
vendored
0
backend/.gitignore → .gitignore
vendored
|
|
@ -6,16 +6,12 @@
|
|||
packages = [
|
||||
pkgs.docker
|
||||
pkgs.docker-compose
|
||||
pkgs.sudo
|
||||
];
|
||||
|
||||
# Sets environment variables in the workspace
|
||||
env = {
|
||||
PORT = "6000";
|
||||
};
|
||||
env = {};
|
||||
services.docker.enable = true;
|
||||
|
||||
|
||||
idx = {
|
||||
# Search for the extensions you want on https://open-vsx.org/ and use "publisher.id"
|
||||
extensions = [
|
||||
21
LICENSE
21
LICENSE
|
|
@ -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
151
README.md
|
|
@ -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.
|
||||
|
||||
## 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.
|
||||
- **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.
|
||||
- **Interactive Frontend**: A Vue.js-powered frontend for seamless interaction with the backend API, including real-time updates and dynamic views.
|
||||
|
||||
## Technologies Used
|
||||
|
||||
### Backend
|
||||
- **Framework**: Django, Django Rest Framework (DRF)
|
||||
- **Backend**: Django, Django Rest Framework (DRF)
|
||||
- **Authentication**: dj-rest-auth & django-alluth
|
||||
- **Database**: PostgreSQL
|
||||
- **API Documentation**: Auto-generated using drf-spectacular browsable API.
|
||||
- **Project Scaffold**: Cookiecutter Django
|
||||
- **API Documentation**: Auto-generated using drf_yasg browsable API.
|
||||
|
||||
### Frontend
|
||||
- **Framework**: Vue.js 3
|
||||
- **Routing**: Vue Router
|
||||
- **UI Framework**: Tailwind CSS, DaisyUI
|
||||
## Getting Started
|
||||
|
||||
|
||||
### Integration
|
||||
# Prerequisites
|
||||
|
||||
1. **API Consumption**: Use Axios or Fetch API in Vue.js to interact with the backend API endpoints.
|
||||
2. **Authentication**: Implement login and token storage using Vuex/Pinia or localStorage.
|
||||
3. **Components**: Create reusable Vue components for courses, modules, authentication, and navigation.
|
||||
4. **Routing**: Use Vue Router to manage navigation between pages like course lists, module details, and user authentication.
|
||||
- [Docker](https://docs.docker.com/docker-for-mac/install/)
|
||||
|
||||
## Local Development
|
||||
|
||||
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
|
||||
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.
|
||||
- **MkDocs Material Interface**: A clean and customizable documentation tool with a modern Material Design theme.
|
||||
- **Auto-generated**: No need to write documentation manually; drf-spectacularyasg extracts the information from DRF views and serializers.
|
||||
* Interactive Swagger UI: Test API endpoints directly within the browser.
|
||||
* ReDoc Interface: Professionally styled documentation for better readabi* lity.
|
||||
* Auto-generated: No need to write documentation manually; drf_yasg extracts t* he information from DRF views and serializers.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
@ -57,7 +59,106 @@ Contributions are welcome! If you’d like to contribute, please follow these st
|
|||
|
||||
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.
|
||||
|
||||
|
||||
|
||||
|
||||
[](https://github.com/cookiecutter/cookiecutter-django/)
|
||||
[](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).
|
||||
|
|
|
|||
|
|
@ -1,91 +0,0 @@
|
|||
[](https://github.com/cookiecutter/cookiecutter-django/)
|
||||
[](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).
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
# tasks.py
|
||||
|
||||
from celery import shared_task
|
||||
|
||||
@shared_task
|
||||
def print_message(message):
|
||||
print(f"الرسالة هي: {message}")
|
||||
return message
|
||||
|
|
@ -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")
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}{% autoescape off %}{% blocktrans %}You're receiving this em{% endblocktrans %}
|
||||
|
||||
{{ password_reset_url }}
|
||||
|
|
@ -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
|
||||
62
compose/local/docs/Dockerfile
Normal file
62
compose/local/docs/Dockerfile
Normal 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
7
compose/local/docs/start
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
set -o nounset
|
||||
|
||||
exec make livehtml
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
FROM docker.io/traefik:3.3.0
|
||||
FROM docker.io/traefik:3.3.2
|
||||
RUN mkdir -p /etc/traefik/acme \
|
||||
&& touch /etc/traefik/acme/acme.json \
|
||||
&& chmod 600 /etc/traefik/acme/acme.json
|
||||
|
|
@ -75,7 +75,6 @@ THIRD_PARTY_APPS = [
|
|||
"crispy_forms",
|
||||
"crispy_bootstrap5",
|
||||
"allauth",
|
||||
'allauth.headless',
|
||||
"allauth.account",
|
||||
"allauth.mfa",
|
||||
"allauth.socialaccount",
|
||||
|
|
@ -234,7 +233,7 @@ EMAIL_TIMEOUT = 5
|
|||
# Django Admin URL.
|
||||
ADMIN_URL = "admin/"
|
||||
# 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
|
||||
MANAGERS = ADMINS
|
||||
# https://cookiecutter-django.readthedocs.io/en/latest/settings.html#other-environment-settings
|
||||
|
|
@ -246,23 +245,23 @@ DJANGO_ADMIN_FORCE_ALLAUTH = env.bool("DJANGO_ADMIN_FORCE_ALLAUTH", default=Fals
|
|||
# https://docs.djangoproject.com/en/dev/ref/settings/#logging
|
||||
# See https://docs.djangoproject.com/en/dev/topics/logging for
|
||||
# more details on how to customize your logging configuration.
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"verbose": {
|
||||
"format": "%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s",
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"level": "DEBUG",
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "verbose",
|
||||
},
|
||||
},
|
||||
"root": {"level": "INFO", "handlers": ["console"]},
|
||||
}
|
||||
# LOGGING = {
|
||||
# "version": 1,
|
||||
# "disable_existing_loggers": False,
|
||||
# "formatters": {
|
||||
# "verbose": {
|
||||
# "format": "%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s",
|
||||
# },
|
||||
# },
|
||||
# "handlers": {
|
||||
# "console": {
|
||||
# "level": "DEBUG",
|
||||
# "class": "logging.StreamHandler",
|
||||
# "formatter": "verbose",
|
||||
# },
|
||||
# },
|
||||
# "root": {"level": "INFO", "handlers": ["console"]},
|
||||
# }
|
||||
|
||||
REDIS_URL = env("REDIS_URL", default="redis://redis:6379/0")
|
||||
REDIS_SSL = REDIS_URL.startswith("rediss://")
|
||||
|
|
@ -320,37 +319,19 @@ ACCOUNT_USERNAME_REQUIRED = False
|
|||
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
|
||||
# https://docs.allauth.org/en/latest/account/configuration.html
|
||||
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
|
||||
ACCOUNT_LOGIN_METHODS = {"email"}
|
||||
ACCOUNT_LOGOUT_ON_GET = True
|
||||
ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = False
|
||||
LOGOUT_ON_PASSWORD_CHANGE = False
|
||||
ACCOUNT_CHANGE_EMAIL = True
|
||||
# ACCOUNT_EMAIL_VERIFICATION_BY_CODE_ENABLED = True
|
||||
HEADLESS_SERVE_SPECIFICATION = True
|
||||
|
||||
ACCOUNT_EMAIL_CONFIRMATION_HMAC = True
|
||||
ACCOUNT_CONFIRM_EMAIL_ON_GET = True
|
||||
ACCOUNT_MAX_EMAIL_ADDRESSES = 2
|
||||
|
||||
ACCOUNT_EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL = None
|
||||
ACCOUNT_EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL = None
|
||||
# ACCOUNT_RATE_LIMITS = {
|
||||
# "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
|
||||
# ACCOUNT_ADAPTER = "lms.accounts.adapters.CustomAccountAdapter"
|
||||
ACCOUNT_ADAPTER = "lms.accounts.adapters.CustomAccountAdapter"
|
||||
# https://docs.allauth.org/en/latest/account/forms.html
|
||||
# ACCOUNT_FORMS = {"signup": "lms.users.forms.UserSignupForm"}
|
||||
# https://docs.allauth.org/en/latest/socialaccount/configuration.html
|
||||
|
|
@ -372,13 +353,10 @@ REST_FRAMEWORK = {
|
|||
'dj_rest_auth.jwt_auth.JWTCookieAuthentication',
|
||||
),
|
||||
"DEFAULT_PERMISSION_CLASSES": (
|
||||
# 'rest_framework.permissions.AllowAny',
|
||||
"rest_framework.permissions.IsAuthenticated",
|
||||
),
|
||||
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||
'EXCEPTION_HANDLER': 'utils.exception_handler.custom_exception_handler',
|
||||
}
|
||||
|
||||
REST_AUTH = {
|
||||
'LOGIN_SERIALIZER': 'lms.accounts.serializers.CustomLoginSerializer',
|
||||
'REGISTER_SERIALIZER': 'lms.accounts.serializers.CustomRegisterSerializer',
|
||||
|
|
@ -390,15 +368,15 @@ REST_AUTH = {
|
|||
from datetime import timedelta
|
||||
SIMPLE_JWT = {
|
||||
'ACCESS_TOKEN_LIFETIME': timedelta(hours=5),
|
||||
'REFRESH_TOKEN_LIFETIME': timedelta(days=15),
|
||||
'REFRESH_TOKEN_LIFETIME': timedelta(days=30),
|
||||
'ROTATE_REFRESH_TOKENS': True,
|
||||
'BLACKLIST_AFTER_ROTATION': True,
|
||||
'ALGORITHM': 'HS256',
|
||||
'SIGNING_KEY': 'env("SIGNING_KEY")',
|
||||
'SIGNING_KEY': 'SECRET_KEY',
|
||||
}
|
||||
|
||||
# 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
|
||||
# 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",
|
||||
"DESCRIPTION": "Documentation of API endpoints of Learning Management System",
|
||||
"VERSION": "1.0.0",
|
||||
# "SERVE_PERMISSIONS": ["rest_framework.permissions.IsAdminUser"],
|
||||
"SERVE_PERMISSIONS": ["rest_framework.permissions.IsAdminUser"],
|
||||
"SCHEMA_PATH_PREFIX": "/api/",
|
||||
'SERVE_INCLUDE_SCHEMA': False,
|
||||
}
|
||||
# Your stuff...
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
@ -16,13 +16,13 @@ SECRET_KEY = env(
|
|||
)
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
|
||||
ALLOWED_HOSTS = ["127.0.0.1", "localhost",
|
||||
"8000-idx-learning-management-systemgit-1737467650700.cluster-y34ecccqenfhcuavp7vbnxv7zk.cloudworkstations.dev"
|
||||
"8000-idx-lms-1736953337728.cluster-p6qcyjpiljdwusmrjxdspyb5m2.cloudworkstations.dev"
|
||||
] # حدد المضيفين المسموح بهم
|
||||
|
||||
CSRF_TRUSTED_ORIGINS = [
|
||||
'http://localhost:3000',
|
||||
'http://127.0.0.1:3000',
|
||||
'https://8000-idx-learning-management-systemgit-1737467650700.cluster-y34ecccqenfhcuavp7vbnxv7zk.cloudworkstations.dev'
|
||||
'https://8000-idx-lms-1736953337728.cluster-p6qcyjpiljdwusmrjxdspyb5m2.cloudworkstations.dev'
|
||||
]
|
||||
CORS_ORIGIN_ALLOW_ALL = True
|
||||
CORS_ALLOW_CREDENTIALS = False
|
||||
|
|
@ -45,7 +45,6 @@ CORS_ALLOW_HEADERS = [
|
|||
"user-agent",
|
||||
"x-csrftoken",
|
||||
"x-requested-with",
|
||||
"x-session-token",
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -12,18 +12,13 @@ from drf_spectacular.views import SpectacularSwaggerView
|
|||
from rest_framework.authtoken.views import obtain_auth_token
|
||||
from lms.accounts.views import *
|
||||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import AllowAny
|
||||
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("", TemplateView.as_view(template_name="pages/home.html"), name="home"),
|
||||
# Django Admin, use {% url 'admin:index' %}
|
||||
path(settings.ADMIN_URL, admin.site.urls),
|
||||
|
||||
path("authwed/", include("allauth.urls")),
|
||||
# User management
|
||||
path("accounts/", include("allauth.urls")),
|
||||
# Your stuff: custom urls includes go here
|
||||
# ...
|
||||
# Media files
|
||||
|
|
@ -35,12 +30,18 @@ if settings.DEBUG:
|
|||
|
||||
# API URLS
|
||||
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/docs/",
|
||||
|
|
@ -12,5 +12,5 @@ services:
|
|||
- ./config:/app/config:z
|
||||
- ./lms:/app/lms:z
|
||||
ports:
|
||||
- '6000:6000'
|
||||
command: mkdocs serve
|
||||
- '9000:9000'
|
||||
command: /start-docs
|
||||
29
docs/Makefile
Normal file
29
docs/Makefile
Normal 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
1
docs/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Included so that Django's startproject comment runs against the docs directory
|
||||
63
docs/conf.py
Normal file
63
docs/conf.py
Normal 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
38
docs/howto.rst
Normal 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
23
docs/index.rst
Normal 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
46
docs/make.bat
Normal 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
15
docs/users.rst
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
.. _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:
|
||||
|
||||
|
|
@ -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
30
frontend/.gitignore
vendored
|
|
@ -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
|
||||
6
frontend/.vscode/extensions.json
vendored
6
frontend/.vscode/extensions.json
vendored
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"Vue.volar",
|
||||
"vitest.explorer"
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
|
@ -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
10678
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +0,0 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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 |
|
|
@ -1,3 +0,0 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
@ -1,174 +0,0 @@
|
|||
<template>
|
||||
<div v-if="isVisible" class="flex items-center justify-center fixed inset-0 bg-black bg-opacity-50 z-50">
|
||||
<div class="hourglass relative w-12 h-20 bg-gradient-to-b from-blue-500 via-gray-400 to-blue-500 clip-path-hourglass animate-flip">
|
||||
<div class="absolute top-1 left-1 w-10 h-16 bg-gray-800 clip-path-fill animate-fill"></div>
|
||||
<div class="absolute top-0 left-0 w-full h-full bg-gradient-to-r from-transparent via-gray-300 to-transparent opacity-30 animate-glare"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Hourglass",
|
||||
props: {
|
||||
isVisible: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@keyframes fill {
|
||||
from {
|
||||
clip-path: polygon(
|
||||
0% 0%,
|
||||
100% 0%,
|
||||
100% 24%,
|
||||
50% 47%,
|
||||
50% 47%,
|
||||
50% 47%,
|
||||
50% 47%,
|
||||
50% 47%,
|
||||
50% 47%,
|
||||
50% 47%,
|
||||
50% 47%,
|
||||
0% 24%
|
||||
);
|
||||
}
|
||||
10% {
|
||||
clip-path: polygon(
|
||||
0% 4%,
|
||||
100% 4%,
|
||||
100% 24%,
|
||||
55% 45%,
|
||||
55% 100%,
|
||||
55% 100%,
|
||||
55% 100%,
|
||||
45% 100%,
|
||||
45% 100%,
|
||||
45% 100%,
|
||||
45% 45%,
|
||||
0% 24%
|
||||
);
|
||||
}
|
||||
45% {
|
||||
clip-path: polygon(
|
||||
0% 24%,
|
||||
100% 24%,
|
||||
100% 24%,
|
||||
55% 45%,
|
||||
55% 80%,
|
||||
100% 100%,
|
||||
100% 100%,
|
||||
0% 100%,
|
||||
0% 100%,
|
||||
45% 80%,
|
||||
45% 45%,
|
||||
0% 24%
|
||||
);
|
||||
}
|
||||
80% {
|
||||
clip-path: polygon(
|
||||
45% 45%,
|
||||
55% 45%,
|
||||
55% 45%,
|
||||
55% 45%,
|
||||
55% 58%,
|
||||
100% 76%,
|
||||
100% 100%,
|
||||
0% 100%,
|
||||
0% 76%,
|
||||
45% 58%,
|
||||
45% 45%,
|
||||
45% 45%
|
||||
);
|
||||
}
|
||||
85%,
|
||||
to {
|
||||
clip-path: polygon(
|
||||
50% 53%,
|
||||
50% 53%,
|
||||
50% 53%,
|
||||
50% 53%,
|
||||
50% 53%,
|
||||
100% 76%,
|
||||
100% 100%,
|
||||
0% 100%,
|
||||
0% 76%,
|
||||
50% 53%,
|
||||
50% 53%,
|
||||
50% 53%
|
||||
);
|
||||
}
|
||||
}
|
||||
@keyframes glare {
|
||||
from,
|
||||
90% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
to {
|
||||
transform: translateX(3em);
|
||||
}
|
||||
}
|
||||
@keyframes flip {
|
||||
from,
|
||||
90% {
|
||||
transform: rotate(0);
|
||||
}
|
||||
to {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.clip-path-hourglass {
|
||||
clip-path: polygon(
|
||||
0% 0%,
|
||||
100% 0%,
|
||||
100% 5.55%,
|
||||
95% 5.55%,
|
||||
95% 28%,
|
||||
60% 46%,
|
||||
60% 54%,
|
||||
95% 72%,
|
||||
95% 94.45%,
|
||||
100% 94.45%,
|
||||
100% 100%,
|
||||
0% 100%,
|
||||
0% 94.45%,
|
||||
5% 94.45%,
|
||||
5% 72%,
|
||||
40% 54%,
|
||||
40% 46%,
|
||||
5% 28%,
|
||||
5% 5.55%,
|
||||
0% 5.55%
|
||||
);
|
||||
}
|
||||
.clip-path-fill {
|
||||
clip-path: polygon(
|
||||
0% 0%,
|
||||
100% 0%,
|
||||
100% 24%,
|
||||
50% 47%,
|
||||
50% 47%,
|
||||
50% 47%,
|
||||
50% 47%,
|
||||
50% 47%,
|
||||
50% 47%,
|
||||
50% 47%,
|
||||
50% 47%,
|
||||
0% 24%
|
||||
);
|
||||
}
|
||||
.animate-flip {
|
||||
animation: flip 2s ease-in-out infinite;
|
||||
}
|
||||
.animate-fill {
|
||||
animation: fill 2s linear infinite;
|
||||
}
|
||||
.animate-glare {
|
||||
animation: glare 2s linear infinite;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
<template>
|
||||
<div class="navbar bg-base-300">
|
||||
<div class="flex ml-auto scale-90 z-30">
|
||||
<!-- Dropdown for Theme -->
|
||||
<ChangeTheme />
|
||||
|
||||
<!-- Dropdown for Language -->
|
||||
<ChangeLanguage />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ChangeTheme from "./ChangeTheme.vue";
|
||||
import ChangeLanguage from "./ChangeLanguage.vue";
|
||||
export default {
|
||||
name: "Navbar",
|
||||
components: {
|
||||
ChangeTheme,
|
||||
ChangeLanguage,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<!-- زر فتح القائمة الجانبية -->
|
||||
<button
|
||||
@click="toggleSidebar"
|
||||
class="fixed top-4 left-4 z-50 p-3 btn btn-primary rounded-lg shadow-lg hover:bg-primary-focus focus:outline-none focus:ring-2 focus:ring-primary focus:ring-opacity-50 transition-all duration-300"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- القائمة الجانبية -->
|
||||
<div
|
||||
:class="[
|
||||
'fixed rounded-r-2xl inset-y-0 left-0 w-64 bg-base-200 dark:bg-base-300 shadow-2xl transform transition-transform duration-300 ease-in-out z-40',
|
||||
isSidebarOpen ? 'translate-x-0' : '-translate-x-full',
|
||||
]"
|
||||
>
|
||||
<div class="p-6 mt-10">
|
||||
<ul class="space-y-4">
|
||||
<li>
|
||||
<router-link
|
||||
:to="{ name: 'Home' }"
|
||||
active-class="active-link"
|
||||
class="flex items-center p-3 text-base-content dark:text-base-content hover:bg-base-100 dark:hover:bg-base-100 rounded-lg transition-all duration-200"
|
||||
>
|
||||
<span class="ml-3">{{ $t('sidebar.home') }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link
|
||||
:to="{ name: 'Dashboard' }"
|
||||
active-class="active-link"
|
||||
class="flex items-center p-3 text-base-content dark:text-base-content hover:bg-base-100 dark:hover:bg-base-100 rounded-lg transition-all duration-200"
|
||||
>
|
||||
<span class="ml-3">{{ $t('sidebar.dashboard') }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link
|
||||
:to="{ name: 'EnrollCourses' }"
|
||||
active-class="active-link"
|
||||
class="flex items-center p-3 text-base-content dark:text-base-content hover:bg-base-100 dark:hover:bg-base-100 rounded-lg transition-all duration-200"
|
||||
>
|
||||
<span class="ml-3">{{ $t('sidebar.enrollCourses') }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link
|
||||
:to="{ name: 'Settings' }"
|
||||
active-class="active-link"
|
||||
class="flex items-center p-3 text-base-content dark:text-base-content hover:bg-base-100 dark:hover:bg-base-100 rounded-lg transition-all duration-200"
|
||||
>
|
||||
<span class="ml-3">{{ $t('sidebar.settings') }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
class="flex items-center p-3 text-base-content dark:text-base-content hover:bg-base-100 dark:hover:bg-base-100 rounded-lg transition-all duration-200"
|
||||
>
|
||||
<span class="ml-3">{{ $t('sidebar.contactUs') }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- خلفية سوداء عند فتح القائمة -->
|
||||
<div
|
||||
v-if="isSidebarOpen"
|
||||
@click="toggleSidebar"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 z-30 backdrop-blur-sm"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
isSidebarOpen: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
toggleSidebar() {
|
||||
this.isSidebarOpen = !this.isSidebarOpen;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
$route() {
|
||||
this.isSidebarOpen = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.active-link {
|
||||
background-color: hsl(var(--p)); /* DaisyUI primary color */
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
import { mount } from '@vue/test-utils'
|
||||
import HelloWorld from '../HelloWorld.vue'
|
||||
|
||||
describe('HelloWorld', () => {
|
||||
it('renders properly', () => {
|
||||
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
|
||||
expect(wrapper.text()).toContain('Hello Vitest')
|
||||
})
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue