diff --git a/static/ts/documentation-notes.ts b/static/ts/documentation-notes.ts new file mode 100644 index 000000000..0f3ac18e2 --- /dev/null +++ b/static/ts/documentation-notes.ts @@ -0,0 +1,316 @@ +/** + * Documentation Notes UI Module + * Handles section navigation, highlighting, and smooth scrolling + */ + +interface SectionData { + slug: string; + title: string; + element?: HTMLElement; +} + +interface DocumentationConfig { + topicSlug: string; + sections: SectionData[]; + currentSectionSlug: string; +} + +class DocumentationNotes { + private config: DocumentationConfig; + private currentSection: string; + private sections: Map = new Map(); + + constructor(config: DocumentationConfig) { + this.config = config; + this.currentSection = config.currentSectionSlug; + this.initializeSections(); + this.attachEventListeners(); + this.updateActiveSection(); + } + + /** + * Initialize sections map from config + */ + private initializeSections(): void { + this.config.sections.forEach((section) => { + this.sections.set(section.slug, section); + }); + } + + /** + * Attach event listeners to navigation elements + */ + private attachEventListeners(): void { + // Sidebar section links + document.querySelectorAll('[data-section-link]').forEach((link) => { + link.addEventListener('click', (e) => this.onSectionLinkClick(e)); + }); + + // Previous/Next buttons + const prevBtn = document.querySelector('[data-nav-prev]'); + const nextBtn = document.querySelector('[data-nav-next]'); + + if (prevBtn) { + prevBtn.addEventListener('click', () => this.goToPreviousSection()); + } + + if (nextBtn) { + nextBtn.addEventListener('click', () => this.goToNextSection()); + } + + // Use event delegation for anchor links (works for dynamically added content) + document.addEventListener('click', (e) => { + const link = (e.target as HTMLElement).closest('a[href^="#"]'); + if (link) { + this.onAnchorClick(e as MouseEvent); + } + }); + } + + /** + * Handle section link click from sidebar + */ + private onSectionLinkClick(event: Event): void { + const link = event.currentTarget as HTMLAnchorElement; + const sectionSlug = link.dataset.sectionLink; + + if (sectionSlug) { + event.preventDefault(); + this.navigateToSection(sectionSlug); + } + } + + /** + * Handle anchor link clicks for smooth scrolling + */ + private onAnchorClick(event: Event): void { + const link = event.currentTarget as HTMLAnchorElement; + const href = link.getAttribute('href'); + + if (href?.startsWith('#')) { + event.preventDefault(); + const element = document.querySelector(href); + if (element) { + this.smoothScroll(element); + } + } + } + + /** + * Navigate to a specific section + */ + public navigateToSection(sectionSlug: string): void { + const section = this.sections.get(sectionSlug); + if (!section) return; + + this.currentSection = sectionSlug; + this.updateActiveSection(); + this.loadSectionContent(sectionSlug); + } + + /** + * Go to previous section + */ + public goToPreviousSection(): void { + const sectionArray = Array.from(this.sections.values()); + const currentIndex = sectionArray.findIndex((s) => s.slug === this.currentSection); + + if (currentIndex > 0) { + this.navigateToSection(sectionArray[currentIndex - 1].slug); + } + } + + /** + * Go to next section + */ + public goToNextSection(): void { + const sectionArray = Array.from(this.sections.values()); + const currentIndex = sectionArray.findIndex((s) => s.slug === this.currentSection); + + if (currentIndex < sectionArray.length - 1) { + this.navigateToSection(sectionArray[currentIndex + 1].slug); + } + } + + /** + * Update active section highlighting + */ + private updateActiveSection(): void { + // Remove active state from all sidebar items + document.querySelectorAll('[data-section-link]').forEach((item) => { + item.classList.remove( + 'bg-teal-50', + 'dark:bg-teal-900/20', + 'text-teal-700', + 'dark:text-teal-300', + 'font-semibold', + 'border-l-teal-500' + ); + item.classList.add('text-gray-700', 'dark:text-gray-300', 'border-l-transparent'); + }); + + // Add active state to current section + const activeLink = document.querySelector(`[data-section-link="${this.currentSection}"]`); + if (activeLink) { + activeLink.classList.remove('text-gray-700', 'dark:text-gray-300', 'border-l-transparent'); + activeLink.classList.add( + 'bg-teal-50', + 'dark:bg-teal-900/20', + 'text-teal-700', + 'dark:text-teal-300', + 'font-semibold' + ); + } + + // Update progress counter + this.updateProgressCounter(); + } + + /** + * Load section content via AJAX + */ + private async loadSectionContent(sectionSlug: string): Promise { + try { + const url = `/docs/${this.config.topicSlug}/${sectionSlug}/`; + const response = await fetch(url); + + if (!response.ok) throw new Error('Failed to load section'); + + const html = await response.text(); + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + const newContent = doc.querySelector('[data-doc-content]'); + + if (newContent) { + const contentArea = document.querySelector('[data-doc-content]'); + if (contentArea) { + contentArea.innerHTML = newContent.innerHTML; + contentArea.scrollIntoView({ behavior: 'smooth', block: 'start' }); + + // Re-render markdown if needed + this.renderMarkdown(contentArea); + // Mark section as viewed + this.markSectionViewed(sectionSlug); + } + } + } catch (error) { + console.error('Error loading section:', error); + } + } + + /** + * Mark section as viewed in the backend + */ + private async markSectionViewed(sectionSlug: string): Promise { + try { + // Get CSRF token from meta tag (cookie-based) or form input + let csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'); + if (!csrfToken) { + const csrfInput = document.querySelector('input[name="csrfmiddlewaretoken"]') as HTMLInputElement; + csrfToken = csrfInput?.value; + } + if (!csrfToken) return; + + const url = `/docs/${this.config.topicSlug}/section/${sectionSlug}/viewed/`; + const response = await fetch(url, { + method: 'POST', + headers: { + 'X-CSRFToken': csrfToken, + 'Content-Type': 'application/json', + }, + }); + + if (response.ok) { + const data = await response.json(); + this.updateProgressBar(data.completion_percentage); + } + } catch (error) { + console.error('Error marking section as viewed:', error); + } + } + + /** + * Update progress bar + */ + private updateProgressBar(percentage: number): void { + const progressBar = document.querySelector('[data-progress-bar]') as HTMLElement; + if (progressBar) { + const bar = progressBar.querySelector('div') as HTMLElement; + if (bar) { + bar.style.width = `${percentage}%`; + } + } + } + + /** + * Update progress counter (e.g., "2 / 6") + */ + private updateProgressCounter(): void { + const counter = document.querySelector('[data-progress-counter]'); + if (counter) { + const sectionArray = Array.from(this.sections.values()); + const currentIndex = sectionArray.findIndex((s) => s.slug === this.currentSection); + counter.textContent = `${currentIndex + 1} / ${sectionArray.length}`; + } + } + + /** + * Render markdown content + */ + private renderMarkdown(element: Element): void { + if (!(window as any).marked) return; + + // Look for both doc-markdown divs (AJAX loaded content) + const markdownContainers = element.querySelectorAll('.doc-markdown'); + markdownContainers.forEach((el) => { + // Find corresponding markdown source (usually next to or parent sibling) + const parent = el.parentElement; + const markdownSource = parent?.querySelector('#doc-markdown-source'); + if (markdownSource) { + const content = markdownSource.textContent; + if (content && (content as any).trim()) { + (el as any).innerHTML = (window as any).marked.parse(content).replace(/<script/gi, '&lt;script').replace(/<iframe/gi, '&lt;iframe').replace(/<object/gi, '&lt;object'); + } + } + }); + + // Render LaTeX if MathJax is available + if ((window as any).MathJax) { + (window as any).MathJax.typesetPromise([element]).catch((err: any) => console.log(err)); + } + } + + /** + * Smooth scroll to element + */ + private smoothScroll(element: Element): void { + element.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + } + + /** + * Get current section slug + */ + public getCurrentSection(): string { + return this.currentSection; + } + + /** + * Get all sections + */ + public getSections(): SectionData[] { + return Array.from(this.sections.values()); + } +} + +/** + * Initialize documentation notes when DOM is ready + */ +export function initializeDocumentationNotes(config: DocumentationConfig): DocumentationNotes { + return new DocumentationNotes(config); +} + +// Export for use in global scope +(window as any).initializeDocumentationNotes = initializeDocumentationNotes; diff --git a/web/admin.py b/web/admin.py index a0eb4328f..4d245cd3e 100644 --- a/web/admin.py +++ b/web/admin.py @@ -21,6 +21,10 @@ Course, CourseMaterial, CourseProgress, + DocumentationNoteContent, + DocumentationNoteProgress, + DocumentationNoteSection, + DocumentationNoteTopic, Donation, Enrollment, ForumCategory, @@ -889,3 +893,74 @@ class VideoRequestAdmin(admin.ModelAdmin): list_display = ("title", "status", "category", "requester", "created_at") list_filter = ("status", "category") search_fields = ("title", "description", "requester__username") + + +class DocumentationNoteSectionInline(admin.TabularInline): + model = DocumentationNoteSection + extra = 1 + fields = ("title", "slug", "description", "order", "icon") + prepopulated_fields = {"slug": ("title",)} + + +@admin.register(DocumentationNoteTopic) +class DocumentationNoteTopicAdmin(admin.ModelAdmin): + list_display = ("title", "is_published", "order", "created_at", "updated_at") + list_filter = ("is_published", "created_at") + search_fields = ("title", "description") + prepopulated_fields = {"slug": ("title",)} + inlines = [DocumentationNoteSectionInline] + fieldsets = ( + (None, {"fields": ("title", "slug", "description")}), + ("Display Options", {"fields": ("icon", "color", "order")}), + ("Publication", {"fields": ("is_published",)}), + ("Timestamps", {"fields": ("created_at", "updated_at"), "classes": ("collapse", "wide")}), + ) + readonly_fields = ("created_at", "updated_at") + ordering = ("order", "title") + + +@admin.register(DocumentationNoteSection) +class DocumentationNoteSectionAdmin(admin.ModelAdmin): + list_display = ("title", "topic", "order", "created_at") + list_filter = ("topic", "created_at") + search_fields = ("title", "description") + raw_id_fields = ("topic",) + prepopulated_fields = {"slug": ("title",)} + fieldsets = ( + (None, {"fields": ("topic", "title", "slug", "description")}), + ("Display Options", {"fields": ("icon", "order")}), + ("Timestamps", {"fields": ("created_at", "updated_at"), "classes": ("collapse", "wide")}), + ) + readonly_fields = ("created_at", "updated_at") + ordering = ("topic", "order") + + +@admin.register(DocumentationNoteContent) +class DocumentationNoteContentAdmin(admin.ModelAdmin): + list_display = ("section", "created_at", "updated_at") + list_filter = ("section__topic", "created_at") + search_fields = ("section__title", "markdown_content") + raw_id_fields = ("section", "last_edited_by") + readonly_fields = ("html_content", "created_at", "updated_at", "last_edited_by") + fieldsets = ( + (None, {"fields": ("section",)}), + ("Content", {"fields": ("markdown_content",)}), + ("Generated HTML", {"fields": ("html_content",), "classes": ("collapse",)}), + ("Edit Information", {"fields": ("last_edited_by", "created_at", "updated_at"), "classes": ("collapse",)}), + ) + + +@admin.register(DocumentationNoteProgress) +class DocumentationNoteProgressAdmin(admin.ModelAdmin): + list_display = ("user", "topic", "completion_percentage", "completed_at", "last_accessed_at") + list_filter = ("topic", "completed_at", "started_at") + search_fields = ("user__username", "user__email", "topic__title") + raw_id_fields = ("user", "topic", "current_section") + readonly_fields = ("started_at", "last_accessed_at", "completed_at", "sections_viewed") + fieldsets = ( + (None, {"fields": ("user", "topic", "current_section")}), + ("Progress", {"fields": ("completion_percentage", "sections_viewed")}), + ("Timestamps", {"fields": ("started_at", "last_accessed_at", "completed_at"), "classes": ("collapse",)}), + ) + date_hierarchy = "started_at" + ordering = ("-last_accessed_at",) diff --git a/web/documentation_views.py b/web/documentation_views.py new file mode 100644 index 000000000..272c8c51a --- /dev/null +++ b/web/documentation_views.py @@ -0,0 +1,212 @@ +""" +Views for documentation-style notes feature. + +This module provides views for displaying and managing documentation notes, +including topic listings, section viewing, and progress tracking. +""" + +from django.contrib.auth.decorators import login_required +from django.http import JsonResponse +from django.shortcuts import get_object_or_404, render +from django.views.decorators.http import require_GET, require_POST + +from .models import ( + DocumentationNoteContent, + DocumentationNoteProgress, + DocumentationNoteSection, + DocumentationNoteTopic, +) + + +@require_GET +def documentation_topics_list(request): + """ + Display a list of all published documentation topics. + + Context: + topics: QuerySet of published DocumentationNoteTopic objects + """ + from django.db.models import Count + + topics = DocumentationNoteTopic.objects.filter(is_published=True).annotate(section_count=Count("sections")) + + user_progress = {} + if request.user.is_authenticated: + progress_records = DocumentationNoteProgress.objects.filter(user=request.user).values_list( + "topic_id", + "completion_percentage", + ) + user_progress = {topic_id: completion for topic_id, completion in progress_records} + + context = { + "topics": topics, + "user_progress": user_progress, + } + return render(request, "documentation_notes/topics_list.html", context) + + +@require_GET +def documentation_topic_detail(request, topic_slug): + """ + Display a documentation topic with its first section. + + Redirects to the first section of the topic if no section is specified. + + Args: + topic_slug: The slug of the documentation topic + + Context: + topic: The DocumentationNoteTopic object + sections: QuerySet of sections for the topic + current_section: The current section being viewed + content: The content of the current section + progress: User's progress on this topic (if authenticated) + next_section: The next section (if available) + previous_section: The previous section (if available) + """ + topic = get_object_or_404(DocumentationNoteTopic, slug=topic_slug, is_published=True) + sections = topic.sections.all() + + if not sections.exists(): + from django.http import Http404 + + raise Http404("This topic has no sections yet.") + + # Get the first section or specified section + first_section = sections.first() + section = first_section + + # Load section content (allow sections with no content) + content = DocumentationNoteContent.objects.filter(section=section).first() + + # Get user progress (read-only on GET - progress updates happen via POST only) + progress = None + if request.user.is_authenticated: + progress, _ = DocumentationNoteProgress.objects.get_or_create( + user=request.user, + topic=topic, + ) + + section_ids = list(sections.values_list("id", flat=True)) + current_section_index = section_ids.index(section.id) + 1 if section.id in section_ids else 1 + + context = { + "topic": topic, + "sections": sections, + "current_section": section, + "current_section_index": current_section_index, + "content": content, + "progress": progress, + "next_section": section.get_next_section(), + "previous_section": section.get_previous_section(), + } + return render(request, "documentation_notes/topic_detail.html", context) + + +@require_GET +def documentation_section_detail(request, topic_slug, section_slug): + """ + Display a specific section of a documentation topic. + + Args: + topic_slug: The slug of the documentation topic + section_slug: The slug of the documentation section + + Context: + topic: The DocumentationNoteTopic object + sections: QuerySet of sections for the topic + current_section: The current section being viewed + content: The content of the current section + progress: User's progress on this topic (if authenticated) + next_section: The next section (if available) + previous_section: The previous section (if available) + """ + topic = get_object_or_404(DocumentationNoteTopic, slug=topic_slug, is_published=True) + section = get_object_or_404(DocumentationNoteSection, slug=section_slug, topic=topic) + sections = topic.sections.all() + + # Load section content (allow sections with no content) + content = DocumentationNoteContent.objects.filter(section=section).first() + + # Get user progress (read-only on GET - progress updates happen via POST only) + progress = None + if request.user.is_authenticated: + progress, _ = DocumentationNoteProgress.objects.get_or_create( + user=request.user, + topic=topic, + ) + + section_ids = list(sections.values_list("id", flat=True)) + current_section_index = section_ids.index(section.id) + 1 if section.id in section_ids else 1 + + context = { + "topic": topic, + "sections": sections, + "current_section": section, + "current_section_index": current_section_index, + "content": content, + "progress": progress, + "next_section": section.get_next_section(), + "previous_section": section.get_previous_section(), + } + return render(request, "documentation_notes/topic_detail.html", context) + + +@login_required +@require_POST +def mark_section_viewed(request, topic_slug, section_slug): + """ + AJAX endpoint to mark a section as viewed and update progress. + + Args: + topic_slug: The slug of the documentation topic + section_slug: The slug of the documentation section + + Returns: + JSON response with updated progress data + """ + topic = get_object_or_404(DocumentationNoteTopic, slug=topic_slug, is_published=True) + section = get_object_or_404(DocumentationNoteSection, slug=section_slug, topic=topic) + + progress, _ = DocumentationNoteProgress.objects.get_or_create( + user=request.user, + topic=topic, + ) + progress.mark_section_as_viewed(section) + + return JsonResponse( + { + "success": True, + "completion_percentage": progress.completion_percentage, + "sections_count": progress.sections_viewed.count(), + "total_sections": topic.sections.count(), + } + ) + + +@login_required +@require_GET +def user_progress(request): + """ + Display user's progress on all documentation topics. + + Returns: + JSON response with user progress data + """ + if request.headers.get("X-Requested-With") != "XMLHttpRequest": + from django.http import Http404 + + raise Http404() + + progress_records = DocumentationNoteProgress.objects.filter(user=request.user).values( + "topic__title", + "topic__slug", + "completion_percentage", + "completed_at", + ) + + return JsonResponse( + { + "progress": list(progress_records), + } + ) diff --git a/web/management/commands/__init__.py b/web/management/commands/__init__.py index e69de29bb..e0527f9ab 100644 --- a/web/management/commands/__init__.py +++ b/web/management/commands/__init__.py @@ -0,0 +1,3 @@ +""" +Django management commands for the documentation notes feature. +""" diff --git a/web/management/commands/create_sample_docs.py b/web/management/commands/create_sample_docs.py new file mode 100644 index 000000000..5591315c2 --- /dev/null +++ b/web/management/commands/create_sample_docs.py @@ -0,0 +1,473 @@ +# Usage: python manage.py create_sample_docs + + +from django.core.management.base import BaseCommand +from django.utils.text import slugify + +from web.models import ( + DocumentationNoteContent, + DocumentationNoteSection, + DocumentationNoteTopic, +) + +PROJECTILE_MOTION_CONTENT = { + "title": "Projectile Motion", + "description": "A comprehensive guide to understanding projectile motion, from basic concepts to real-world applications.", + "sections": [ + { + "title": "Introduction", + "description": "Understand what projectile motion is and its basic principles", + "content": """# Introduction to Projectile Motion + +Projectile motion is a form of motion experienced by an object or particle that is projected near Earth's surface and moves along a curved path under the influence of gravity. + +## Key Concepts + +- **Projectile**: An object that moves in two dimensions under the influence of gravity +- **Trajectory**: The curved path followed by the projectile +- **Initial Velocity**: The velocity at which the projectile is launched +- **Angle of Projection**: The angle at which the projectile is launched relative to the horizontal + +## Assumptions + +In projectile motion, we typically assume: + +1. Air resistance is negligible +2. The effect of Earth's rotation is negligible +3. The acceleration due to gravity is constant (g ≈ 9.8 m/s²) +4. The motion is analyzed in a vertical plane + +## Applications + +Projectile motion can be observed in: +- Sports (baseball, basketball, golf) +- Military applications +- Fireworks +- Fountains +- Rocket trajectories +""", + }, + { + "title": "Equations of Motion", + "description": "Mathematical representations of projectile motion", + "content": """# Equations of Motion for Projectile Motion + +## Components of Velocity + +For a projectile launched with initial velocity $v_0$ at angle $\\theta$ to the horizontal: + +**Horizontal component:** +$$v_x = v_0 \\cos(\\theta)$$ + +**Vertical component:** +$$v_y = v_0 \\sin(\\theta)$$ + +## Position Equations + +At time $t$: + +**Horizontal position:** +$$x(t) = v_0 \\cos(\\theta) \\cdot t$$ + +**Vertical position:** +$$y(t) = v_0 \\sin(\\theta) \\cdot t - \\frac{1}{2}gt^2$$ + +Where $g = 9.8 \\text{ m/s}^2$ + +## Velocity Equations + +At time $t$: + +**Horizontal velocity:** +$$v_x(t) = v_0 \\cos(\\theta)$$ (constant) + +**Vertical velocity:** +$$v_y(t) = v_0 \\sin(\\theta) - gt$$ + +## Special Cases + +### When launched horizontally ($\\theta = 0°$) +- $x(t) = v_0 t$ +- $y(t) = -\\frac{1}{2}gt^2$ + +### When launched vertically ($\\theta = 90°$) +- $x(t) = 0$ +- $y(t) = v_0 t - \\frac{1}{2}gt^2$ +""", + }, + { + "title": "Derivation", + "description": "Step-by-step derivation of projectile motion equations", + "content": """# Derivation of Projectile Motion Equations + +## Starting from Newton's Laws + +Newton's second law states: $\\vec{F} = m\\vec{a}$ + +For projectile motion, the only force acting is gravity (downward): +- $F_x = 0$ → $a_x = 0$ (no acceleration in horizontal direction) +- $F_y = -mg$ → $a_y = -g$ (constant acceleration downward) + +## Horizontal Motion (Uniform) + +Since $a_x = 0$: + +$$v_x = v_{0x} = v_0 \\cos(\\theta) = \\text{constant}$$ + +Integrating for position: +$$x(t) = v_{0x} \\cdot t = v_0 \\cos(\\theta) \\cdot t$$ + +## Vertical Motion (Uniformly Accelerated) + +Since $a_y = -g$: + +Using $v = u + at$: +$$v_y(t) = v_{0y} - gt = v_0 \\sin(\\theta) - gt$$ + +Using $s = ut + \\frac{1}{2}at^2$: +$$y(t) = v_{0y} \\cdot t - \\frac{1}{2}gt^2 = v_0 \\sin(\\theta) \\cdot t - \\frac{1}{2}gt^2$$ + +## Trajectory Equation + +To find the equation of path, eliminate $t$: + +$$t = \\frac{x}{v_0 \\cos(\\theta)}$$ + +Substitute in $y$ equation: + +$$y = v_0 \\sin(\\theta) \\cdot \\frac{x}{v_0 \\cos(\\theta)} - \\frac{1}{2}g\\left(\\frac{x}{v_0 \\cos(\\theta)}\\right)^2$$ + +$$y = x \\tan(\\theta) - \\frac{gx^2}{2v_0^2 \\cos^2(\\theta)}$$ + +This is a **parabola**! + +## Range Formula + +The range $R$ is the horizontal distance when $y = 0$: + +$$R = \\frac{v_0^2 \\sin(2\\theta)}{g}$$ + +**Maximum range** occurs at $\\theta = 45°$: $R_{max} = \\frac{v_0^2}{g}$ + +## Maximum Height + +The maximum height $H$ occurs when $v_y = 0$: + +$$H = \\frac{v_0^2 \\sin^2(\\theta)}{2g}$$ +""", + }, + { + "title": "Graphical Interpretation", + "description": "Visual representation of projectile motion", + "content": """# Graphical Interpretation of Projectile Motion + +## Trajectory Graph + +The path of a projectile is a **parabola**. Key features: + +``` + Maximum Height + ↑ + y │ /‾‾‾‾‾‾ + │ │ / ‾‾\\ + │ │ / ‾‾ + │ │ / + │ │/ + ─────┴──────┴─────────────→ x + 0 Initial Range + Point +``` + +- The trajectory is symmetric about the maximum height +- The parabola is wider for larger launch angles (up to 45°) +- Higher initial velocities produce larger ranges + +## Velocity Vector Diagram + +The velocity vector changes direction throughout the flight: + +``` + At Launch At Maximum Height At Landing + (angle θ) (horizontal v_y = 0) (angle -θ) + + v ↗ v → v ↘ + / │ │ │ / + / │v_y │v_x │ + / │ │ │ + / │ │ │ + ← v_x → │ ← v_x → +``` + +## Position vs Time Graphs + +### Horizontal Position vs Time +``` +x +│ +│ / +│ / +│ / +│ / +└─────→ t +(Linear: x = v₀cos(θ)·t) +``` + +### Vertical Position vs Time +``` +y +│ /‾‾‾‾‾ +│ / ‾‾ +│ / ‾‾ +│ / ‾‾\ +│_______________ ‾ → t +(Parabolic: y = v₀sin(θ)·t - ½gt²) +``` + +### Vertical Velocity vs Time +``` +v_y +│ +│ ╱‾‾‾‾ +│ ╱ ─── v_x (constant) +│ ╱ +│ ╲ +│ ╲ +└────────── ╲─ → t + +(Linear: v_y = v₀sin(θ) - gt) +``` +""", + }, + { + "title": "Real-world Applications", + "description": "Practical applications of projectile motion", + "content": """# Real-world Applications of Projectile Motion + +## Sports + +### Basketball +- Players must calculate the projection angle and initial velocity to make accurate shots +- Arc of the ball follows projectile motion +- Professional players develop intuition for the correct launch angle + +### Golf +- Golfers optimize launch angle to maximize distance while accounting for spin +- Wind resistance (air drag) significantly affects the trajectory +- The 45° angle maximizes distance in ideal conditions + +### Baseball +- Outfielders use projectile motion to intercept fly balls +- The angle of the hit determines whether it's a home run or an easy out +- Spin on the ball causes deviation from ideal parabolic path + +## Military Applications + +### Artillery +- Gunners calculate firing angle and muzzle velocity for target accuracy +- Range tables use projectile motion equations +- Wind and air resistance are critical factors + +### Rocket Launches +- Initial launch angle and velocity are critical for payload delivery +- Atmospheric effects become important at high altitudes + +## Other Examples + +### Water Fountains +- Fountain designers use projectile motion to create aesthetic water patterns +- Multiple projectiles at different angles create complex designs + +### Fireworks +- Launch angle and initial velocity determine burst location +- Multiple projectiles create coordinated explosions + +### Construction +- Materials thrown during demolition follow projectile paths +- Safety zones are calculated based on projectile motion equations + +## Real-world Complications + +In practice, several factors complicate ideal projectile motion: + +1. **Air Resistance**: Opposes motion and reduces range +2. **Wind**: Affects horizontal motion +3. **Spin**: Causes Magnus effect (curves the trajectory) +4. **Rotation of Earth**: Significant for long-range military applications +5. **Curvature of Earth**: Important for space missions +6. **Variable Gravity**: Gravity changes with altitude + +## Example Calculation + +A ball is thrown from ground level at 20 m/s at 45° angle: + +**Given:** +- $v_0 = 20$ m/s +- $\\theta = 45°$ +- $g = 9.8$ m/s² + +**Maximum Height:** +$$H = \\frac{v_0^2 \\sin^2(\\theta)}{2g} = \\frac{400 \\times 0.5}{19.6} ≈ 10.2 \\text{ m}$$ + +**Range:** +$$R = \\frac{v_0^2 \\sin(2\\theta)}{g} = \\frac{400 \\times 1}{9.8} ≈ 40.8 \\text{ m}$$ + +**Flight Time:** +$$t = \\frac{2v_0 \\sin(\\theta)}{g} = \\frac{2 \\times 20 \\times 0.707}{9.8} ≈ 2.88 \\text{ s}$$ +""", + }, + { + "title": "Practice Problems", + "description": "Test your understanding with practice problems", + "content": """# Practice Problems: Projectile Motion + +## Difficulty Level: Easy + +### Problem 1.1 +A ball is thrown horizontally from a building 45 m tall with an initial velocity of 15 m/s. + +**Find:** +- a) Time of flight +- b) Horizontal distance traveled +- c) Final velocity + +**Solution:** + +a) Using $y = h - \\frac{1}{2}gt^2$: +$$0 = 45 - \\frac{1}{2}(9.8)t^2$$ +$$t = \\sqrt{\\frac{2 \\times 45}{9.8}} ≈ 3.03 \\text{ s}$$ + +b) $x = v_0 t = 15 \\times 3.03 ≈ 45.5 \\text{ m}$ + +c) $v_y = gt = 9.8 \\times 3.03 ≈ 29.7$ m/s + $v = \\sqrt{v_x^2 + v_y^2} = \\sqrt{15^2 + 29.7^2} ≈ 33.0$ m/s + +--- + +## Difficulty Level: Medium + +### Problem 2.1 +A projectile is launched at 30° with initial velocity 40 m/s. + +**Find:** +- a) Maximum height +- b) Range +- c) Time to reach maximum height + +**Solution:** + +a) $H = \\frac{v_0^2 \\sin^2(\\theta)}{2g} = \\frac{1600 \\times 0.25}{19.6} ≈ 20.4$ m + +b) $R = \\frac{v_0^2 \\sin(2\\theta)}{g} = \\frac{1600 \\times 0.866}{9.8} ≈ 141.3$ m + +c) $t = \\frac{v_0 \\sin(\\theta)}{g} = \\frac{40 \\times 0.5}{9.8} ≈ 2.04$ s + +--- + +## Difficulty Level: Hard + +### Problem 3.1 +A cannon fires a projectile from ground level at 50 m/s. An obstacle 2 m high is located 80 m away. + +**Find the range of launch angles to clear the obstacle.** + +This requires solving the trajectory equation with constraints. + +--- + +## Challenge Problem + +A basketball player shoots from 2 m away at a basket 3.05 m high (2 m above ground). The ball leaves the player's hands at 2.1 m height with speed 8 m/s. + +**What launch angle(s) will result in a basket?** + +This requires the ball to pass through the point (2 m, 3.05 m) relative to launch point. + +--- + +## Answers Summary + +| Problem | Part | Answer | +|---------|------|--------| +| 1.1 | a) | 3.03 s | +| 1.1 | b) | 45.5 m | +| 1.1 | c) | 33.0 m/s | +| 2.1 | a) | 20.4 m | +| 2.1 | b) | 141.3 m | +| 2.1 | c) | 2.04 s | +""", + }, + ], +} + + +class Command(BaseCommand): + help = "Create sample documentation notes for testing and demonstration" + + def add_arguments(self, parser): + parser.add_argument( + "--delete", + action="store_true", + help="Delete existing sample documentation notes first", + ) + + def handle(self, *args, **options): + if options["delete"]: + self.stdout.write("Deleting existing sample documentation notes...") + DocumentationNoteTopic.objects.filter(slug="projectile-motion").delete() + + self.stdout.write("Creating sample documentation notes...") + self.create_projectile_motion_topic() + + self.stdout.write(self.style.SUCCESS("Successfully created sample documentation notes!")) + + def create_projectile_motion_topic(self): + """Create the Projectile Motion documentation topic with all sections""" + + # Create topic + topic, created = DocumentationNoteTopic.objects.get_or_create( + slug="projectile-motion", + defaults={ + "title": PROJECTILE_MOTION_CONTENT["title"], + "description": PROJECTILE_MOTION_CONTENT["description"], + "icon": "beaker", + "color": "blue", + "order": 1, + "is_published": True, + }, + ) + + if created: + self.stdout.write(self.style.SUCCESS(f"✓ Created topic: {topic.title}")) + else: + self.stdout.write(f" Topic already exists: {topic.title}") + + # Create sections and content + for index, section_data in enumerate(PROJECTILE_MOTION_CONTENT["sections"]): + section, section_created = DocumentationNoteSection.objects.get_or_create( + topic=topic, + slug=slugify(section_data["title"]), + defaults={ + "title": section_data["title"], + "description": section_data["description"], + "order": index, + "icon": "", + }, + ) + + if section_created: + self.stdout.write(f" ✓ Created section: {section.title}") + else: + self.stdout.write(f" Section already exists: {section.title}") + + # Create or update content + content, content_created = DocumentationNoteContent.objects.get_or_create( + section=section, + defaults={ + "markdown_content": section_data["content"], + }, + ) + + if content_created: + self.stdout.write(f" ✓ Created content for: {section.title}") + else: + self.stdout.write(f" Content already exists for: {section.title}") diff --git a/web/migrations/0064_add_documentation_notes.py b/web/migrations/0064_add_documentation_notes.py new file mode 100644 index 000000000..c30d1683a --- /dev/null +++ b/web/migrations/0064_add_documentation_notes.py @@ -0,0 +1,183 @@ +import django.db.models.deletion +import markdownx.models +from django.conf import settings +from django.core.validators import MaxValueValidator, MinValueValidator +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("web", "0063_virtualclassroom_virtualclassroomcustomization_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="DocumentationNoteTopic", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("title", models.CharField(max_length=255, unique=True)), + ("slug", models.SlugField(unique=True)), + ("description", models.TextField(blank=True)), + ( + "icon", + models.CharField( + default="book", + help_text="Icon class name (e.g., 'book', 'beaker', 'calculator')", + max_length=50, + ), + ), + ( + "color", + models.CharField( + default="teal", + help_text="Tailwind color class (e.g., 'teal', 'blue', 'purple')", + max_length=20, + ), + ), + ("order", models.PositiveIntegerField(default=0, help_text="Display order in lists")), + ("is_published", models.BooleanField(default=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "verbose_name": "Documentation Note Topic", + "verbose_name_plural": "Documentation Note Topics", + "ordering": ["order", "title"], + }, + ), + migrations.CreateModel( + name="DocumentationNoteSection", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("title", models.CharField(max_length=255)), + ("slug", models.SlugField()), + ("description", models.CharField(blank=True, max_length=500)), + ("order", models.PositiveIntegerField(default=0, help_text="Display order within topic")), + ( + "icon", + models.CharField(blank=True, help_text="Optional icon for section navigation", max_length=50), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "topic", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="sections", + to="web.documentationnotetopic", + ), + ), + ], + options={ + "verbose_name": "Documentation Note Section", + "verbose_name_plural": "Documentation Note Sections", + "ordering": ["order"], + }, + ), + migrations.CreateModel( + name="DocumentationNoteProgress", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "completion_percentage", + models.FloatField(default=0.0, validators=[MinValueValidator(0), MaxValueValidator(100)]), + ), + ("started_at", models.DateTimeField(auto_now_add=True)), + ("last_accessed_at", models.DateTimeField(auto_now=True)), + ("completed_at", models.DateTimeField(blank=True, null=True)), + ( + "current_section", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="viewing_users", + to="web.documentationnotesection", + ), + ), + ( + "topic", + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="web.documentationnotetopic"), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="note_progress", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "verbose_name": "Documentation Note Progress", + "verbose_name_plural": "Documentation Note Progress", + }, + ), + migrations.CreateModel( + name="DocumentationNoteContent", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "markdown_content", + markdownx.models.MarkdownxField(help_text="Supports Markdown formatting with LaTeX math support"), + ), + ("html_content", models.TextField(blank=True, editable=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "last_edited_by", + models.ForeignKey( + blank=True, + editable=False, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "section", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="content", + to="web.documentationnotesection", + ), + ), + ], + options={ + "verbose_name": "Documentation Note Content", + "verbose_name_plural": "Documentation Note Contents", + }, + ), + migrations.AddField( + model_name="documentationnoteprogress", + name="sections_viewed", + field=models.ManyToManyField( + blank=True, help_text="Sections the user has viewed", to="web.documentationnotesection" + ), + ), + migrations.AddIndex( + model_name="documentationnotetopic", + index=models.Index(fields=["slug"], name="web_document_slug_idx"), + ), + migrations.AddIndex( + model_name="documentationnotetopic", + index=models.Index(fields=["is_published"], name="web_document_published_idx"), + ), + migrations.AddIndex( + model_name="documentationnotesection", + index=models.Index(fields=["topic", "order"], name="web_document_section_order_idx"), + ), + migrations.AddIndex( + model_name="documentationnoteprogress", + index=models.Index(fields=["user", "topic"], name="web_document_progress_user_topic_idx"), + ), + migrations.AlterUniqueTogether( + name="documentationnotesection", + unique_together={("topic", "slug")}, + ), + migrations.AlterUniqueTogether( + name="documentationnoteprogress", + unique_together={("user", "topic")}, + ), + ] diff --git a/web/models.py b/web/models.py index 6b8ea8ef4..f76c9f44b 100644 --- a/web/models.py +++ b/web/models.py @@ -3176,3 +3176,173 @@ class Meta: ordering = ["-last_updated"] verbose_name = "Virtual Classroom Whiteboard" verbose_name_plural = "Virtual Classroom Whiteboards" + + +class DocumentationNoteTopic(models.Model): + """Model for documentation-style note topics (e.g., Physics, Mathematics)""" + + title = models.CharField(max_length=255, unique=True) + slug = models.SlugField(unique=True) + description = models.TextField(blank=True) + icon = models.CharField( + max_length=50, + default="book", + help_text="Icon class name (e.g., 'book', 'beaker', 'calculator')", + ) + color = models.CharField( + max_length=20, + default="teal", + help_text="Tailwind color class (e.g., 'teal', 'blue', 'purple')", + ) + order = models.PositiveIntegerField(default=0, help_text="Display order in lists") + is_published = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ["order", "title"] + verbose_name = "Documentation Note Topic" + verbose_name_plural = "Documentation Note Topics" + indexes = [models.Index(fields=["slug"]), models.Index(fields=["is_published"])] + + def __str__(self): + return self.title + + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.title) + super().save(*args, **kwargs) + + +class DocumentationNoteSection(models.Model): + """Model for sections within a documentation note topic""" + + topic = models.ForeignKey( + DocumentationNoteTopic, + on_delete=models.CASCADE, + related_name="sections", + ) + title = models.CharField(max_length=255) + slug = models.SlugField() + description = models.CharField(max_length=500, blank=True) + order = models.PositiveIntegerField(default=0, help_text="Display order within topic") + icon = models.CharField( + max_length=50, + blank=True, + help_text="Optional icon for section navigation", + ) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ["order"] + unique_together = ("topic", "slug") + verbose_name = "Documentation Note Section" + verbose_name_plural = "Documentation Note Sections" + indexes = [models.Index(fields=["topic", "order"])] + + def __str__(self): + return f"{self.topic.title} - {self.title}" + + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.title) + super().save(*args, **kwargs) + + def get_previous_section(self): + """Get the previous section in the topic""" + return ( + DocumentationNoteSection.objects.filter(topic=self.topic, order__lt=self.order).order_by("-order").first() + ) + + def get_next_section(self): + """Get the next section in the topic""" + return DocumentationNoteSection.objects.filter(topic=self.topic, order__gt=self.order).order_by("order").first() + + +class DocumentationNoteContent(models.Model): + """Model for storing actual content of documentation notes""" + + section = models.OneToOneField( + DocumentationNoteSection, + on_delete=models.CASCADE, + related_name="content", + ) + markdown_content = MarkdownxField(help_text="Supports Markdown formatting with LaTeX math support") + html_content = models.TextField(blank=True, editable=False) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + last_edited_by = models.ForeignKey( + User, + on_delete=models.SET_NULL, + null=True, + blank=True, + editable=False, + ) + + class Meta: + verbose_name = "Documentation Note Content" + verbose_name_plural = "Documentation Note Contents" + + def __str__(self): + return f"Content for {self.section.title}" + + +class DocumentationNoteProgress(models.Model): + """Model to track user progress through documentation notes""" + + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="note_progress") + topic = models.ForeignKey(DocumentationNoteTopic, on_delete=models.CASCADE) + sections_viewed = models.ManyToManyField( + DocumentationNoteSection, + blank=True, + help_text="Sections the user has viewed", + ) + current_section = models.ForeignKey( + DocumentationNoteSection, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="viewing_users", + ) + completion_percentage = models.FloatField(default=0.0, validators=[MinValueValidator(0), MaxValueValidator(100)]) + started_at = models.DateTimeField(auto_now_add=True) + last_accessed_at = models.DateTimeField(auto_now=True) + completed_at = models.DateTimeField(null=True, blank=True) + + class Meta: + unique_together = ("user", "topic") + verbose_name = "Documentation Note Progress" + verbose_name_plural = "Documentation Note Progress" + indexes = [models.Index(fields=["user", "topic"])] + + def __str__(self): + return f"{self.user.username} - {self.topic.title}" + + def update_progress(self): + """Calculate and update completion percentage""" + total_sections = self.topic.sections.count() + if total_sections == 0: + return + + viewed_sections = self.sections_viewed.count() + self.completion_percentage = (viewed_sections / total_sections) * 100 + + if self.completion_percentage == 100 and not self.completed_at: + self.completed_at = timezone.now() + + self.save() + + def mark_section_as_viewed(self, section): + """Mark a section as viewed and update progress + + Only allows sections that belong to this progress object's topic + to prevent corrupt progress state. + """ + # Enforce topic consistency - only allow sections from this topic + if section.topic_id != self.topic_id: + raise ValueError("Section does not belong to this progress topic") + + self.sections_viewed.add(section) + self.current_section = section + self.update_progress() diff --git a/web/templates/documentation_notes/base.html b/web/templates/documentation_notes/base.html new file mode 100644 index 000000000..e1e401159 --- /dev/null +++ b/web/templates/documentation_notes/base.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} + +{% block title %}{{ title }} - Documentation | Alpha One Labs{% endblock %} +{% block content %} +
+ {% include "documentation_notes/components/breadcrumb.html" %} +
+
+ +
{% include "documentation_notes/components/sidebar.html" %}
+ +
+ {% block doc_content %}{% endblock %} +
+
+
+
+ + + {% block doc_scripts %}{% endblock %} +{% endblock %} diff --git a/web/templates/documentation_notes/components/breadcrumb.html b/web/templates/documentation_notes/components/breadcrumb.html new file mode 100644 index 000000000..53a2e0c9a --- /dev/null +++ b/web/templates/documentation_notes/components/breadcrumb.html @@ -0,0 +1,36 @@ + + diff --git a/web/templates/documentation_notes/components/navigation.html b/web/templates/documentation_notes/components/navigation.html new file mode 100644 index 000000000..f9e651248 --- /dev/null +++ b/web/templates/documentation_notes/components/navigation.html @@ -0,0 +1,68 @@ + +
+
+ +
+ {% if previous_section %} + + + + + + Previous + + + {% else %} +
+ + + + Previous +
+ {% endif %} +
+ +
+
+

Progress

+

+ {% if sections.count > 0 %} + {{ current_section_index }}/ {{ sections.count }} + {% endif %} +

+
+
+ +
+ {% if next_section %} + + Next + + + + + {% else %} +
+ Next + + + +
+ {% endif %} +
+
+
diff --git a/web/templates/documentation_notes/components/sidebar.html b/web/templates/documentation_notes/components/sidebar.html new file mode 100644 index 000000000..cd379e107 --- /dev/null +++ b/web/templates/documentation_notes/components/sidebar.html @@ -0,0 +1,72 @@ + +
+ + + +

+ {{ topic.title }} +

+ + + +
+
+

+ {{ sections.count }} sections in this topic +

+ {% if user.is_authenticated and progress %} +
+ {{ progress.sections_viewed.count }} sections viewed +
+ {% endif %} +
+
+ +
+ +
+
diff --git a/web/templates/documentation_notes/topic_detail.html b/web/templates/documentation_notes/topic_detail.html new file mode 100644 index 000000000..ca29fd448 --- /dev/null +++ b/web/templates/documentation_notes/topic_detail.html @@ -0,0 +1,299 @@ +{% extends "base.html" %} + +{% block title %}{{ topic.title }} - Documentation | Alpha One Labs{% endblock %} +{% block content %} + + +
+ +
+
+
+ Home + / + Docs + / + {{ topic.title }} +
+
+
+
+
+ + {% include "documentation_notes/components/sidebar.html" %} + +
+ +
+
+
+

{{ topic.title }}

+ {% if topic.description %}

{{ topic.description }}

{% endif %} +
+
+ + {% if user.is_authenticated and progress %} +
+
+ Your Progress + {{ progress.completion_percentage|floatformat:0 }}% +
+
+
+
+
+ {% endif %} +
+ +
+ +
+
+
+

{{ current_section.title }}

+ {% if current_section.description %} +

{{ current_section.description }}

+ {% endif %} +
+ {% if current_section.icon %}
{{ current_section.icon }}
{% endif %} +
+
+ +
+
+ {% if content.markdown_content %} + +
+ {% else %} +

This section doesn't have content yet.

+ {% endif %} +
+
+ + {% include "documentation_notes/components/navigation.html" %} +
+ +
+ + {% if previous_section %} + +
+ + + +
+
+

Previous

+

+ {{ previous_section.title }} +

+
+
+ {% endif %} + + {% if next_section %} + +
+

Next

+

+ {{ next_section.title }} +

+
+
+ + + +
+
+ {% endif %} +
+
+
+
+
+ + + + + +{% endblock %} diff --git a/web/templates/documentation_notes/topics_list.html b/web/templates/documentation_notes/topics_list.html new file mode 100644 index 000000000..efde8ae30 --- /dev/null +++ b/web/templates/documentation_notes/topics_list.html @@ -0,0 +1,100 @@ +{% extends "base.html" %} + +{% block title %}Documentation Topics - Alpha One Labs{% endblock %} +{% block content %} +
+ +
+
+
+

Documentation Topics

+

+ Explore comprehensive guides and documentation for various topics. Learn at your own pace with interactive sections and examples. +

+
+
+
+ +
+ {% if topics %} + + {% else %} +
+
+ + + + +
+

No documentation available

+

More topics coming soon. Stay tuned!

+
+ {% endif %} +
+ + {% if user.is_authenticated %} +
+

Your Learning Progress

+
+

+ Continue learning where you left off. Your progress is automatically saved as you explore each section. +

+
+
+ {% endif %} +
+{% endblock %} diff --git a/web/tests/test_views.py b/web/tests/test_views.py index a5ff705f3..7564cd95d 100644 --- a/web/tests/test_views.py +++ b/web/tests/test_views.py @@ -305,9 +305,15 @@ def test_form_pages_have_forms(self): def test_index_shows_recent_courses(self): """Test that index page shows 6 most recent published courses""" - # Create 8 published courses with different creation times + from datetime import timedelta + + from django.utils import timezone + + # Create 8 published courses and set their creation times + base_time = timezone.now() + courses = [] for i in range(8): - Course.objects.create( + course = Course.objects.create( title=f"Test Course {i}", description=f"Test Description {i}", teacher=self.teacher, @@ -319,6 +325,11 @@ def test_index_shows_recent_courses(self): status="published", is_featured=(i < 3), # Only first 3 are featured ) + courses.append(course) + + # Manually update created_at timestamps since auto_now_add=True ignores explicit values + for i, course in enumerate(courses): + Course.objects.filter(id=course.id).update(created_at=base_time + timedelta(minutes=i)) # Get the index page response = self.client.get(self.urls_to_test["index"]) diff --git a/web/urls.py b/web/urls.py index 3fb4e298a..820cb01c0 100644 --- a/web/urls.py +++ b/web/urls.py @@ -6,7 +6,7 @@ from django.urls import include, path from django.views.generic.base import RedirectView -from . import admin_views, peer_challenge_views, quiz_views, views, views_avatar, views_whiteboard +from . import admin_views, documentation_views, peer_challenge_views, quiz_views, views, views_avatar, views_whiteboard from .secure_messaging import ( compose_message, download_message, @@ -79,6 +79,20 @@ path("blog/create/", views.create_blog_post, name="create_blog_post"), path("blog/tag//", views.blog_tag, name="blog_tag"), path("blog//", views.blog_detail, name="blog_detail"), + # Documentation Notes URLs + path("docs/", documentation_views.documentation_topics_list, name="docs_topics_list"), + path("docs/user/progress/", documentation_views.user_progress, name="docs_user_progress"), + path("docs//", documentation_views.documentation_topic_detail, name="docs_topic_detail"), + path( + "docs///", + documentation_views.documentation_section_detail, + name="docs_section_detail", + ), + path( + "docs//section//viewed/", + documentation_views.mark_section_viewed, + name="docs_mark_section_viewed", + ), # Leaderboard URLs path("leaderboards/", views.all_leaderboards, name="leaderboards"), # Success Stories URLs