u
This commit is contained in:
parent
a760df7cc4
commit
310be0f53d
23 changed files with 407 additions and 100 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
set -o errexit
|
# set -o errexit
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
set -o nounset
|
set -o nounset
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ MIGRATION_MODULES = {"sites": "lms.contrib.sites.migrations"}
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#authentication-backends
|
# https://docs.djangoproject.com/en/dev/ref/settings/#authentication-backends
|
||||||
AUTHENTICATION_BACKENDS = [
|
AUTHENTICATION_BACKENDS = [
|
||||||
"django.contrib.auth.backends.ModelBackend",
|
# "django.contrib.auth.backends.ModelBackend",
|
||||||
"allauth.account.auth_backends.AuthenticationBackend",
|
"allauth.account.auth_backends.AuthenticationBackend",
|
||||||
]
|
]
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#auth-user-model
|
# 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
|
# https://docs.djangoproject.com/en/dev/ref/settings/#middleware
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
"django.middleware.security.SecurityMiddleware",
|
|
||||||
"corsheaders.middleware.CorsMiddleware",
|
"corsheaders.middleware.CorsMiddleware",
|
||||||
|
"django.middleware.security.SecurityMiddleware",
|
||||||
"whitenoise.middleware.WhiteNoiseMiddleware",
|
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
"django.middleware.locale.LocaleMiddleware",
|
"django.middleware.locale.LocaleMiddleware",
|
||||||
|
|
@ -192,7 +192,6 @@ TEMPLATES = [
|
||||||
"django.template.context_processors.static",
|
"django.template.context_processors.static",
|
||||||
"django.template.context_processors.tz",
|
"django.template.context_processors.tz",
|
||||||
"django.contrib.messages.context_processors.messages",
|
"django.contrib.messages.context_processors.messages",
|
||||||
"lms.users.context_processors.allauth_settings",
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -372,11 +371,11 @@ SIMPLE_JWT = {
|
||||||
'ROTATE_REFRESH_TOKENS': True,
|
'ROTATE_REFRESH_TOKENS': True,
|
||||||
'BLACKLIST_AFTER_ROTATION': True,
|
'BLACKLIST_AFTER_ROTATION': True,
|
||||||
'ALGORITHM': 'HS256',
|
'ALGORITHM': 'HS256',
|
||||||
# 'SIGNING_KEY': SECRET_KEY,
|
'SIGNING_KEY': 'SECRET_KEY',
|
||||||
}
|
}
|
||||||
|
|
||||||
# django-cors-headers - https://github.com/adamchainz/django-cors-headers#setup
|
# django-cors-headers - https://github.com/adamchainz/django-cors-headers#setup
|
||||||
CORS_URLS_REGEX = r"^/api/.*$"
|
# CORS_URLS_REGEX = r"^/api/.*$"
|
||||||
|
|
||||||
# By Default swagger ui is available only to admin user(s). You can change permission classes to change that
|
# By Default swagger ui is available only to admin user(s). You can change permission classes to change that
|
||||||
# See more configuration options at https://drf-spectacular.readthedocs.io/en/latest/settings.html#settings
|
# See more configuration options at https://drf-spectacular.readthedocs.io/en/latest/settings.html#settings
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,35 @@ SECRET_KEY = env(
|
||||||
default="DM837WrWz7KIfZM2eb4swzqGlIG0VhhAIFNXf9KgamMtT42DTkHIEXfpF4N9rh2Y",
|
default="DM837WrWz7KIfZM2eb4swzqGlIG0VhhAIFNXf9KgamMtT42DTkHIEXfpF4N9rh2Y",
|
||||||
)
|
)
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
|
# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
|
||||||
ALLOWED_HOSTS = ["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
|
# CACHES
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
@ -83,4 +111,3 @@ CELERY_TASK_EAGER_PROPAGATES = True
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
SIMPLE_JWT["SIGNING_KEY"]=SECRET_KEY
|
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,14 @@ from django.conf.urls.static import static
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||||
from django.urls import include
|
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 import defaults as default_views
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
from drf_spectacular.views import SpectacularAPIView
|
from drf_spectacular.views import SpectacularAPIView
|
||||||
from drf_spectacular.views import SpectacularSwaggerView
|
from drf_spectacular.views import SpectacularSwaggerView
|
||||||
from rest_framework.authtoken.views import obtain_auth_token
|
from rest_framework.authtoken.views import obtain_auth_token
|
||||||
from dj_rest_auth.views import PasswordResetConfirmView
|
from lms.accounts.views import *
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", TemplateView.as_view(template_name="pages/home.html"), name="home"),
|
path("", TemplateView.as_view(template_name="pages/home.html"), name="home"),
|
||||||
|
|
@ -29,17 +30,11 @@ if settings.DEBUG:
|
||||||
|
|
||||||
# API URLS
|
# API URLS
|
||||||
urlpatterns += [
|
urlpatterns += [
|
||||||
path(
|
|
||||||
'auth/password/reset/confirm/<uidb64>/<token>/',
|
path('authw/', include('dj_rest_auth.urls')),
|
||||||
PasswordResetConfirmView.as_view(),
|
|
||||||
name='password_reset_confirm',
|
|
||||||
),
|
|
||||||
path('auth/registration/', include('dj_rest_auth.registration.urls')),
|
|
||||||
path('auth/', include('dj_rest_auth.urls')),
|
|
||||||
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||||
|
|
||||||
|
|
||||||
path('auth/', include('lms.accounts.urls')),
|
path('auth/', include('lms.accounts.urls')),
|
||||||
|
|
||||||
path('app/', include('lms.app.urls')),
|
path('app/', include('lms.app.urls')),
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ even if the default User model is sufficient for you.
|
||||||
This model behaves identically to the default user model,
|
This model behaves identically to the default user model,
|
||||||
but you’ll be able to customize it in the future if the need arises.
|
but you’ll be able to customize it in the future if the need arises.
|
||||||
|
|
||||||
.. automodule:: lms.users.models
|
.. automodule:: lms.accounts.models
|
||||||
:members:
|
:members:
|
||||||
:noindex:
|
:noindex:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
from allauth.account.adapter import DefaultAccountAdapter
|
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()
|
current_site = Site.objects.get_current()
|
||||||
site_domain = "current_site.domain"
|
site_domain = current_site.domain
|
||||||
|
|
||||||
class CustomAccountAdapter(DefaultAccountAdapter):
|
class CustomAccountAdapter(DefaultAccountAdapter):
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ class CustomUserAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
# تخصيص الحقول
|
# تخصيص الحقول
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {'fields': ('username', 'password')}),
|
|
||||||
('Personal Info', {'fields': ('email', 'full_name', 'role')}),
|
('Personal Info', {'fields': ('email', 'full_name', 'role')}),
|
||||||
('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}),
|
('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}),
|
||||||
('Important Dates', {'fields': ('last_login', 'date_joined')}),
|
('Important Dates', {'fields': ('last_login', 'date_joined')}),
|
||||||
|
|
@ -16,12 +15,11 @@ class CustomUserAdmin(admin.ModelAdmin):
|
||||||
add_fieldsets = (
|
add_fieldsets = (
|
||||||
(None, {
|
(None, {
|
||||||
'classes': ('wide',),
|
'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')
|
list_display = ('email', 'full_name', 'role', 'is_staff', 'is_active')
|
||||||
search_fields = ('username', 'email', 'full_name')
|
search_fields = ('email', 'full_name')
|
||||||
ordering = ('username',)
|
|
||||||
|
|
||||||
admin.site.register(CustomUser, CustomUserAdmin)
|
admin.site.register(CustomUser, CustomUserAdmin)
|
||||||
|
|
|
||||||
44
lms/accounts/migrations/0001_initial.py
Normal file
44
lms/accounts/migrations/0001_initial.py
Normal 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()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
18
lms/accounts/migrations/0002_alter_customuser_role.py
Normal file
18
lms/accounts/migrations/0002_alter_customuser_role.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
lms/accounts/migrations/0003_alter_customuser_role.py
Normal file
18
lms/accounts/migrations/0003_alter_customuser_role.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
lms/accounts/migrations/0004_alter_customuser_role.py
Normal file
18
lms/accounts/migrations/0004_alter_customuser_role.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
lms/accounts/migrations/0005_alter_customuser_role.py
Normal file
18
lms/accounts/migrations/0005_alter_customuser_role.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
0
lms/accounts/migrations/__init__.py
Normal file
0
lms/accounts/migrations/__init__.py
Normal file
|
|
@ -1,18 +1,45 @@
|
||||||
from django.db import models
|
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):
|
class CustomUser(AbstractUser):
|
||||||
role = {
|
# إزالة الحقل username من AbstractUser
|
||||||
('student', 'student'),
|
username = None
|
||||||
('instructor', 'instructor'),
|
|
||||||
('admin', 'admin'),
|
|
||||||
}
|
|
||||||
first_name = None
|
first_name = None
|
||||||
last_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)
|
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):
|
def __str__(self):
|
||||||
return self.email
|
return self.email
|
||||||
|
|
|
||||||
|
|
@ -5,84 +5,82 @@ from django.contrib.auth import authenticate
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from allauth.account.models import EmailAddress
|
from allauth.account.models import EmailAddress
|
||||||
from dj_rest_auth.registration.serializers import RegisterSerializer
|
from dj_rest_auth.registration.serializers import RegisterSerializer
|
||||||
|
from 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):
|
class CustomLoginSerializer(LoginSerializer):
|
||||||
email = serializers.EmailField(required=True)
|
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):
|
def validate(self, attrs):
|
||||||
email = attrs.get('email')
|
email = attrs.get('email')
|
||||||
password = attrs.get('password')
|
password = attrs.get('password')
|
||||||
|
|
||||||
if not email or not 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)
|
users = User.objects.filter(email=email)
|
||||||
|
|
||||||
if not users.exists():
|
if not users.exists():
|
||||||
raise serializers.ValidationError("Incorrect email.")
|
raise serializers.ValidationError(_("No account found with this email."))
|
||||||
|
|
||||||
if users.count() > 1:
|
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()
|
user = users.first()
|
||||||
|
|
||||||
if not user.check_password(password):
|
if not user.check_password(password):
|
||||||
raise serializers.ValidationError("Incorrect password.")
|
raise serializers.ValidationError(_("Incorrect password."))
|
||||||
|
|
||||||
if not self.is_email_verified(user):
|
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
|
attrs['user'] = user
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
def is_email_verified(self, user):
|
def is_email_verified(self, user):
|
||||||
if hasattr(user, 'email_verified'):
|
"""
|
||||||
return user.email_verified
|
التحقق من حالة التحقق من البريد الإلكتروني.
|
||||||
else:
|
"""
|
||||||
try:
|
try:
|
||||||
email_address = EmailAddress.objects.get(user=user, email=user.email)
|
# استخدام نموذج EmailAddress للتحقق
|
||||||
return email_address.verified
|
email_address = EmailAddress.objects.get(user=user, email=user.email)
|
||||||
except EmailAddress.DoesNotExist:
|
return email_address.verified
|
||||||
return False
|
except EmailAddress.DoesNotExist:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class CustomRegisterSerializer(RegisterSerializer):
|
class CustomRegisterSerializer(RegisterSerializer):
|
||||||
full_name = serializers.CharField(required=True)
|
full_name = serializers.CharField(required=True)
|
||||||
|
|
||||||
def save(self, request):
|
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 = super().save(request)
|
||||||
user.full_name = self.data.get('full_name', '')
|
user.full_name = self.data.get('full_name', '')
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
|
send_email_confirmation(request, user)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
from . import views
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
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',
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -7,17 +7,27 @@ from rest_framework.permissions import AllowAny
|
||||||
from .serializers import ChangeEmailSerializer
|
from .serializers import ChangeEmailSerializer
|
||||||
|
|
||||||
|
|
||||||
class CustomConfirmEmailView(APIView):
|
class ConfirmEmailAPIView(APIView):
|
||||||
|
permission_classes = [AllowAny]
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
serializer = ChangeEmailSerializer(data=request.data)
|
key = request.data.get("key")
|
||||||
if serializer.is_valid():
|
if not key:
|
||||||
user = request.user
|
return Response({"detail": _("Key is required.")}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
new_email = serializer.validated_data['email']
|
|
||||||
|
|
||||||
# تغيير البريد الإلكتروني
|
try:
|
||||||
email_address, created = EmailAddress.objects.get_or_create(user=user, email=new_email)
|
# Attempt to retrieve the email confirmation using HMAC key
|
||||||
if not email_address.verified:
|
email_confirmation = EmailConfirmationHMAC.from_key(key)
|
||||||
send_email_confirmation(request, user, email=email_address.email)
|
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)
|
if email_confirmation.email_address.verified:
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
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)
|
||||||
|
|
|
||||||
79
lms/app/migrations/0001_initial.py
Normal file
79
lms/app/migrations/0001_initial.py
Normal 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')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
0
lms/app/migrations/__init__.py
Normal file
0
lms/app/migrations/__init__.py
Normal file
|
|
@ -1,14 +1,14 @@
|
||||||
import pytest
|
# import pytest
|
||||||
|
|
||||||
from lms.users.models import User
|
# from lms.users.models import User
|
||||||
from lms.users.tests.factories import UserFactory
|
# from lms.users.tests.factories import UserFactory
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
# @pytest.fixture(autouse=True)
|
||||||
def _media_storage(settings, tmpdir) -> None:
|
# def _media_storage(settings, tmpdir) -> None:
|
||||||
settings.MEDIA_ROOT = tmpdir.strpath
|
# settings.MEDIA_ROOT = tmpdir.strpath
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
# @pytest.fixture
|
||||||
def user(db) -> User:
|
# def user(db) -> User:
|
||||||
return UserFactory()
|
# return UserFactory()
|
||||||
|
|
|
||||||
|
|
@ -78,9 +78,7 @@
|
||||||
<li class="nav-item active">
|
<li class="nav-item active">
|
||||||
<a class="nav-link" href="{% url 'home' %}">Home <span class="visually-hidden">(current)</span></a>
|
<a class="nav-link" href="{% url 'home' %}">Home <span class="visually-hidden">(current)</span></a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="{% url 'about' %}">About</a>
|
|
||||||
</li>
|
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link"
|
<a class="nav-link"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue