From 5fd1143f76eeb07965f6b23eed127a3864b4917e Mon Sep 17 00:00:00 2001 From: pateljannat Date: Wed, 2 Jun 2021 13:52:50 +0530 Subject: [PATCH 01/19] feat: lesson progress --- community/lms/api.py | 2 +- community/lms/doctype/exercise/exercise.py | 5 ++ community/lms/doctype/lesson/lesson.py | 51 ++++++++++++ .../doctype/lms_course_progress/__init__.py | 0 .../lms_course_progress.js | 8 ++ .../lms_course_progress.json | 78 ++++++++++++++++++ .../lms_course_progress.py | 8 ++ .../test_lms_course_progress.py | 8 ++ community/lms/widgets/ChapterTeaser.html | 3 + community/lms/widgets/CourseOutline.html | 2 +- community/public/css/style.css | 33 ++++++++ community/public/css/style.less | 2 +- community/public/images/Vector.png | Bin 0 -> 206 bytes community/www/batch/home.html | 2 +- community/www/batch/learn.html | 2 +- community/www/batch/learn.js | 11 +++ 16 files changed, 210 insertions(+), 5 deletions(-) create mode 100644 community/lms/doctype/lms_course_progress/__init__.py create mode 100644 community/lms/doctype/lms_course_progress/lms_course_progress.js create mode 100644 community/lms/doctype/lms_course_progress/lms_course_progress.json create mode 100644 community/lms/doctype/lms_course_progress/lms_course_progress.py create mode 100644 community/lms/doctype/lms_course_progress/test_lms_course_progress.py create mode 100644 community/public/images/Vector.png create mode 100644 community/www/batch/learn.js diff --git a/community/lms/api.py b/community/lms/api.py index 9cf2fc9d..1ed198e1 100644 --- a/community/lms/api.py +++ b/community/lms/api.py @@ -43,7 +43,7 @@ def save_current_lesson(batch_name, lesson_name): doctype="LMS Batch Membership", filters={ "batch": batch_name, - "member_email": frappe.session.user + "member": frappe.session.user }, fieldname="name") if not name: diff --git a/community/lms/doctype/exercise/exercise.py b/community/lms/doctype/exercise/exercise.py index 151b993a..7b7439d6 100644 --- a/community/lms/doctype/exercise/exercise.py +++ b/community/lms/doctype/exercise/exercise.py @@ -4,6 +4,7 @@ import frappe from frappe.model.document import Document from ..lms_sketch.livecode import livecode_to_svg +from ..lesson.lesson import update_progress class Exercise(Document): def before_save(self): @@ -55,5 +56,9 @@ class Exercise(Document): image=image, solution=code) doc.insert(ignore_permissions=True) + + if not course.is_mentor(frappe.session.user): + update_progress(self.lesson) + return doc diff --git a/community/lms/doctype/lesson/lesson.py b/community/lms/doctype/lesson/lesson.py index 979515ea..f22592ca 100644 --- a/community/lms/doctype/lesson/lesson.py +++ b/community/lms/doctype/lesson/lesson.py @@ -43,3 +43,54 @@ class Lesson(Document): The return value would be like 1.2, 2.1 etc. It will be None if there is no next lesson. """ + + def get_progress(self): + return frappe.db.get_value("LMS Course Progress", {"lesson": self.name, "owner": frappe.session.user}, "status") + + def get_slugified_class(self): + if self.get_progress(): + return ("").join([ s for s in self.get_progress().lower().split() ]) + return + +@frappe.whitelist() +def save_progress(lesson): + if frappe.db.exists("LMS Course Progress", + { + "lesson": lesson, + "owner": frappe.session.user + }): + return + + lesson_details = frappe.get_doc("Lesson", lesson) + dynamic_content = frappe.db.count("LMS Section", + filters={ + "type": ["not in", ["example", "text"]], + "parent": lesson_details.name + }) + + status = "Complete" + if dynamic_content: + status = "Partially Complete" + + frappe.get_doc({ + "doctype": "LMS Course Progress", + "lesson": lesson_details.name, + "status": status + }).save(ignore_permissions=True) + +def update_progress(lesson): + user = frappe.session.user + if not all_dynamic_content_submitted(lesson, user): + return + course_progress = frappe.get_doc("LMS Course Progress", {"lesson": lesson, "owner": user}) + course_progress.status = "Complete" + course_progress.save() + +def all_dynamic_content_submitted(lesson, user): + exercises = frappe.get_all("Exercise", {"lesson": lesson}, ["name"]) + all_exercises_submitted = True + for exercise in exercises: + if not frappe.db.count("Exercise Submission", {"exercise": exercise.name, "owner": user}): + all_exercises_submitted = False + + return all_exercises_submitted diff --git a/community/lms/doctype/lms_course_progress/__init__.py b/community/lms/doctype/lms_course_progress/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/community/lms/doctype/lms_course_progress/lms_course_progress.js b/community/lms/doctype/lms_course_progress/lms_course_progress.js new file mode 100644 index 00000000..77ef3c16 --- /dev/null +++ b/community/lms/doctype/lms_course_progress/lms_course_progress.js @@ -0,0 +1,8 @@ +// Copyright (c) 2021, FOSS United and contributors +// For license information, please see license.txt + +frappe.ui.form.on('LMS Course Progress', { + // refresh: function(frm) { + + // } +}); diff --git a/community/lms/doctype/lms_course_progress/lms_course_progress.json b/community/lms/doctype/lms_course_progress/lms_course_progress.json new file mode 100644 index 00000000..33d785f6 --- /dev/null +++ b/community/lms/doctype/lms_course_progress/lms_course_progress.json @@ -0,0 +1,78 @@ +{ + "actions": [], + "creation": "2021-05-31 17:20:13.388453", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "status", + "column_break_3", + "lesson", + "chapter", + "course" + ], + "fields": [ + { + "fetch_from": "chapter.course", + "fieldname": "course", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Course", + "options": "LMS Course", + "read_only": 1 + }, + { + "fetch_from": "lesson.chapter", + "fieldname": "chapter", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Chapter", + "options": "Chapter", + "read_only": 1 + }, + { + "fieldname": "lesson", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Lesson", + "options": "Lesson" + }, + { + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "options": "Complete\nPartially Complete\nIncomplete" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-06-02 13:05:31.114939", + "modified_by": "Administrator", + "module": "LMS", + "name": "LMS Course Progress", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/community/lms/doctype/lms_course_progress/lms_course_progress.py b/community/lms/doctype/lms_course_progress/lms_course_progress.py new file mode 100644 index 00000000..98a022b7 --- /dev/null +++ b/community/lms/doctype/lms_course_progress/lms_course_progress.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, FOSS United and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class LMSCourseProgress(Document): + pass diff --git a/community/lms/doctype/lms_course_progress/test_lms_course_progress.py b/community/lms/doctype/lms_course_progress/test_lms_course_progress.py new file mode 100644 index 00000000..9eabd5d3 --- /dev/null +++ b/community/lms/doctype/lms_course_progress/test_lms_course_progress.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, FOSS United and Contributors +# See license.txt + +# import frappe +import unittest + +class TestLMSCourseProgress(unittest.TestCase): + pass diff --git a/community/lms/widgets/ChapterTeaser.html b/community/lms/widgets/ChapterTeaser.html index 49f3af78..1678b1bb 100644 --- a/community/lms/widgets/ChapterTeaser.html +++ b/community/lms/widgets/ChapterTeaser.html @@ -8,6 +8,9 @@ {% for lesson in chapter.get_lessons() %}
{{ lesson.title }} + {% if show_progress and not course.is_mentor(frappe.session.user) and lesson.get_progress() %} + {{ lesson.get_progress() }} + {% endif %}
{% endfor %} diff --git a/community/lms/widgets/CourseOutline.html b/community/lms/widgets/CourseOutline.html index 9dd26835..249013ff 100644 --- a/community/lms/widgets/CourseOutline.html +++ b/community/lms/widgets/CourseOutline.html @@ -1,5 +1,5 @@

Course Outline

{% for chapter in course.get_chapters() %} -{{ widgets.ChapterTeaser(index=loop.index, chapter=chapter, course=course, batch=batch, show_link=show_link)}} +{{ widgets.ChapterTeaser(index=loop.index, chapter=chapter, course=course, batch=batch, show_link=show_link, show_progress=show_progress)}} {% endfor %} diff --git a/community/public/css/style.css b/community/public/css/style.css index 60eb4ed2..699d8dfc 100644 --- a/community/public/css/style.css +++ b/community/public/css/style.css @@ -238,3 +238,36 @@ section { .page-card .btn { margin-top: 30px; } + +.partiallycomplete { + background: #FEF4E2; + color: #976417; +} + +.partiallycomplete img { + background: #976417; +} + +.complete { + background: #EAF5EE; + color: #38A160; +} + +.complete img { + background: #38A160; +} + +.incomplete { + background: #FEECEC; + color: #E24C4C; +} + +.incomplete img { + background: #E24C4C; +} + +.progress-image { + margin-right: 3px; + border-radius: 50px; + padding: 5px; +} diff --git a/community/public/css/style.less b/community/public/css/style.less index f95f643c..c265dafe 100644 --- a/community/public/css/style.less +++ b/community/public/css/style.less @@ -285,7 +285,7 @@ section.lightgray { } .lesson-teaser { - line-height: 35px; + line-height: 40px; } #hero h1 { diff --git a/community/public/images/Vector.png b/community/public/images/Vector.png new file mode 100644 index 0000000000000000000000000000000000000000..9e3cbc7f216d7b9986fb24adfa3f949b4977335d GIT binary patch literal 206 zcmeAS@N?(olHy`uVBq!ia0vp^JV4CF!3HE7boT!OQk(@Ik;M!Q+`=Ht$S`Y;1W=H% zILO_JVcj{Imp~3nx}&cn1H;CC?mvmFKz_2Pi(^Oy{{ batch.title }}
- {{ widgets.CourseOutline(course=course, batch=batch, show_link=True) }} + {{ widgets.CourseOutline(course=course, batch=batch, show_link=True, show_progress=True) }}

Batch Schedule

diff --git a/community/www/batch/learn.html b/community/www/batch/learn.html index 36136ddb..9eae61bd 100644 --- a/community/www/batch/learn.html +++ b/community/www/batch/learn.html @@ -28,7 +28,7 @@ {{ widgets.BatchTabs(course=course, batch=batch) }}
-

{{ lesson.title }}

+

{{ lesson.title }}

{% for s in lesson.get_sections() %}
diff --git a/community/www/batch/learn.js b/community/www/batch/learn.js new file mode 100644 index 00000000..c3a6519d --- /dev/null +++ b/community/www/batch/learn.js @@ -0,0 +1,11 @@ +frappe.ready(() => { + console.log($(".title").hasClass("is_mentor")) + if (!$(".title").hasClass("is_mentor")) { + frappe.call({ + method: "community.lms.doctype.lesson.lesson.save_progress", + args: { + lesson: $(".title").attr("data-name") + } + }) + } +}) From 4fd7af053be6ff65ca95b3fd8b8a8fd88c57f906 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Wed, 2 Jun 2021 16:47:17 +0530 Subject: [PATCH 02/19] fix: tests --- community/lms/doctype/exercise/exercise.py | 2 +- .../web_form/add_a_new_batch/add_a_new_batch.json | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/community/lms/doctype/exercise/exercise.py b/community/lms/doctype/exercise/exercise.py index 7b7439d6..ed92d173 100644 --- a/community/lms/doctype/exercise/exercise.py +++ b/community/lms/doctype/exercise/exercise.py @@ -57,7 +57,7 @@ class Exercise(Document): solution=code) doc.insert(ignore_permissions=True) - if not course.is_mentor(frappe.session.user): + if not (course.is_mentor(frappe.session.user) or frappe.flags.in_test): update_progress(self.lesson) return doc diff --git a/community/lms/web_form/add_a_new_batch/add_a_new_batch.json b/community/lms/web_form/add_a_new_batch/add_a_new_batch.json index e1785069..76735390 100644 --- a/community/lms/web_form/add_a_new_batch/add_a_new_batch.json +++ b/community/lms/web_form/add_a_new_batch/add_a_new_batch.json @@ -19,7 +19,7 @@ "is_standard": 1, "login_required": 1, "max_attachment_size": 0, - "modified": "2021-04-30 11:22:18.188712", + "modified": "2021-06-02 15:52:06.383260", "modified_by": "Administrator", "module": "LMS", "name": "add-a-new-batch", @@ -38,13 +38,13 @@ { "allow_read_on_all_link_options": 0, "fieldname": "course", - "fieldtype": "Data", - "hidden": 0, + "fieldtype": "Link", + "hidden": 1, "label": "Course", "max_length": 0, "max_value": 0, - "options": "", - "read_only": 1, + "options": "LMS Course", + "read_only": 0, "reqd": 0, "show_in_filter": 0 }, @@ -111,4 +111,4 @@ "show_in_filter": 0 } ] -} +} \ No newline at end of file From 671b4a065091d191ed5239c5314eeff8c566e46e Mon Sep 17 00:00:00 2001 From: pateljannat Date: Wed, 2 Jun 2021 20:19:36 +0530 Subject: [PATCH 03/19] fix: api and orm --- community/lms/doctype/exercise/exercise.py | 4 --- .../exercise_submission.py | 9 ++++-- community/lms/doctype/lesson/lesson.py | 29 +++++++++++++------ community/www/batch/learn.html | 2 +- community/www/batch/learn.js | 4 +-- 5 files changed, 30 insertions(+), 18 deletions(-) diff --git a/community/lms/doctype/exercise/exercise.py b/community/lms/doctype/exercise/exercise.py index ed92d173..09b5be8b 100644 --- a/community/lms/doctype/exercise/exercise.py +++ b/community/lms/doctype/exercise/exercise.py @@ -4,7 +4,6 @@ import frappe from frappe.model.document import Document from ..lms_sketch.livecode import livecode_to_svg -from ..lesson.lesson import update_progress class Exercise(Document): def before_save(self): @@ -57,8 +56,5 @@ class Exercise(Document): solution=code) doc.insert(ignore_permissions=True) - if not (course.is_mentor(frappe.session.user) or frappe.flags.in_test): - update_progress(self.lesson) - return doc diff --git a/community/lms/doctype/exercise_submission/exercise_submission.py b/community/lms/doctype/exercise_submission/exercise_submission.py index fd631eb8..886de170 100644 --- a/community/lms/doctype/exercise_submission/exercise_submission.py +++ b/community/lms/doctype/exercise_submission/exercise_submission.py @@ -1,8 +1,13 @@ # Copyright (c) 2021, FOSS United and contributors # For license information, please see license.txt -# import frappe +import frappe from frappe.model.document import Document +from ..lesson.lesson import update_progress class ExerciseSubmission(Document): - pass + + def after_insert(self): + course_details = frappe.get_doc("LMS Course", self.course) + if not (course_details.is_mentor(frappe.session.user) or frappe.flags.in_test): + update_progress(self.lesson) diff --git a/community/lms/doctype/lesson/lesson.py b/community/lms/doctype/lesson/lesson.py index f22592ca..5129e878 100644 --- a/community/lms/doctype/lesson/lesson.py +++ b/community/lms/doctype/lesson/lesson.py @@ -53,7 +53,13 @@ class Lesson(Document): return @frappe.whitelist() -def save_progress(lesson): +def save_progress(lesson, batch): + if not frappe.db.exists("LMS Batch Membership", + { + "member": frappe.session.user, + "batch": batch + }): + return if frappe.db.exists("LMS Course Progress", { "lesson": lesson, @@ -82,15 +88,20 @@ def update_progress(lesson): user = frappe.session.user if not all_dynamic_content_submitted(lesson, user): return - course_progress = frappe.get_doc("LMS Course Progress", {"lesson": lesson, "owner": user}) - course_progress.status = "Complete" - course_progress.save() + if frappe.db.exists("LMS Course Progress", {"lesson": lesson, "owner": user}): + course_progress = frappe.get_doc("LMS Course Progress", {"lesson": lesson, "owner": user}) + course_progress.status = "Complete" + course_progress.save() def all_dynamic_content_submitted(lesson, user): - exercises = frappe.get_all("Exercise", {"lesson": lesson}, ["name"]) - all_exercises_submitted = True - for exercise in exercises: - if not frappe.db.count("Exercise Submission", {"exercise": exercise.name, "owner": user}): - all_exercises_submitted = False + exercise_names = frappe.get_list("Exercise", {"lesson": lesson}, ["name"], pluck="name") + all_exercises_submitted = False + print(exercise_names) + query = { + "exercise": ["in", exercise_names], + "owner": user + } + if frappe.db.count("Exercise Submission", query) == len(exercise_names): + all_exercises_submitted = True return all_exercises_submitted diff --git a/community/www/batch/learn.html b/community/www/batch/learn.html index 9eae61bd..d0271c61 100644 --- a/community/www/batch/learn.html +++ b/community/www/batch/learn.html @@ -28,7 +28,7 @@ {{ widgets.BatchTabs(course=course, batch=batch) }}
-

{{ lesson.title }}

+

{{ lesson.title }}

{% for s in lesson.get_sections() %}
diff --git a/community/www/batch/learn.js b/community/www/batch/learn.js index c3a6519d..4410fb08 100644 --- a/community/www/batch/learn.js +++ b/community/www/batch/learn.js @@ -1,10 +1,10 @@ frappe.ready(() => { - console.log($(".title").hasClass("is_mentor")) if (!$(".title").hasClass("is_mentor")) { frappe.call({ method: "community.lms.doctype.lesson.lesson.save_progress", args: { - lesson: $(".title").attr("data-name") + lesson: $(".title").attr("data-name"), + batch: $(".title").attr("data-batch") } }) } From ef0c3e4a24eca3a55f526db5a65a227d4d2e9ae1 Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Tue, 8 Jun 2021 10:36:12 +0530 Subject: [PATCH 04/19] feat: pluggable profile tabs Added ProfileTab class to represent a profile tab and made the profile page render the tabs specified in the hook `profile_tabs`. This allows plugging in new tabs in the profile page without makeing any changes to the community module. --- community/community/profile_tab.py | 38 +++++++++++++++++++++++++++++ community/www/profiles/profile.html | 33 ++++++++++++------------- community/www/profiles/profile.py | 15 ++++++++++-- 3 files changed, 66 insertions(+), 20 deletions(-) create mode 100644 community/community/profile_tab.py diff --git a/community/community/profile_tab.py b/community/community/profile_tab.py new file mode 100644 index 00000000..62e54dcb --- /dev/null +++ b/community/community/profile_tab.py @@ -0,0 +1,38 @@ +""" +The profile_tab module provides a pluggable way to add tabs to user +profiles. + +This is achieved by specifying the profile_tabs in the hooks. + + profile_tabs = [ + 'myapp.myapp.profile_tabs.SketchesTab' + ] + +When a profile page is rendered, these classes specified in the +profile_hooks are instanciated with the user as argument and used to +render the tabs. +""" + +class ProfileTab: + """Base class for profile tabs. + + Every subclass of ProfileTab must implement two methods: + - get_title() + - render() + """ + def __init__(self, user): + self.user = user + + def get_title(self): + """Returns the title of the tab. + + Every subclass must implement this. + """ + raise NotImplementedError() + + def render(self): + """Renders the contents of the tab as HTML. + + Every subclass must implement this. + """ + raise NotImplementedError() diff --git a/community/www/profiles/profile.html b/community/www/profiles/profile.html index d7f3daae..68e9a58c 100644 --- a/community/www/profiles/profile.html +++ b/community/www/profiles/profile.html @@ -72,29 +72,26 @@
-
-
-
- {% if sketches %} - {% for sketch in sketches %} -
- {{ widgets.SketchTeaser(sketch=sketch) }} -
- {% endfor %} - {% endif %} + {% for tab in profile_tabs %} + {% set slug = title.lower().replace(" ", "-") %} +
+
+ {{ tab.render() }}
- {% if not sketches %} -

{{member.full_name}} has not created any skecth yet.

- {% endif %}
-
+ {% endfor %}
diff --git a/community/www/profiles/profile.py b/community/www/profiles/profile.py index 5f0bf355..b6856cc0 100644 --- a/community/www/profiles/profile.py +++ b/community/www/profiles/profile.py @@ -3,9 +3,20 @@ from community.lms.models import Sketch def get_context(context): context.no_cache = 1 + try: context.member = frappe.get_doc("User", {"username": frappe.form_dict["username"]}) except: context.template = "www/404.html" - else: - context.sketches = Sketch.get_recent_sketches(owner=context.member.email) + return + + context.profile_tabs = get_profile_tabs(context.member) + +def get_profile_tabs(user): + """Returns the enabled ProfileTab objects. + + Each ProfileTab is rendered as a tab on the profile page and the + they are specified as profile_tabs hook. + """ + tabs = frappe.get_hooks("profile_tabs") or [] + return [frappe.get_attr(tab)(user) for tab in tabs] From 5363fb7eb37ee5af34460163eca36276266d39bf Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Wed, 9 Jun 2021 22:30:20 +0530 Subject: [PATCH 05/19] feat: extend markdown to support macros With this feature, the exercises can be added to the lesson as: {{ Exercise("two-circles") }} This also added fenced_code extension that allows adding id and classes to code blocks. This uses Python-Markdown library instead of Markdown2 that is used everywhere in Frappe. The Python-Markdown is more easily extensible than Markdown2. Issue #115 --- community/lms/md.py | 78 +++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 3 ++ 2 files changed, 81 insertions(+) create mode 100644 community/lms/md.py diff --git a/community/lms/md.py b/community/lms/md.py new file mode 100644 index 00000000..55f2fe12 --- /dev/null +++ b/community/lms/md.py @@ -0,0 +1,78 @@ +""" +The md module extends markdown to add macros. + +Macros can be added to the markdown text in the following format. + + {{ MacroName("macro-argument") }} + +These macros will be rendered using a pluggable mechanism. + +Apps can provide a hook community_markdown_macro_renderers, a +dictionary mapping the macro name to the function that to render +that macro. The function will get the argument passed to the macro +as argument. +""" + +import frappe +import re +from bs4 import BeautifulSoup +import markdown +from markdown import Extension +from markdown.inlinepatterns import InlineProcessor +import xml.etree.ElementTree as etree + +def markdown_to_html(text): + """Renders markdown text into html. + """ + return markdown.markdown(text, extensions=['fenced_code', MacroExtension()]) + +def get_macro_registry(): + d = frappe.get_hooks("community_markdown_macro_renderers") or {} + return {name: frappe.get_attr(klass[0]) for name, klass in d.items()} + +def render_macro(macro_name, macro_argument): + # stripping the quotes on either side of the argument + macro_argument = macro_argument.strip(" '\"") + + registry = get_macro_registry() + if macro_name in registry: + return registry[macro_name](macro_argument) + else: + return f"

Unknown macro: {macro_name}

" + +class MacroExtension(Extension): + """MacroExtension is a markdown extension to support macro syntax. + """ + def extendMarkdown(self, md): + self.md = md + + MACRO_RE = r'{{ *(\w+)\(([^{}]*)\) *}}' + pattern = MacroInlineProcessor(MACRO_RE) + pattern.md = md + md.inlinePatterns.register(pattern, 'macro', 75) + +class MacroInlineProcessor(InlineProcessor): + """MacroInlineProcessor is class that is handles the logic + of how to render each macro occurence in the markdown text. + """ + def handleMatch(self, m, data): + """Handles each macro match and return rendered contents + for that macro as an etree node. + """ + macro = m.group(1) + arg = m.group(2) + html = render_macro(macro, arg) + html = sanitize_html(str(html)) + e = etree.fromstring(html) + return e, m.start(0), m.end(0) + +def sanitize_html(html): + """Sanotize the html using BeautifulSoup. + + The markdown processor request the correct markup and crashes on + any broken tags. This makes sures that all those things are fixed + before passing to the etree parser. + """ + soup = BeautifulSoup(html, features="lxml") + nodes = soup.body.children + return "
" + "\n".join(str(node) for node in nodes) + "
" diff --git a/requirements.txt b/requirements.txt index 900c38db..293d21a8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,5 @@ frappe websocket_client +markdown +beautifulsoup4 +lxml From d9185c0b6b84d857174b5227ee114e30ddd884e5 Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Wed, 9 Jun 2021 23:49:18 +0530 Subject: [PATCH 06/19] feat: integrated lesson markup - added PageExtension plugin to inject custom styles scripts in a page - removed the livecode integration and enabled PageExtension plugins for learn page - also merged the profile_tab.py with plugins.py - added a utility to find the macros from given text - updated the before_save of lesson to find exercises using the macros and update the exercises as before Issue #115 --- community/community/profile_tab.py | 38 --------------- community/hooks.py | 12 +++++ community/lms/doctype/lesson/lesson.py | 34 ++++++++----- community/lms/md.py | 35 ++++++++++++-- community/plugins.py | 66 ++++++++++++++++++++++++++ community/www/batch/learn.html | 45 ++++-------------- community/www/batch/learn.py | 8 +++- 7 files changed, 145 insertions(+), 93 deletions(-) delete mode 100644 community/community/profile_tab.py create mode 100644 community/plugins.py diff --git a/community/community/profile_tab.py b/community/community/profile_tab.py deleted file mode 100644 index 62e54dcb..00000000 --- a/community/community/profile_tab.py +++ /dev/null @@ -1,38 +0,0 @@ -""" -The profile_tab module provides a pluggable way to add tabs to user -profiles. - -This is achieved by specifying the profile_tabs in the hooks. - - profile_tabs = [ - 'myapp.myapp.profile_tabs.SketchesTab' - ] - -When a profile page is rendered, these classes specified in the -profile_hooks are instanciated with the user as argument and used to -render the tabs. -""" - -class ProfileTab: - """Base class for profile tabs. - - Every subclass of ProfileTab must implement two methods: - - get_title() - - render() - """ - def __init__(self, user): - self.user = user - - def get_title(self): - """Returns the title of the tab. - - Every subclass must implement this. - """ - raise NotImplementedError() - - def render(self): - """Renders the contents of the tab as HTML. - - Every subclass must implement this. - """ - raise NotImplementedError() diff --git a/community/hooks.py b/community/hooks.py index e6916c0b..ea739a8f 100644 --- a/community/hooks.py +++ b/community/hooks.py @@ -176,3 +176,15 @@ profile_rules = [ website_route_rules = primary_rules + whitelist_rules + profile_rules update_website_context = 'community.widgets.update_website_context' + +## Specify the additional tabs to be included in the user profile page. +## Each entry must be a subclass of community.community.plugins.ProfileTab +# profile_tabs = [] + +## Specify the extension to be used to control what scripts and stylesheets +## to be included in lesson pages. The specified value must be be a +## subclass of community.community.plugins.PageExtension +# community_lesson_page_extension = None + +## Markdown Macros for Lessons +# community_markdown_macro_renderers = {"Exercise": "myapp.mymodule.plugins.render_exercise"} diff --git a/community/lms/doctype/lesson/lesson.py b/community/lms/doctype/lesson/lesson.py index ef1f9825..52d8bf28 100644 --- a/community/lms/doctype/lesson/lesson.py +++ b/community/lms/doctype/lesson/lesson.py @@ -6,28 +6,28 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document from ...section_parser import SectionParser +from ...md import markdown_to_html, find_macros class Lesson(Document): def before_save(self): - sections = SectionParser().parse(self.body or "") - self.sections = [self.make_lms_section(i, s) for i, s in enumerate(sections)] + macros = find_macros(self.body) + exercises = [value for name, value in macros if name == "Exercise"] index = 1 - for s in self.sections: - if s.type == "exercise": - e = s.get_exercise() - e.lesson = self.name - e.index_ = index - e.save() - index += 1 - self.update_orphan_exercises() + for name in exercises: + e = frappe.get_doc("Exercise", name) + e.lesson = self.name + e.index_ = index + e.save() + index += 1 + self.update_orphan_exercises(exercises) - def update_orphan_exercises(self): + def update_orphan_exercises(self, active_exercises): """Updates the exercises that were previously part of this lesson, but not any more. """ linked_exercises = {row['name'] for row in frappe.get_all('Exercise', {"lesson": self.name})} - active_exercises = {s.id for s in self.get("sections") if s.type=="exercise"} + active_exercises = set(active_exercises) orphan_exercises = linked_exercises - active_exercises for name in orphan_exercises: ex = frappe.get_doc("Exercise", name) @@ -36,11 +36,19 @@ class Lesson(Document): ex.index_label = "" ex.save() + def render_html(self): + return markdown_to_html(self.body) + def get_sections(self): return sorted(self.get('sections'), key=lambda s: s.index) def get_exercises(self): - return [frappe.get_doc("Exercise", s.id) for s in self.get("sections") if s.type=="exercise"] + if not self.body: + return [] + + macros = find_macros(self.body) + exercises = [value for name, value in macros if name == "Exercise"] + return [frappe.get_doc("Exercise", name) for name in exercises] def make_lms_section(self, index, section): s = frappe.new_doc('LMS Section', parent_doc=self, parentfield='sections') diff --git a/community/lms/md.py b/community/lms/md.py index 55f2fe12..b256eb72 100644 --- a/community/lms/md.py +++ b/community/lms/md.py @@ -26,13 +26,42 @@ def markdown_to_html(text): """ return markdown.markdown(text, extensions=['fenced_code', MacroExtension()]) +def find_macros(text): + """Returns all macros in the given text. + + >>> find_macros(text) + [ + ('YouTubeVideo': 'abcd1234') + ('Exercise', 'two-circles'), + ('Exercise', 'four-circles') + ] + """ + macros = re.findall(MACRO_RE, text) + # remove the quotes around the argument + return [(name, _remove_quotes(arg)) for name, arg in macros] + +def _remove_quotes(value): + """Removes quotes around a value. + + Also strips the whitespace. + + >>> _remove_quotes('"hello"') + 'hello' + >>> _remove_quotes("'hello'") + 'hello' + >>> _remove_quotes("hello") + 'hello' + """ + return value.strip(" '\"") + + def get_macro_registry(): d = frappe.get_hooks("community_markdown_macro_renderers") or {} return {name: frappe.get_attr(klass[0]) for name, klass in d.items()} def render_macro(macro_name, macro_argument): # stripping the quotes on either side of the argument - macro_argument = macro_argument.strip(" '\"") + macro_argument = _remove_quotes(macro_argument) registry = get_macro_registry() if macro_name in registry: @@ -40,13 +69,13 @@ def render_macro(macro_name, macro_argument): else: return f"

