From 4a256e4a4e1fb5a94854b9bafe676d29242b4e9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A9lita=20Makanda?= Date: Thu, 18 Dec 2025 18:03:48 +0100 Subject: [PATCH 1/4] Upgrade to Python 3.13 and Django 6 --- README.md | 3 +- myelearning/settings.py | 62 ++++++++++++--------------------- myelearning/urls.py | 62 +++++++++++++-------------------- requirements-dev.txt | 76 ++++++++++++++++++++++------------------- requirements.txt | 74 +++++++++++++++++++-------------------- runtime.txt | 2 +- 6 files changed, 124 insertions(+), 155 deletions(-) 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/myelearning/settings.py b/myelearning/settings.py index 8cc3e16..2e16330 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') @@ -62,6 +49,7 @@ 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 +61,6 @@ 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware', 'django.contrib.admindocs.middleware.XViewMiddleware', - 'corsheaders.middleware.CorsMiddleware', ] # if DEBUG == False: @@ -87,7 +74,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 +92,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 +116,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 +190,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 +198,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 +215,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 1b13280..bbd959c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,36 +1,40 @@ -certifi==2024.7.4 -chardet==3.0.4 -Django==4.2.24 -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==10.3.0 -python3-memcached -pytz==2017.2 -requests==2.32.4 -six==1.11.0 -urllib3==2.5.0 -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.1.1 -django-taggit==1.1.0 -django-taggit-serializer==0.1.7 -mistune==2.0.3 -pygments==2.15.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>=3.1.0 +django-memcache-status>=1.2 +djangorestframework>=3.15.2 +idna>=3.7 +olefile>=0.47 +Pillow>=10.4.0 +python3-memcached>=1.59 +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 +django-taggit-serializer>=0.1.12 +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 2e0b7f3..54e8bf6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,38 +1,36 @@ -certifi -chardet -Django==4.2.24 -django-braces==1.15.0 -django-embed-video -djangorestframework==3.15.2 -idna -olefile -Pillow==10.3.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.1.5 -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.1.1 -django-taggit==1.1.0 -django-taggit-serializer==0.1.7 -mistune==2.0.3 -pygments==2.15.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>=3.1.0 +djangorestframework>=3.15.2 +idna>=3.7 +olefile>=0.47 +Pillow>=10.4.0 +python3-memcached>=1.59 +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 +django-taggit-serializer>=0.1.12 +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 From 23fef1fabb6a81db568b5d97bf86652577f8c260 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 May 2026 21:36:31 +0000 Subject: [PATCH 2/4] downgrade django-embed-video to 1.4.10 Agent-Logs-Url: https://github.com/delitamakanda/elearning/sessions/8c7d1cdb-5d5a-4c6a-a7d0-428c58b8f215 Co-authored-by: delitamakanda <9744074+delitamakanda@users.noreply.github.com> --- requirements-dev.txt | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index bbd959c..d33e24e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,7 +2,7 @@ certifi>=2024.8.30 chardet>=5.2.0 Django>=6.0a1,<7.0 django-braces>=1.15.0 -django-embed-video>=3.1.0 +django-embed-video==1.4.10 django-memcache-status>=1.2 djangorestframework>=3.15.2 idna>=3.7 diff --git a/requirements.txt b/requirements.txt index 54e8bf6..7c60eee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ certifi>=2024.8.30 chardet>=5.2.0 Django>=6.0a1,<7.0 django-braces>=1.15.0 -django-embed-video>=3.1.0 +django-embed-video==1.4.10 djangorestframework>=3.15.2 idna>=3.7 olefile>=0.47 From 331ae5707ade9bc363a0a73a70353ff48f38a573 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 May 2026 21:40:43 +0000 Subject: [PATCH 3/4] downgrade python3-memcached to 1.51 Agent-Logs-Url: https://github.com/delitamakanda/elearning/sessions/730cc001-069c-432e-b5d3-3ec21d735874 Co-authored-by: delitamakanda <9744074+delitamakanda@users.noreply.github.com> --- requirements-dev.txt | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index d33e24e..230ba20 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ djangorestframework>=3.15.2 idna>=3.7 olefile>=0.47 Pillow>=10.4.0 -python3-memcached>=1.59 +python3-memcached==1.51 requests>=2.32.3 six>=1.16.0 urllib3>=2.2.2 diff --git a/requirements.txt b/requirements.txt index 7c60eee..32b7d32 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ djangorestframework>=3.15.2 idna>=3.7 olefile>=0.47 Pillow>=10.4.0 -python3-memcached>=1.59 +python3-memcached==1.51 whitenoise>=6.7.0 dj-database-url>=2.2.0 gunicorn>=23.0.0 From f9cf2b9c3053320412f7381891b106dea398de43 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 May 2026 21:48:34 +0000 Subject: [PATCH 4/4] replace django-taggit-serializer with inline TagListSerializerField and TaggitSerializer Agent-Logs-Url: https://github.com/delitamakanda/elearning/sessions/00a5534c-f903-4450-8799-edaeabb86786 Co-authored-by: delitamakanda <9744074+delitamakanda@users.noreply.github.com> --- apps/common/serializers.py | 84 ++++++++++++++++++++++++++++++++++++++ myelearning/settings.py | 1 - requirements-dev.txt | 1 - requirements.txt | 1 - 4 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 apps/common/serializers.py 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 2e16330..d075a4d 100644 --- a/myelearning/settings.py +++ b/myelearning/settings.py @@ -43,7 +43,6 @@ 'widget_tweaks', 'corsheaders', 'taggit', - 'taggit_serializer', ] MIDDLEWARE = [ diff --git a/requirements-dev.txt b/requirements-dev.txt index 230ba20..a9891f5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -26,7 +26,6 @@ django-cors-headers>=4.4.0 Markdown>=3.6 django-crispy-forms>=2.1 django-taggit>=6.1.0 -django-taggit-serializer>=0.1.12 mistune>=3.0.2 pygments>=2.18.0 celery>=5.4.0 diff --git a/requirements.txt b/requirements.txt index 32b7d32..f24fe7c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,7 +27,6 @@ django-cors-headers>=4.4.0 Markdown>=3.6 django-crispy-forms>=2.1 django-taggit>=6.1.0 -django-taggit-serializer>=0.1.12 mistune>=3.0.2 pygments>=2.18.0 celery>=5.4.0