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 #!/bin/bash
set -o errexit # set -o errexit
set -o pipefail set -o pipefail
set -o nounset 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 # 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

View file

@ -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

View file

@ -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')),

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, This model behaves identically to the default user model,
but youll be able to customize it in the future if the need arises. but youll be able to customize it in the future if the need arises.
.. automodule:: lms.users.models .. automodule:: lms.accounts.models
:members: :members:
:noindex: :noindex:

View file

@ -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):

View file

@ -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)

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.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

View file

@ -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

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 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',
),
] ]

View file

@ -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)

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.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()

View file

@ -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"