Unknown macro: {macro_name}

" +MACRO_RE = r'{{ *(\w+)\(([^{}]*)\) *}}' + class MacroExtension(Extension): """MacroExtension is a markdown extension to support macro syntax. """ def extendMarkdown(self, md): self.md = md - - MACRO_RE = r'{{ *(\w+)\(([^{}]*)\) *}}' pattern = MacroInlineProcessor(MACRO_RE) pattern.md = md md.inlinePatterns.register(pattern, 'macro', 75) diff --git a/community/plugins.py b/community/plugins.py new file mode 100644 index 00000000..58fb2765 --- /dev/null +++ b/community/plugins.py @@ -0,0 +1,66 @@ +""" +The plugins module provides various plugins to change the default +behaviour some parts of the community app. + +A site specify what plugins to use using appropriate entries in the frappe +hooks, written in the `hooks.py`. + +This module exposes two plugins: ProfileTab and PageExtension. + +The ProfileTab is used to specify any additional tabs to be displayed +on the profile page of the user. + +The PageExtension is used to load additinal stylesheets and scripts to +be loaded in a webpage. +""" + +class PageExtension: + """PageExtension is a plugin to inject custom styles and scripts + into a web page. + + The subclasses should overwrite the `render_header()` and + `render_footer()` methods to inject whatever styles/scripts into + the webpage. + """ + + def render_header(self): + """Returns the HTML snippet to be included in the head section + of the web page. + + Typically used to include the stylesheets and javascripts to be + included in the of the webpage. + """ + return "" + + def render_footer(self): + """Returns the HTML snippet to be included in the body tag at + the end of web page. + + Typically used to include javascripts that need to be executed + after the page is loaded. + """ + return "" + +class ProfileTab: + """Base class for profile tabs. + + Every subclass of ProfileTab must implement two methods: + - get_title() + - render() + """ + def __init__(self, user): + self.user = user + + def get_title(self): + """Returns the title of the tab. + + Every subclass must implement this. + """ + raise NotImplementedError() + + def render(self): + """Renders the contents of the tab as HTML. + + Every subclass must implement this. + """ + raise NotImplementedError() diff --git a/community/www/batch/learn.html b/community/www/batch/learn.html index d0271c61..17a12ff2 100644 --- a/community/www/batch/learn.html +++ b/community/www/batch/learn.html @@ -9,16 +9,13 @@ - - - - +{% for ext in page_extensions %} + {{ ext.render_header() }} +{% endfor %} - - {% endblock %} @@ -30,11 +27,7 @@

{{ lesson.title }}

- {% for s in lesson.get_sections() %} -
- {{ render_section(s) }} -
- {% endfor %} + {{ lesson.render_html() }} {{ pagination(prev_chap, prev_url, next_chap, next_url) }}
@@ -42,30 +35,6 @@ {% endblock %} -{% macro render_section(s) %} -{% if s.type == "text" %} -{{ render_section_text(s) }} -{% elif s.type == "example" or s.type == "code" %} -{{ LiveCodeEditor(s.name, - code=s.get_latest_code_for_user(), - reset_code=s.contents, - is_exercise=False) - }} -{% elif s.type == "exercise" %} -{{ widgets.Exercise(exercise=s.get_exercise())}} -{% else %} -
Unknown section type: {{s.type}}
-{% endif %} -{% endmacro %} - -{% macro render_section_text(s) %} -
-
- {{ frappe.utils.md_to_html(s.contents) }} -
-
-{% endmacro %} - {% macro pagination(prev_chap, prev_url, next_chap, next_url) %}
{% if prev_url %} @@ -84,8 +53,6 @@ {%- block script %} {{ super() }} -{{ LiveCodeEditorJS() }} - +{% for ext in page_extensions %} + {{ ext.render_footer() }} +{% endfor %} + {%- endblock %} diff --git a/community/www/batch/learn.py b/community/www/batch/learn.py index 4f031eab..d0d4f38e 100644 --- a/community/www/batch/learn.py +++ b/community/www/batch/learn.py @@ -28,7 +28,7 @@ def get_context(context): context.next_url = context.batch.get_learn_url(next_) context.prev_url = context.batch.get_learn_url(prev_) - + context.page_extensions = get_page_extensions() def get_chapter_title(course_name, lesson_number): if not lesson_number: @@ -42,4 +42,8 @@ def get_lesson_index(course, batch, user): lesson = batch.get_current_lesson(user) return lesson and course.get_lesson_index(lesson) - +def get_page_extensions(): + default_value = ["community.community.plugins.PageExtension"] + classnames = frappe.get_hooks("community_lesson_page_extensions") or default_value + extensions = [frappe.get_attr(name)() for name in classnames] + return extensions From 1e3152e303cb8a977375d3d1a344f1c75a781b8e Mon Sep 17 00:00:00 2001 From: pateljannat Date: Thu, 10 Jun 2021 13:41:11 +0530 Subject: [PATCH 07/19] fix: ui --- community/hooks.py | 18 +++--- community/lms/doctype/chapter/chapter.py | 1 + community/lms/doctype/lesson/lesson.json | 11 +++- community/lms/doctype/lms_batch/lms_batch.py | 2 +- .../lms_batch_membership.py | 3 + .../lms/doctype/lms_course/lms_course.py | 7 ++- community/lms/models.py | 2 +- community/lms/widgets/BatchTabs.html | 18 +++--- community/lms/widgets/ChapterTeaser.html | 6 +- community/lms/widgets/CourseOutline.html | 10 ++-- community/lms/widgets/InstructorSection.html | 7 ++- community/public/css/style.css | 1 + community/public/css/style.less | 2 +- community/www/batch/home.html | 18 +++--- community/www/batch/learn.py | 9 ++- community/www/batch/members.html | 18 +++--- community/www/batch/progress.html | 2 +- community/www/batch/progress.py | 7 ++- community/www/batch/utils.py | 9 ++- community/www/courses/course.html | 58 +++++++++---------- community/www/courses/index.html | 10 ++-- community/www/hackathons/macros/card.html | 6 +- 22 files changed, 120 insertions(+), 105 deletions(-) diff --git a/community/hooks.py b/community/hooks.py index e6916c0b..150c88bc 100644 --- a/community/hooks.py +++ b/community/hooks.py @@ -136,15 +136,15 @@ primary_rules = [ {"from_route": "/hackathons//", "to_route": "hackathons/project"}, {"from_route": "/dashboard", "to_route": ""}, {"from_route": "/add-a-new-batch", "to_route": "add-a-new-batch"}, - {"from_route": "/courses///home", "to_route": "batch/home"}, - {"from_route": "/courses///learn", "to_route": "batch/learn"}, - {"from_route": "/courses///learn/.", "to_route": "batch/learn"}, - {"from_route": "/courses///schedule", "to_route": "batch/schedule"}, - {"from_route": "/courses///members", "to_route": "batch/members"}, - {"from_route": "/courses///discuss", "to_route": "batch/discuss"}, - {"from_route": "/courses///about", "to_route": "batch/about"}, - {"from_route": "/courses///progress", "to_route": "batch/progress"}, - {"from_route": "/courses///join", "to_route": "batch/join"} + {"from_route": "/courses//home", "to_route": "batch/home"}, + {"from_route": "/courses//learn", "to_route": "batch/learn"}, + {"from_route": "/courses//learn/.", "to_route": "batch/learn"}, + {"from_route": "/courses//schedule", "to_route": "batch/schedule"}, + {"from_route": "/courses//members", "to_route": "batch/members"}, + {"from_route": "/courses//discuss", "to_route": "batch/discuss"}, + {"from_route": "/courses//about", "to_route": "batch/about"}, + {"from_route": "/courses//progress", "to_route": "batch/progress"}, + {"from_route": "/courses//join", "to_route": "batch/join"} ] # Any frappe default URL is blocked by profile-rules, add it here to unblock it diff --git a/community/lms/doctype/chapter/chapter.py b/community/lms/doctype/chapter/chapter.py index b616de2c..6c160455 100644 --- a/community/lms/doctype/chapter/chapter.py +++ b/community/lms/doctype/chapter/chapter.py @@ -12,4 +12,5 @@ class Chapter(Document): filters={"chapter": self.name}, fields='name', order_by="index_") + print("rows", rows) return [frappe.get_doc('Lesson', row['name']) for row in rows] diff --git a/community/lms/doctype/lesson/lesson.json b/community/lms/doctype/lesson/lesson.json index 4772b255..59df2394 100644 --- a/community/lms/doctype/lesson/lesson.json +++ b/community/lms/doctype/lesson/lesson.json @@ -12,7 +12,8 @@ "index_", "index_label", "body", - "sections" + "sections", + "include_in_preview" ], "fields": [ { @@ -59,11 +60,17 @@ "in_list_view": 1, "label": "Index Label", "read_only": 1 + }, + { + "default": "0", + "fieldname": "include_in_preview", + "fieldtype": "Check", + "label": "Include In Preview" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-06-01 05:30:48.127494", + "modified": "2021-06-10 10:06:39.176891", "modified_by": "Administrator", "module": "LMS", "name": "Lesson", diff --git a/community/lms/doctype/lms_batch/lms_batch.py b/community/lms/doctype/lms_batch/lms_batch.py index 185134ae..93ed20bd 100644 --- a/community/lms/doctype/lms_batch/lms_batch.py +++ b/community/lms/doctype/lms_batch/lms_batch.py @@ -83,7 +83,7 @@ class LMSBatch(Document): def get_learn_url(self, lesson_number): if not lesson_number: return - return f"/courses/{self.course}/{self.name}/learn/{lesson_number}" + return f"/courses/{self.course}/learn/{lesson_number}" @frappe.whitelist() def save_message(message, batch): diff --git a/community/lms/doctype/lms_batch_membership/lms_batch_membership.py b/community/lms/doctype/lms_batch_membership/lms_batch_membership.py index 7d714d46..cda7c53b 100644 --- a/community/lms/doctype/lms_batch_membership/lms_batch_membership.py +++ b/community/lms/doctype/lms_batch_membership/lms_batch_membership.py @@ -43,6 +43,9 @@ class LMSBatchMembership(Document): member_name = frappe.db.get_value("User", self.member, "full_name") frappe.throw(_("{0} is already a {1} of {2} course through {3} batch").format(member_name, membership.member_type, course, membership.batch)) + def get_user_batch(course, user=frappe.session.user): + return frappe.db.get_value("LMS Batch Membership", {"member": user, "course": course}, "batch") + @frappe.whitelist() def create_membership(batch, member=None, member_type="Student", role="Member"): frappe.get_doc({ diff --git a/community/lms/doctype/lms_course/lms_course.py b/community/lms/doctype/lms_course/lms_course.py index b3d08ef9..315a9092 100644 --- a/community/lms/doctype/lms_course/lms_course.py +++ b/community/lms/doctype/lms_course/lms_course.py @@ -8,6 +8,7 @@ from frappe.model.document import Document import json from ...utils import slugify from community.query import find, find_all +from frappe.utils import flt class LMSCourse(Document): @staticmethod @@ -112,7 +113,7 @@ class LMSCourse(Document): """Returns all chapters of this course. """ # TODO: chapters should have a way to specify the order - return find_all("Chapter", course=self.name, order_by="creation") + return find_all("Chapter", course=self.name, order_by="index_") def get_batch(self, batch_name): return find("LMS Batch", name=batch_name, course=self.name) @@ -197,6 +198,7 @@ class CourseOutline: self.lessons = self.get_lessons() def get_next(self, current): + current = flt(current) numbers = sorted(lesson['number'] for lesson in self.lessons) try: index = numbers.index(current) @@ -205,6 +207,7 @@ class CourseOutline: return None def get_prev(self, current): + current = flt(current) numbers = sorted(lesson['number'] for lesson in self.lessons) try: index = numbers.index(current) @@ -228,7 +231,7 @@ class CourseOutline: chapter_numbers = {c['name']: c['index_'] for c in self.chapters} for lesson in lessons: - lesson['number'] = "{}.{}".format(chapter_numbers[lesson['chapter']], lesson['index_']) + lesson['number'] = flt("{}.{}".format(chapter_numbers[lesson['chapter']], lesson['index_'])) return lessons @frappe.whitelist() diff --git a/community/lms/models.py b/community/lms/models.py index 00c910c3..2e7249d7 100644 --- a/community/lms/models.py +++ b/community/lms/models.py @@ -2,4 +2,4 @@ """ from .doctype.lms_course.lms_course import LMSCourse as Course from .doctype.lms_sketch.lms_sketch import LMSSketch as Sketch - +from .doctype.lms_batch_membership.lms_batch_membership import LMSBatchMembership as Membership diff --git a/community/lms/widgets/BatchTabs.html b/community/lms/widgets/BatchTabs.html index 48378660..983b5655 100644 --- a/community/lms/widgets/BatchTabs.html +++ b/community/lms/widgets/BatchTabs.html @@ -1,28 +1,30 @@
- Courses /{% if course.is_mentor(frappe.session.user) %} {{ course.title }} {% else %} {{ course.title }} {% endif %} + Courses /{% if course.is_mentor(frappe.session.user) %} {{ course.title }} {% else %} + {{ course.title }} {% endif %}
diff --git a/community/lms/widgets/ChapterTeaser.html b/community/lms/widgets/ChapterTeaser.html index 1678b1bb..fce2a11a 100644 --- a/community/lms/widgets/ChapterTeaser.html +++ b/community/lms/widgets/ChapterTeaser.html @@ -1,15 +1,15 @@
-

