diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index 1234a61..e28f11d 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -13,7 +13,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: ["3.10"] + python-version: ["3.13"] steps: - uses: actions/checkout@v2 diff --git a/README.md b/README.md index 5012f23..8f6c237 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # my-elearning -A Django-based e-learning platform built with Python 3. +A Django 6-based e-learning platform built for Python 3.13. [![Django CI](https://github.com/delitamakanda/elearning/actions/workflows/django.yml/badge.svg?branch=master)](https://github.com/delitamakanda/elearning/actions/workflows/django.yml) @@ -14,6 +14,7 @@ A Django-based e-learning platform built with Python 3. ### Installation ```bash +pip install --upgrade pip pip install -r requirements.txt python manage.py migrate python manage.py createsuperuser diff --git a/apps/common/serializers.py b/apps/common/serializers.py new file mode 100644 index 0000000..9287be7 --- /dev/null +++ b/apps/common/serializers.py @@ -0,0 +1,84 @@ +""" +Custom taggit serializer field and mixin to replace the abandoned +django-taggit-serializer package. + +Usage +----- + from apps.common.serializers import TaggitSerializer, TagListSerializerField + + class MySerializer(TaggitSerializer, serializers.ModelSerializer): + tags = TagListSerializerField() + + class Meta: + model = MyModel + fields = ('id', 'tags', ...) +""" + +from rest_framework import serializers + + +class TagListSerializerField(serializers.Field): + """Serializes a django-taggit ``TaggableManager`` as a flat list of tag names.""" + + child = serializers.CharField() + + def __init__(self, *args, **kwargs): + kwargs.setdefault("default", list) + super().__init__(*args, **kwargs) + + def to_internal_value(self, data): + if isinstance(data, str): + data = [tag.strip() for tag in data.split(",") if tag.strip()] + if not isinstance(data, list): + raise serializers.ValidationError("Expected a list of tag strings.") + errors = [] + for item in data: + try: + self.child.run_validation(item) + except serializers.ValidationError as e: + errors.append(e.detail) + else: + errors.append({}) + if any(errors): + raise serializers.ValidationError(errors) + return data + + def to_representation(self, value): + if not value: + return [] + if hasattr(value, "all"): + return [tag.name for tag in value.all()] + return list(value) + + +class TaggitSerializer(serializers.Serializer): + """ + Mixin for ModelSerializer classes that include one or more + ``TagListSerializerField`` fields backed by a ``TaggableManager``. + + Handles saving tags after the instance is created or updated. + """ + + def _get_tag_fields(self): + return { + field_name: field + for field_name, field in self.fields.items() + if isinstance(field, TagListSerializerField) + } + + def create(self, validated_data): + tag_fields = self._get_tag_fields() + tag_data = {name: validated_data.pop(name, []) for name in tag_fields} + instance = super().create(validated_data) + for field_name, tags in tag_data.items(): + getattr(instance, field_name).set(tags) + return instance + + def update(self, instance, validated_data): + tag_fields = self._get_tag_fields() + tag_data = {name: validated_data.pop(name, None) for name in tag_fields} + instance = super().update(instance, validated_data) + for field_name, tags in tag_data.items(): + if tags is not None: + getattr(instance, field_name).set(tags) + return instance diff --git a/myelearning/settings.py b/myelearning/settings.py index 8cc3e16..d075a4d 100644 --- a/myelearning/settings.py +++ b/myelearning/settings.py @@ -1,29 +1,16 @@ -""" -Django settings for myelearning project. +"""Django settings for myelearning project.""" -Generated by 'django-admin startproject' using Django 1.11.5. - -For more information on this file, see -https://docs.djangoproject.com/en/1.11/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/1.11/ref/settings/ -""" - -import os +from pathlib import Path import sys from decouple import config from django.urls import reverse_lazy -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +BASE_DIR = Path(__file__).resolve().parent.parent -# Add the parent directory to sys.path -PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -sys.path.insert(0, PROJECT_ROOT) +PROJECT_ROOT = BASE_DIR +sys.path.insert(0, str(PROJECT_ROOT)) # Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = config('SECRET_KEY', 'dummy_secret_key') @@ -56,12 +43,12 @@ 'widget_tweaks', 'corsheaders', 'taggit', - 'taggit_serializer', ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware', + 'corsheaders.middleware.CorsMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'apps.students.middleware.SessionTimeoutMiddleware', # 'django.middleware.cache.UpdateCacheMiddleware', @@ -73,7 +60,6 @@ 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware', 'django.contrib.admindocs.middleware.XViewMiddleware', - 'corsheaders.middleware.CorsMiddleware', ] # if DEBUG == False: @@ -87,7 +73,7 @@ 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [ # template at the root of the project - os.path.join(BASE_DIR, 'templates'), + BASE_DIR / 'templates', ], 'APP_DIRS': True, 'OPTIONS': { @@ -105,20 +91,14 @@ SITE_ID = 1 -# Database -# https://docs.djangoproject.com/en/1.11/ref/settings/#databases - DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + 'NAME': BASE_DIR / 'db.sqlite3', } } -# Password validation -# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators - AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', @@ -135,33 +115,27 @@ ] -# Internationalization -# https://docs.djangoproject.com/en/1.11/topics/i18n/ - LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' USE_I18N = True -USE_L10N = True - -USE_TZ = False +USE_TZ = True -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.11/howto/static-files/ - STATIC_URL = '/static/' MEDIA_URL = '/media/' -STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') -MEDIA_ROOT = os.path.join(BASE_DIR, 'media') +STATIC_ROOT = BASE_DIR / 'staticfiles' +MEDIA_ROOT = BASE_DIR / 'media' STATICFILES_DIRS = ( - os.path.join(BASE_DIR, 'static'), + BASE_DIR / 'static', ) +STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' + # Custom auth AUTH_USER_MODEL = 'students.User' @@ -215,7 +189,7 @@ CORS_ORIGIN_ALLOW_ALL = False CORS_ALLOW_CREDENTIALS = True -CORS_ORIGIN_WHITELIST = ( +CORS_ALLOWED_ORIGINS = [ 'https://myelearning.herokuapp.com', 'http://localhost:8080', 'http://localhost:8100', @@ -223,13 +197,16 @@ 'http://localhost:3000', 'https://pwa-myelearning.netlify.app', 'http://localhost', -) +] +CORS_ALLOW_ALL_ORIGINS = False CORS_ALLOW_METHODS = ( 'GET', 'POST', 'PUT', + 'PATCH', 'DELETE', + 'OPTIONS', ) # Task async @@ -237,3 +214,5 @@ CELERY_RESULT_BACKEND = config('REDIS_URL', 'redis://localhost:6379/0', cast=str) CRISPY_TEMPLATE_PACK = 'bootstrap4' + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/myelearning/urls.py b/myelearning/urls.py index ec54a9c..88ba659 100644 --- a/myelearning/urls.py +++ b/myelearning/urls.py @@ -1,19 +1,5 @@ -"""myelearning URL Configuration - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/1.11/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.conf.urls import url, include - 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) -""" -from django.urls import re_path as url, include +"""myelearning URL Configuration for Django 6.""" +from django.urls import include, path, re_path from django.conf import settings from django.views import generic from django.conf.urls.static import static @@ -24,28 +10,28 @@ from django.views.generic import TemplateView urlpatterns = [ - url(r'^$', generic.RedirectView.as_view(url='/course/', permanent=True)), - - url(r'^accounts/login/$', auth_views.LoginView.as_view(), name='login'), - url(r'^accounts/logout/$', auth_views.LogoutView.as_view(), name='logout'), - url(r'^accounts/signup/$', classroom.SignupView.as_view(), name='signup'), - url(r'^password-change/$', auth_views.PasswordChangeView.as_view(), name='password_change'), - url(r'^password-change/done/$', auth_views.PasswordChangeDoneView.as_view(), name='password_change_done'), - url(r'^password-reset/$', auth_views.PasswordResetView.as_view(), name='password_reset'), - url(r'^password-reset/done/$', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'), - url(r'^password-reset/confirm/(?P[-\w]+)/(?P[-\w]+)/$', auth_views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'), - url(r'^password-reset/complete/$', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'), - - url(r'^admin/', admin.site.urls), - url(r'^admin/doc/', include('django.contrib.admindocs.urls')), - - url(r'^course/', include(('apps.courses.urls', 'courses'))), - url(r'^students/', include(('apps.students.urls', 'students'))), - - url(r'^api/', include(('apps.courses.api.urls', 'api'), namespace='api')), - - url(r'^sw.js', (TemplateView.as_view(template_name="service-worker.js", content_type='application/javascript', )), name='sw.js'), - url(r'^offline.html', (TemplateView.as_view(template_name="offline.html")), name='offline.html'), + path('', generic.RedirectView.as_view(url='/course/', permanent=True)), + + path('accounts/login/', auth_views.LoginView.as_view(), name='login'), + path('accounts/logout/', auth_views.LogoutView.as_view(), name='logout'), + path('accounts/signup/', classroom.SignupView.as_view(), name='signup'), + path('password-change/', auth_views.PasswordChangeView.as_view(), name='password_change'), + path('password-change/done/', auth_views.PasswordChangeDoneView.as_view(), name='password_change_done'), + path('password-reset/', auth_views.PasswordResetView.as_view(), name='password_reset'), + path('password-reset/done/', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'), + re_path(r'^password-reset/confirm/(?P[-\w]+)/(?P[-\w]+)/$', auth_views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'), + path('password-reset/complete/', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'), + + path('admin/', admin.site.urls), + path('admin/doc/', include('django.contrib.admindocs.urls')), + + path('course/', include(('apps.courses.urls', 'courses'))), + path('students/', include(('apps.students.urls', 'students'))), + + path('api/', include(('apps.courses.api.urls', 'api'), namespace='api')), + + path('sw.js', TemplateView.as_view(template_name="service-worker.js", content_type='application/javascript'), name='sw.js'), + path('offline.html', TemplateView.as_view(template_name="offline.html"), name='offline.html'), ] if settings.DEBUG: diff --git a/requirements-dev.txt b/requirements-dev.txt index da33d07..a9891f5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,36 +1,39 @@ -certifi==2024.7.4 -chardet==3.0.4 -Django==4.2.30 -django-braces==1.15.0 -django-embed-video==1.0.0 -django-memcache-status==1.1 -djangorestframework==3.15.2 -idna==3.7 -olefile==0.44 -Pillow==12.2.0 -python3-memcached -pytz==2017.2 -requests==2.33.0 -six==1.11.0 -urllib3==2.6.3 -whitenoise==5.0.1 -python-decouple==3.1 -django-widget-tweaks==1.4.3 -google-api-python-client==1.7.4 -numpy==1.22.0 -pandas==1.2.1 -scikit-learn==1.5.0 -scipy==1.10.0 -sklearn==0.0 -django-storages==1.7.1 -django-webpack-loader==0.6.0 -django-cors-headers==4.2.0 -Markdown==3.8.1 -django-taggit==1.1.0 -django-taggit-serializer==0.1.7 -mistune==2.0.3 -pygments==2.20.0 -celery==5.4.0 -redis==4.4.4 -django-autoslug==1.9.8 -pymemcache==4.0.0 +certifi>=2024.8.30 +chardet>=5.2.0 +Django>=6.0a1,<7.0 +django-braces>=1.15.0 +django-embed-video==1.4.10 +django-memcache-status>=1.2 +djangorestframework>=3.15.2 +idna>=3.7 +olefile>=0.47 +Pillow>=10.4.0 +python3-memcached==1.51 +requests>=2.32.3 +six>=1.16.0 +urllib3>=2.2.2 +whitenoise>=6.7.0 +python-decouple>=3.8 +django-widget-tweaks>=1.5.0 +google-api-python-client>=2.137.0 +numpy>=2.1.1 +pandas>=2.2.2 +scikit-learn>=1.5.1 +scipy>=1.11.4 +django-storages>=1.14.4 +django-webpack-loader>=1.8.1 +django-cors-headers>=4.4.0 +Markdown>=3.6 +django-crispy-forms>=2.1 +django-taggit>=6.1.0 +mistune>=3.0.2 +pygments>=2.18.0 +celery>=5.4.0 +redis>=5.0.7 +django-autoslug>=1.9.9 +pymemcache>=4.0.0 +dj-database-url>=2.2.0 +psycopg2-binary>=2.9.9 +gunicorn>=23.0.0 +django-redis>=5.4.0 +boto3>=1.34.160 diff --git a/requirements.txt b/requirements.txt index 7f3a2b5..f24fe7c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,38 +1,35 @@ -certifi -chardet -Django==4.2.30 -django-braces==1.15.0 -django-embed-video -djangorestframework==3.15.2 -idna -olefile -Pillow==12.2.0 -python3-memcached -pytz -whitenoise==5.0.1 -dj-database-url -gunicorn -psycopg2-binary -python-decouple -django-redis -django-storages -boto3 -django-widget-tweaks==1.4.3 -google-api-python-client==1.7.4 -numpy==1.22.0 -pandas==1.4.4 -scikit-learn==1.5.0 -scipy==1.10.0 -sklearn==0.0 -django-webpack-loader==0.6.0 -django-cors-headers==4.2.0 -Markdown==3.8.1 -django-taggit==1.1.0 -django-taggit-serializer==0.1.7 -mistune==2.0.3 -pygments==2.20.0 -celery==5.4.0 -redis==4.4.4 -django-autoslug==1.9.8 -django-crispy-forms==1.9.2 -pymemcache==4.0.0 +certifi>=2024.8.30 +chardet>=5.2.0 +Django>=6.0a1,<7.0 +django-braces>=1.15.0 +django-embed-video==1.4.10 +djangorestframework>=3.15.2 +idna>=3.7 +olefile>=0.47 +Pillow>=10.4.0 +python3-memcached==1.51 +whitenoise>=6.7.0 +dj-database-url>=2.2.0 +gunicorn>=23.0.0 +psycopg2-binary>=2.9.9 +python-decouple>=3.8 +django-redis>=5.4.0 +django-storages>=1.14.4 +boto3>=1.34.160 +django-widget-tweaks>=1.5.0 +google-api-python-client>=2.137.0 +numpy>=2.1.1 +pandas>=2.2.2 +scikit-learn>=1.5.1 +scipy>=1.11.4 +django-webpack-loader>=1.8.1 +django-cors-headers>=4.4.0 +Markdown>=3.6 +django-crispy-forms>=2.1 +django-taggit>=6.1.0 +mistune>=3.0.2 +pygments>=2.18.0 +celery>=5.4.0 +redis>=5.0.7 +django-autoslug>=1.9.9 +pymemcache>=4.0.0 diff --git a/runtime.txt b/runtime.txt index 4458b43..dae7fec 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.9.19 +python-3.13.0