diff --git a/assets/pasted-20260207-201453-28f355e3.jpg b/assets/pasted-20260207-201453-28f355e3.jpg new file mode 100644 index 0000000..97c946b Binary files /dev/null and b/assets/pasted-20260207-201453-28f355e3.jpg differ diff --git a/assets/vm-shot-2026-02-07T20-14-48-715Z.jpg b/assets/vm-shot-2026-02-07T20-14-48-715Z.jpg new file mode 100644 index 0000000..97c946b Binary files /dev/null and b/assets/vm-shot-2026-02-07T20-14-48-715Z.jpg differ diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 96bce55..dc4c78d 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 0b85e94..0de8fbb 100644 Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ diff --git a/config/settings.py b/config/settings.py index 291d043..24a7558 100644 --- a/config/settings.py +++ b/config/settings.py @@ -180,3 +180,6 @@ if EMAIL_USE_SSL: # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +LOGIN_REDIRECT_URL = '/' +LOGOUT_REDIRECT_URL = '/' diff --git a/config/urls.py b/config/urls.py index bcfc074..2001e5f 100644 --- a/config/urls.py +++ b/config/urls.py @@ -1,19 +1,3 @@ -""" -URL configuration for config project. - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/5.2/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" from django.contrib import admin from django.urls import include, path from django.conf import settings @@ -21,9 +5,10 @@ from django.conf.urls.static import static urlpatterns = [ path("admin/", admin.site.urls), + path("accounts/", include("django.contrib.auth.urls")), path("", include("core.urls")), ] if settings.DEBUG: urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets") - urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) \ No newline at end of file diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index a5ed392..4a272e8 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc new file mode 100644 index 0000000..7d41c72 Binary files /dev/null and b/core/__pycache__/forms.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index e061640..76f0a4f 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 5a69659..b4b4993 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 2a36fd6..1bf81ae 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 8c38f3f..df68cb9 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,3 +1,24 @@ from django.contrib import admin +from .models import Course, Module, Lesson, UserProgress -# Register your models here. +@admin.register(Course) +class CourseAdmin(admin.ModelAdmin): + list_display = ('title', 'created_at') + search_fields = ('title',) + +@admin.register(Module) +class ModuleAdmin(admin.ModelAdmin): + list_display = ('title', 'course', 'order') + list_filter = ('course',) + ordering = ('course', 'order') + +@admin.register(Lesson) +class LessonAdmin(admin.ModelAdmin): + list_display = ('title', 'module', 'order') + list_filter = ('module__course', 'module') + ordering = ('module', 'order') + +@admin.register(UserProgress) +class UserProgressAdmin(admin.ModelAdmin): + list_display = ('user', 'lesson', 'completed_at') + list_filter = ('user',) \ No newline at end of file diff --git a/core/forms.py b/core/forms.py new file mode 100644 index 0000000..7ffba8e --- /dev/null +++ b/core/forms.py @@ -0,0 +1,33 @@ +from django import forms +from django.contrib.auth.models import User +from .models import Profile, Course, Module, Lesson + +class UserUpdateForm(forms.ModelForm): + email = forms.EmailField() + + class Meta: + model = User + fields = ['username', 'email', 'first_name', 'last_name'] + +class ProfileUpdateForm(forms.ModelForm): + class Meta: + model = Profile + fields = ['bio', 'location', 'birth_date', 'avatar'] + widgets = { + 'birth_date': forms.DateInput(attrs={'type': 'date'}), + } + +class CourseForm(forms.ModelForm): + class Meta: + model = Course + fields = ['title', 'description', 'image'] + +class ModuleForm(forms.ModelForm): + class Meta: + model = Module + fields = ['title', 'order'] + +class LessonForm(forms.ModelForm): + class Meta: + model = Lesson + fields = ['title', 'content', 'order'] \ No newline at end of file diff --git a/core/management/__init__.py b/core/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/management/__pycache__/__init__.cpython-311.pyc b/core/management/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..309c8b7 Binary files /dev/null and b/core/management/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/management/commands/__init__.py b/core/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/management/commands/__pycache__/__init__.cpython-311.pyc b/core/management/commands/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..349369e Binary files /dev/null and b/core/management/commands/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/management/commands/__pycache__/seed_data.cpython-311.pyc b/core/management/commands/__pycache__/seed_data.cpython-311.pyc new file mode 100644 index 0000000..878f849 Binary files /dev/null and b/core/management/commands/__pycache__/seed_data.cpython-311.pyc differ diff --git a/core/management/commands/seed_data.py b/core/management/commands/seed_data.py new file mode 100644 index 0000000..fccb727 --- /dev/null +++ b/core/management/commands/seed_data.py @@ -0,0 +1,61 @@ +from django.core.management.base import BaseCommand +from core.models import Course, Module, Lesson + +class Command(BaseCommand): + help = 'Seeds initial Python course data' + + def handle(self, *args, **kwargs): + # Python Fundamentals + course1, created = Course.objects.get_or_create( + title='Python Fundamentals', + defaults={'description': 'A comprehensive guide to Python for absolute beginners. High-contrast learning.'} + ) + + m1, _ = Module.objects.get_or_create(course=course1, title='Getting Started', order=1) + m2, _ = Module.objects.get_or_create(course=course1, title='Control Flow', order=2) + m3, _ = Module.objects.get_or_create(course=course1, title='Data Structures', order=3) + + Lesson.objects.get_or_create( + module=m1, title='Installation & Setup', + defaults={'content': 'In this lesson, we will install Python 3.11 and set up a minimalist IDE like VS Code or just use a terminal.', 'order': 1} + ) + Lesson.objects.get_or_create( + module=m1, title='Variables & Data Types', + defaults={'content': 'Python is dynamically typed. Learn about integers, strings, and booleans.', 'order': 2} + ) + Lesson.objects.get_or_create( + module=m2, title='If-Else Statements', + defaults={'content': 'Logic flows. If this then that. Else something else.', 'order': 1} + ) + Lesson.objects.get_or_create( + module=m3, title='Lists & Tuples', + defaults={'content': 'Learn how to store multiple items in a single variable using lists and immutable tuples.', 'order': 1} + ) + Lesson.objects.get_or_create( + module=m3, title='Dictionaries & Sets', + defaults={'content': 'Master key-value pairs and unique collections of items.', 'order': 2} + ) + + # Advanced Python + course2, created = Course.objects.get_or_create( + title='Advanced Python', + defaults={'description': 'Take your Python skills to the next level with complex architectures and patterns.'} + ) + + am1, _ = Module.objects.get_or_create(course=course2, title='Object Oriented Programming', order=1) + am2, _ = Module.objects.get_or_create(course=course2, title='Decorators & Generators', order=2) + + Lesson.objects.get_or_create( + module=am1, title='Classes and Objects', + defaults={'content': 'Understand the core concepts of OOP: how to define classes and instantiate objects.', 'order': 1} + ) + Lesson.objects.get_or_create( + module=am1, title='Inheritance', + defaults={'content': 'Learn how to create subclasses that inherit attributes and methods from a parent class.', 'order': 2} + ) + Lesson.objects.get_or_create( + module=am2, title='Function Decorators', + defaults={'content': 'Enhance your functions without modifying their source code using decorators.', 'order': 1} + ) + + self.stdout.write(self.style.SUCCESS('Successfully seeded extended Python Lern data')) \ No newline at end of file diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..19f6299 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,66 @@ +# Generated by Django 5.2.7 on 2026-02-05 15:28 + +import django.db.models.deletion +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.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255)), + ('description', models.TextField()), + ('image', models.ImageField(blank=True, null=True, upload_to='courses/')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + ), + migrations.CreateModel( + name='Module', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255)), + ('order', models.PositiveIntegerField(default=0)), + ('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='modules', to='core.course')), + ], + options={ + 'ordering': ['order'], + }, + ), + migrations.CreateModel( + name='Lesson', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255)), + ('content', models.TextField()), + ('order', models.PositiveIntegerField(default=0)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('module', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lessons', to='core.module')), + ], + options={ + 'ordering': ['order'], + }, + ), + migrations.CreateModel( + name='UserProgress', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('completed_at', models.DateTimeField(auto_now_add=True)), + ('lesson', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.lesson')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'unique_together': {('user', 'lesson')}, + }, + ), + ] diff --git a/core/migrations/0002_profile.py b/core/migrations/0002_profile.py new file mode 100644 index 0000000..7815d45 --- /dev/null +++ b/core/migrations/0002_profile.py @@ -0,0 +1,27 @@ +# Generated by Django 5.2.7 on 2026-02-07 19:56 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Profile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('bio', models.TextField(blank=True, max_length=500)), + ('location', models.CharField(blank=True, max_length=100)), + ('birth_date', models.DateField(blank=True, null=True)), + ('avatar', models.ImageField(blank=True, null=True, upload_to='avatars/')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/core/migrations/0003_course_author.py b/core/migrations/0003_course_author.py new file mode 100644 index 0000000..f76afcd --- /dev/null +++ b/core/migrations/0003_course_author.py @@ -0,0 +1,21 @@ +# Generated by Django 5.2.7 on 2026-02-07 20:06 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_profile'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='course', + name='author', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='courses_created', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..b437832 Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0002_profile.cpython-311.pyc b/core/migrations/__pycache__/0002_profile.cpython-311.pyc new file mode 100644 index 0000000..5e6f4de Binary files /dev/null and b/core/migrations/__pycache__/0002_profile.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0003_course_author.cpython-311.pyc b/core/migrations/__pycache__/0003_course_author.cpython-311.pyc new file mode 100644 index 0000000..43ed5e6 Binary files /dev/null and b/core/migrations/__pycache__/0003_course_author.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a8362..e701445 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,72 @@ from django.db import models +from django.contrib.auth.models import User +from django.db.models.signals import post_save +from django.dispatch import receiver -# Create your models here. +class Profile(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE) + bio = models.TextField(max_length=500, blank=True) + location = models.CharField(max_length=100, blank=True) + birth_date = models.DateField(null=True, blank=True) + avatar = models.ImageField(upload_to='avatars/', null=True, blank=True) + + def __str__(self): + return f"{self.user.username}'s Profile" + +@receiver(post_save, sender=User) +def create_user_profile(sender, instance, created, **kwargs): + if created: + Profile.objects.get_or_create(user=instance) + +@receiver(post_save, sender=User) +def save_user_profile(sender, instance, **kwargs): + if hasattr(instance, 'profile'): + instance.profile.save() + else: + Profile.objects.create(user=instance) + +class Course(models.Model): + author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='courses_created', null=True, blank=True) + title = models.CharField(max_length=255) + description = models.TextField() + image = models.ImageField(upload_to='courses/', null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.title + +class Module(models.Model): + course = models.ForeignKey(Course, related_name='modules', on_delete=models.CASCADE) + title = models.CharField(max_length=255) + order = models.PositiveIntegerField(default=0) + + class Meta: + ordering = ['order'] + + def __str__(self): + return f"{self.course.title} - {self.title}" + +class Lesson(models.Model): + module = models.ForeignKey(Module, related_name='lessons', on_delete=models.CASCADE) + title = models.CharField(max_length=255) + content = models.TextField() + order = models.PositiveIntegerField(default=0) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['order'] + + def __str__(self): + return self.title + +class UserProgress(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + lesson = models.ForeignKey(Lesson, on_delete=models.CASCADE) + completed_at = models.DateTimeField(auto_now_add=True) + + class Meta: + unique_together = ('user', 'lesson') + + def __str__(self): + return f"{self.user.username} completed {self.lesson.title}" \ No newline at end of file diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..5a629d3 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,25 +1,109 @@ +{% load static %} - - - {% block title %}Knowledge Base{% endblock %} - {% if project_description %} - - - - {% endif %} - {% if project_image_url %} - - - {% endif %} - {% load static %} - - {% block head %}{% endblock %} + + + DevLearn - Modern Learning for Developers + + + + - - {% block content %}{% endblock %} - + +
+ {% if messages %} +
+ {% for message in messages %} + + {% endfor %} +
+ {% endif %} + + {% block content %} + {% endblock %} +
+ + + + + diff --git a/core/templates/core/course_confirm_delete.html b/core/templates/core/course_confirm_delete.html new file mode 100644 index 0000000..6a4bfb7 --- /dev/null +++ b/core/templates/core/course_confirm_delete.html @@ -0,0 +1,18 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+
+

