This commit is contained in:
AN 2025-01-11 12:11:18 +00:00
parent d2be7b1d05
commit fdc9bb8f23
23 changed files with 407 additions and 100 deletions

View file

@ -1,6 +1,6 @@
#!/bin/bash
set -o errexit
# set -o errexit
set -o pipefail
set -o nounset

View file

@ -102,7 +102,7 @@ MIGRATION_MODULES = {"sites": "lms.contrib.sites.migrations"}
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#authentication-backends
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
# "django.contrib.auth.backends.ModelBackend",
"allauth.account.auth_backends.AuthenticationBackend",
]
# https://docs.djangoproject.com/en/dev/ref/settings/#auth-user-model
@ -136,8 +136,8 @@ AUTH_PASSWORD_VALIDATORS = [
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#middleware
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"corsheaders.middleware.CorsMiddleware",
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.locale.LocaleMiddleware",
@ -192,7 +192,6 @@ TEMPLATES = [
"django.template.context_processors.static",
"django.template.context_processors.tz",
"django.contrib.messages.context_processors.messages",
"lms.users.context_processors.allauth_settings",
],
},
},
@ -372,11 +371,11 @@ SIMPLE_JWT = {
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
'ALGORITHM': 'HS256',
# 'SIGNING_KEY': SECRET_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

View file

@ -14,7 +14,35 @@ SECRET_KEY = env(
default="DM837WrWz7KIfZM2eb4swzqGlIG0VhhAIFNXf9KgamMtT42DTkHIEXfpF4N9rh2Y",
)
# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
ALLOWED_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1"] # noqa: S104
ALLOWED_HOSTS = ["127.0.0.1", "localhost"] # حدد المضيفين المسموح بهم
CSRF_TRUSTED_ORIGINS = [
'http://localhost:3000',
'http://127.0.0.1:3000',
]
CORS_ORIGIN_ALLOW_ALL = True
CORS_ALLOW_CREDENTIALS = False
CORS_ALLOW_METHODS = [
"GET",
"POST",
"PUT",
"PATCH",
"DELETE",
"OPTIONS",
]
CORS_ALLOW_HEADERS = [
"content-type",
"authorization",
"accept",
"origin",
"user-agent",
"x-csrftoken",
"x-requested-with",
]
# CACHES
# ------------------------------------------------------------------------------
@ -83,4 +111,3 @@ CELERY_TASK_EAGER_PROPAGATES = True
# ------------------------------------------------------------------------------
SIMPLE_JWT["SIGNING_KEY"]=SECRET_KEY

View file

@ -4,13 +4,14 @@ from django.conf.urls.static import static
from django.contrib import admin
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.urls import include
from django.urls import path
from django.urls import path, re_path
from django.views import defaults as default_views
from django.views.generic import TemplateView
from drf_spectacular.views import SpectacularAPIView
from drf_spectacular.views import SpectacularSwaggerView
from rest_framework.authtoken.views import obtain_auth_token
from dj_rest_auth.views import PasswordResetConfirmView
from lms.accounts.views import *
urlpatterns = [
path("", TemplateView.as_view(template_name="pages/home.html"), name="home"),
@ -29,17 +30,11 @@ if settings.DEBUG:
# API URLS
urlpatterns += [
path(
'auth/password/reset/confirm/<uidb64>/<token>/',
PasswordResetConfirmView.as_view(),
name='password_reset_confirm',
),
path('auth/registration/', include('dj_rest_auth.registration.urls')),
path('auth/', include('dj_rest_auth.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('app/', include('lms.app.urls')),

View file

@ -9,7 +9,7 @@ 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.users.models
.. automodule:: lms.accounts.models
:members:
:noindex:

View file

@ -1,8 +1,8 @@
from allauth.account.adapter import DefaultAccountAdapter
# from django.contrib.sites.models import Site
from django.contrib.sites.models import Site
# current_site = Site.objects.get_current()
site_domain = "current_site.domain"
current_site = Site.objects.get_current()
site_domain = current_site.domain
class CustomAccountAdapter(DefaultAccountAdapter):

View file

@ -7,7 +7,6 @@ class CustomUserAdmin(admin.ModelAdmin):
# تخصيص الحقول
fieldsets = (
(None, {'fields': ('username', 'password')}),
('Personal Info', {'fields': ('email', 'full_name', 'role')}),
('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}),
('Important Dates', {'fields': ('last_login', 'date_joined')}),
@ -16,12 +15,11 @@ class CustomUserAdmin(admin.ModelAdmin):
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('username', 'password1', 'password2', 'email', 'full_name', 'role'),
'fields': ('password1', 'password2', 'email', 'full_name', 'role'),
}),
)
list_display = ('username', 'email', 'full_name', 'role', 'is_staff', 'is_active')
search_fields = ('username', 'email', 'full_name')
ordering = ('username',)
list_display = ('email', 'full_name', 'role', 'is_staff', 'is_active')
search_fields = ('email', 'full_name')
admin.site.register(CustomUser, CustomUserAdmin)

View file

@ -0,0 +1,44 @@
# Generated by Django 5.0.10 on 2025-01-11 07:41
import django.contrib.auth.models
import django.contrib.auth.validators
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='CustomUser',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('email', models.EmailField(max_length=254, unique=True)),
('full_name', models.CharField(max_length=255, null=True)),
('role', models.CharField(choices=[('admin', 'admin'), ('student', 'student'), ('instructor', 'instructor')], max_length=255, null=True)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 5.0.10 on 2025-01-11 07:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='customuser',
name='role',
field=models.CharField(choices=[('instructor', 'instructor'), ('admin', 'admin'), ('student', 'student')], max_length=255, null=True),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 5.0.10 on 2025-01-11 07:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0002_alter_customuser_role'),
]
operations = [
migrations.AlterField(
model_name='customuser',
name='role',
field=models.CharField(choices=[('student', 'student'), ('admin', 'admin'), ('instructor', 'instructor')], max_length=255, null=True),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 5.0.10 on 2025-01-11 07:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0003_alter_customuser_role'),
]
operations = [
migrations.AlterField(
model_name='customuser',
name='role',
field=models.CharField(choices=[('student', 'student'), ('instructor', 'instructor'), ('admin', 'admin')], max_length=255, null=True),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 5.0.10 on 2025-01-11 10:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0004_alter_customuser_role'),
]
operations = [
migrations.AlterField(
model_name='customuser',
name='role',
field=models.CharField(choices=[('student', 'student'), ('admin', 'admin'), ('instructor', 'instructor')], max_length=255, null=True),
),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 5.0.10 on 2025-01-11 10:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0005_alter_customuser_role'),
]
operations = [
migrations.AlterModelManagers(
name='customuser',
managers=[
],
),
migrations.AlterField(
model_name='customuser',
name='role',
field=models.CharField(choices=[('instructor', 'instructor'), ('student', 'student'), ('admin', 'admin')], max_length=255, null=True),
),
]

View file

@ -0,0 +1,27 @@
# Generated by Django 5.0.10 on 2025-01-11 10:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0006_alter_customuser_managers_alter_customuser_role'),
]
operations = [
migrations.RemoveField(
model_name='customuser',
name='username',
),
migrations.AlterField(
model_name='customuser',
name='full_name',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name='customuser',
name='role',
field=models.CharField(blank=True, choices=[('student', 'Student'), ('instructor', 'Instructor'), ('admin', 'Admin')], max_length=20, null=True),
),
]

View file

View file

@ -1,18 +1,45 @@
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.models import AbstractUser, BaseUserManager
class CustomUserManager(BaseUserManager):
def create_user(self, email, password=None, **extra_fields):
if not email:
raise ValueError('The Email field must be set')
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, password=None, **extra_fields):
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
return self.create_user(email, password, **extra_fields)
class CustomUser(AbstractUser):
role = {
('student', 'student'),
('instructor', 'instructor'),
('admin', 'admin'),
}
# إزالة الحقل username من AbstractUser
username = None
first_name = None
last_name = None
email = models.EmailField(unique=True)
full_name = models.CharField(max_length=255, null=True)
role = models.CharField(max_length=255, null=True, choices=role)
last_name = None
# الحقول الخاصة بالمستخدم المخصص
email = models.EmailField(unique=True)
full_name = models.CharField(max_length=255, null=True, blank=True)
ROLE_CHOICES = [
('student', 'Student'),
('instructor', 'Instructor'),
('admin', 'Admin'),
]
role = models.CharField(max_length=20, choices=ROLE_CHOICES, null=True, blank=True)
# تخصيص مدير المستخدم
objects = CustomUserManager()
# تحديد الحقول الأساسية
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = [] # قائمة الحقول المطلوبة لإنشاء مستخدم عبر createsuperuser
def __str__(self):
return self.email

View file

@ -5,84 +5,82 @@ 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 django.contrib.auth import get_user_model
from rest_framework.exceptions import ValidationError
from allauth.account.utils import send_email_confirmation
from rest_framework.response import Response
User = get_user_model()
class CustomLoginSerializer(LoginSerializer):
email = serializers.EmailField(required=True)
password = serializers.CharField(style={'input_type': 'password'})
password = serializers.CharField(style={'input_type': 'password'}, write_only=True)
def validate(self, attrs):
email = attrs.get('email')
password = attrs.get('password')
if not email or not password:
raise serializers.ValidationError("Please enter both email and password.")
raise serializers.ValidationError(_("Please enter both email and password."))
User = get_user_model()
# البحث عن المستخدم بالبريد الإلكتروني
users = User.objects.filter(email=email)
if not users.exists():
raise serializers.ValidationError("Incorrect email.")
raise serializers.ValidationError(_("No account found with this email."))
if users.count() > 1:
raise serializers.ValidationError("Multiple users found with this email. Please contact support.")
raise serializers.ValidationError(_("Multiple accounts found with this email. Please contact support."))
user = users.first()
if not user.check_password(password):
raise serializers.ValidationError("Incorrect password.")
raise serializers.ValidationError(_("Incorrect password."))
if not self.is_email_verified(user):
raise serializers.ValidationError("Email not verified. Please verify your email first.")
raise serializers.ValidationError(_("Email not verified. Please verify your email first."))
# إضافة المستخدم إلى الـ attrs
attrs['user'] = user
return attrs
def is_email_verified(self, user):
if hasattr(user, 'email_verified'):
return user.email_verified
else:
try:
email_address = EmailAddress.objects.get(user=user, email=user.email)
return email_address.verified
except EmailAddress.DoesNotExist:
return False
"""
التحقق من حالة التحقق من البريد الإلكتروني.
"""
try:
# استخدام نموذج EmailAddress للتحقق
email_address = EmailAddress.objects.get(user=user, email=user.email)
return email_address.verified
except EmailAddress.DoesNotExist:
return False
class CustomRegisterSerializer(RegisterSerializer):
full_name = serializers.CharField(required=True)
def save(self, request):
email = self.data.get('email')
email_address = EmailAddress.objects.filter(email=email).first()
if email_address:
if email_address.verified:
raise ValidationError({'email': 'This email is already.'})
else:
send_email_confirmation(request, email_address.user)
raise ValidationError({'email': 'A confirmation email has been sent. Please confirm your email.'})
user = super().save(request)
user.full_name = self.data.get('full_name', '')
user.save()
send_email_confirmation(request, user)
return user

View file

@ -1,5 +1,15 @@
from django.urls import path
from django.urls import path, re_path, include
from dj_rest_auth.views import PasswordResetConfirmView
from . import views
urlpatterns = [
path("change-email/", views.CustomConfirmEmailView.as_view(), name="change-email")
path('', include('dj_rest_auth.urls')),
path('registration/', include('dj_rest_auth.registration.urls')),
path("registration/account-confirm-email/", views.ConfirmEmailAPIView.as_view(),name="account_confirm_email",),
path(
'password/reset/confirm/<uidb64>/<token>/',
PasswordResetConfirmView.as_view(),
name='password_reset_confirm',
),
]

View file

@ -7,17 +7,27 @@ from rest_framework.permissions import AllowAny
from .serializers import ChangeEmailSerializer
class CustomConfirmEmailView(APIView):
class ConfirmEmailAPIView(APIView):
permission_classes = [AllowAny]
def post(self, request, *args, **kwargs):
serializer = ChangeEmailSerializer(data=request.data)
if serializer.is_valid():
user = request.user
new_email = serializer.validated_data['email']
key = request.data.get("key")
if not key:
return Response({"detail": _("Key is required.")}, status=status.HTTP_400_BAD_REQUEST)
# تغيير البريد الإلكتروني
email_address, created = EmailAddress.objects.get_or_create(user=user, email=new_email)
if not email_address.verified:
send_email_confirmation(request, user, email=email_address.email)
try:
# Attempt to retrieve the email confirmation using HMAC key
email_confirmation = EmailConfirmationHMAC.from_key(key)
if email_confirmation is None:
# If HMAC fails, fallback to database key
email_confirmation = EmailConfirmation.objects.get(key=key)
except EmailConfirmation.DoesNotExist:
return Response({"detail": _("Invalid or expired key.")}, status=status.HTTP_400_BAD_REQUEST)
return Response({"message": "تم إرسال بريد تأكيد إلى البريد الإلكتروني الجديد."}, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
if email_confirmation.email_address.verified:
return Response({"detail": _("Email is already verified.")}, status=status.HTTP_200_OK)
# Verify the email
email_confirmation.confirm(request)
return Response({"detail": _("Email successfully verified.")}, status=status.HTTP_200_OK)

View file

@ -0,0 +1,79 @@
# Generated by Django 5.0.10 on 2025-01-11 07:54
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Course',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('title', models.CharField(max_length=255, verbose_name='Course Title')),
('description', models.TextField(verbose_name='Course Description')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
('instructor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='courses_taught', to=settings.AUTH_USER_MODEL, verbose_name='Instructor')),
],
),
migrations.CreateModel(
name='Certificate',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('issued_at', models.DateTimeField(auto_now_add=True, verbose_name='Issued At')),
('certificate_file', models.FileField(upload_to='certificates/', verbose_name='Certificate File')),
('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='certificates', to=settings.AUTH_USER_MODEL, verbose_name='Student')),
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='certificates', to='app.course', verbose_name='Course')),
],
),
migrations.CreateModel(
name='Enrollment',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('enrolled_at', models.DateTimeField(auto_now_add=True, verbose_name='Enrollment Date')),
('completed', models.BooleanField(default=False, verbose_name='Completed')),
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='enrollments', to='app.course', verbose_name='Course')),
('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='enrollments', to=settings.AUTH_USER_MODEL, verbose_name='Student')),
],
),
migrations.CreateModel(
name='Module',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('title', models.CharField(max_length=255, verbose_name='Module Title')),
('description', models.TextField(verbose_name='Module Description')),
('order', models.PositiveIntegerField(default=0, unique=True, verbose_name='Order')),
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='modules', to='app.course', verbose_name='Course')),
],
),
migrations.CreateModel(
name='Lesson',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('title', models.CharField(max_length=255, verbose_name='Lesson Title')),
('content', models.TextField(verbose_name='Lesson Content')),
('order', models.PositiveIntegerField(default=0, verbose_name='Order')),
('file', models.FileField(blank=True, null=True, upload_to='lesson_files/', verbose_name='Attached File')),
('module', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lessons', to='app.module', verbose_name='Module')),
],
),
migrations.CreateModel(
name='Quiz',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('title', models.CharField(max_length=255, verbose_name='Quiz Title')),
('questions', models.JSONField(null=True, verbose_name='Questions')),
('module', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quiz', to='app.module', verbose_name='Module')),
],
),
]

View file

View file

@ -1,14 +1,14 @@
import pytest
# import pytest
from lms.users.models import User
from lms.users.tests.factories import UserFactory
# from lms.users.models import User
# from lms.users.tests.factories import UserFactory
@pytest.fixture(autouse=True)
def _media_storage(settings, tmpdir) -> None:
settings.MEDIA_ROOT = tmpdir.strpath
# @pytest.fixture(autouse=True)
# def _media_storage(settings, tmpdir) -> None:
# settings.MEDIA_ROOT = tmpdir.strpath
@pytest.fixture
def user(db) -> User:
return UserFactory()
# @pytest.fixture
# def user(db) -> User:
# return UserFactory()

View file

@ -78,9 +78,7 @@
<li class="nav-item active">
<a class="nav-link" href="{% url 'home' %}">Home <span class="visually-hidden">(current)</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'about' %}">About</a>
</li>
{% if request.user.is_authenticated %}
<li class="nav-item">
<a class="nav-link"