u
This commit is contained in:
parent
d2be7b1d05
commit
fdc9bb8f23
23 changed files with 407 additions and 100 deletions
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -o errexit
|
||||
# set -o errexit
|
||||
set -o pipefail
|
||||
set -o nounset
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')),
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ 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.users.models
|
||||
.. automodule:: lms.accounts.models
|
||||
:members:
|
||||
:noindex:
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
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.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)
|
||||
|
||||
# الحقول الخاصة بالمستخدم المخصص
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
# استخدام نموذج 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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
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.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()
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue