feat: added learn page

- added sections to the lesson to handle multiple sesions like examples and exercises
- added livecode integration to lesson pages
- autosave and submiting the answers is not done yet
This commit is contained in:
Anand Chitipothu
2021-05-14 12:11:45 +05:30
parent 1cf57c4823
commit 49b41749e8
12 changed files with 205 additions and 33 deletions

View File

@@ -145,6 +145,7 @@ primary_rules = [
{"from_route": "/dashboard", "to_route": ""}, {"from_route": "/dashboard", "to_route": ""},
{"from_route": "/add-a-new-batch", "to_route": "add-a-new-batch"}, {"from_route": "/add-a-new-batch", "to_route": "add-a-new-batch"},
{"from_route": "/courses/<course>/<batch>/learn", "to_route": "courses/learn"}, {"from_route": "/courses/<course>/<batch>/learn", "to_route": "courses/learn"},
{"from_route": "/courses/<course>/<batch>/learn/<int:chapter>.<int:lesson>", "to_route": "courses/learn"},
{"from_route": "/courses/<course>/<batch>/schedule", "to_route": "courses/schedule"}, {"from_route": "/courses/<course>/<batch>/schedule", "to_route": "courses/schedule"},
{"from_route": "/courses/<course>/<batch>/members", "to_route": "courses/members"}, {"from_route": "/courses/<course>/<batch>/members", "to_route": "courses/members"},
{"from_route": "/courses/<course>/<batch>/discuss", "to_route": "courses/discuss"}, {"from_route": "/courses/<course>/<batch>/discuss", "to_route": "courses/discuss"},

View File

@@ -9,7 +9,8 @@
"course", "course",
"title", "title",
"description", "description",
"locked" "locked",
"index_"
], ],
"fields": [ "fields": [
{ {
@@ -35,6 +36,12 @@
"in_list_view": 1, "in_list_view": 1,
"label": "Course", "label": "Course",
"options": "LMS Course" "options": "LMS Course"
},
{
"default": "1",
"fieldname": "index_",
"fieldtype": "Int",
"label": "Index"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
@@ -45,7 +52,7 @@
"link_fieldname": "chapter" "link_fieldname": "chapter"
} }
], ],
"modified": "2021-05-03 06:52:10.894328", "modified": "2021-05-13 21:05:20.531890",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Chapter", "name": "Chapter",

View File