{{index}}. {{ chapter.title }}

+
{{index}}. {{ chapter.title }}
{{ chapter.description or "" }}
{% for lesson in chapter.get_lessons() %}
- {{ lesson.title }} + {{ lesson.title }} {% if show_progress and not course.is_mentor(frappe.session.user) and lesson.get_progress() %} - {{ lesson.get_progress() }} + {{ lesson.get_progress() }} {% endif %}
{% endfor %} diff --git a/community/lms/widgets/CourseOutline.html b/community/lms/widgets/CourseOutline.html index 249013ff..b4b5629c 100644 --- a/community/lms/widgets/CourseOutline.html +++ b/community/lms/widgets/CourseOutline.html @@ -1,5 +1,7 @@ -

Course Outline

+
+

Course Outline

-{% for chapter in course.get_chapters() %} -{{ widgets.ChapterTeaser(index=loop.index, chapter=chapter, course=course, batch=batch, show_link=show_link, show_progress=show_progress)}} -{% endfor %} + {% for chapter in course.get_chapters() %} + {{ widgets.ChapterTeaser(index=loop.index, chapter=chapter, course=course, batch=batch, show_link=show_link, show_progress=show_progress)}} + {% endfor %} +
diff --git a/community/lms/widgets/InstructorSection.html b/community/lms/widgets/InstructorSection.html index d7634b65..367148dd 100644 --- a/community/lms/widgets/InstructorSection.html +++ b/community/lms/widgets/InstructorSection.html @@ -1,5 +1,6 @@ -

