From 1cf57c4823bb65e00c38bdff5368c6a568dc1299 Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Fri, 14 May 2021 12:06:37 +0530 Subject: [PATCH 1/3] feat: redirect the course page to learn page when the visitor is a student of a batch --- .../lms/doctype/lms_course/lms_course.py | 29 +++++++++++++++++++ community/www/courses/course.py | 5 ++++ 2 files changed, 34 insertions(+) diff --git a/community/lms/doctype/lms_course/lms_course.py b/community/lms/doctype/lms_course/lms_course.py index 7dea7153..c746bcea 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) 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 + From 49b41749e882e5f5cc2142d2e44581d17ab1ac46 Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Fri, 14 May 2021 12:11:45 +0530 Subject: [PATCH 2/3] 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 --- community/hooks.py | 1 + community/lms/doctype/chapter/chapter.json | 11 +- community/lms/doctype/lesson/lesson.json | 17 ++- community/lms/doctype/lesson/lesson.py | 18 +++- .../lms/doctype/lms_course/lms_course.py | 13 +++ .../lms/doctype/lms_topic/section_parser.py | 5 + community/public/css/style.css | 4 +- community/public/css/style.less | 6 +- community/www/courses/discuss/index.html | 10 +- community/www/courses/learn/index.html | 101 +++++++++++++++++- community/www/courses/learn/index.py | 13 +++ community/www/macros/livecode.html | 39 ++++--- 12 files changed, 205 insertions(+), 33 deletions(-) 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..9d63a4b2 100644 --- a/community/lms/doctype/lesson/lesson.py +++ b/community/lms/doctype/lesson/lesson.py @@ -3,8 +3,22 @@ # 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 diff --git a/community/lms/doctype/lms_course/lms_course.py b/community/lms/doctype/lms_course/lms_course.py index c746bcea..56fc4001 100644 --- a/community/lms/doctype/lms_course/lms_course.py +++ b/community/lms/doctype/lms_course/lms_course.py @@ -177,3 +177,16 @@ 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) 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..5bb00f30 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 { + margin: 20px 0px 20px 50px; +} 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..8145e872 100644 --- a/community/www/courses/learn/index.html +++ b/community/www/courses/learn/index.html @@ -1,13 +1,106 @@ {% 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) }} -
+
+ +

{{ lesson.title }} - {{ lesson.name }}

+ + {% 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..36b25048 100644 --- a/community/www/courses/learn/index.py +++ b/community/www/courses/learn/index.py @@ -6,6 +6,8 @@ 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") course = Course.find(course_name) if not course: @@ -17,5 +19,16 @@ 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() + +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..de553e4d 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 %} + Last submitted {{last_submitted}} + {% endif %} + {% endif %}
-
-
-
- -

+  
+
+
+
+
+ +
+
+
+ +

       
- {% endmacro %} From 8b657f2f400a76323dbde49efc31dbd71e775ff0 Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Fri, 14 May 2021 15:29:44 +0530 Subject: [PATCH 3/3] feat: added next/prev links to learn pages --- community/lms/doctype/lesson/lesson.py | 15 +++++++ .../lms/doctype/lms_course/lms_course.py | 43 +++++++++++++++++++ community/public/css/style.less | 4 +- community/www/courses/learn/index.html | 28 +++++++++--- community/www/courses/learn/index.py | 12 ++++++ community/www/macros/livecode.html | 2 +- 6 files changed, 95 insertions(+), 9 deletions(-) diff --git a/community/lms/doctype/lesson/lesson.py b/community/lms/doctype/lesson/lesson.py index 9d63a4b2..b1105240 100644 --- a/community/lms/doctype/lesson/lesson.py +++ b/community/lms/doctype/lesson/lesson.py @@ -22,3 +22,18 @@ class Lesson(Document): 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 56fc4001..914e5c90 100644 --- a/community/lms/doctype/lms_course/lms_course.py +++ b/community/lms/doctype/lms_course/lms_course.py @@ -190,3 +190,46 @@ class LMSCourse(Document): {"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/public/css/style.less b/community/public/css/style.less index 5bb00f30..b0800ff3 100644 --- a/community/public/css/style.less +++ b/community/public/css/style.less @@ -303,6 +303,6 @@ section.lightgray { color: black !important; } -.lesson { - margin: 20px 0px 20px 50px; +.lesson-page { + margin: 20px 0px; } diff --git a/community/www/courses/learn/index.html b/community/www/courses/learn/index.html index 8145e872..4804f508 100644 --- a/community/www/courses/learn/index.html +++ b/community/www/courses/learn/index.html @@ -23,15 +23,31 @@ {% block content %} {{ Sidebar(course.name, batch.name) }} -
-

{{ lesson.title }} - {{ lesson.name }}

+
+
+
+ {% if prev_url %} + ← Prev + {% endif %} + {% if next_url %} + Next → + {% endif %} +
- {% for s in lesson.get_sections() %} -
- {{ render_section(s) }} +

{{ lesson.title }}

+ + {% for s in lesson.get_sections() %} +
+ {{ render_section(s) }} +
+ {% endfor %} + +
- {% endfor %}
{% endblock %} diff --git a/community/www/courses/learn/index.py b/community/www/courses/learn/index.py index 36b25048..4aea3388 100644 --- a/community/www/courses/learn/index.py +++ b/community/www/courses/learn/index.py @@ -8,6 +8,7 @@ def get_context(context): 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: @@ -30,5 +31,16 @@ def get_context(context): 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 de553e4d..94502e53 100644 --- a/community/www/macros/livecode.html +++ b/community/www/macros/livecode.html @@ -34,7 +34,7 @@ {% if is_exercise %} {% if last_submitted %} - Last submitted {{last_submitted}} + Submitted on {{last_submitted}} {% endif %} {% endif %}