Delete Course?

+

Are you sure you want to delete "{{ object.title }}"? This action cannot be undone and all associated modules and lessons will be lost.

+ +
+ {% csrf_token %} + + Cancel +
+
+
+
+{% endblock %} diff --git a/core/templates/core/course_detail.html b/core/templates/core/course_detail.html new file mode 100644 index 0000000..3c4f1cb --- /dev/null +++ b/core/templates/core/course_detail.html @@ -0,0 +1,49 @@ +{% extends 'base.html' %} + +{% block title %}{{ course.title }} - Python Lern App{% endblock %} + +{% block content %} +
+
+ ← BACK_TO_PATHS +

{{ course.title }}

+

{{ course.description }}

+
+
+ +
+
+
+
+

SYLLABUS_MAP

+
+ {% for module in modules %} +
+

{{ module.title|upper }}

+
+ {% for lesson in module.lessons.all %} + + {{ lesson.title }} + START + + {% endfor %} +
+
+ {% empty %} +

THIS PATH IS CURRENTLY UNDER CONSTRUCTION.

+ {% endfor %} +
+
+
+
+

PATH_PROGRESS

+
+
0%
+
+

COMPLETE LESSONS TO TRACK YOUR PROGRESS.

+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/core/templates/core/course_form.html b/core/templates/core/course_form.html new file mode 100644 index 0000000..2de088e --- /dev/null +++ b/core/templates/core/course_form.html @@ -0,0 +1,78 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} +
+
+
+ + +

{{ title }}

+ +
+
+
+ {% csrf_token %} + {% for field in form %} +
+ + {{ field }} + {% if field.errors %} +
{{ field.errors }}
+ {% endif %} +
+ {% endfor %} + +
+
+
+ + {% if form.instance.pk %} +
+

Modules

+ Add Module +
+ +
+ {% for module in form.instance.modules.all %} +
+
+
+
{{ module.title }}
+ Order: {{ module.order }} +
+ +
+ + {% if module.lessons.all %} +
+
Lessons:
+
    + {% for lesson in module.lessons.all %} +
  • + {{ lesson.title }} (Order: {{ lesson.order }}) + Edit +
  • + {% endfor %} +
+
+ {% endif %} +
+ {% empty %} +
+

No modules yet.

+
+ {% endfor %} +
+ {% endif %} +
+
+
+{% endblock %} diff --git a/core/templates/core/dashboard.html b/core/templates/core/dashboard.html new file mode 100644 index 0000000..89e56bb --- /dev/null +++ b/core/templates/core/dashboard.html @@ -0,0 +1,35 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+

Management Dashboard

+ Add New Course +
+ +
+ {% for course in courses %} +
+
+
+
{{ course.title }}
+

{{ course.description|truncatewords:20 }}

+
+
+ Edit + Delete +
+ Created: {{ course.created_at|date:"M d, Y" }} +
+
+
+
+ {% empty %} +
+

You haven't created any courses yet.

+ Get Started +
+ {% endfor %} +
+
+{% endblock %} diff --git a/core/templates/core/index.html b/core/templates/core/index.html index faec813..10449ca 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,145 +1,52 @@ -{% extends "base.html" %} - -{% block title %}{{ project_name }}{% endblock %} - -{% block head %} - - - - -{% endblock %} +{% extends 'base.html' %} +{% load static %} {% block content %} -
-
-

Analyzing your requirements and generating your app…

-
- Loading… +
+
+
+

PYTHON_MASTER

+

STRONGLY TYPED. MINIMALLY DESIGNED.

+ +
-

AppWizzy AI is collecting your requirements and applying the first changes.

-

This page will refresh automatically as the plan is implemented.

-

- Runtime: Django {{ django_version }} · Python {{ python_version }} - — UTC {{ current_time|date:"Y-m-d H:i:s" }} -

-
-
- + + +
+
+

LEARNING_PATHS

+
+ {% for course in courses %} +
+
+

{{ course.title }}

+

{{ course.description|truncatewords:20 }}

+
+ VIEW PATH +
+
+
+ {% empty %} +
+

NO PATHS AVAILABLE YET. CHECK BACK SOON.

+ {% if user.is_staff %} + ADD FIRST COURSE + {% endif %} +
+ {% endfor %} +
+
+
+ +
+
+

WHY_B&W?

+

+ WE REMOVE ALL DISTRACTIONS. NO COLORS. NO FLUFF. JUST YOU AND THE CODE. + FOCUS ON LOGIC, SYNTAX, AND ARCHITECTURE. +

+
+
{% endblock %} \ No newline at end of file diff --git a/core/templates/core/lesson_detail.html b/core/templates/core/lesson_detail.html new file mode 100644 index 0000000..5155024 --- /dev/null +++ b/core/templates/core/lesson_detail.html @@ -0,0 +1,36 @@ +{% extends 'base.html' %} + +{% block title %}{{ lesson.title }} - Python Lern App{% endblock %} + +{% block content %} +
+
+
+ ← BACK_TO_MODULE + {{ lesson.module.title|upper }} +
+
+
+ +
+
+
+
+

{{ lesson.title }}

+ +
+ {{ lesson.content|safe|linebreaks }} +
+ +
+

LESSON_COMPLETE?

+
+ YES, NEXT LESSON + +
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/core/templates/core/lesson_form.html b/core/templates/core/lesson_form.html new file mode 100644 index 0000000..06fb927 --- /dev/null +++ b/core/templates/core/lesson_form.html @@ -0,0 +1,36 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+
+ + +

Lesson Management

+

Module: {{ module.title }}

+ +
+
+
+ {% csrf_token %} + {% for field in form %} +
+ + {{ field }} + {% if field.errors %} +
{{ field.errors }}
+ {% endif %} +
+ {% endfor %} + +
+
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/module_form.html b/core/templates/core/module_form.html new file mode 100644 index 0000000..ff9ec9f --- /dev/null +++ b/core/templates/core/module_form.html @@ -0,0 +1,36 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+
+ + +

Module Management

+

Course: {{ course.title }}

+ +
+
+
+ {% csrf_token %} + {% for field in form %} +
+ + {{ field }} + {% if field.errors %} +
{{ field.errors }}
+ {% endif %} +
+ {% endfor %} + +
+
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/profile.html b/core/templates/core/profile.html new file mode 100644 index 0000000..465bb6f --- /dev/null +++ b/core/templates/core/profile.html @@ -0,0 +1,139 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} +
+
+
+
+
+
+
+ {% if user.profile.avatar %} + {{ user.username }} + {% else %} +
+ {{ user.username|first|upper }} +
+ {% endif %} +

{{ user.username }}

+

{{ user.email }}

+
+
+