Instructor

-
{{instructor.full_name}}
-
Created {{instructor.get_course_count()}} courses
+ {{ widgets.Avatar(member=instructor, avatar_class="avatar-medium") }} + {{ instructor.full_name }} +
Course Creator
+
diff --git a/community/public/css/style.css b/community/public/css/style.css index 699d8dfc..c1ff3462 100644 --- a/community/public/css/style.css +++ b/community/public/css/style.css @@ -82,6 +82,7 @@ body { border-radius: 10px; margin: 10px 0px; background: white; + box-shadow: 0px 5px 10px rgb(0 0 0 / 10%); border: 1px solid #ddc; } diff --git a/community/public/css/style.less b/community/public/css/style.less index c265dafe..9580e195 100644 --- a/community/public/css/style.less +++ b/community/public/css/style.less @@ -10,6 +10,7 @@ h2 { .teaser-body { padding: 20px; + box-shadow: 0px 5px 10px rgb(0 0 0 / 10%) } .teaser-footer { padding: 20px; @@ -162,7 +163,6 @@ section.lightgray { // } .instructor-title { - font-weight: bold; color: black; } diff --git a/community/www/batch/home.html b/community/www/batch/home.html index db439963..a1f6a4a9 100644 --- a/community/www/batch/home.html +++ b/community/www/batch/home.html @@ -11,26 +11,28 @@ {% set invite_link = frappe.utils.get_url() + "/courses/" + course.name + "/" + batch.name + "/join" %}
{{ widgets.BatchTabs(course=course, batch=batch) }} -
+ +
{{ widgets.CourseOutline(course=course, batch=batch, show_link=True, show_progress=True) }}
-

Batch Schedule

+

Batch Schedule

{{ widgets.RenderBatch(course=course, batch=batch) }}
{% if batch.description %} -

Batch Details

- {{ frappe.utils.md_to_html(batch.description) }} +
+

Batch Details

+ {{ frappe.utils.md_to_html(batch.description) }} +
{% endif %} {% if course.is_mentor(frappe.session.user) %} diff --git a/community/www/batch/learn.py b/community/www/batch/learn.py index 4f031eab..08ccb05e 100644 --- a/community/www/batch/learn.py +++ b/community/www/batch/learn.py @@ -1,6 +1,7 @@ from re import I import frappe from . import utils +from frappe.utils import cstr def get_context(context): utils.get_common_context(context) @@ -10,9 +11,10 @@ def get_context(context): lesson_number = f"{chapter_index}.{lesson_index}" course_name = context.course.name - + print(chapter_index, lesson_index) if not chapter_index or not lesson_index: index_ = get_lesson_index(context.course, context.batch, frappe.session.user) or "1.1" + print(index_) frappe.local.flags.redirect_location = context.batch.get_learn_url(index_) raise frappe.Redirect @@ -33,8 +35,9 @@ def get_context(context): def get_chapter_title(course_name, lesson_number): if not lesson_number: return - chapter_index = lesson_number.split(".")[0] - lesson_index = lesson_number.split(".")[1] + lesson_split = cstr(lesson_number).split(".") + chapter_index = lesson_split[0] + lesson_index = lesson_split[1] chapter_name = frappe.db.get_value("Chapter", {"course": course_name, "index_": chapter_index}, "name") return frappe.db.get_value("Lesson", {"chapter": chapter_name, "index_": lesson_index}, "title") diff --git a/community/www/batch/members.html b/community/www/batch/members.html index 035e12dd..32baa20d 100644 --- a/community/www/batch/members.html +++ b/community/www/batch/members.html @@ -19,23 +19,19 @@ {% macro MembersList(members) %}
{% for member in members %} -
-
- {{ widgets.Avatar(member=member, avatar_class="avatar-large") }} -
-
-
- +
+ {{ widgets.Avatar(member=member, avatar_class="avatar-large") }} +
+
+

{{ member.full_name }}

{% if course.is_mentor(member.name) %} -
-
Mentor
-
+
Mentor
{% endif %}
{% if member.bio %} - {{member.bio}} + {{member.bio}} {% endif %}
diff --git a/community/www/batch/progress.html b/community/www/batch/progress.html index 983077b1..0762d835 100644 --- a/community/www/batch/progress.html +++ b/community/www/batch/progress.html @@ -26,7 +26,7 @@
{{ widgets.BatchTabs(course=course, batch=batch) }}
-

Batch Progress

+

Batch Progress

{% for exercise in report.exercises %}

Exercise {{exercise.index_label}}: {{exercise.title}}

diff --git a/community/www/batch/progress.py b/community/www/batch/progress.py index adfb72dd..13578ac1 100644 --- a/community/www/batch/progress.py +++ b/community/www/batch/progress.py @@ -14,12 +14,13 @@ def get_context(context): context.exercise = exercise context.report = BatchReport(context.course, context.batch) + print(context.report) class BatchReport: def __init__(self, course, batch): self.submissions = get_submissions(batch) self.exercises = self.get_exercises(course.name) - + print(self.submissions) self.submissions_by_exercise = defaultdict(list) for s in self.submissions: self.submissions_by_exercise[s.exercise].append(s) @@ -34,7 +35,7 @@ def get_submissions(batch): students = batch.get_students() students_map = {s.email: s for s in students} names, values = nparams("s", students_map.keys()) - + print(students, names, values) sql = """ select owner, exercise, name, solution, creation, image from ( @@ -45,7 +46,7 @@ def get_submissions(batch): """.format(names) data = frappe.db.sql(sql, values=values, as_dict=True) - + print(data) for row in data: row['owner'] = students_map[row['owner']] return data diff --git a/community/www/batch/utils.py b/community/www/batch/utils.py index 24b5dd32..76c47228 100644 --- a/community/www/batch/utils.py +++ b/community/www/batch/utils.py @@ -1,21 +1,20 @@ import frappe -from community.lms.models import Course +from community.lms.models import Course, Membership def get_common_context(context): context.no_cache = 1 course_name = frappe.form_dict["course"] - batch_name = frappe.form_dict["batch"] course = Course.find(course_name) if not course: context.template = "www/404.html" return - + batch_name = Membership.get_user_batch(course_name) batch = course.get_batch(batch_name) - if not batch or not batch.is_member(frappe.session.user): + """ if not batch or not batch.is_member(frappe.session.user): frappe.local.flags.redirect_location = "/courses/" + course_name - raise frappe.Redirect + raise frappe.Redirect """ context.course = course context.batch = batch diff --git a/community/www/courses/course.html b/community/www/courses/course.html index 0d4d7ccc..0963437f 100644 --- a/community/www/courses/course.html +++ b/community/www/courses/course.html @@ -12,28 +12,20 @@
Courses / {{ course.title }}
-

{{course.title}}

+

{{course.title}}

{{ course.short_introduction }}
-
-
+
+
{{ CourseVideo(course) }} - {{ CourseDescription(course) }} + {{ widgets.InstructorSection(instructor=course.get_instructor()) }} {{ BatchSection(course) }} {{ widgets.CourseOutline(course=course, show_link=False) }}
-
- - -
{% endblock %} @@ -49,28 +41,31 @@ {% endmacro %} {% macro CourseDescription(course) %} -

Course Description

+
+

Course Description

-
- {{ frappe.utils.md_to_html(course.description) }} +
+ {{ frappe.utils.md_to_html(course.description) }} +
{% endmacro %} {% macro BatchSection(course) %} -{% if course.is_mentor(frappe.session.user) %} -{{ BatchSectionForMentors(course, course.get_batches(mentor=frappe.session.user)) }} -{% else %} -{{ BatchSectionForStudents(course, course.get_upcoming_batches()) }} -{% endif %} +
+
+ {% if course.is_mentor(frappe.session.user) %} + {{ BatchSectionForMentors(course, course.get_batches(mentor=frappe.session.user)) }} + {% else %} + {{ BatchSectionForStudents(course, course.get_upcoming_batches()) }} + {% endif %} +
+
{% endmacro %} {% macro BatchSectionForMentors(course, mentor_batches) %}

Your Batches

{% if mentor_batches %} -
{% for batch in mentor_batches %} @@ -92,14 +87,15 @@ {% macro BatchSectionForStudents(course, upcoming_batches) %} {% if upcoming_batches %} -

Upcoming Batches

- -
- {% for batch in upcoming_batches %} -
- {{ widgets.RenderBatch(course=course, batch=batch, can_join=True) }} +
+

Upcoming Batches

+
+ {% for batch in upcoming_batches %} +
+ {{ widgets.RenderBatch(course=course, batch=batch, can_join=True) }} +
+ {% endfor %}
- {% endfor %} + {% endif %}
-{% endif %} {% endmacro %} diff --git a/community/www/courses/index.html b/community/www/courses/index.html index 6a69235c..2533c6b0 100644 --- a/community/www/courses/index.html +++ b/community/www/courses/index.html @@ -10,19 +10,17 @@ {% block content %}
-
-

{{ 'Courses' }}

-
+

{{ 'All Courses' }}

{% for course in courses %} {{ course_card(course) }} {% endfor %} - {% if courses %} +
@@ -32,7 +30,7 @@ {% macro course_card(course) %}
-
+
{{ course.title }}
{% if course.description %} diff --git a/community/www/hackathons/macros/card.html b/community/www/hackathons/macros/card.html index 6eed31e0..912a3cda 100644 --- a/community/www/hackathons/macros/card.html +++ b/community/www/hackathons/macros/card.html @@ -1,7 +1,7 @@ {% macro hackathon_card(hackathon) %}
-
+
{{ hackathon.name }}
@@ -12,7 +12,7 @@ {% macro null_card() %}
-
+
-{% endmacro %} \ No newline at end of file +{% endmacro %} From 8f8d4901fff142a8d7d76a04c0e746276a6f0b2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20de=20Ryckel?= Date: Thu, 10 Jun 2021 18:04:40 +0300 Subject: [PATCH 08/19] frappe wasn't imported error with NameError: name 'frappe' is not defined --- .../community_project_member/community_project_member.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/community/hackathon/doctype/community_project_member/community_project_member.py b/community/hackathon/doctype/community_project_member/community_project_member.py index 50fb2d4a..7a7b7559 100644 --- a/community/hackathon/doctype/community_project_member/community_project_member.py +++ b/community/hackathon/doctype/community_project_member/community_project_member.py @@ -3,7 +3,7 @@ # For license information, please see license.txt from __future__ import unicode_literals -# import frappe +import frappe from frappe.model.document import Document class CommunityProjectMember(Document): From 6b5ddcd54a1f71da3c6eb9563041436670dfe4f9 Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Fri, 11 Jun 2021 13:57:42 +0530 Subject: [PATCH 09/19] fix: fixed error on saving exercises Removed the image generation when exercise is saved. The library used for exercises has changed and generating the image doesn't work any more. --- community/lms/doctype/exercise/exercise.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/community/lms/doctype/exercise/exercise.py b/community/lms/doctype/exercise/exercise.py index 98da02f4..9fb83621 100644 --- a/community/lms/doctype/exercise/exercise.py +++ b/community/lms/doctype/exercise/exercise.py @@ -3,12 +3,9 @@ import frappe from frappe.model.document import Document -from ..lms_sketch.livecode import livecode_to_svg +# from ..lms_sketch.livecode import livecode_to_svg class Exercise(Document): - def before_save(self): - self.image = livecode_to_svg(None, self.answer) - def get_user_submission(self): """Returns the latest submission for this user. """ @@ -42,8 +39,6 @@ class Exercise(Document): course = frappe.get_doc("LMS Course", self.course) batch = course.get_student_batch(user) - image = livecode_to_svg(None, code) - doc = frappe.get_doc( doctype="Exercise Submission", exercise=self.name, @@ -51,7 +46,6 @@ class Exercise(Document): course=self.course, lesson=self.lesson, batch=batch and batch.name, - image=image, solution=code) doc.insert(ignore_permissions=True) From 7840512a13a8dfc4bd23a785003c7debee0c1c9f Mon Sep 17 00:00:00 2001 From: pateljannat Date: Mon, 14 Jun 2021 18:45:46 +0530 Subject: [PATCH 10/19] fix: ui, preview, progress, batches --- community/lms/api.py | 7 - community/lms/doctype/chapter/chapter.py | 1 - .../doctype/invite_request/invite_request.py | 3 +- community/lms/doctype/lesson/lesson.json | 23 +-- community/lms/doctype/lesson/lesson.py | 39 +---- community/lms/doctype/lms_batch/lms_batch.py | 5 - .../lms_batch_membership.json | 13 +- .../lms_batch_membership.py | 13 +- .../lms/doctype/lms_course/lms_course.py | 16 ++ community/lms/doctype/lms_section/__init__.py | 0 .../lms/doctype/lms_section/lms_section.json | 66 -------- .../lms/doctype/lms_section/lms_section.py | 33 ---- community/lms/section_parser.py | 84 ---------- .../add_a_new_batch/add_a_new_batch.json | 4 +- .../lms/web_form/join_a_batch/__init__.py | 0 .../lms/web_form/join_a_batch/join_a_batch.js | 3 - .../web_form/join_a_batch/join_a_batch.json | 48 ------ .../lms/web_form/join_a_batch/join_a_batch.py | 7 - community/lms/widgets/BatchTabs.html | 43 ++++- community/lms/widgets/ChapterTeaser.html | 38 ++++- community/lms/widgets/RenderBatch.html | 5 +- community/public/css/style.css | 13 -- community/public/css/style.less | 20 +++ community/www/batch/learn.html | 21 +-- community/www/batch/learn.js | 8 +- community/www/batch/learn.py | 15 +- community/www/batch/utils.py | 16 +- community/www/courses/course.html | 2 +- community/www/courses/course.js | 150 ++++++++++-------- community/www/courses/course.py | 2 +- community/www/courses/index.html | 2 +- 31 files changed, 275 insertions(+), 425 deletions(-) delete mode 100644 community/lms/doctype/lms_section/__init__.py delete mode 100644 community/lms/doctype/lms_section/lms_section.json delete mode 100644 community/lms/doctype/lms_section/lms_section.py delete mode 100644 community/lms/section_parser.py delete mode 100644 community/lms/web_form/join_a_batch/__init__.py delete mode 100644 community/lms/web_form/join_a_batch/join_a_batch.js delete mode 100644 community/lms/web_form/join_a_batch/join_a_batch.json delete mode 100644 community/lms/web_form/join_a_batch/join_a_batch.py diff --git a/community/lms/api.py b/community/lms/api.py index 1ed198e1..089ad86d 100644 --- a/community/lms/api.py +++ b/community/lms/api.py @@ -15,13 +15,6 @@ def autosave_section(section, code): doc.insert() return {"name": doc.name} -@frappe.whitelist() -def get_section(name): - """Saves the code edited in one of the sections. - """ - doc = frappe.get_doc("LMS Section", name) - return doc and doc.as_dict() - @frappe.whitelist() def submit_solution(exercise, code): """Submits a solution. diff --git a/community/lms/doctype/chapter/chapter.py b/community/lms/doctype/chapter/chapter.py index 6c160455..b616de2c 100644 --- a/community/lms/doctype/chapter/chapter.py +++ b/community/lms/doctype/chapter/chapter.py @@ -12,5 +12,4 @@ class Chapter(Document): filters={"chapter": self.name}, fields='name', order_by="index_") - print("rows", rows) return [frappe.get_doc('Lesson', row['name']) for row in rows] diff --git a/community/lms/doctype/invite_request/invite_request.py b/community/lms/doctype/invite_request/invite_request.py index 00bc18c2..325f2611 100644 --- a/community/lms/doctype/invite_request/invite_request.py +++ b/community/lms/doctype/invite_request/invite_request.py @@ -58,7 +58,8 @@ def create_invite_request(invite_email): frappe.get_doc({ "doctype": "Invite Request", - "invite_email": invite_email + "invite_email": invite_email, + "status": "Approved" }).save(ignore_permissions=True) return "OK" diff --git a/community/lms/doctype/lesson/lesson.json b/community/lms/doctype/lesson/lesson.json index 59df2394..18c20147 100644 --- a/community/lms/doctype/lesson/lesson.json +++ b/community/lms/doctype/lesson/lesson.json @@ -8,12 +8,13 @@ "field_order": [ "chapter", "lesson_type", + "include_in_preview", + "column_break_4", "title", "index_", "index_label", - "body", - "sections", - "include_in_preview" + "section_break_6", + "body" ], "fields": [ { @@ -48,12 +49,6 @@ "fieldtype": "Markdown Editor", "label": "Body" }, - { - "fieldname": "sections", - "fieldtype": "Table", - "label": "Sections", - "options": "LMS Section" - }, { "fieldname": "index_label", "fieldtype": "Data", @@ -66,11 +61,19 @@ "fieldname": "include_in_preview", "fieldtype": "Check", "label": "Include In Preview" + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-06-10 10:06:39.176891", + "modified": "2021-06-11 19:03:23.138165", "modified_by": "Administrator", "module": "LMS", "name": "Lesson", diff --git a/community/lms/doctype/lesson/lesson.py b/community/lms/doctype/lesson/lesson.py index 52d8bf28..49abb2d7 100644 --- a/community/lms/doctype/lesson/lesson.py +++ b/community/lms/doctype/lesson/lesson.py @@ -5,7 +5,6 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document -from ...section_parser import SectionParser from ...md import markdown_to_html, find_macros class Lesson(Document): @@ -39,9 +38,6 @@ class Lesson(Document): def render_html(self): return markdown_to_html(self.body) - def get_sections(self): - return sorted(self.get('sections'), key=lambda s: s.index) - def get_exercises(self): if not self.body: return [] @@ -50,30 +46,6 @@ class Lesson(Document): exercises = [value for name, value in macros if name == "Exercise"] return [frappe.get_doc("Exercise", name) for name in exercises] - def make_lms_section(self, index, section): - s = frappe.new_doc('LMS Section', parent_doc=self, parentfield='sections') - s.type = section.type - s.id = section.id - 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. - """ - def get_progress(self): return frappe.db.get_value("LMS Course Progress", {"lesson": self.name, "owner": frappe.session.user}, "status") @@ -98,11 +70,7 @@ def save_progress(lesson, batch): return lesson_details = frappe.get_doc("Lesson", lesson) - dynamic_content = frappe.db.count("LMS Section", - filters={ - "type": ["not in", ["example", "text"]], - "parent": lesson_details.name - }) + dynamic_content = find_macros(lesson_details.body) status = "Complete" if dynamic_content: @@ -121,12 +89,11 @@ def update_progress(lesson): if frappe.db.exists("LMS Course Progress", {"lesson": lesson, "owner": user}): course_progress = frappe.get_doc("LMS Course Progress", {"lesson": lesson, "owner": user}) course_progress.status = "Complete" - course_progress.save() + course_progress.save(ignore_permissions=True) def all_dynamic_content_submitted(lesson, user): - exercise_names = frappe.get_list("Exercise", {"lesson": lesson}, ["name"], pluck="name") + exercise_names = frappe.get_list("Exercise", {"lesson": lesson}, pluck="name") all_exercises_submitted = False - print(exercise_names) query = { "exercise": ["in", exercise_names], "owner": user diff --git a/community/lms/doctype/lms_batch/lms_batch.py b/community/lms/doctype/lms_batch/lms_batch.py index 93ed20bd..391eac7e 100644 --- a/community/lms/doctype/lms_batch/lms_batch.py +++ b/community/lms/doctype/lms_batch/lms_batch.py @@ -80,11 +80,6 @@ class LMSBatch(Document): membership = self.get_membership(user) return membership and membership.current_lesson - def get_learn_url(self, lesson_number): - if not lesson_number: - return - return f"/courses/{self.course}/learn/{lesson_number}" - @frappe.whitelist() def save_message(message, batch): doc = frappe.get_doc({ diff --git a/community/lms/doctype/lms_batch_membership/lms_batch_membership.json b/community/lms/doctype/lms_batch_membership/lms_batch_membership.json index 68d2eaed..6a0cc143 100644 --- a/community/lms/doctype/lms_batch_membership/lms_batch_membership.json +++ b/community/lms/doctype/lms_batch_membership/lms_batch_membership.json @@ -13,7 +13,8 @@ "course", "member_type", "role", - "current_lesson" + "current_lesson", + "is_current" ], "fields": [ { @@ -80,11 +81,19 @@ "fieldtype": "Data", "label": "Memeber Username", "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_current", + "fieldtype": "Check", + "hidden": 1, + "label": "Is Currently Being Used", + "read_only": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-05-24 12:40:57.125694", + "modified": "2021-06-14 10:24:35.425498", "modified_by": "Administrator", "module": "LMS", "name": "LMS Batch Membership", diff --git a/community/lms/doctype/lms_batch_membership/lms_batch_membership.py b/community/lms/doctype/lms_batch_membership/lms_batch_membership.py index cda7c53b..f7a39b74 100644 --- a/community/lms/doctype/lms_batch_membership/lms_batch_membership.py +++ b/community/lms/doctype/lms_batch_membership/lms_batch_membership.py @@ -43,9 +43,6 @@ class LMSBatchMembership(Document): member_name = frappe.db.get_value("User", self.member, "full_name") frappe.throw(_("{0} is already a {1} of {2} course through {3} batch").format(member_name, membership.member_type, course, membership.batch)) - def get_user_batch(course, user=frappe.session.user): - return frappe.db.get_value("LMS Batch Membership", {"member": user, "course": course}, "batch") - @frappe.whitelist() def create_membership(batch, member=None, member_type="Student", role="Member"): frappe.get_doc({ @@ -56,3 +53,13 @@ def create_membership(batch, member=None, member_type="Student", role="Member"): "member": member or frappe.session.user }).save(ignore_permissions=True) return "OK" + +@frappe.whitelist() +def update_current_membership(batch, course, member=frappe.session.user): + all_memberships = frappe.get_all("LMS Batch Membership", {"member": member, "course": course}) + for membership in all_memberships: + frappe.db.set_value("LMS Batch Membership", membership.name, "is_current", 0) + + current_membership = frappe.get_all("LMS Batch Membership", {"batch": batch, "member": member}) + if len(current_membership): + frappe.db.set_value("LMS Batch Membership", current_membership[0].name, "is_current", 1) diff --git a/community/lms/doctype/lms_course/lms_course.py b/community/lms/doctype/lms_course/lms_course.py index 315a9092..30e25b85 100644 --- a/community/lms/doctype/lms_course/lms_course.py +++ b/community/lms/doctype/lms_course/lms_course.py @@ -187,6 +187,22 @@ class LMSCourse(Document): exercise.save() i += 1 + def get_learn_url(self, lesson_number): + if not lesson_number: + return + return f"/courses/{self.name}/learn/{lesson_number}" + + def get_current_batch(self, member=frappe.session.user): + current_membership = frappe.get_all("LMS Batch Membership", {"member": member, "course": self.name, "is_current": 1}, pluck="batch") + if len(current_membership): + return current_membership[0] + return frappe.db.get_value("LMS Batch Membership", {"member": member, "course": self.name}, "batch") + + def get_all_memberships(self, member=frappe.session.user): + all_memberships = frappe.get_all("LMS Batch Membership", {"member": member, "course": self.name}, ["batch", "is_current"]) + for membership in all_memberships: + membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title") + return all_memberships def get_outline(self): return CourseOutline(self) diff --git a/community/lms/doctype/lms_section/__init__.py b/community/lms/doctype/lms_section/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/community/lms/doctype/lms_section/lms_section.json b/community/lms/doctype/lms_section/lms_section.json deleted file mode 100644 index 3b056485..00000000 --- a/community/lms/doctype/lms_section/lms_section.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "actions": [], - "creation": "2021-03-05 15:10:53.906006", - "doctype": "DocType", - "engine": "InnoDB", - "field_order": [ - "label", - "type", - "contents", - "code", - "attrs", - "index", - "id" - ], - "fields": [ - { - "fieldname": "label", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Label" - }, - { - "fieldname": "type", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Type" - }, - { - "fieldname": "contents", - "fieldtype": "Markdown Editor", - "label": "Contents" - }, - { - "fieldname": "code", - "fieldtype": "Code", - "label": "Code" - }, - { - "fieldname": "attrs", - "fieldtype": "Long Text", - "label": "attrs" - }, - { - "fieldname": "index", - "fieldtype": "Int", - "label": "Index" - }, - { - "fieldname": "id", - "fieldtype": "Data", - "label": "id" - } - ], - "index_web_pages_for_search": 1, - "istable": 1, - "links": [], - "modified": "2021-05-19 18:55:26.019625", - "modified_by": "Administrator", - "module": "LMS", - "name": "LMS Section", - "owner": "Administrator", - "permissions": [], - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/community/lms/doctype/lms_section/lms_section.py b/community/lms/doctype/lms_section/lms_section.py deleted file mode 100644 index 65c33f46..00000000 --- a/community/lms/doctype/lms_section/lms_section.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2021, FOSS United and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe -from frappe.model.document import Document - -class LMSSection(Document): - def __repr__(self): - return f"" - - def get_exercise(self): - if self.type == "exercise": - return frappe.get_doc("Exercise", self.id) - - def get_latest_code_for_user(self): - """Returns the latest code for the logged in user. - """ - if not frappe.session.user or frappe.session.user == "Guest": - return self.contents - result = frappe.get_all('Code Revision', - fields=["code"], - filters={ - "author": frappe.session.user, - "section": self.name - }, - order_by="creation desc", - page_length=1) - if result: - return result[0]['code'] - else: - return self.contents diff --git a/community/lms/section_parser.py b/community/lms/section_parser.py deleted file mode 100644 index 183a7c6b..00000000 --- a/community/lms/section_parser.py +++ /dev/null @@ -1,84 +0,0 @@ -"""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 -import re -from typing import List, Tuple, Dict, Iterator - -RE_SECTION = re.compile(r"^\{\{\s(\w+)\s*(?:\((.*)\))?\s*\}\}\s*") -class SectionParser: - def parse(self, text: str) -> Iterator[Section]: - """Parses given text into sections and return an iterator over sections. - """ - lines = text.splitlines() - marked_lines = self.parse_lines(lines) - return self.group_sections(marked_lines) - - def parse_lines(self, lines: List[str]) -> List[Tuple[str, str, str]]: - for line in lines: - m = RE_SECTION.match(line) - if m: - yield m.group(1), self.parse_attrs(m.group(2)), None - else: - yield None, None, line - - def parse_attrs(self, attrs_str: str) -> Dict[str, str]: - # XXX-Anand: Hack - code = "dict({})".format(attrs_str or "") - return eval(code) - - def group_sections(self, marked_lines) -> Iterator[Section]: - index = 0 - - def make_section(type='text', id=None, label=None, **attrs): - nonlocal index - index += 1 - - id = id or f"section-{index}" - label = label or id - return Section( - type=type, - id=id, - label=label, - attrs=attrs) - - section = make_section("text") - - for mark, attrs, line in marked_lines: - if not mark: - section.append(line) - continue - - yield section - - if mark == 'end': - section = make_section(type='text') - else: - section = make_section(**attrs) - - yield section - -@dataclass -class Section: - """One section of the Topic. - """ - type: str - id: str - label: str - contents: str = "" - attrs: dict = None - - def append(self, line): - if not line.endswith("\n"): - line = line + "\n" - self.contents += line - - def __repr__(self): - attrs = dict(type=self.type, id=self.id, label=self.label, **self.attrs) - attrs_str = ", ".join(f'{k}="{v}"' for k, v in attrs.items()) - return f'' diff --git a/community/lms/web_form/add_a_new_batch/add_a_new_batch.json b/community/lms/web_form/add_a_new_batch/add_a_new_batch.json index 76735390..caf901ff 100644 --- a/community/lms/web_form/add_a_new_batch/add_a_new_batch.json +++ b/community/lms/web_form/add_a_new_batch/add_a_new_batch.json @@ -11,7 +11,7 @@ "apply_document_permissions": 0, "button_label": "Save", "creation": "2021-04-20 11:37:49.135114", - "custom_css": ".datepicker.active {\n background-color: white;\n}", + "custom_css": ".datepicker.active {\n background-color: white;\n}\n\n[data-doctype=\"Web Form\"] {\n max-width: 720px;\n margin: 6rem auto;\n}", "doc_type": "LMS Batch", "docstatus": 0, "doctype": "Web Form", @@ -19,7 +19,7 @@ "is_standard": 1, "login_required": 1, "max_attachment_size": 0, - "modified": "2021-06-02 15:52:06.383260", + "modified": "2021-06-14 15:28:08.206622", "modified_by": "Administrator", "module": "LMS", "name": "add-a-new-batch", diff --git a/community/lms/web_form/join_a_batch/__init__.py b/community/lms/web_form/join_a_batch/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/community/lms/web_form/join_a_batch/join_a_batch.js b/community/lms/web_form/join_a_batch/join_a_batch.js deleted file mode 100644 index 699703c5..00000000 --- a/community/lms/web_form/join_a_batch/join_a_batch.js +++ /dev/null @@ -1,3 +0,0 @@ -frappe.ready(function() { - // bind events here -}) \ No newline at end of file diff --git a/community/lms/web_form/join_a_batch/join_a_batch.json b/community/lms/web_form/join_a_batch/join_a_batch.json deleted file mode 100644 index 9afd7801..00000000 --- a/community/lms/web_form/join_a_batch/join_a_batch.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "accept_payment": 0, - "allow_comments": 0, - "allow_delete": 0, - "allow_edit": 0, - "allow_incomplete": 0, - "allow_multiple": 0, - "allow_print": 0, - "amount": 0.0, - "amount_based_on_field": 0, - "apply_document_permissions": 0, - "button_label": "Save", - "creation": "2021-04-15 13:32:14.171328", - "doc_type": "LMS Batch Membership", - "docstatus": 0, - "doctype": "Web Form", - "idx": 0, - "is_standard": 1, - "login_required": 1, - "max_attachment_size": 0, - "modified": "2021-04-15 13:32:14.171328", - "modified_by": "Administrator", - "module": "LMS", - "name": "join-a-batch", - "owner": "Administrator", - "payment_button_label": "Buy Now", - "published": 1, - "route": "join-a-batch", - "route_to_success_link": 0, - "show_attachments": 0, - "show_in_grid": 0, - "show_sidebar": 0, - "sidebar_items": [], - "success_url": "/join-a-batch", - "title": "Join a Batch", - "web_form_fields": [ - { - "allow_read_on_all_link_options": 0, - "fieldtype": "Attach", - "hidden": 0, - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 0, - "show_in_filter": 0 - } - ] -} \ No newline at end of file diff --git a/community/lms/web_form/join_a_batch/join_a_batch.py b/community/lms/web_form/join_a_batch/join_a_batch.py deleted file mode 100644 index 2334f8b2..00000000 --- a/community/lms/web_form/join_a_batch/join_a_batch.py +++ /dev/null @@ -1,7 +0,0 @@ -from __future__ import unicode_literals - -import frappe - -def get_context(context): - # do your magic here - pass diff --git a/community/lms/widgets/BatchTabs.html b/community/lms/widgets/BatchTabs.html index 983b5655..5a3b8a0f 100644 --- a/community/lms/widgets/BatchTabs.html +++ b/community/lms/widgets/BatchTabs.html @@ -2,9 +2,28 @@
Courses /{% if course.is_mentor(frappe.session.user) %} {{ course.title }} {% else %} {{ course.title }} {% endif %} + {% set all_memberships = course.get_all_memberships() %} + {% if all_memberships | length > 1 %} + + + {% endif %}
+{% if not batch %} +{% set display_class = "hide" %} +{% else %} +{% set display_class = "" %} +{% endif %}
+ + diff --git a/community/lms/widgets/RenderBatch.html b/community/lms/widgets/RenderBatch.html index 60e77784..4a9431bb 100644 --- a/community/lms/widgets/RenderBatch.html +++ b/community/lms/widgets/RenderBatch.html @@ -1,6 +1,6 @@
-
Session every {{batch.sessions_on}}
+
Session every {{batch.sessions_on}}
{{frappe.utils.format_time(batch.start_time, "short")}} - {{frappe.utils.format_time(batch.end_time, "short")}}
@@ -18,7 +18,8 @@
{% if can_manage %} - Manage + Manage {% elif can_join %} diff --git a/community/public/css/style.css b/community/public/css/style.css index c1ff3462..448b6431 100644 --- a/community/public/css/style.css +++ b/community/public/css/style.css @@ -157,19 +157,6 @@ img.profile-photo { line-height: 51px; } -.anchor_style { - color: inherit; -} - -a:hover { - text-decoration: none; - color: inherit; -} - -.anchor_style:hover { - text-decoration: underline -} - section { padding: 5rem 0 5rem 0; } diff --git a/community/public/css/style.less b/community/public/css/style.less index 9580e195..b74cd666 100644 --- a/community/public/css/style.less +++ b/community/public/css/style.less @@ -67,6 +67,20 @@ h2 { } } + +.anchor_style { + color: inherit; +} + +.anchor_style:hover { + text-decoration: none +} + +.no-preview:hover { + cursor: pointer; + color: #2490ef; +} + section { padding: 60px 0px; } @@ -331,3 +345,9 @@ section.lightgray { margin: 40px 0px 0px 20px; } } + +.no-preview-message { + width: fit-content; + margin: 50px 0px 50px; + color: black; +} diff --git a/community/www/batch/learn.html b/community/www/batch/learn.html index 17a12ff2..da9834b4 100644 --- a/community/www/batch/learn.html +++ b/community/www/batch/learn.html @@ -25,9 +25,16 @@ {{ widgets.BatchTabs(course=course, batch=batch) }}
-

{{ lesson.title }}

+

{{ lesson.title }}

+ {% if batch or lesson.include_in_preview %} {{ lesson.render_html() }} + {% else %} +
+ This lesson is not available for Preview. Please join a batch to access the complete course. + Checkout Upcoming Batches +
+ {% endif %} {{ pagination(prev_chap, prev_url, next_chap, next_url) }}
@@ -53,18 +60,6 @@ {%- block script %} {{ super() }} - - {% for ext in page_extensions %} {{ ext.render_footer() }} {% endfor %} diff --git a/community/www/batch/learn.js b/community/www/batch/learn.js index 4410fb08..05fbcb33 100644 --- a/community/www/batch/learn.js +++ b/community/www/batch/learn.js @@ -1,5 +1,5 @@ frappe.ready(() => { - if (!$(".title").hasClass("is_mentor")) { + if ($(".title").attr("data-batch") && !$(".title").hasClass("is_mentor")) { frappe.call({ method: "community.lms.doctype.lesson.lesson.save_progress", args: { @@ -8,4 +8,10 @@ frappe.ready(() => { } }) } + if ($(".title").attr("data-batch")) { + frappe.call("community.lms.api.save_current_lesson", { + "batch_name": $(".title").attr("data-batch"), + "lesson_name": $(".title").attr("data-name") + }) + } }) diff --git a/community/www/batch/learn.py b/community/www/batch/learn.py index ceb5fa36..241bc1e7 100644 --- a/community/www/batch/learn.py +++ b/community/www/batch/learn.py @@ -3,6 +3,8 @@ import frappe from . import utils from frappe.utils import cstr +from community.www import batch + def get_context(context): utils.get_common_context(context) @@ -11,11 +13,12 @@ def get_context(context): lesson_number = f"{chapter_index}.{lesson_index}" course_name = context.course.name - print(chapter_index, lesson_index) if not chapter_index or not lesson_index: - index_ = get_lesson_index(context.course, context.batch, frappe.session.user) or "1.1" - print(index_) - frappe.local.flags.redirect_location = context.batch.get_learn_url(index_) + if context.batch: + index_ = get_lesson_index(context.course, context.batch, frappe.session.user) or "1.1" + else: + index_ = "1.1" + frappe.local.flags.redirect_location = context.course.get_learn_url(index_) raise frappe.Redirect context.lesson = context.course.get_lesson(chapter_index, lesson_index) @@ -27,8 +30,8 @@ def get_context(context): next_ = outline.get_next(lesson_number) context.prev_chap = get_chapter_title(course_name, prev_) context.next_chap = get_chapter_title(course_name, next_) - context.next_url = context.batch.get_learn_url(next_) - context.prev_url = context.batch.get_learn_url(prev_) + context.next_url = context.course.get_learn_url(next_) + context.prev_url = context.course.get_learn_url(prev_) context.page_extensions = get_page_extensions() diff --git a/community/www/batch/utils.py b/community/www/batch/utils.py index 76c47228..4405ba2c 100644 --- a/community/www/batch/utils.py +++ b/community/www/batch/utils.py @@ -1,5 +1,5 @@ import frappe -from community.lms.models import Course, Membership +from community.lms.models import Course def get_common_context(context): context.no_cache = 1 @@ -10,16 +10,16 @@ def get_common_context(context): if not course: context.template = "www/404.html" return - batch_name = Membership.get_user_batch(course_name) + + batch_name = course.get_current_batch() batch = course.get_batch(batch_name) - """ if not batch or not batch.is_member(frappe.session.user): - frappe.local.flags.redirect_location = "/courses/" + course_name - raise frappe.Redirect """ + context.batch = batch + if batch_name: + context.members = batch.get_mentors() + batch.get_students() + context.member_count = len(context.members) + context.course = course - context.batch = batch - context.members = batch.get_mentors() + batch.get_students() - context.member_count = len(context.members) context.livecode_url = get_livecode_url() def get_livecode_url(): diff --git a/community/www/courses/course.html b/community/www/courses/course.html index 0963437f..7a34fb21 100644 --- a/community/www/courses/course.html +++ b/community/www/courses/course.html @@ -88,7 +88,7 @@ {% macro BatchSectionForStudents(course, upcoming_batches) %} {% if upcoming_batches %}
-

Upcoming Batches

+

Upcoming Batches

{% for batch in upcoming_batches %}
diff --git a/community/www/courses/course.js b/community/www/courses/course.js index ae77bdb0..58928149 100644 --- a/community/www/courses/course.js +++ b/community/www/courses/course.js @@ -1,72 +1,92 @@ frappe.ready(() => { - if (frappe.session.user != "Guest") { - frappe.call({ - 'method': 'community.lms.doctype.lms_mentor_request.lms_mentor_request.has_requested', - 'args': { - course: decodeURIComponent($("#course-title").attr("data-course")), - }, - 'callback': (data) => { - if (data.message > 0) { - $("#mentor-request").addClass("hide"); - $("#already-applied").removeClass("hide") - } - } - }) - } + if (frappe.session.user != "Guest") { + frappe.call({ + 'method': 'community.lms.doctype.lms_mentor_request.lms_mentor_request.has_requested', + 'args': { + course: decodeURIComponent($("#course-title").attr("data-course")), + }, + 'callback': (data) => { + if (data.message > 0) { + $("#mentor-request").addClass("hide"); + $("#already-applied").removeClass("hide") + } + } + }) + } - $("#apply-now").click((e) => { + $("#apply-now").click((e) => { e.preventDefault(); - if (frappe.session.user == "Guest") { - window.location.href = `/login?redirect-to=/courses/${$(e.currentTarget).attr("data-course")}`; - return; - } - frappe.call({ - "method": "community.lms.doctype.lms_mentor_request.lms_mentor_request.create_request", - "args": { - "course": decodeURIComponent($(e.currentTarget).attr("data-course")) - }, - "callback": (data) => { - if (data.message == "OK") { - $("#mentor-request").addClass("hide"); - $("#already-applied").removeClass("hide") - } - } - }) - }) + if (frappe.session.user == "Guest") { + window.location.href = `/login?redirect-to=/courses/${$(e.currentTarget).attr("data-course")}`; + return; + } + frappe.call({ + "method": "community.lms.doctype.lms_mentor_request.lms_mentor_request.create_request", + "args": { + "course": decodeURIComponent($(e.currentTarget).attr("data-course")) + }, + "callback": (data) => { + if (data.message == "OK") { + $("#mentor-request").addClass("hide"); + $("#already-applied").removeClass("hide") + } + } + }) + }) - $("#cancel-request").click((e) => { + $("#cancel-request").click((e) => { e.preventDefault() - frappe.call({ - "method": "community.lms.doctype.lms_mentor_request.lms_mentor_request.cancel_request", - "args": { - "course": decodeURIComponent($(e.currentTarget).attr("data-course")) - }, - "callback": (data) => { - if (data.message == "OK") { - $("#mentor-request").removeClass("hide"); - $("#already-applied").addClass("hide") - } - } - }) - }) + frappe.call({ + "method": "community.lms.doctype.lms_mentor_request.lms_mentor_request.cancel_request", + "args": { + "course": decodeURIComponent($(e.currentTarget).attr("data-course")) + }, + "callback": (data) => { + if (data.message == "OK") { + $("#mentor-request").removeClass("hide"); + $("#already-applied").addClass("hide") + } + } + }) + }) - $(".join-batch").click((e) => { - e.preventDefault() - if (frappe.session.user == "Guest") { - window.location.href = `/login?redirect-to=/courses/${$(e.currentTarget).attr("data-course")}`; - return; - } - batch = decodeURIComponent($(e.currentTarget).attr("data-batch")) - frappe.call({ - "method": "community.lms.doctype.lms_batch_membership.lms_batch_membership.create_membership", - "args": { - "batch": batch - }, - "callback": (data) => { - if (data.message == "OK") { - frappe.msgprint(__("You are now a student of this course.")) - } - } - }) - }) + $(".join-batch").click((e) => { + e.preventDefault(); + var course = $(e.currentTarget).attr("data-course") + if (frappe.session.user == "Guest") { + window.location.href = `/login?redirect-to=/courses/${course}`; + return; + } + batch = decodeURIComponent($(e.currentTarget).attr("data-batch")) + frappe.call({ + "method": "community.lms.doctype.lms_batch_membership.lms_batch_membership.create_membership", + "args": { + "batch": batch + }, + "callback": (data) => { + if (data.message == "OK") { + frappe.msgprint(__("You are now a student of this course.")); + setTimeout(function () { + window.location.href = `/courses/${course}/home`; + }, 2000); + } + } + }) + }) + + $(".manage-batch").click((e) => { + e.preventDefault(); + var batch = decodeURIComponent($(e.currentTarget).attr("data-batch")); + var course = decodeURIComponent($(e.currentTarget).attr("data-course")); + frappe.call({ + method: "community.lms.doctype.lms_batch_membership.lms_batch_membership.update_current_membership", + args: { + batch: batch, + course: course + }, + callback: (data) => { + window.location.href = `/courses/${course}/home`; + } + }) + }) }) diff --git a/community/www/courses/course.py b/community/www/courses/course.py index 69957fb3..e2ff459f 100644 --- a/community/www/courses/course.py +++ b/community/www/courses/course.py @@ -19,6 +19,6 @@ def get_context(context): batch = course.get_student_batch(frappe.session.user) if batch: - frappe.local.flags.redirect_location = f"/courses/{course.name}/{batch.name}/learn" + frappe.local.flags.redirect_location = f"/courses/{course.name}/learn" raise frappe.Redirect diff --git a/community/www/courses/index.html b/community/www/courses/index.html index 2533c6b0..89de4c19 100644 --- a/community/www/courses/index.html +++ b/community/www/courses/index.html @@ -29,7 +29,7 @@ {% macro course_card(course) %}
- +
{{ course.title }}
From 17f03aeee76814fdb2902314754764e1a730f54b Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 15 Jun 2021 13:01:57 +0530 Subject: [PATCH 11/19] fix: join batch, removed code revision, redirects for other pages if batch missing --- .../community_project_member.py | 5 +- .../lms/doctype/code_revision/__init__.py | 0 .../doctype/code_revision/code_revision.js | 8 --- .../doctype/code_revision/code_revision.json | 58 ------------------- .../doctype/code_revision/code_revision.py | 10 ---- .../code_revision/test_code_revision.py | 10 ---- community/lms/doctype/lesson/lesson.py | 2 +- .../lms/doctype/lms_course/lms_course.py | 4 ++ community/lms/widgets/ChapterTeaser.html | 4 +- community/www/batch/discuss.py | 2 + community/www/batch/home.html | 6 +- community/www/batch/home.py | 2 + community/www/batch/join.html | 36 +++++++----- community/www/batch/members.py | 3 +- community/www/batch/utils.py | 3 + community/www/courses/course.html | 4 +- 16 files changed, 49 insertions(+), 108 deletions(-) delete mode 100644 community/lms/doctype/code_revision/__init__.py delete mode 100644 community/lms/doctype/code_revision/code_revision.js delete mode 100644 community/lms/doctype/code_revision/code_revision.json delete mode 100644 community/lms/doctype/code_revision/code_revision.py delete mode 100644 community/lms/doctype/code_revision/test_code_revision.py diff --git a/community/hackathon/doctype/community_project_member/community_project_member.py b/community/hackathon/doctype/community_project_member/community_project_member.py index 50fb2d4a..6068c39c 100644 --- a/community/hackathon/doctype/community_project_member/community_project_member.py +++ b/community/hackathon/doctype/community_project_member/community_project_member.py @@ -3,13 +3,14 @@ # For license information, please see license.txt from __future__ import unicode_literals -# import frappe +import frappe from frappe.model.document import Document +from frappe import _ class CommunityProjectMember(Document): def validate(self): self.validate_if_already_member() - + def validate_if_already_member(self): if frappe.get_all("Community Project Member", {"owner": self.owner}): frappe.throw(_("You have already applied for the membership of this project.")) diff --git a/community/lms/doctype/code_revision/__init__.py b/community/lms/doctype/code_revision/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/community/lms/doctype/code_revision/code_revision.js b/community/lms/doctype/code_revision/code_revision.js deleted file mode 100644 index 4f9451ce..00000000 --- a/community/lms/doctype/code_revision/code_revision.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2021, FOSS United and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Code Revision', { - // refresh: function(frm) { - - // } -}); diff --git a/community/lms/doctype/code_revision/code_revision.json b/community/lms/doctype/code_revision/code_revision.json deleted file mode 100644 index 4b2cc036..00000000 --- a/community/lms/doctype/code_revision/code_revision.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "actions": [], - "creation": "2021-04-07 00:26:28.806520", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "section", - "code", - "author" - ], - "fields": [ - { - "fieldname": "section", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Section", - "options": "LMS Section" - }, - { - "fieldname": "code", - "fieldtype": "Code", - "label": "Code" - }, - { - "fieldname": "author", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Author", - "options": "User" - } - ], - "index_web_pages_for_search": 1, - "links": [], - "modified": "2021-04-14 11:26:19.628317", - "modified_by": "Administrator", - "module": "LMS", - "name": "Code Revision", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "section", - "track_changes": 1 -} \ No newline at end of file diff --git a/community/lms/doctype/code_revision/code_revision.py b/community/lms/doctype/code_revision/code_revision.py deleted file mode 100644 index 5ca32baa..00000000 --- a/community/lms/doctype/code_revision/code_revision.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2021, FOSS United and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -# import frappe -from frappe.model.document import Document - -class CodeRevision(Document): - pass diff --git a/community/lms/doctype/code_revision/test_code_revision.py b/community/lms/doctype/code_revision/test_code_revision.py deleted file mode 100644 index 3ce8a443..00000000 --- a/community/lms/doctype/code_revision/test_code_revision.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2021, FOSS United and Contributors -# See license.txt -from __future__ import unicode_literals - -# import frappe -import unittest - -class TestCodeRevision(unittest.TestCase): - pass diff --git a/community/lms/doctype/lesson/lesson.py b/community/lms/doctype/lesson/lesson.py index 49abb2d7..c96d3315 100644 --- a/community/lms/doctype/lesson/lesson.py +++ b/community/lms/doctype/lesson/lesson.py @@ -92,7 +92,7 @@ def update_progress(lesson): course_progress.save(ignore_permissions=True) def all_dynamic_content_submitted(lesson, user): - exercise_names = frappe.get_list("Exercise", {"lesson": lesson}, pluck="name") + exercise_names = frappe.get_list("Exercise", {"lesson": lesson}, pluck="name", ignore_permissions=True) all_exercises_submitted = False query = { "exercise": ["in", exercise_names], diff --git a/community/lms/doctype/lms_course/lms_course.py b/community/lms/doctype/lms_course/lms_course.py index 30e25b85..e4a35ad2 100644 --- a/community/lms/doctype/lms_course/lms_course.py +++ b/community/lms/doctype/lms_course/lms_course.py @@ -194,12 +194,16 @@ class LMSCourse(Document): def get_current_batch(self, member=frappe.session.user): current_membership = frappe.get_all("LMS Batch Membership", {"member": member, "course": self.name, "is_current": 1}, pluck="batch") + print(current_membership, member, self.name) if len(current_membership): return current_membership[0] + print(frappe.db.get_value("LMS Batch Membership", {"member": member, "course": self.name}, "batch")) return frappe.db.get_value("LMS Batch Membership", {"member": member, "course": self.name}, "batch") def get_all_memberships(self, member=frappe.session.user): + print(member) all_memberships = frappe.get_all("LMS Batch Membership", {"member": member, "course": self.name}, ["batch", "is_current"]) + print(all_memberships) for membership in all_memberships: membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title") return all_memberships diff --git a/community/lms/widgets/ChapterTeaser.html b/community/lms/widgets/ChapterTeaser.html index 2d900a60..b0cf37c9 100644 --- a/community/lms/widgets/ChapterTeaser.html +++ b/community/lms/widgets/ChapterTeaser.html @@ -45,7 +45,9 @@ } window.checkout_upcoming_batches = () => { - $('html,body').animate({scrollTop: $(".upcoming").offset().top}, 300); + if ($(".upcoming").length > 0) { + $('html,body').animate({ scrollTop: $(".upcoming").offset().top }, 300); + } frappe.hide_msgprint(); } }) diff --git a/community/www/batch/discuss.py b/community/www/batch/discuss.py index 5da3db2d..95ffcb34 100644 --- a/community/www/batch/discuss.py +++ b/community/www/batch/discuss.py @@ -4,3 +4,5 @@ from . import utils def get_context(context): utils.get_common_context(context) context.messages = context.batch.get_messages() + if not context.batch: + utils.redirect_to_lesson(context.course) diff --git a/community/www/batch/home.html b/community/www/batch/home.html index a1f6a4a9..4b930430 100644 --- a/community/www/batch/home.html +++ b/community/www/batch/home.html @@ -8,7 +8,7 @@ {% endblock %} {% block content %} -{% set invite_link = frappe.utils.get_url() + "/courses/" + course.name + "/" + batch.name + "/join" %} +{% set invite_link = frappe.utils.get_url() + "/courses/" + course.name + "/join?batch=" + batch.name %}
{{ widgets.BatchTabs(course=course, batch=batch) }} diff --git a/community/lms/widgets/RenderBatch.html b/community/lms/widgets/RenderBatch.html index cf23b7a4..05bce4b7 100644 --- a/community/lms/widgets/RenderBatch.html +++ b/community/lms/widgets/RenderBatch.html @@ -7,7 +7,7 @@
Starting {{frappe.utils.format_date(batch.start_date, "medium")}}
mentors
- {% for m in batch.get_mentors() %} + {% for m in course.get_mentors(batch.name) %}
diff --git a/community/public/css/style.css b/community/public/css/style.css index 448b6431..30d07de8 100644 --- a/community/public/css/style.css +++ b/community/public/css/style.css @@ -23,6 +23,7 @@ --cta-color: var(--c4); --send-message: var(--c7); --received-message: var(--c8); + --control-bg: var(--gray-100); } body { diff --git a/community/www/batch/discuss.html b/community/www/batch/discuss.html index a31b0f92..75a6963f 100644 --- a/community/www/batch/discuss.html +++ b/community/www/batch/discuss.html @@ -11,7 +11,7 @@ {% block content %}
- {{ widgets.BatchTabs(course=course, batch=batch) }} + {{ widgets.BatchTabs(course=course, membership=membership) }}
{{ widgets.BatchHeader(batch_name=batch.title, member_count=member_count)}}
    diff --git a/community/www/batch/discuss.py b/community/www/batch/discuss.py index 95ffcb34..dd9718db 100644 --- a/community/www/batch/discuss.py +++ b/community/www/batch/discuss.py @@ -4,5 +4,5 @@ from . import utils def get_context(context): utils.get_common_context(context) context.messages = context.batch.get_messages() - if not context.batch: + if not context.membership: utils.redirect_to_lesson(context.course) diff --git a/community/www/batch/home.html b/community/www/batch/home.html index 4b930430..d53888d4 100644 --- a/community/www/batch/home.html +++ b/community/www/batch/home.html @@ -8,15 +8,13 @@ {% endblock %} {% block content %} -{% set invite_link = frappe.utils.get_url() + "/courses/" + course.name + "/join?batch=" + batch.name %}
    - {{ widgets.BatchTabs(course=course, batch=batch) }} - + {{ widgets.BatchTabs(course=course, membership=membership) }}
    {{ widgets.CourseOutline(course=course, batch=batch, show_link=True, show_progress=True) }}
    + + {% if batch %}

    Batch Schedule

    {{ widgets.RenderBatch(course=course, batch=batch) }} @@ -28,8 +26,9 @@ {{ frappe.utils.md_to_html(batch.description) }}
    {% endif %} - + {% endif %} {% if course.is_mentor(frappe.session.user) %} + {% set invite_link = frappe.utils.get_url() + "/courses/" + course.name + "/join?batch=" + batch.name %}

    Invite Members

    Get Batch Invitation diff --git a/community/www/batch/home.py b/community/www/batch/home.py index bb2a53a2..48be81c9 100644 --- a/community/www/batch/home.py +++ b/community/www/batch/home.py @@ -3,5 +3,3 @@ from . import utils def get_context(context): utils.get_common_context(context) - if not context.batch: - utils.redirect_to_lesson(context.course) diff --git a/community/www/batch/learn.html b/community/www/batch/learn.html index da9834b4..a5db728e 100644 --- a/community/www/batch/learn.html +++ b/community/www/batch/learn.html @@ -13,7 +13,7 @@ {% for ext in page_extensions %} - {{ ext.render_header() }} +{{ ext.render_header() }} {% endfor %} {% endblock %} @@ -22,17 +22,18 @@ {% block content %}
    - {{ widgets.BatchTabs(course=course, batch=batch) }} + {{ widgets.BatchTabs(course=course, membership=membership) }}
    -

    {{ lesson.title }}

    +

    {{ lesson.title }}

    - {% if batch or lesson.include_in_preview %} + {% if membership or lesson.include_in_preview %} {{ lesson.render_html() }} {% else %}
    {% endif %} @@ -61,7 +62,26 @@ {%- block script %} {{ super() }} {% for ext in page_extensions %} - {{ ext.render_footer() }} +{{ ext.render_footer() }} {% endfor %} - + {%- endblock %} diff --git a/community/www/batch/learn.js b/community/www/batch/learn.js index 05fbcb33..c8c59be0 100644 --- a/community/www/batch/learn.js +++ b/community/www/batch/learn.js @@ -1,5 +1,5 @@ frappe.ready(() => { - if ($(".title").attr("data-batch") && !$(".title").hasClass("is_mentor")) { + /* if ($(".title").attr("data-batch") && !$(".title").hasClass("is_mentor")) { frappe.call({ method: "community.lms.doctype.lesson.lesson.save_progress", args: { @@ -13,5 +13,5 @@ frappe.ready(() => { "batch_name": $(".title").attr("data-batch"), "lesson_name": $(".title").attr("data-name") }) - } + } */ }) diff --git a/community/www/batch/members.html b/community/www/batch/members.html index 32baa20d..82448d6d 100644 --- a/community/www/batch/members.html +++ b/community/www/batch/members.html @@ -10,7 +10,7 @@ {% block content %}
    - {{ widgets.BatchTabs(course=course, batch=batch) }} + {{ widgets.BatchTabs(course=course, membership=membership) }} {{ MembersList(members)}}
    {% endblock %} diff --git a/community/www/batch/members.py b/community/www/batch/members.py index bb2a53a2..937af7ea 100644 --- a/community/www/batch/members.py +++ b/community/www/batch/members.py @@ -3,5 +3,5 @@ from . import utils def get_context(context): utils.get_common_context(context) - if not context.batch: + if not context.membership: utils.redirect_to_lesson(context.course) diff --git a/community/www/batch/utils.py b/community/www/batch/utils.py index cd7e6b50..3adb081a 100644 --- a/community/www/batch/utils.py +++ b/community/www/batch/utils.py @@ -11,11 +11,16 @@ def get_common_context(context): context.template = "www/404.html" return - batch_name = course.get_current_batch(frappe.session.user) - batch = course.get_batch(batch_name) - context.batch = batch - if batch_name: - context.members = batch.get_mentors() + batch.get_students() + membership = course.get_current_membership(frappe.session.user) + + if membership: + context.membership = membership + batch = course.get_batch(membership.batch) + + if batch: + context.batch = batch + + context.members = course.get_mentors(membership.batch) + course.get_students(membership.batch) context.member_count = len(context.members) diff --git a/community/www/courses/course.html b/community/www/courses/course.html index d18dc765..437a55a5 100644 --- a/community/www/courses/course.html +++ b/community/www/courses/course.html @@ -12,7 +12,12 @@
    Courses / {{ course.title }}
    -

    {{course.title}}

    +
    +

    {{course.title}}

    +
    + +
    +
    {{ course.short_introduction }}
    @@ -96,8 +101,8 @@
    {% endfor %}
    -{% else %} -
    There are no Upcoming Batches for this course currently.
    -{% endif %} + {% else %} +
    There are no Upcoming Batches for this course currently.
    + {% endif %}
    {% endmacro %} diff --git a/community/www/courses/course.js b/community/www/courses/course.js index 142318e3..308b9636 100644 --- a/community/www/courses/course.js +++ b/community/www/courses/course.js @@ -57,11 +57,13 @@ frappe.ready(() => { window.location.href = `/login?redirect-to=/courses/${course}`; return; } - batch = decodeURIComponent($(e.currentTarget).attr("data-batch")) + var batch = $(e.currentTarget).attr("data-batch"); + batch = batch ? decodeURIComponent(batch) : ""; frappe.call({ "method": "community.lms.doctype.lms_batch_membership.lms_batch_membership.create_membership", "args": { - "batch": batch + "batch": batch ? batch : "", + "course": course }, "callback": (data) => { if (data.message == "OK") { From 3384f974e547eb5ebffa0b61c3c9e25b15f208f6 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 22 Jun 2021 10:11:21 +0530 Subject: [PATCH 16/19] fix: batch switch with query parameters --- community/lms/doctype/lms_batch/lms_batch.py | 2 +- .../lms_batch_membership.json | 13 +----- .../lms_batch_membership.py | 16 ++++--- .../lms/doctype/lms_course/lms_course.json | 9 +++- .../lms/doctype/lms_course/lms_course.py | 18 ++++---- community/lms/widgets/BatchTabs.html | 42 +++++++------------ community/lms/widgets/ChapterTeaser.html | 2 +- community/lms/widgets/RenderBatch.html | 2 +- community/www/batch/join.html | 17 +------- community/www/batch/learn.html | 13 +++--- community/www/batch/learn.py | 6 +-- community/www/batch/progress.html | 2 +- community/www/batch/progress.py | 8 ++-- community/www/batch/utils.py | 15 ++++--- community/www/courses/course.html | 4 +- community/www/courses/course.js | 17 -------- community/www/courses/course.py | 11 +++-- 17 files changed, 83 insertions(+), 114 deletions(-) diff --git a/community/lms/doctype/lms_batch/lms_batch.py b/community/lms/doctype/lms_batch/lms_batch.py index 301f5f40..8ed2b990 100644 --- a/community/lms/doctype/lms_batch/lms_batch.py +++ b/community/lms/doctype/lms_batch/lms_batch.py @@ -19,7 +19,7 @@ class LMSBatch(Document): frappe.throw(_("You are not a mentor of the course {0}").format(course.title)) def after_insert(self): - create_membership(batch=self.name, member_type="Mentor") + create_membership(batch=self.name, course=self.course, member_type="Mentor") def is_member(self, email, member_type=None): """Checks if a person is part of a batch. diff --git a/community/lms/doctype/lms_batch_membership/lms_batch_membership.json b/community/lms/doctype/lms_batch_membership/lms_batch_membership.json index 6a0cc143..ba43cd69 100644 --- a/community/lms/doctype/lms_batch_membership/lms_batch_membership.json +++ b/community/lms/doctype/lms_batch_membership/lms_batch_membership.json @@ -13,8 +13,7 @@ "course", "member_type", "role", - "current_lesson", - "is_current" + "current_lesson" ], "fields": [ { @@ -81,19 +80,11 @@ "fieldtype": "Data", "label": "Memeber Username", "read_only": 1 - }, - { - "default": "0", - "fieldname": "is_current", - "fieldtype": "Check", - "hidden": 1, - "label": "Is Currently Being Used", - "read_only": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-06-14 10:24:35.425498", + "modified": "2021-06-21 12:10:28.808803", "modified_by": "Administrator", "module": "LMS", "name": "LMS Batch Membership", diff --git a/community/lms/doctype/lms_batch_membership/lms_batch_membership.py b/community/lms/doctype/lms_batch_membership/lms_batch_membership.py index c170dc4b..5dd33f09 100644 --- a/community/lms/doctype/lms_batch_membership/lms_batch_membership.py +++ b/community/lms/doctype/lms_batch_membership/lms_batch_membership.py @@ -14,18 +14,22 @@ class LMSBatchMembership(Document): self.validate_membership_in_different_batch_same_course() def validate_membership_in_same_batch(self): + filters={ + "member": self.member, + "course": self.course, + "name": ["!=", self.name] + } + if self.batch: + filters["batch"] = self.batch previous_membership = frappe.db.get_value("LMS Batch Membership", - filters={ - "member": self.member, - "batch": self.batch, - "name": ["!=", self.name] - }, + filters, fieldname=["member_type","member"], as_dict=1) if previous_membership: member_name = frappe.db.get_value("User", self.member, "full_name") - frappe.throw(_("{0} is already a {1} of {2}").format(member_name, previous_membership.member_type, self.batch)) + course_title = frappe.db.get_value("LMS Course", self.course, "title") + frappe.throw(_("{0} is already a {1} of the course {2}").format(member_name, previous_membership.member_type, course_title)) def validate_membership_in_different_batch_same_course(self): course = frappe.db.get_value("LMS Batch", self.batch, "course") diff --git a/community/lms/doctype/lms_course/lms_course.json b/community/lms/doctype/lms_course/lms_course.json index 187a24e1..8b46e3ec 100644 --- a/community/lms/doctype/lms_course/lms_course.json +++ b/community/lms/doctype/lms_course/lms_course.json @@ -22,6 +22,7 @@ "field_order": [ "title", "is_published", + "disable_self_learning", "column_break_3", "short_code", "video_link", @@ -73,6 +74,12 @@ "fieldtype": "Small Text", "label": "Short Introduction", "reqd": 1 + }, + { + "default": "0", + "fieldname": "disable_self_learning", + "fieldtype": "Check", + "label": "Disable Self Learning" } ], "index_web_pages_for_search": 1, @@ -99,7 +106,7 @@ "link_fieldname": "course" } ], - "modified": "2021-06-01 04:36:45.696776", + "modified": "2021-06-21 11:34:04.552376", "modified_by": "Administrator", "module": "LMS", "name": "LMS Course", diff --git a/community/lms/doctype/lms_course/lms_course.py b/community/lms/doctype/lms_course/lms_course.py index 4e70f2e3..0f3439ec 100644 --- a/community/lms/doctype/lms_course/lms_course.py +++ b/community/lms/doctype/lms_course/lms_course.py @@ -192,16 +192,17 @@ class LMSCourse(Document): return return f"/courses/{self.name}/learn/{lesson_number}" - def get_current_membership(self, member): - current_membership = frappe.get_all("LMS Batch Membership", {"member": member, "course": self.name, "is_current": 1}, ["name", "batch"]) - if len(current_membership): - return current_membership[0] - return frappe.db.get_value("LMS Batch Membership", {"member": member, "course": self.name}, ["name","batch"], as_dict=True) + def get_membership(self, member, batch): + filters = { + "member": member, + "course": self.name + } + if batch: + filters["batch"] = batch + return frappe.db.get_value("LMS Batch Membership", filters, ["name","batch", "current_lesson"], as_dict=True) def get_all_memberships(self, member=frappe.session.user): - print(member, frappe.session.user) - all_memberships = frappe.get_all("LMS Batch Membership", {"member": member, "course": self.name}, ["batch", "is_current"]) - print(all_memberships) + all_memberships = frappe.get_all("LMS Batch Membership", {"member": member, "course": self.name}, ["batch"]) for membership in all_memberships: membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title") return all_memberships @@ -214,7 +215,6 @@ class LMSCourse(Document): if batch: filters["batch"] = batch - print(filters) memberships = frappe.get_all( "LMS Batch Membership", filters, diff --git a/community/lms/widgets/BatchTabs.html b/community/lms/widgets/BatchTabs.html index 2c7dc758..2a5fc913 100644 --- a/community/lms/widgets/BatchTabs.html +++ b/community/lms/widgets/BatchTabs.html @@ -4,14 +4,14 @@ {{ course.title }} {% endif %} {% set all_memberships = course.get_all_memberships() %} {% if all_memberships | length > 1 %} - @@ -22,18 +22,21 @@ {% else %} {% set display_class = "" %} {% endif %} + +{% block script %} +{% endblock %} diff --git a/community/lms/widgets/ChapterTeaser.html b/community/lms/widgets/ChapterTeaser.html index 505c2d96..b4a0179f 100644 --- a/community/lms/widgets/ChapterTeaser.html +++ b/community/lms/widgets/ChapterTeaser.html @@ -8,7 +8,7 @@ {% for lesson in chapter.get_lessons() %}
    {{ lesson.title }} {% if show_progress and not course.is_mentor(frappe.session.user) and lesson.get_progress() %} {{ lesson.get_progress() }} diff --git a/community/lms/widgets/RenderBatch.html b/community/lms/widgets/RenderBatch.html index 05bce4b7..bb70c2c5 100644 --- a/community/lms/widgets/RenderBatch.html +++ b/community/lms/widgets/RenderBatch.html @@ -18,7 +18,7 @@
    {% if can_manage %} - Manage {% elif can_join %} +
    + {% endif %}
    {{ course.short_introduction }}
    diff --git a/community/www/courses/course.js b/community/www/courses/course.js index 308b9636..f2cad0a2 100644 --- a/community/www/courses/course.js +++ b/community/www/courses/course.js @@ -75,21 +75,4 @@ frappe.ready(() => { } }) }) - - $(".manage-batch").click((e) => { - e.preventDefault(); - var batch = decodeURIComponent($(e.currentTarget).attr("data-batch")); - var course = decodeURIComponent($(e.currentTarget).attr("data-course")); - frappe.call({ - method: "community.lms.doctype.lms_batch_membership.lms_batch_membership.update_current_membership", - args: { - batch: batch, - course: course, - member: frappe.session.user - }, - callback: (data) => { - window.location.href = `/courses/${course}/home`; - } - }) - }) }) diff --git a/community/www/courses/course.py b/community/www/courses/course.py index e2ff459f..3bc2f50f 100644 --- a/community/www/courses/course.py +++ b/community/www/courses/course.py @@ -16,9 +16,8 @@ def get_context(context): raise frappe.Redirect context.course = course - - batch = course.get_student_batch(frappe.session.user) - if batch: - frappe.local.flags.redirect_location = f"/courses/{course.name}/learn" - raise frappe.Redirect - + if not course.is_mentor(frappe.session.user): + batch = course.get_membership(frappe.session.user) + if batch: + frappe.local.flags.redirect_location = f"/courses/{course.name}/learn" + raise frappe.Redirect From ecfcc8a2f7fe16fa12ed364208a11435c766bbe3 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 22 Jun 2021 10:45:07 +0530 Subject: [PATCH 17/19] fix: redirects and urls --- .../lms/doctype/lms_course/lms_course.py | 3 ++- community/lms/widgets/BatchTabs.html | 4 +-- community/www/batch/learn.html | 27 ++----------------- community/www/batch/learn.js | 14 +++++----- community/www/batch/utils.py | 4 +-- 5 files changed, 15 insertions(+), 37 deletions(-) diff --git a/community/lms/doctype/lms_course/lms_course.py b/community/lms/doctype/lms_course/lms_course.py index 0f3439ec..0583700e 100644 --- a/community/lms/doctype/lms_course/lms_course.py +++ b/community/lms/doctype/lms_course/lms_course.py @@ -192,7 +192,7 @@ class LMSCourse(Document): return return f"/courses/{self.name}/learn/{lesson_number}" - def get_membership(self, member, batch): + def get_membership(self, member, batch=None): filters = { "member": member, "course": self.name @@ -205,6 +205,7 @@ class LMSCourse(Document): all_memberships = frappe.get_all("LMS Batch Membership", {"member": member, "course": self.name}, ["batch"]) for membership in all_memberships: membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title") + print(all_memberships) return all_memberships def get_mentors(self, batch): diff --git a/community/lms/widgets/BatchTabs.html b/community/lms/widgets/BatchTabs.html index 2a5fc913..1b6d61d9 100644 --- a/community/lms/widgets/BatchTabs.html +++ b/community/lms/widgets/BatchTabs.html @@ -28,7 +28,7 @@ Home @@ -44,7 +44,7 @@ - {% if membership.batch and course.is_mentor(frappe.session.user) %} + {% if membership and membership.batch and course.is_mentor(frappe.session.user) %} diff --git a/community/www/batch/learn.html b/community/www/batch/learn.html index cca415a7..a65dc4e0 100644 --- a/community/www/batch/learn.html +++ b/community/www/batch/learn.html @@ -25,8 +25,8 @@ {{ widgets.BatchTabs(course=course, membership=membership) }}
    -

    {{ lesson.title }}

    +

    {{ lesson.title }}

    {% if membership or lesson.include_in_preview %} {{ lesson.render_html() }} @@ -64,27 +64,4 @@ {% for ext in page_extensions %} {{ ext.render_footer() }} {% endfor %} - {%- endblock %} diff --git a/community/www/batch/learn.js b/community/www/batch/learn.js index c8c59be0..e15c5c5c 100644 --- a/community/www/batch/learn.js +++ b/community/www/batch/learn.js @@ -1,17 +1,17 @@ frappe.ready(() => { - /* if ($(".title").attr("data-batch") && !$(".title").hasClass("is_mentor")) { + if ($(".title").attr("data-membership") && !$(".title").hasClass("is_mentor")) { frappe.call({ method: "community.lms.doctype.lesson.lesson.save_progress", args: { - lesson: $(".title").attr("data-name"), - batch: $(".title").attr("data-batch") + lesson: $(".title").attr("data-lesson"), + course: $(".title").attr("data-course") } }) } - if ($(".title").attr("data-batch")) { + if ($(".title").attr("data-membership")) { frappe.call("community.lms.api.save_current_lesson", { - "batch_name": $(".title").attr("data-batch"), - "lesson_name": $(".title").attr("data-name") + course_name: $(".title").attr("data-course"), + lesson_name: $(".title").attr("data-lesson") }) - } */ + } }) diff --git a/community/www/batch/utils.py b/community/www/batch/utils.py index 24326964..23060680 100644 --- a/community/www/batch/utils.py +++ b/community/www/batch/utils.py @@ -26,8 +26,8 @@ def get_common_context(context): context.members = course.get_mentors(membership.batch) + course.get_students(membership.batch) context.member_count = len(context.members) - context.course.query_parameter = "?batch=" + batch.name if batch else "" - print(context.membership) + + context.course.query_parameter = "?batch=" + membership.batch if membership and membership.batch else "" context.livecode_url = get_livecode_url() def get_livecode_url(): From 2c570ea214cb8d84fcddb3bb9197f8395aa93bb1 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 22 Jun 2021 10:48:33 +0530 Subject: [PATCH 18/19] fix: added default value for arguements --- community/lms/doctype/lms_course/lms_course.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/community/lms/doctype/lms_course/lms_course.py b/community/lms/doctype/lms_course/lms_course.py index 0583700e..7f77cd5d 100644 --- a/community/lms/doctype/lms_course/lms_course.py +++ b/community/lms/doctype/lms_course/lms_course.py @@ -208,7 +208,7 @@ class LMSCourse(Document): print(all_memberships) return all_memberships - def get_mentors(self, batch): + def get_mentors(self, batch=None): filters = { "course": self.name, "member_type": "Mentor" @@ -223,7 +223,7 @@ class LMSCourse(Document): member_names = [m['member'] for m in memberships] return find_all("User", name=["IN", member_names]) - def get_students(self, batch): + def get_students(self, batch=None): """Returns (email, full_name, username) of all the students of this batch as a list of dict. """ filters = { From 6c751cdf39e81c82b0b5c9b5fc0265b6b43352cc Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 22 Jun 2021 12:17:06 +0530 Subject: [PATCH 19/19] fix: test --- community/lms/doctype/lms_course/test_lms_course.py | 1 - 1 file changed, 1 deletion(-) diff --git a/community/lms/doctype/lms_course/test_lms_course.py b/community/lms/doctype/lms_course/test_lms_course.py index db17c8de..81193128 100644 --- a/community/lms/doctype/lms_course/test_lms_course.py +++ b/community/lms/doctype/lms_course/test_lms_course.py @@ -26,7 +26,6 @@ class TestLMSCourse(unittest.TestCase): course = self.new_course("Test Course") assert course.title == "Test Course" assert course.name == "test-course" - assert course.get_mentors() == [] def test_find_all(self): courses = LMSCourse.find_all()