diff --git a/community/hooks.py b/community/hooks.py index bc97e3a2..8868509c 100644 --- a/community/hooks.py +++ b/community/hooks.py @@ -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///learn", "to_route": "courses/learn"}, + {"from_route": "/courses///learn/.", "to_route": "courses/learn"}, {"from_route": "/courses///schedule", "to_route": "courses/schedule"}, {"from_route": "/courses///members", "to_route": "courses/members"}, {"from_route": "/courses///discuss", "to_route": "courses/discuss"}, diff --git a/community/lms/doctype/chapter/chapter.json b/community/lms/doctype/chapter/chapter.json index f153ab38..93a1c09f 100644 --- a/community/lms/doctype/chapter/chapter.json +++ b/community/lms/doctype/chapter/chapter.json @@ -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", diff --git a/community/lms/doctype/lesson/lesson.json b/community/lms/doctype/lesson/lesson.json index 9c202d2f..2dbe6c92 100644 --- a/community/lms/doctype/lesson/lesson.json +++ b/community/lms/doctype/lesson/lesson.json @@ -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", diff --git a/community/lms/doctype/lesson/lesson.py b/community/lms/doctype/lesson/lesson.py index 3bf002de..b1105240 100644 --- a/community/lms/doctype/lesson/lesson.py +++ b/community/lms/doctype/lesson/lesson.py @@ -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. + """ diff --git a/community/lms/doctype/lms_course/lms_course.py b/community/lms/doctype/lms_course/lms_course.py index 7dea7153..914e5c90 100644 --- a/community/lms/doctype/lms_course/lms_course.py +++ b/community/lms/doctype/lms_course/lms_course.py @@ -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 diff --git a/community/lms/doctype/lms_topic/section_parser.py b/community/lms/doctype/lms_topic/section_parser.py index 6382d133..183a7c6b 100644 --- a/community/lms/doctype/lms_topic/section_parser.py +++ b/community/lms/doctype/lms_topic/section_parser.py @@ -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 diff --git a/community/public/css/style.css b/community/public/css/style.css index d195eab5..1462ebf4 100644 --- a/community/public/css/style.css +++ b/community/public/css/style.css @@ -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; diff --git a/community/public/css/style.less b/community/public/css/style.less index e253bc1a..b0800ff3 100644 --- a/community/public/css/style.less +++ b/community/public/css/style.less @@ -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; +} diff --git a/community/www/courses/course.py b/community/www/courses/course.py index ee02d342..a397ab26 100644 --- a/community/www/courses/course.py +++ b/community/www/courses/course.py @@ -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 + diff --git a/community/www/courses/discuss/index.html b/community/www/courses/discuss/index.html index 6c8c124b..b4e19f86 100644 --- a/community/www/courses/discuss/index.html +++ b/community/www/courses/discuss/index.html @@ -14,10 +14,12 @@
{{ BatchHearder(course.name, member_count) }}
-
- {{ Messages(messages) }} +
+
+ {{ Messages(messages) }} +
+ {{ TextArea() }}
- {{ TextArea() }}
{% endblock %} @@ -44,4 +46,4 @@ -{% endmacro %} \ No newline at end of file +{% endmacro %} diff --git a/community/www/courses/learn/index.html b/community/www/courses/learn/index.html index a2a07f82..4804f508 100644 --- a/community/www/courses/learn/index.html +++ b/community/www/courses/learn/index.html @@ -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 %} - - + + + + + + + + + + + + + {% endblock %} + {% block content %} {{ Sidebar(course.name, batch.name) }} +
+
+
+ {% if prev_url %} + ← Prev + {% endif %} + {% if next_url %} + Next → + {% endif %} +
+ +

{{ lesson.title }}

+ + {% for s in lesson.get_sections() %} +
+ {{ render_section(s) }} +
+ {% endfor %} + + +
{% 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 %} +
Unknown section type: {{s.type}}
+ {% endif %} +{% endmacro %} + +{% macro render_section_text(s) %} +
+
+ {{ frappe.utils.md_to_html(s.contents) }} +
+
+{% endmacro %} + +{%- block script %} + {{ super() }} + {{ LiveCodeEditorJS() }} + + + +{%- endblock %} diff --git a/community/www/courses/learn/index.py b/community/www/courses/learn/index.py index 83b3df0e..4aea3388 100644 --- a/community/www/courses/learn/index.py +++ b/community/www/courses/learn/index.py @@ -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") diff --git a/community/www/macros/livecode.html b/community/www/macros/livecode.html index bb8b6993..94502e53 100644 --- a/community/www/macros/livecode.html +++ b/community/www/macros/livecode.html @@ -24,29 +24,36 @@ {% endmacro %} -{% macro LiveCodeEditor(name, code) %} -
+{% macro LiveCodeEditor(name, code, is_exercise, last_submitted) %} +
-
-
- -
- - Reset - Clear +
+
+ + + {% if is_exercise %} + + {% if last_submitted %} + Submitted on {{last_submitted}} + {% endif %} + {% endif %}
-
-
-
- -

+  
+
+
+
+
+ +
+
+
+ +

       
- {% endmacro %}