Location: {{ user.profile.location|default:"NOT_SET" }}

+

Joined: {{ user.date_joined|date:"M d, Y" }}

+
+
+
+
+
+
+
+

UPDATE_PROFILE

+
+ {% csrf_token %} +
+
+ + {{ u_form.username }} +
+
+ + {{ u_form.email }} +
+
+
+
+ + {{ u_form.first_name }} +
+
+ + {{ u_form.last_name }} +
+
+
+ + {{ p_form.bio }} +
+
+
+ + {{ p_form.location }} +
+
+ + {{ p_form.birth_date }} +
+
+
+ + {{ p_form.avatar }} +
+ +
+
+
+ +
+
+

YOUR_PROGRESS

+ {% if user_progress %} +
+ + + + + + + + + + {% for progress in user_progress %} + + + + + + {% endfor %} + +
COURSELESSONCOMPLETED
{{ progress.lesson.module.course.title }} + {{ progress.lesson.title }} + {{ progress.completed_at|date:"M d, Y" }}
+
+ {% else %} +

YOU_HAVENT_STARTED_ANY_LESSONS_YET

+ BROWSE_COURSES + {% endif %} +
+
+
+
+
+
+
+ + diff --git a/core/templates/registration/login.html b/core/templates/registration/login.html new file mode 100644 index 0000000..ca49a82 --- /dev/null +++ b/core/templates/registration/login.html @@ -0,0 +1,48 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} +
+
+
+

AUTHENTICATE

+ +
+ {% csrf_token %} + {% if form.errors %} +
+ INVALID_CREDENTIALS. PLEASE_TRY_AGAIN. +
+ {% endif %} + + {% for field in form %} +
+ + + {% if field.help_text %} +
{{ field.help_text|safe }}
+ {% endif %} + {% for error in field.errors %} +
{{ error }}
+ {% endfor %} +
+ {% endfor %} + +
+ + +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/core/templates/registration/password_reset_complete.html b/core/templates/registration/password_reset_complete.html new file mode 100644 index 0000000..a94280b --- /dev/null +++ b/core/templates/registration/password_reset_complete.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} +
+
+
+

RESET_COMPLETE

+

YOUR_PASSWORD_HAS_BEEN_SUCCESSFULLY_UPDATED.

+
+ LOG_IN +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/core/templates/registration/password_reset_confirm.html b/core/templates/registration/password_reset_confirm.html new file mode 100644 index 0000000..532d79a --- /dev/null +++ b/core/templates/registration/password_reset_confirm.html @@ -0,0 +1,45 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} +
+
+
+

SET_NEW_PASSWORD

+ + {% if validlink %} +
+ {% csrf_token %} + {% for field in form %} +
+ + + {% if field.help_text %} +
{{ field.help_text|safe }}
+ {% endif %} + {% for error in field.errors %} +
{{ error }}
+ {% endfor %} +
+ {% endfor %} +
+ +
+
+ {% else %} +
+

INVALID_OR_EXPIRED_LINK.

+ +
+ {% endif %} +
+
+
+{% endblock %} \ No newline at end of file diff --git a/core/templates/registration/password_reset_done.html b/core/templates/registration/password_reset_done.html new file mode 100644 index 0000000..cfdb1e9 --- /dev/null +++ b/core/templates/registration/password_reset_done.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} +
+
+
+

EMAIL_SENT

+

CHECK_YOUR_INBOX_FOR_FURTHER_INSTRUCTIONS.

+ +
+
+
+{% endblock %} \ No newline at end of file diff --git a/core/templates/registration/password_reset_form.html b/core/templates/registration/password_reset_form.html new file mode 100644 index 0000000..f8c3160 --- /dev/null +++ b/core/templates/registration/password_reset_form.html @@ -0,0 +1,37 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} +
+
+
+

RESET_PASSWORD

+

ENTER_EMAIL_TO_RECEIVE_INSTRUCTIONS

+ +
+ {% csrf_token %} + {% for field in form %} +
+ + + {% for error in field.errors %} +
{{ error }}
+ {% endfor %} +
+ {% endfor %} +
+ + +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/core/templates/registration/signup.html b/core/templates/registration/signup.html new file mode 100644 index 0000000..9cb427a --- /dev/null +++ b/core/templates/registration/signup.html @@ -0,0 +1,48 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} +
+
+
+

REGISTER

