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:
@@ -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"},
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,11 +14,13 @@
|
|||||||
<div class="batch-header">
|
<div class="batch-header">
|
||||||
{{ BatchHearder(course.name, member_count) }}
|
{{ BatchHearder(course.name, member_count) }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="messages">
|
||||||
<div class="message-section">
|
<div class="message-section">
|
||||||
{{ Messages(messages) }}
|
{{ Messages(messages) }}
|
||||||
</div>
|
</div>
|
||||||
{{ TextArea() }}
|
{{ TextArea() }}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% macro Messages(messages) %}
|
{% macro Messages(messages) %}
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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">
|
||||||
|
<button class="run">Run</button>
|
||||||
|
<button class="reset">Reset</button>
|
||||||
|
{% if is_exercise %}
|
||||||
|
<button class="submit pull-right btn-primary">Submit</button>
|
||||||
|
{% if last_submitted %}
|
||||||
|
<span class="pull-right" style="padding-right: 10px;">Last submitted {{last_submitted}}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="code-editor">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8 col-md-6">
|
||||||
|
<div class="code-wrapper">
|
||||||
<textarea class="code">{{code}}</textarea>
|
<textarea class="code">{{code}}</textarea>
|
||||||
<div class="livecode-controls">
|
|
||||||
<button type="button" class="run">Run</button>
|
|
||||||
<a href="javascript:;" class="reset">Reset</a>
|
|
||||||
<a href="javascript:;" class="clear">Clear</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col-lg-4 col-md-6 canvas-wrapper">
|
||||||
<div class="col-md-3">
|
<canvas width="300" height="300"></canvas>
|
||||||
<div class="canvas-wrapper">
|
|
||||||
<canvas class="canvas" width="150" height="150"></canvas>
|
|
||||||
<pre class="output"></pre>
|
<pre class="output"></pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user