@@ -9,7 +9,9 @@
"chapter", "chapter",
"lesson_type", "lesson_type",
"title", "title",
"index_" "index_",
"body",
"sections"
], ],
"fields": [ "fields": [
{ {
@@ -38,11 +40,22 @@
"fieldname": "index_", "fieldname": "index_",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Index" "label": "Index"
},
{
"fieldname": "body",
"fieldtype": "Markdown Editor",
"label": "Body"
},
{
"fieldname": "sections",
"fieldtype": "Table",
"label": "Sections",
"options": "LMS Section"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-05-08 13:25:13.965162", "modified": "2021-05-13 20:03:51.510605",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Lesson", "name": "Lesson",

View File

@@ -3,8 +3,22 @@
# For license information, please see license.txt # For license information, please see license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
# import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from ..lms_topic.section_parser import SectionParser
class Lesson(Document): class Lesson(Document):
pass def before_save(self):
sections = SectionParser().parse(self.body or "")
self.sections = [self.make_lms_section(i, s) for i, s in enumerate(sections)]
def get_sections(self):
return sorted(self.get('sections'), key=lambda s: s.index)
def make_lms_section(self, index, section):
s = frappe.new_doc('LMS Section', parent_doc=self, parentfield='sections')
s.type = section.type
s.label = section.label
s.contents = section.contents
s.index = index
return s

View File

@@ -177,3 +177,16 @@ class LMSCourse(Document):
visibility="Public") visibility="Public")
return batches return batches
def get_chapter(self, index):
return find("Chapter", course=self.name, index_=index)
def get_lesson(self, chapter_index, lesson_index):
chapter_name = frappe.get_value(
"Chapter",
{"course": self.name, "index_": chapter_index},
"name")
lesson_name = chapter_name and frappe.get_value(
"Lesson",
{"chapter": chapter_name, "index_": lesson_index},
"name")
return lesson_name and frappe.get_doc("Lesson", lesson_name)

View File

@@ -1,4 +1,9 @@
"""Utility to split the text in the topic into multiple sections. """Utility to split the text in the topic into multiple sections.
{{ section(type="example", id="foo") }}
circle(100, 100, 50)
{{ end }}
""" """
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass

View File

@@ -77,7 +77,7 @@ body {
.lessons { .lessons {
padding-left: 20px; padding-left: 20px;
} }
.lesson { .lessons .lesson {
margin: 5px 0px; margin: 5px 0px;
font-weight: bold; font-weight: bold;
} }
@@ -167,7 +167,7 @@ img.profile-photo {
} }
.msger-inputarea { .msger-inputarea {
position: fixed; position: absolute;
bottom: 0; bottom: 0;
width: 100%; width: 100%;
display: flex; display: flex;

View File

@@ -235,7 +235,7 @@ section.lightgray {
// LiveCode editor // LiveCode editor
.livecode-editor-large { .livecode-editor {
.CodeMirror { .CodeMirror {
border: 1px solid #ddd; border: 1px solid #ddd;
@@ -302,3 +302,7 @@ section.lightgray {
#hero h1 { #hero h1 {
color: black !important; color: black !important;
} }
.lesson {
margin: 20px 0px 20px 50px;
}

View File

@@ -14,10 +14,12 @@
<div class="batch-header"> <div class="batch-header">
{{ BatchHearder(course.name, member_count) }} {{ BatchHearder(course.name, member_count) }}
</div> </div>
<div class="message-section"> <div class="messages">
{{ Messages(messages) }} <div class="message-section">
{{ Messages(messages) }}
</div>
{{ TextArea() }}
</div> </div>
{{ TextArea() }}
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -1,13 +1,106 @@
{% extends "templates/base.html" %} {% extends "templates/base.html" %}
{% from "www/macros/sidebar.html" import Sidebar %} {% from "www/macros/sidebar.html" import Sidebar %}
{% block title %}Learn{% endblock %} {% from "www/macros/livecode.html" import LiveCodeEditorJS, LiveCodeEditor with context %}
{% block title %}{{ lesson.title }}{% endblock %}
{% block head_include %} {% block head_include %}
<meta name="description" content="Courses" /> <meta name="description" content="{{lesson.title}} - {{course.title}}" />
<meta name="keywords" content="" /> <meta name="keywords" content="{{lesson.title}} - {{course.title}}" />
<style>
</style>
<link rel="stylesheet" href="{{ livecode_url }}/static/codemirror/lib/codemirror.css">
<link rel="stylesheet" href="/assets/css/lms.css">
<script src="{{ livecode_url }}/static/codemirror/lib/codemirror.js"></script>
<script src="{{ livecode_url }}/static/codemirror/mode/python/python.js"></script>
<script src="{{ livecode_url }}/static/codemirror/keymap/sublime.js"></script>
<script src="{{ livecode_url }}/static/codemirror/addon/edit/matchbrackets.js"></script>
<script src="{{ livecode_url }}/static/codemirror/addon/comment/comment.js"></script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{{ Sidebar(course.name, batch.name) }} {{ Sidebar(course.name, batch.name) }}
<div class="container"> <div class="container lesson">
<h1>{{ lesson.title }} - {{ lesson.name }}</h1>
{% for s in lesson.get_sections() %}
<div class="section section-{{ s.type }}">
{{ render_section(s) }}
</div>
{% endfor %}
</div> </div>
{% endblock %} {% endblock %}
{% macro render_section(s) %}
{% if s.type == "text" %}
{{ render_section_text(s) }}
{% elif s.type == "example" or s.type == "code" or s.type == "exercise" %}
{{ LiveCodeEditor(s.name, s.get_latest_code_for_user(), s.type=="exercise", "2 hours ago") }}
{% else %}
<div>Unknown section type: {{s.type}}</div>
{% endif %}
{% endmacro %}
{% macro render_section_text(s) %}
<div class="row">
<div class="col-md-9">
{{ frappe.utils.md_to_html(s.contents) }}
</div>
</div>
{% endmacro %}
{%- block script %}
{{ super() }}
{{ LiveCodeEditorJS() }}
<!-- <script type="text/javascript">
$(function() {
var editorLookup = {};
$(".canvas-editor").each((i, e) => {
var data = $(e).data();
var editor = new LiveCodeEditor(e, {
runtime: "python-canvas",
base_url: "{{ livecode_url }}",
codemirror: true,
userdata: data,
autosave: function(editor, code) {
// can't autosave when user is Guest
if (frappe.session.user == "Guest") {
return;
}
var data = editor.options.userdata;
var code = editor.codemirror.doc.getValue();
// console.log("autosaving...")
frappe.call("community.lms.api.autosave_section", {
section: data.section,
code: code
}).then((r) => {
// TODO: verify
})
}
})
editorLookup[data.section] = editor;
})
$(".canvas-editor .reset").each((i, e) => {
$(e).on("click", function(event) {
var data = $(this).parents(".canvas-editor").data();
var section = data.section;
frappe.call("community.lms.api.get_section", {
name: section
}).then(r => {
var editor = editorLookup[data.section];
editor.codemirror.doc.setValue(r.message.contents);
})
})
})
})
</script> -->
{%- endblock %}

View File

@@ -6,6 +6,8 @@ def get_context(context):
course_name = frappe.form_dict["course"] course_name = frappe.form_dict["course"]
batch_name = frappe.form_dict["batch"] batch_name = frappe.form_dict["batch"]
chapter_index = frappe.form_dict.get("chapter")
lesson_index = frappe.form_dict.get("lesson")
course = Course.find(course_name) course = Course.find(course_name)
if not course: if not course:
@@ -17,5 +19,16 @@ def get_context(context):
frappe.local.flags.redirect_location = "/courses/" + course_name frappe.local.flags.redirect_location = "/courses/" + course_name
raise frappe.Redirect raise frappe.Redirect
if not chapter_index or not lesson_index:
frappe.local.flags.redirect_location = f"/courses/{course_name}/{batch_name}/learn/1.1"
raise frappe.Redirect
context.course = course context.course = course
context.batch = batch context.batch = batch
context.lesson = course.get_lesson(chapter_index, lesson_index)
context.lesson_index = lesson_index
context.chapter_index = chapter_index
context.livecode_url = get_livecode_url()
def get_livecode_url():
return frappe.db.get_single_value("LMS Settings", "livecode_url")

View File

@@ -24,29 +24,36 @@
</div> </div>
{% endmacro %} {% endmacro %}
{% macro LiveCodeEditor(name, code) %} {% macro LiveCodeEditor(name, code, is_exercise, last_submitted) %}
<div class="livecode-editor canvas-editor" id="editor-{{name}}" <div class="livecode-editor livecode-editor-inline" id="editor-{{name}}">
data-section="{{name}}">
<div class="row"> <div class="row">
<div class="col-md-9"> <div class="col-lg-8 col-md-6">
<div> <div class="controls">
<textarea class="code">{{code}}</textarea> <button class="run">Run</button>
<div class="livecode-controls"> <button class="reset">Reset</button>
<button type="button" class="run">Run</button> {% if is_exercise %}
<a href="javascript:;" class="reset">Reset</a> <button class="submit pull-right btn-primary">Submit</button>
<a href="javascript:;" class="clear">Clear</a> {% if last_submitted %}
<span class="pull-right" style="padding-right: 10px;">Last submitted {{last_submitted}}</span>
{% endif %}
{% endif %}
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="code-editor">
<div class="canvas-wrapper"> <div class="row">
<canvas class="canvas" width="150" height="150"></canvas> <div class="col-lg-8 col-md-6">
<pre class="output"></pre> <div class="code-wrapper">
<textarea class="code">{{code}}</textarea>
</div>
</div>
<div class="col-lg-4 col-md-6 canvas-wrapper">
<canvas width="300" height="300"></canvas>
<pre class="output"></pre>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endmacro %} {% endmacro %}