+ +
+ {% csrf_token %} + + {% if form.errors %} +
+ REGISTRATION_FAILED. CHECK_DATA. +
+ {% endif %} + + {% for field in form %} +
+ + + {% if field.help_text %} +
{{ field.help_text|safe }}
+ {% endif %} + {% for error in field.errors %} +
{{ error }}
+ {% endfor %} +
+ {% endfor %} + +
+ +
+ ALREADY_HAVE_AN_ACCOUNT? + LOG_IN +
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/core/urls.py b/core/urls.py index 6299e3d..30385e4 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,18 @@ from django.urls import path - -from .views import home +from . import views urlpatterns = [ - path("", home, name="home"), -] + path('', views.index, name='index'), + path('signup/', views.signup, name='signup'), + path('course//', views.course_detail, name='course_detail'), + path('lesson//', views.lesson_detail, name='lesson_detail'), + path('profile/', views.profile, name='profile'), + path('dashboard/', views.dashboard, name='dashboard'), + path('course/add/', views.course_create, name='course_create'), + path('course//edit/', views.course_edit, name='course_edit'), + path('course//delete/', views.course_delete, name='course_delete'), + path('course//module/add/', views.module_create, name='module_create'), + path('module//edit/', views.module_edit, name='module_edit'), + path('module//lesson/add/', views.lesson_create, name='lesson_create'), + path('lesson//edit/', views.lesson_edit, name='lesson_edit'), +] \ No newline at end of file diff --git a/core/views.py b/core/views.py index c9aed12..deae9f8 100644 --- a/core/views.py +++ b/core/views.py @@ -1,25 +1,158 @@ -import os -import platform +from django.shortcuts import render, get_object_or_404, redirect +from django.contrib.auth.decorators import login_required +from django.contrib import messages +from django.contrib.auth.forms import UserCreationForm +from django.contrib.auth import login as auth_login +from .models import Course, Module, Lesson, UserProgress +from .forms import UserUpdateForm, ProfileUpdateForm, CourseForm, ModuleForm, LessonForm -from django import get_version as django_version -from django.shortcuts import render -from django.utils import timezone +def index(request): + courses = Course.objects.all() + return render(request, 'core/index.html', {'courses': courses}) +def signup(request): + if request.method == 'POST': + form = UserCreationForm(request.POST) + if form.is_valid(): + user = form.save() + auth_login(request, user) + messages.success(request, 'Registration successful!') + return redirect('index') + else: + form = UserCreationForm() + return render(request, 'registration/signup.html', {'form': form}) -def home(request): - """Render the landing screen with loader and environment details.""" - host_name = request.get_host().lower() - agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic" - now = timezone.now() +def course_detail(request, pk): + course = get_object_or_404(Course, pk=pk) + return render(request, 'core/course_detail.html', {'course': course}) + +def lesson_detail(request, pk): + lesson = get_object_or_404(Lesson, pk=pk) + course = lesson.module.course + + if request.user.is_authenticated: + UserProgress.objects.get_or_create(user=request.user, lesson=lesson) + + return render(request, 'core/lesson_detail.html', {'lesson': lesson, 'course': course}) + +@login_required +def profile(request): + if request.method == 'POST': + u_form = UserUpdateForm(request.POST, instance=request.user) + p_form = ProfileUpdateForm(request.POST, request.FILES, instance=request.user.profile) + if u_form.is_valid() and p_form.is_valid(): + u_form.save() + p_form.save() + messages.success(request, f'Your account has been updated!') + return redirect('profile') + else: + u_form = UserUpdateForm(instance=request.user) + p_form = ProfileUpdateForm(instance=request.user.profile) + + user_progress = UserProgress.objects.filter(user=request.user) + completed_lessons = [progress.lesson for progress in user_progress] context = { - "project_name": "New Style", - "agent_brand": agent_brand, - "django_version": django_version(), - "python_version": platform.python_version(), - "current_time": now, - "host_name": host_name, - "project_description": os.getenv("PROJECT_DESCRIPTION", ""), - "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), + 'u_form': u_form, + 'p_form': p_form, + 'completed_lessons': completed_lessons } - return render(request, "core/index.html", context) + + return render(request, 'core/profile.html', context) + +@login_required +def dashboard(request): + authored_courses = Course.objects.filter(author=request.user) + return render(request, 'core/dashboard.html', {'courses': authored_courses}) + +@login_required +def course_create(request): + if request.method == 'POST': + form = CourseForm(request.POST, request.FILES) + if form.is_valid(): + course = form.save(commit=False) + course.author = request.user + course.save() + messages.success(request, 'Course created successfully!') + return redirect('dashboard') + else: + form = CourseForm() + return render(request, 'core/course_form.html', {'form': form, 'title': 'Create Course'}) + +@login_required +def course_edit(request, pk): + course = get_object_or_404(Course, pk=pk, author=request.user) + if request.method == 'POST': + form = CourseForm(request.POST, request.FILES, instance=course) + if form.is_valid(): + form.save() + messages.success(request, 'Course updated successfully!') + return redirect('dashboard') + else: + form = CourseForm(instance=course) + return render(request, 'core/course_form.html', {'form': form, 'title': 'Edit Course'}) + +@login_required +def course_delete(request, pk): + course = get_object_or_404(Course, pk=pk, author=request.user) + if request.method == 'POST': + course.delete() + messages.success(request, 'Course deleted successfully!') + return redirect('dashboard') + return render(request, 'core/course_confirm_delete.html', {'object': course}) + +@login_required +def module_create(request, course_pk): + course = get_object_or_404(Course, pk=course_pk, author=request.user) + if request.method == 'POST': + form = ModuleForm(request.POST) + if form.is_valid(): + module = form.save(commit=False) + module.course = course + module.save() + messages.success(request, 'Module added successfully!') + return redirect('course_edit', pk=course.pk) + else: + form = ModuleForm() + return render(request, 'core/module_form.html', {'form': form, 'course': course}) + +@login_required +def module_edit(request, pk): + module = get_object_or_404(Module, pk=pk, course__author=request.user) + if request.method == 'POST': + form = ModuleForm(request.POST, instance=module) + if form.is_valid(): + form.save() + messages.success(request, 'Module updated successfully!') + return redirect('course_edit', pk=module.course.pk) + else: + form = ModuleForm(instance=module) + return render(request, 'core/module_form.html', {'form': form, 'course': module.course}) + +@login_required +def lesson_create(request, module_pk): + module = get_object_or_404(Module, pk=module_pk, course__author=request.user) + if request.method == 'POST': + form = LessonForm(request.POST) + if form.is_valid(): + lesson = form.save(commit=False) + lesson.module = module + lesson.save() + messages.success(request, 'Lesson added successfully!') + return redirect('course_edit', pk=module.course.pk) + else: + form = LessonForm() + return render(request, 'core/lesson_form.html', {'form': form, 'module': module}) + +@login_required +def lesson_edit(request, pk): + lesson = get_object_or_404(Lesson, pk=pk, module__course__author=request.user) + if request.method == 'POST': + form = LessonForm(request.POST, instance=lesson) + if form.is_valid(): + form.save() + messages.success(request, 'Lesson updated successfully!') + return redirect('course_edit', pk=lesson.module.course.pk) + else: + form = LessonForm(instance=lesson) + return render(request, 'core/lesson_form.html', {'form': form, 'module': lesson.module}) \ No newline at end of file diff --git a/static/css/custom.css b/static/css/custom.css index 925f6ed..09fc132 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -1,4 +1,300 @@ -/* Custom styles for the application */ -body { - font-family: system-ui, -apple-system, sans-serif; +/* Black & White Theme for Python Lern App */ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&family=JetBrains+Mono:wght@400;700&display=swap'); + +:root { + --bw-bg: #ffffff; + --bw-text: #000000; + --bw-black: #000000; + --bw-white: #ffffff; + --bw-grey-light: #f5f5f5; + --bw-grey-medium: #888888; + --bw-grey-dark: #333333; + --bw-border: #000000; } + +[data-theme='dark'] { + --bw-bg: #000000; + --bw-text: #ffffff; + --bw-grey-light: #1a1a1a; + --bw-border: #ffffff; +} + +html, body { + background-color: var(--bw-bg) !important; + color: var(--bw-text) !important; +} + +body { + font-family: 'Inter', sans-serif; + line-height: 1.6; + transition: background-color 0.3s ease, color 0.3s ease; +} + +h1, h2, h3, h4, h5, h6, .font-mono { + font-family: 'JetBrains Mono', monospace; + font-weight: 700; + text-transform: uppercase; + letter-spacing: -1px; + color: var(--bw-text); +} + +/* Utilities that respect theme */ +.bw-text { color: var(--bw-text) !important; } +.bw-bg { background-color: var(--bw-bg) !important; } +.bw-border { border-color: var(--bw-border) !important; } + +/* Override some Bootstrap defaults to respect theme */ +.text-muted { + color: var(--bw-grey-medium) !important; +} + +.list-group-item { + background-color: transparent !important; + color: var(--bw-text) !important; + border-color: var(--bw-border) !important; +} + +.list-group-item-action:hover { + background-color: var(--bw-grey-light) !important; + color: var(--bw-text) !important; +} + +/* Brutalist UI Elements */ +.btn-bw { + background-color: var(--bw-text) !important; + color: var(--bw-bg) !important; + border: 2px solid var(--bw-text) !important; + border-radius: 0; + padding: 10px 25px; + font-family: 'JetBrains Mono', monospace; + font-weight: 700; + transition: all 0.2s ease; +} + +.btn-bw:hover { + background-color: var(--bw-bg) !important; + color: var(--bw-text) !important; +} + +.btn-bw-outline { + background-color: var(--bw-bg) !important; + color: var(--bw-text) !important; + border: 2px solid var(--bw-text) !important; + border-radius: 0; + padding: 10px 25px; + font-family: 'JetBrains Mono', monospace; + font-weight: 700; + transition: all 0.2s ease; +} + +.btn-bw-outline:hover { + background-color: var(--bw-text) !important; + color: var(--bw-bg) !important; +} + +.card-bw { + border: 2px solid var(--bw-border) !important; + border-radius: 0; + background-color: var(--bw-bg) !important; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.card-bw:hover { + transform: translate(-4px, -4px); + box-shadow: 4px 4px 0px var(--bw-border); +} + +.border-bw { + border: 2px solid var(--bw-border) !important; + border-radius: 0 !important; +} + +/* Section Inverted logic */ +.section-inverted { + background-color: var(--bw-text) !important; + color: var(--bw-bg) !important; +} + +.section-inverted h1, +.section-inverted h2, +.section-inverted h3, +.section-inverted h4, +.section-inverted h5, +.section-inverted h6, +.section-inverted p, +.section-inverted li, +.section-inverted div, +.section-inverted span, +.section-inverted .font-mono, +.section-inverted .lead { + color: var(--bw-bg) !important; +} + +/* Adjust components inside inverted section */ +.section-inverted .btn-bw { + background-color: var(--bw-bg) !important; + color: var(--bw-text) !important; + border-color: var(--bw-bg) !important; +} + +.section-inverted .btn-bw-outline { + background-color: transparent !important; + color: var(--bw-bg) !important; + border-color: var(--bw-bg) !important; +} + +/* Hero Section */ +.hero-bw { + padding: 100px 0; + border-bottom: 2px solid var(--bw-border); + background-image: radial-gradient(var(--bw-text) 1px, transparent 1px); + background-size: 20px 20px; +} + +.hero-content { + background-color: var(--bw-bg) !important; + padding: 40px; + border: 4px solid var(--bw-border) !important; + display: inline-block; + width: 100%; + max-width: 800px; +} + +/* Navbar */ +.navbar-bw { + border-bottom: 2px solid var(--bw-border); + padding: 20px 0; + background-color: var(--bw-bg) !important; +} + +.navbar-brand-bw { + font-family: 'JetBrains Mono', monospace; + font-weight: 700; + font-size: 1.5rem; + color: var(--bw-text) !important; +} + +.nav-link { + color: var(--bw-text) !important; +} + +.navbar-toggler { + border-color: var(--bw-border) !important; + border-radius: 0; +} + +[data-theme='dark'] .navbar-toggler-icon { + filter: invert(1); +} + +/* Timeline/Modules */ +.module-list { + border-left: 2px solid var(--bw-border); + padding-left: 30px; + margin-left: 10px; +} + +.module-item { + position: relative; + margin-bottom: 40px; +} + +.module-item::before { + content: ''; + position: absolute; + left: -41px; + top: 5px; + width: 20px; + height: 20px; + background-color: var(--bw-text); +} + +/* Code block styling */ +pre, code { + font-family: 'JetBrains Mono', monospace; + background-color: var(--bw-grey-light) !important; + border: 1px solid var(--bw-border) !important; + padding: 10px; + color: var(--bw-text) !important; +} + +/* Progress Bar */ +.progress { + background-color: var(--bw-grey-light) !important; + border-color: var(--bw-border) !important; + border-radius: 0; +} + +.progress-bar { + background-color: var(--bw-text) !important; + color: var(--bw-bg) !important; +} + +/* Theme Toggle Button */ +.theme-toggle { + cursor: pointer; + border: 2px solid var(--bw-text); + background: var(--bw-bg); + color: var(--bw-text); + padding: 5px 15px; + font-family: 'JetBrains Mono', monospace; + font-weight: 700; + margin-left: 15px; + transition: all 0.2s ease; +} + +.theme-toggle:hover { + background: var(--bw-text); + color: var(--bw-bg); +} + +/* Footer fixes */ +footer.bw-border { + border-top: 2px solid var(--bw-border) !important; +} + +/* Global Form Styling */ +.form-control, .form-select { + background-color: transparent !important; + border: 2px solid var(--bw-border) !important; + color: var(--bw-text) !important; + border-radius: 0 !important; + font-family: 'JetBrains Mono', monospace; + padding: 10px 15px; +} + +.form-control:focus, .form-select:focus { + box-shadow: 4px 4px 0px var(--bw-border) !important; + background-color: transparent !important; + color: var(--bw-text) !important; +} + +/* Alert Styling */ +.alert-bw { + background-color: var(--bw-bg) !important; + color: var(--bw-text) !important; + border: 2px solid var(--bw-border) !important; + border-radius: 0; + font-family: 'JetBrains Mono', monospace; + text-transform: uppercase; +} + +.btn-close { + filter: invert(0); +} + +[data-theme='dark'] .btn-close { + filter: invert(1); +} + +/* Table Styling */ +.table-bw { + border-color: var(--bw-border); + color: var(--bw-text); +} + +.table-bw td, .table-bw th { + border-color: var(--bw-border); + background-color: transparent !important; + color: var(--bw-text) !important; +} \ No newline at end of file