Merge pull request #86 from fossunited/learn
Implemented learning section for batches
This commit is contained in:
@@ -145,6 +145,7 @@ primary_rules = [
|
||||
{"from_route": "/dashboard", "to_route": ""},
|
||||
{"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/<int:chapter>.<int:lesson>", "to_route": "courses/learn"},
|
||||
{"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>/discuss", "to_route": "courses/discuss"},
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
"course",
|
||||
"title",
|
||||
"description",
|
||||
"locked"
|
||||
"locked",
|
||||
"index_"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -35,6 +36,12 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Course",
|
||||
"options": "LMS Course"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "index_",
|
||||
"fieldtype": "Int",
|
||||
"label": "Index"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
@@ -45,7 +52,7 @@
|
||||
"link_fieldname": "chapter"
|
||||
}
|
||||
],
|
||||
"modified": "2021-05-03 06:52:10.894328",
|
||||
"modified": "2021-05-13 21:05:20.531890",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Chapter",
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
"chapter",
|
||||
"lesson_type",
|
||||
"title",
|
||||
"index_"
|
||||
"index_",
|
||||
"body",
|
||||
"sections"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -38,11 +40,22 @@
|
||||
"fieldname": "index_",
|
||||
"fieldtype": "Int",
|
||||
"label": "Index"
|
||||
},
|
||||
{
|
||||
"fieldname": "body",
|
||||
"fieldtype": "Markdown Editor",
|
||||
"label": "Body"
|
||||
},
|
||||
{
|
||||
"fieldname": "sections",
|
||||
"fieldtype": "Table",
|
||||
"label": "Sections",
|
||||
"options": "LMS Section"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-05-08 13:25:13.965162",
|
||||
"modified": "2021-05-13 20:03:51.510605",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Lesson",
|
||||
|
||||
@@ -3,8 +3,37 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from ..lms_topic.section_parser import SectionParser
|
||||
|
||||
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
|
||||
|
||||
def get_next(self):
|
||||
"""Returns the number for the next lesson.
|
||||
|
||||
The return value would be like 1.2, 2.1 etc.
|
||||
It will be None if there is no next lesson.
|
||||
"""
|
||||
|
||||
|
||||
def get_prev(self):
|
||||
"""Returns the number for the prev lesson.
|
||||
|
||||
The return value would be like 1.2, 2.1 etc.
|
||||
It will be None if there is no next lesson.
|
||||
"""
|
||||
|
||||
@@ -114,6 +114,35 @@ class LMSCourse(Document):
|
||||
"mentor": member
|
||||
})
|
||||
|
||||
def get_student_batch(self, email):
|
||||
"""Returns the batch the given student is part of.
|
||||
|
||||
Returns None if the student is not part of any batch.
|
||||
"""
|
||||
if not email:
|
||||
return False
|
||||
member = self.get_community_member(email)
|
||||
result = frappe.db.get_all(
|
||||
"LMS Batch Membership",
|
||||
filters={
|
||||
"member": member,
|
||||
"member_type": "Student",
|
||||
},
|
||||
fields=['batch']
|
||||
)
|
||||
batches = [row['batch'] for row in result]
|
||||
|
||||
# filter the batches that are for this course
|
||||
result = frappe.db.get_all(
|
||||
"LMS Batch",
|
||||
filters={
|
||||
"course": self.name,
|
||||
"name": ["IN", batches]
|
||||
})
|
||||
batches = [row['name'] for row in result]
|
||||
if batches:
|
||||
return frappe.get_doc("LMS Batch", batches[0])
|
||||
|
||||
def get_instructor(self):
|
||||
member_name = self.get_community_member(self.owner)
|
||||
return frappe.get_doc("Community Member", member_name)
|
||||
@@ -148,3 +177,59 @@ class LMSCourse(Document):
|
||||
visibility="Public")
|
||||
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)
|
||||
|
||||
def get_outline(self):
|
||||
return CourseOutline(self)
|
||||
|
||||
class CourseOutline:
|
||||
def __init__(self, course):
|
||||
self.course = course
|
||||
self.chapters = self.get_chapters()
|
||||
self.lessons = self.get_lessons()
|
||||
|
||||
def get_next(self, current):
|
||||
numbers = sorted(lesson['number'] for lesson in self.lessons)
|
||||
try:
|
||||
index = numbers.index(current)
|
||||
return numbers[index+1]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
def get_prev(self, current):
|
||||
numbers = sorted(lesson['number'] for lesson in self.lessons)
|
||||
try:
|
||||
index = numbers.index(current)
|
||||
if index == 0:
|
||||
return None
|
||||
return numbers[index-1]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
def get_chapters(self):
|
||||
return frappe.db.get_all("Chapter",
|
||||
filters={"course": self.course.name},
|
||||
fields=["name", "title", "index_"])
|
||||
|
||||
def get_lessons(self):
|
||||
chapters = [c['name'] for c in self.chapters]
|
||||
lessons = frappe.db.get_all("Lesson",
|
||||
filters={"chapter": ["IN", chapters]},
|
||||
fields=["name", "title", "chapter", "index_"])
|
||||
|
||||
chapter_numbers = {c['name']: c['index_'] for c in self.chapters}
|
||||
for lesson in lessons:
|
||||
lesson['number'] = "{}.{}".format(chapter_numbers[lesson['chapter']], lesson['index_'])
|
||||
return lessons
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
"""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 dataclasses import dataclass
|
||||
|
||||
@@ -77,7 +77,7 @@ body {
|
||||
.lessons {
|
||||
padding-left: 20px;
|
||||
}
|
||||
.lesson {
|
||||
.lessons .lesson {
|
||||
margin: 5px 0px;
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -167,7 +167,7 @@ img.profile-photo {
|
||||
}
|
||||
|
||||
.msger-inputarea {
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
||||
@@ -235,7 +235,7 @@ section.lightgray {
|
||||
|
||||
// LiveCode editor
|
||||
|
||||
.livecode-editor-large {
|
||||
.livecode-editor {
|
||||
|
||||
.CodeMirror {
|
||||
border: 1px solid #ddd;
|
||||
@@ -302,3 +302,7 @@ section.lightgray {
|
||||
#hero h1 {
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
.lesson-page {
|
||||
margin: 20px 0px;
|
||||
}
|
||||
|
||||
@@ -19,3 +19,8 @@ def get_context(context):
|
||||
|
||||
context.course = course
|
||||
|
||||
batch = course.get_student_batch(frappe.session.user)
|
||||
if batch:
|
||||
frappe.local.flags.redirect_location = f"/courses/{course.name}/{batch.name}/learn"
|
||||
raise frappe.Redirect
|
||||
|
||||
|
||||
@@ -14,10 +14,12 @@
|
||||
<div class="batch-header">
|
||||
{{ BatchHearder(course.name, member_count) }}
|
||||
</div>
|
||||
<div class="message-section">
|
||||
{{ Messages(messages) }}
|
||||
<div class="messages">
|
||||
<div class="message-section">
|
||||
{{ Messages(messages) }}
|
||||
</div>
|
||||
{{ TextArea() }}
|
||||
</div>
|
||||
{{ TextArea() }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -44,4 +46,4 @@
|
||||
<input type="text" class="msger-input" placeholder="Write your message...">
|
||||
<button type="submit" class="msger-send-btn" data-batch="{{batch.name | urlencode }}">Send</button>
|
||||
</form>
|
||||
{% endmacro %}
|
||||
{% endmacro %}
|
||||
|
||||
@@ -1,13 +1,122 @@
|
||||
{% extends "templates/base.html" %}
|
||||
{% 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 %}
|
||||
<meta name="description" content="Courses" />
|
||||
<meta name="keywords" content="" />
|
||||
<meta name="description" content="{{lesson.title}} - {{course.title}}" />
|
||||
<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 %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{{ Sidebar(course.name, batch.name) }}
|
||||
|
||||
<div class="container">
|
||||
<div class="lesson-page">
|
||||
<div class="lesson-pagination">
|
||||
{% if prev_url %}
|
||||
<a href="{{prev_url}}" class="btn">← Prev</a>
|
||||
{% endif %}
|
||||
{% if next_url %}
|
||||
<a href="{{next_url}}" class="btn pull-right">Next →</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<h2>{{ lesson.title }}</h2>
|
||||
|
||||
{% for s in lesson.get_sections() %}
|
||||
<div class="section section-{{ s.type }}">
|
||||
{{ render_section(s) }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="lesson-pagination">
|
||||
<a href="#" class="btn">← Prev</a>
|
||||
<a href="#" class="btn pull-right">Next →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% 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,9 @@ def get_context(context):
|
||||
|
||||
course_name = frappe.form_dict["course"]
|
||||
batch_name = frappe.form_dict["batch"]
|
||||
chapter_index = frappe.form_dict.get("chapter")
|
||||
lesson_index = frappe.form_dict.get("lesson")
|
||||
lesson_number = f"{chapter_index}.{lesson_index}"
|
||||
|
||||
course = Course.find(course_name)
|
||||
if not course:
|
||||
@@ -17,5 +20,27 @@ def get_context(context):
|
||||
frappe.local.flags.redirect_location = "/courses/" + course_name
|
||||
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.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()
|
||||
|
||||
outline = course.get_outline()
|
||||
next_ = outline.get_next(lesson_number)
|
||||
prev_ = outline.get_prev(lesson_number)
|
||||
context.next_url = get_learn_url(course_name, batch_name, next_)
|
||||
context.prev_url = get_learn_url(course_name, batch_name, prev_)
|
||||
|
||||
def get_learn_url(course_name, batch_name, lesson_number):
|
||||
if not lesson_number:
|
||||
return
|
||||
return f"/courses/{course_name}/{batch_name}/learn/{lesson_number}"
|
||||
|
||||
def get_livecode_url():
|
||||
return frappe.db.get_single_value("LMS Settings", "livecode_url")
|
||||
|
||||
@@ -24,29 +24,36 @@
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro LiveCodeEditor(name, code) %}
|
||||
<div class="livecode-editor canvas-editor" id="editor-{{name}}"
|
||||
data-section="{{name}}">
|
||||
{% macro LiveCodeEditor(name, code, is_exercise, last_submitted) %}
|
||||
<div class="livecode-editor livecode-editor-inline" id="editor-{{name}}">
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<div>
|
||||
<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 class="col-lg-8 col-md-6">
|
||||
<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;">Submitted <span class="human-time" data-timestamp="{{last_submitted}}">on {{last_submitted}}</span></span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="canvas-wrapper">
|
||||
<canvas class="canvas" width="150" height="150"></canvas>
|
||||
<pre class="output"></pre>
|
||||
</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>
|
||||
</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>
|
||||
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user