Compare commits

...

28 Commits

Author SHA1 Message Date
Anand Chitipothu
9caf44cdbd feat: make it possible to enable tracking for livecode execution
Tracking of livecode execution is made possible by making the page
context with course, batch and lesson available in js.

Added a global page_context variable in js and the data for that gets
initialzied in the learn.py.
2021-07-02 23:58:59 +05:30
Jannat Patel
67708325ae Merge pull request #141 from fossunited/workspace
feat: lms workspace
2021-06-29 15:24:22 +05:30
pateljannat
3e99577401 feat: lms workspace 2021-06-29 15:15:49 +05:30
Jannat Patel
621d01d502 Merge pull request #140 from fossunited/exercise-refactor
fix: enabled livecode on community
2021-06-28 13:11:26 +05:30
pateljannat
aa20136223 fix: undo status change on livecode 2021-06-28 13:05:20 +05:30
pateljannat
5a7afb3092 fix: added livecode editor in community 2021-06-24 16:38:02 +05:30
Jannat Patel
f8948ac2ef Merge pull request #138 from fossunited/learn-page-fix
fix: learn page
2021-06-24 12:28:34 +05:30
pateljannat
8b1576a028 fix: learn page 2021-06-24 12:21:25 +05:30
Jannat Patel
56d8a72a7d Merge pull request #136 from fossunited/quiz
feat: Quizzes, Youtube Video integration and Other Minor Fixes
2021-06-24 10:34:36 +05:30
pateljannat
f6c11ce52f fix: conflicts 2021-06-24 10:27:01 +05:30
pateljannat
0284c9305c fix: quiz progress and youtube video integration 2021-06-24 10:25:23 +05:30
Jannat Patel
d785fb7562 Merge pull request #127 from fossunited/livecode-cleanup
refactor: removed the portal pages for showing sketches
2021-06-23 13:09:26 +05:30
Anand Chitipothu
9f50af4ebd refactor: removed the portal pages for showing sketches
Moved them to mon_school.
2021-06-23 12:53:35 +05:30
Jannat Patel
4c3645f0d4 Merge pull request #133 from fossunited/mon-fixes-01
Various fixes from mon.school
2021-06-23 11:44:35 +05:30
Anand Chitipothu
20b3ae7d76 fix: error in linking lessons on course page
The course was adding `{{ no such element: community.lms.doctype.lms_course.lms_course.LMSCourse object['query_parameter'] }}`
to the lesson links. Fixed it by setting query_parameter to "".
2021-06-23 10:27:01 +05:30
Anand Chitipothu
f303be4db5 fix: error in find_macros when the input is empty
Added a special case to handle this issue.
2021-06-22 18:12:31 +05:30
Anand Chitipothu
fc1c393f15 feat: allow a student to be mentor of another batch
This is a requirement for mon.school. The students are of the first
batch are now mentors of new batches.
2021-06-22 18:09:21 +05:30
pateljannat
5d96bf544d fix: conflicts 2021-06-22 12:28:12 +05:30
Jannat Patel
5abfa35095 Merge pull request #132 from fossunited/learning-modes
feat: learning modes and batch switching
2021-06-22 12:23:46 +05:30
pateljannat
6c751cdf39 fix: test 2021-06-22 12:17:06 +05:30
pateljannat
2c570ea214 fix: added default value for arguements 2021-06-22 10:48:33 +05:30
pateljannat
ecfcc8a2f7 fix: redirects and urls 2021-06-22 10:45:07 +05:30
pateljannat
3384f974e5 fix: batch switch with query parameters 2021-06-22 10:11:21 +05:30
pateljannat
eb435261fe feat: learning modes 2021-06-18 18:31:10 +05:30
Jannat Patel
dc7eabefb9 Merge pull request #131 from fossunited/minor-fixes
fix: web form, progress ui, title non unique
2021-06-16 13:15:10 +05:30
pateljannat
fed4b5568b fix: web form, progress ui, title non unique 2021-06-16 13:04:45 +05:30
Jannat Patel
aa77c60abd Merge pull request #129 from fossunited/minor-fix
fix: minor issues
2021-06-15 18:46:33 +05:30
pateljannat
1cb81de5c0 feat: lms quizzes 2021-06-09 13:17:42 +05:30
67 changed files with 1346 additions and 485 deletions

View File

@@ -161,10 +161,11 @@ whitelist = [
"/socket.io", "/socket.io",
"/hackathons", "/hackathons",
"/dashboard", "/dashboard",
"/join-request" "/join-request",
"/add-a-new-batch", "/add-a-new-batch",
"/new-sign-up", "/new-sign-up",
"/message" "/message",
"/about"
] ]
whitelist_rules = [{"from_route": p, "to_route": p[1:]} for p in whitelist] whitelist_rules = [{"from_route": p, "to_route": p[1:]} for p in whitelist]
@@ -186,5 +187,13 @@ update_website_context = 'community.widgets.update_website_context'
## subclass of community.community.plugins.PageExtension ## subclass of community.community.plugins.PageExtension
# community_lesson_page_extension = None # community_lesson_page_extension = None
community_lesson_page_extensions = [
"community.plugins.LiveCodeExtension"
]
## Markdown Macros for Lessons ## Markdown Macros for Lessons
# community_markdown_macro_renderers = {"Exercise": "myapp.mymodule.plugins.render_exercise"} community_markdown_macro_renderers = {
"Exercise": "community.plugins.exercise_renderer",
"Quiz": "community.plugins.quiz_renderer",
"YouTubeVideo": "community.plugins.youtube_video_renderer",
}

View File

@@ -29,13 +29,13 @@ def submit_solution(exercise, code):
return {"name": doc.name, "creation": doc.creation} return {"name": doc.name, "creation": doc.creation}
@frappe.whitelist() @frappe.whitelist()
def save_current_lesson(batch_name, lesson_name): def save_current_lesson(course_name, lesson_name):
"""Saves the current lesson for a student/mentor. """Saves the current lesson for a student/mentor.
""" """
name = frappe.get_value( name = frappe.get_value(
doctype="LMS Batch Membership", doctype="LMS Batch Membership",
filters={ filters={
"batch": batch_name, "course": course_name,
"member": frappe.session.user "member": frappe.session.user
}, },
fieldname="name") fieldname="name")

View File

@@ -6,12 +6,17 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"exercise", "exercise",
"solution", "status",
"batch",
"column_break_4",
"exercise_title", "exercise_title",
"course", "course",
"batch",
"lesson", "lesson",
"image" "section_break_8",
"solution",
"image",
"test_results",
"comments"
], ],
"fields": [ "fields": [
{ {
@@ -21,12 +26,6 @@
"label": "Exercise", "label": "Exercise",
"options": "Exercise" "options": "Exercise"
}, },
{
"fieldname": "solution",
"fieldtype": "Code",
"in_list_view": 1,
"label": "Solution"
},
{ {
"fetch_from": "exercise.title", "fetch_from": "exercise.title",
"fieldname": "exercise_title", "fieldname": "exercise_title",
@@ -61,11 +60,41 @@
"fieldtype": "Code", "fieldtype": "Code",
"label": "Image", "label": "Image",
"read_only": 1 "read_only": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "Correct\nIncorrect"
},
{
"fieldname": "test_results",
"fieldtype": "Small Text",
"label": "Test Results"
},
{
"fieldname": "comments",
"fieldtype": "Small Text",
"label": "Comments"
},
{
"fieldname": "solution",
"fieldtype": "Code",
"in_list_view": 1,
"label": "Solution"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_8",
"fieldtype": "Section Break"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-05-21 11:28:45.833018", "modified": "2021-06-24 16:22:50.570845",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Exercise Submission", "name": "Exercise Submission",

View File

@@ -2,7 +2,47 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('Lesson', { frappe.ui.form.on('Lesson', {
// refresh: function(frm) { setup: function (frm) {
frm.trigger('setup_help');
},
setup_help(frm) {
frm.get_field('help').html(`
<p>You can add some more additional content to the lesson using a special syntax. The table below mentions all types of dynamic content that you can add to the lessons and the syntax for the same.</p>
<div class="row font-weight-bold mb-3">
<div class="col-sm-4">
Content Type
</div>
<div class="col-sm-4">
Syntax
</div>
</div>
// } <div class="row mb-3">
<div class="col-sm-4">
YouTube Video
</div>
<div class="col-sm-4">
{{ YouTubeVideo("unique_embed_id") }}
</div>
</div>
<div class="row mb-3">
<div class="col-sm-4">
Exercise
</div>
<div class="col-sm-4">
{{ Exercise("exercise_name") }}
</div>
</div>
<div class="row mb-3">
<div class="col-sm-4">
Quiz
</div>
<div class="col-sm-4">
{{ Quiz("lms_quiz_name") }}
</div>
</div>
`);
}
}); });

View File

@@ -7,14 +7,15 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"chapter", "chapter",
"lesson_type",
"include_in_preview", "include_in_preview",
"column_break_4", "column_break_4",
"title", "title",
"index_", "index_",
"index_label", "index_label",
"section_break_6", "section_break_6",
"body" "body",
"help_section",
"help"
], ],
"fields": [ "fields": [
{ {
@@ -24,14 +25,6 @@
"label": "Chapter", "label": "Chapter",
"options": "Chapter" "options": "Chapter"
}, },
{
"default": "Video",
"fieldname": "lesson_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Lesson Type",
"options": "Video\nText\nQuiz"
},
{ {
"fieldname": "title", "fieldname": "title",
"fieldtype": "Data", "fieldtype": "Data",
@@ -42,6 +35,7 @@
"default": "1", "default": "1",
"fieldname": "index_", "fieldname": "index_",
"fieldtype": "Int", "fieldtype": "Int",
"in_list_view": 1,
"label": "Index" "label": "Index"
}, },
{ {
@@ -52,7 +46,6 @@
{ {
"fieldname": "index_label", "fieldname": "index_label",
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1,
"label": "Index Label", "label": "Index Label",
"read_only": 1 "read_only": 1
}, },
@@ -69,11 +62,20 @@
{ {
"fieldname": "column_break_4", "fieldname": "column_break_4",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "help_section",
"fieldtype": "Section Break",
"label": "Help"
},
{
"fieldname": "help",
"fieldtype": "HTML"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-06-11 19:03:23.138165", "modified": "2021-06-29 13:34:49.077363",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Lesson", "name": "Lesson",

View File

@@ -9,27 +9,35 @@ from ...md import markdown_to_html, find_macros
class Lesson(Document): class Lesson(Document):
def before_save(self): def before_save(self):
macros = find_macros(self.body) dynamic_documents = ["Exercise", "Quiz"]
exercises = [value for name, value in macros if name == "Exercise"] for section in dynamic_documents:
self.update_lesson_name_in_document(section)
def update_lesson_name_in_document(self, section):
doctype_map= {
"Exercise": "Exercise",
"Quiz": "LMS Quiz"
}
macros = find_macros(self.body)
documents = [value for name, value in macros if name == section]
index = 1 index = 1
for name in exercises: for name in documents:
e = frappe.get_doc("Exercise", name) e = frappe.get_doc(doctype_map[section], name)
e.lesson = self.name e.lesson = self.name
e.index_ = index e.index_ = index
e.save() e.save()
index += 1 index += 1
self.update_orphan_exercises(exercises) self.update_orphan_documents(doctype_map[section], documents)
def update_orphan_exercises(self, active_exercises): def update_orphan_documents(self, doctype, documents):
"""Updates the exercises that were previously part of this lesson, """Updates the documents that were previously part of this lesson,
but not any more. but not any more.
""" """
linked_exercises = {row['name'] for row in frappe.get_all('Exercise', {"lesson": self.name})} linked_documents = {row['name'] for row in frappe.get_all(doctype, {"lesson": self.name})}
active_exercises = set(active_exercises) active_documents = set(documents)
orphan_exercises = linked_exercises - active_exercises orphan_documents = linked_documents - active_documents
for name in orphan_exercises: for name in orphan_documents:
ex = frappe.get_doc("Exercise", name) ex = frappe.get_doc(doctype, name)
ex.lesson = None ex.lesson = None
ex.index_ = 0 ex.index_ = 0
ex.index_label = "" ex.index_label = ""
@@ -55,11 +63,11 @@ class Lesson(Document):
return return
@frappe.whitelist() @frappe.whitelist()
def save_progress(lesson, batch): def save_progress(lesson, course):
if not frappe.db.exists("LMS Batch Membership", if not frappe.db.exists("LMS Batch Membership",
{ {
"member": frappe.session.user, "member": frappe.session.user,
"batch": batch "course": course
}): }):
return return
if frappe.db.exists("LMS Course Progress", if frappe.db.exists("LMS Course Progress",
@@ -92,13 +100,30 @@ def update_progress(lesson):
course_progress.save(ignore_permissions=True) course_progress.save(ignore_permissions=True)
def all_dynamic_content_submitted(lesson, user): def all_dynamic_content_submitted(lesson, user):
all_exercises_submitted = check_all_exercise_submission(lesson, user)
all_quiz_submitted = check_all_quiz_submitted(lesson, user)
return all_exercises_submitted and all_quiz_submitted
def check_all_exercise_submission(lesson, user):
exercise_names = frappe.get_list("Exercise", {"lesson": lesson}, pluck="name", ignore_permissions=True) exercise_names = frappe.get_list("Exercise", {"lesson": lesson}, pluck="name", ignore_permissions=True)
all_exercises_submitted = False if not len(exercise_names):
return True
query = { query = {
"exercise": ["in", exercise_names], "exercise": ["in", exercise_names],
"owner": user "owner": user
} }
if frappe.db.count("Exercise Submission", query) == len(exercise_names): if frappe.db.count("Exercise Submission", query) == len(exercise_names):
all_exercises_submitted = True return True
return False
return all_exercises_submitted def check_all_quiz_submitted(lesson, user):
quizzes = frappe.get_list("LMS Quiz", {"lesson": lesson}, pluck="name", ignore_permissions=True)
if not len(quizzes):
return True
query = {
"quiz": ["in", quizzes],
"owner": user
}
if frappe.db.count("LMS Quiz Submission", query) == len(quizzes):
return True
return False

View File

@@ -33,8 +33,7 @@
{ {
"fieldname": "title", "fieldname": "title",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Title", "label": "Title"
"unique": 1
}, },
{ {
"fieldname": "description", "fieldname": "description",
@@ -120,7 +119,7 @@
"link_fieldname": "batch" "link_fieldname": "batch"
} }
], ],
"modified": "2021-05-26 16:43:57.399747", "modified": "2021-06-16 10:51:05.403726",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Batch", "name": "LMS Batch",

View File

@@ -19,15 +19,7 @@ class LMSBatch(Document):
frappe.throw(_("You are not a mentor of the course {0}").format(course.title)) frappe.throw(_("You are not a mentor of the course {0}").format(course.title))
def after_insert(self): def after_insert(self):
create_membership(batch=self.name, member_type="Mentor") create_membership(batch=self.name, course=self.course, member_type="Mentor")
def get_mentors(self):
memberships = frappe.get_all(
"LMS Batch Membership",
{"batch": self.name, "member_type": "Mentor"},
["member"])
member_names = [m['member'] for m in memberships]
return find_all("User", name=["IN", member_names])
def is_member(self, email, member_type=None): def is_member(self, email, member_type=None):
"""Checks if a person is part of a batch. """Checks if a person is part of a batch.
@@ -43,16 +35,6 @@ class LMSBatch(Document):
filters['member_type'] = member_type filters['member_type'] = member_type
return frappe.db.exists("LMS Batch Membership", filters) return frappe.db.exists("LMS Batch Membership", filters)
def get_students(self):
"""Returns (email, full_name, username) of all the students of this batch as a list of dict.
"""
memberships = frappe.get_all(
"LMS Batch Membership",
{"batch": self.name, "member_type": "Student"},
["member"])
member_names = [m['member'] for m in memberships]
return find_all("User", name=["IN", member_names])
def get_messages(self): def get_messages(self):
messages = frappe.get_all("LMS Message", {"batch": self.name}, ["*"], order_by="creation") messages = frappe.get_all("LMS Message", {"batch": self.name}, ["*"], order_by="creation")
for message in messages: for message in messages:

View File

@@ -13,8 +13,7 @@
"course", "course",
"member_type", "member_type",
"role", "role",
"current_lesson", "current_lesson"
"is_current"
], ],
"fields": [ "fields": [
{ {
@@ -81,19 +80,11 @@
"fieldtype": "Data", "fieldtype": "Data",
"label": "Memeber Username", "label": "Memeber Username",
"read_only": 1 "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, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-06-14 10:24:35.425498", "modified": "2021-06-21 12:10:28.808803",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Batch Membership", "name": "LMS Batch Membership",

View File

@@ -14,40 +14,53 @@ class LMSBatchMembership(Document):
self.validate_membership_in_different_batch_same_course() self.validate_membership_in_different_batch_same_course()
def validate_membership_in_same_batch(self): 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", previous_membership = frappe.db.get_value("LMS Batch Membership",
filters={ filters,
"member": self.member,
"batch": self.batch,
"name": ["!=", self.name]
},
fieldname=["member_type","member"], fieldname=["member_type","member"],
as_dict=1) as_dict=1)
if previous_membership: if previous_membership:
member_name = frappe.db.get_value("User", self.member, "full_name") 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): def validate_membership_in_different_batch_same_course(self):
course = frappe.db.get_value("LMS Batch", self.batch, "course") """Ensures that a studnet is only part of one batch.
previous_membership = frappe.get_all("LMS Batch Membership", """
filters={ # nothing to worry if the member is not a student
"member": self.member, if self.member_type != "Student":
"name": ["!=", self.name] return
},
fields=["batch", "member_type", "name"]
)
for membership in previous_membership: course = frappe.db.get_value("LMS Batch", self.batch, "course")
batch_course = frappe.db.get_value("LMS Batch", membership.batch, "course") memberships = frappe.get_all(
if batch_course == course and (membership.member_type == "Student" or self.member_type == "Student"): "LMS Batch Membership",
member_name = frappe.db.get_value("User", self.member, "full_name") filters={
frappe.throw(_("{0} is already a {1} of {2} course through {3} batch").format(member_name, membership.member_type, course, membership.batch)) "member": self.member,
"name": ["!=", self.name],
"member_type": "Student",
"course": self.course
},
fields=["batch", "member_type", "name"]
)
if memberships:
membership = memberships[0]
member_name = frappe.db.get_value("User", self.member, "full_name")
frappe.throw(_("{0} is already a Student of {1} course through {2} batch").format(member_name, course, membership.batch))
@frappe.whitelist() @frappe.whitelist()
def create_membership(batch, member=None, member_type="Student", role="Member"): def create_membership(course, batch=None, member=None, member_type="Student", role="Member"):
frappe.get_doc({ frappe.get_doc({
"doctype": "LMS Batch Membership", "doctype": "LMS Batch Membership",
"batch": batch, "batch": batch,
"course": course,
"role": role, "role": role,
"member_type": member_type, "member_type": member_type,
"member": member or frappe.session.user "member": member or frappe.session.user

View File

@@ -22,6 +22,7 @@
"field_order": [ "field_order": [
"title", "title",
"is_published", "is_published",
"disable_self_learning",
"column_break_3", "column_break_3",
"short_code", "short_code",
"video_link", "video_link",
@@ -73,6 +74,12 @@
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Short Introduction", "label": "Short Introduction",
"reqd": 1 "reqd": 1
},
{
"default": "0",
"fieldname": "disable_self_learning",
"fieldtype": "Check",
"label": "Disable Self Learning"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
@@ -99,7 +106,7 @@
"link_fieldname": "course" "link_fieldname": "course"
} }
], ],
"modified": "2021-06-01 04:36:45.696776", "modified": "2021-06-21 11:34:04.552376",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Course", "name": "LMS Course",

View File

@@ -82,11 +82,11 @@ class LMSCourse(Document):
""" """
if not email: if not email:
return False return False
return frappe.db.exists({ return frappe.db.count("LMS Course Mentor Mapping",
"doctype": "LMS Course Mentor Mapping", {
"course": self.name, "course": self.name,
"mentor": email "mentor": email
}) })
def get_student_batch(self, email): def get_student_batch(self, email):
"""Returns the batch the given student is part of. """Returns the batch the given student is part of.
@@ -192,22 +192,55 @@ class LMSCourse(Document):
return return
return f"/courses/{self.name}/learn/{lesson_number}" return f"/courses/{self.name}/learn/{lesson_number}"
def get_current_batch(self, member): def get_membership(self, member, batch=None):
current_membership = frappe.get_all("LMS Batch Membership", {"member": member, "course": self.name, "is_current": 1}, pluck="batch") filters = {
print(current_membership, member, self.name, frappe.session.user) "member": member,
if len(current_membership): "course": self.name
return current_membership[0] }
print(frappe.db.get_value("LMS Batch Membership", {"member": member, "course": self.name}, "batch")) if batch:
return frappe.db.get_value("LMS Batch Membership", {"member": member, "course": self.name}, "batch") filters["batch"] = batch
membership = frappe.db.get_value("LMS Batch Membership", filters, ["name","batch", "current_lesson"], as_dict=True)
if membership and membership.batch:
membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title")
return membership
def get_all_memberships(self, member=frappe.session.user): def get_all_memberships(self, member):
print(member, frappe.session.user) all_memberships = frappe.get_all("LMS Batch Membership", {"member": member, "course": self.name}, ["batch"])
all_memberships = frappe.get_all("LMS Batch Membership", {"member": member, "course": self.name}, ["batch", "is_current"])
print(all_memberships)
for membership in all_memberships: for membership in all_memberships:
membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title") membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title")
return all_memberships return all_memberships
def get_mentors(self, batch=None):
filters = {
"course": self.name,
"member_type": "Mentor"
}
if batch:
filters["batch"] = batch
memberships = frappe.get_all(
"LMS Batch Membership",
filters,
["member"])
member_names = [m['member'] for m in memberships]
return find_all("User", name=["IN", member_names])
def get_students(self, batch=None):
"""Returns (email, full_name, username) of all the students of this batch as a list of dict.
"""
filters = {
"course": self.name,
"member_type": "Student"
}
if batch:
filters["batch"] = batch
memberships = frappe.get_all(
"LMS Batch Membership",
filters,
["member"])
member_names = [m['member'] for m in memberships]
return find_all("User", name=["IN", member_names])
def get_outline(self): def get_outline(self):
return CourseOutline(self) return CourseOutline(self)

View File

@@ -26,7 +26,6 @@ class TestLMSCourse(unittest.TestCase):
course = self.new_course("Test Course") course = self.new_course("Test Course")
assert course.title == "Test Course" assert course.title == "Test Course"
assert course.name == "test-course" assert course.name == "test-course"
assert course.get_mentors() == []
def test_find_all(self): def test_find_all(self):
courses = LMSCourse.find_all() courses = LMSCourse.find_all()

View File

@@ -0,0 +1,36 @@
{
"actions": [],
"creation": "2021-06-07 10:46:10.402684",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"option",
"is_correct"
],
"fields": [
{
"fieldname": "option",
"fieldtype": "Data",
"label": "Option"
},
{
"default": "0",
"fieldname": "is_correct",
"fieldtype": "Check",
"label": "Is Correct"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-06-07 10:48:45.290227",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Option",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -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 LMSOption(Document):
pass

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2021, FOSS United and contributors
// For license information, please see license.txt
frappe.ui.form.on('LMS Quiz', {
// refresh: function(frm) {
// }
});

View File

@@ -0,0 +1,57 @@
{
"actions": [],
"autoname": "field:title",
"creation": "2021-06-07 10:50:17.893625",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"questions",
"lesson"
],
"fields": [
{
"fieldname": "title",
"fieldtype": "Data",
"label": "Title",
"unique": 1
},
{
"fieldname": "questions",
"fieldtype": "Table",
"label": "Questions",
"options": "LMS Quiz Question"
},
{
"fieldname": "lesson",
"fieldtype": "Link",
"label": "Lesson",
"options": "Lesson"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-06-23 17:58:57.642873",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Quiz",
"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
}

View File

@@ -0,0 +1,79 @@
# Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt
from community.lms.doctype.lesson.lesson import update_progress
import frappe
from frappe.model.document import Document
import json
from frappe import _
from ..lesson.lesson import update_progress
class LMSQuiz(Document):
def validate(self):
self.validate_correct_answers()
def validate_correct_answers(self):
for question in self.questions:
correct_options = self.get_correct_options(question)
if len(correct_options) > 1:
question.multiple = 1
if not len(correct_options):
frappe.throw(_("At least one answer must be correct for this question: {0}").format(frappe.bold(question.question)))
def get_correct_options(self, question):
correct_option_fields = ["is_correct_1", "is_correct_2", "is_correct_3", "is_correct_4"]
return list(filter(lambda x: question.get(x) == 1, correct_option_fields))
def get_last_submission_details(self):
"""Returns the latest submission for this user.
"""
user = frappe.session.user
if not user or user == "Guest":
return
result = frappe.get_all('LMS Quiz Submission',
fields="*",
filters={
"owner": user,
"quiz": self.name
},
order_by="creation desc",
page_length=1)
if result:
return result[0]
@frappe.whitelist()
def submit(quiz, result):
score = 0
answer_map = {
"is_correct_1": "option_1",
"is_correct_2": "option_2",
"is_correct_3": "option_3",
"is_correct_4": "option_4"
}
result = json.loads(result)
quiz_details = frappe.get_doc("LMS Quiz", quiz)
for response in result:
match = list(filter(lambda x: x.question == response.get("question"), quiz_details.questions))[0]
correct_options = quiz_details.get_correct_options(match)
correct_answers = [ match.get(answer_map[option]) for option in correct_options ]
if response.get("answer") == correct_answers:
response["result"] = "Right"
score += 1
else:
response["result"] = "Wrong"
response["answer"] = ("").join([ ans if idx == len(response.get("answer")) -1 else ans + ", " for idx, ans in enumerate(response.get("answer")) ])
frappe.get_doc({
"doctype": "LMS Quiz Submission",
"quiz": quiz,
"result": result,
"score": score
}).save(ignore_permissions=True)
update_progress(quiz_details.lesson)
return score

View File

@@ -0,0 +1,8 @@
# Copyright (c) 2021, FOSS United and Contributors
# See license.txt
# import frappe
import unittest
class TestLMSQuiz(unittest.TestCase):
pass

View File

@@ -0,0 +1,118 @@
{
"actions": [],
"creation": "2021-06-07 10:48:57.994714",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"question",
"options_section",
"option_1",
"is_correct_1",
"section_break_5",
"option_2",
"is_correct_2",
"column_break_4",
"option_3",
"is_correct_3",
"section_break_11",
"option_4",
"is_correct_4",
"multiple"
],
"fields": [
{
"fieldname": "question",
"fieldtype": "Text",
"in_list_view": 1,
"label": "Question",
"reqd": 1
},
{
"fieldname": "option_1",
"fieldtype": "Data",
"label": "Option 1",
"reqd": 1
},
{
"fieldname": "option_2",
"fieldtype": "Data",
"label": "Option 2",
"reqd": 1
},
{
"fieldname": "option_3",
"fieldtype": "Data",
"label": "Option 3"
},
{
"fieldname": "option_4",
"fieldtype": "Data",
"label": "Option 4"
},
{
"default": "0",
"depends_on": "option_1",
"fieldname": "is_correct_1",
"fieldtype": "Check",
"label": "Is Correct"
},
{
"default": "0",
"depends_on": "option_2",
"fieldname": "is_correct_2",
"fieldtype": "Check",
"label": "Is Correct"
},
{
"default": "0",
"depends_on": "option_3",
"fieldname": "is_correct_3",
"fieldtype": "Check",
"label": "Is Correct"
},
{
"default": "0",
"depends_on": "option_4",
"fieldname": "is_correct_4",
"fieldtype": "Check",
"label": "Is Correct"
},
{
"default": "0",
"fieldname": "multiple",
"fieldtype": "Check",
"hidden": 1,
"label": "Multiple Correct Answers",
"read_only": 1
},
{
"fieldname": "options_section",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_4",
"fieldtype": "Section Break"
},
{
"fieldname": "section_break_5",
"fieldtype": "Section Break"
},
{
"fieldname": "section_break_11",
"fieldtype": "Section Break"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-06-22 16:54:13.133859",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Quiz Question",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -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 LMSQuizQuestion(Document):
pass

View File

@@ -0,0 +1,45 @@
{
"actions": [],
"creation": "2021-06-07 14:19:23.683323",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"question",
"answer",
"result"
],
"fields": [
{
"fieldname": "question",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Question"
},
{
"fieldname": "result",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Result",
"options": "Right\nWrong"
},
{
"fieldname": "answer",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Users Response"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-06-22 18:32:28.813159",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Quiz Result",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -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 LMSQuizResult(Document):
pass

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2021, FOSS United and contributors
// For license information, please see license.txt
frappe.ui.form.on('LMS Quiz Submission', {
// refresh: function(frm) {
// }
});

View File

@@ -0,0 +1,55 @@
{
"actions": [],
"creation": "2021-06-07 14:19:54.958989",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"quiz",
"result",
"score"
],
"fields": [
{
"fieldname": "quiz",
"fieldtype": "Link",
"label": "Quiz",
"options": "LMS Quiz"
},
{
"fieldname": "result",
"fieldtype": "Table",
"label": "Result",
"options": "LMS Quiz Result"
},
{
"fieldname": "score",
"fieldtype": "Data",
"label": "Score"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-06-07 14:19:54.958989",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Quiz Submission",
"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
}

View File

@@ -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 LMSQuizSubmission(Document):
pass

View File

@@ -0,0 +1,8 @@
# Copyright (c) 2021, FOSS United and Contributors
# See license.txt
# import frappe
import unittest
class TestLMSQuizSubmission(unittest.TestCase):
pass

View File

@@ -0,0 +1,37 @@
# -*- 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"<LMSSection {self.label!r}>"
def get_exercise(self):
if self.type == "exercise":
return frappe.get_doc("Exercise", self.id)
def get_quiz(self):
if self.type == "quiz":
return frappe.get_doc("LMS Quiz", 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

View File

@@ -36,6 +36,8 @@ def find_macros(text):
('Exercise', 'four-circles') ('Exercise', 'four-circles')
] ]
""" """
if not text:
return []
macros = re.findall(MACRO_RE, text) macros = re.findall(MACRO_RE, text)
# remove the quotes around the argument # remove the quotes around the argument
return [(name, _remove_quotes(arg)) for name, arg in macros] return [(name, _remove_quotes(arg)) for name, arg in macros]

View File

@@ -19,7 +19,7 @@
"is_standard": 1, "is_standard": 1,
"login_required": 1, "login_required": 1,
"max_attachment_size": 0, "max_attachment_size": 0,
"modified": "2021-06-14 15:28:08.206622", "modified": "2021-06-15 18:49:50.530001",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "add-a-new-batch", "name": "add-a-new-batch",
@@ -38,7 +38,7 @@
{ {
"allow_read_on_all_link_options": 0, "allow_read_on_all_link_options": 0,
"fieldname": "course", "fieldname": "course",
"fieldtype": "Link", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"label": "Course", "label": "Course",
"max_length": 0, "max_length": 0,

View File

@@ -2,76 +2,66 @@
<a class="anchor_style" href="/courses">Courses</a> /{% if course.is_mentor(frappe.session.user) %} <a <a class="anchor_style" href="/courses">Courses</a> /{% if course.is_mentor(frappe.session.user) %} <a
class="anchor_style" href="/courses/{{ course.name }}"> {{ course.title }}</a> {% else %} <span class="text-muted"> class="anchor_style" href="/courses/{{ course.name }}"> {{ course.title }}</a> {% else %} <span class="text-muted">
{{ course.title }}</span> {% endif %} {{ course.title }}</span> {% endif %}
{% set all_memberships = course.get_all_memberships() %} {% set all_memberships = course.get_all_memberships(frappe.session.user) %}
{% if all_memberships | length > 1 %} {% if membership and membership.batch and all_memberships | length > 1 %}
<a class="nav-link pull-right" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" <a class="pull-right dropdown-item border rounded" style="width: 10rem;" href="#" id="navbarDropdown" role="button"
aria-expanded="false"> data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Switch Batch {{ membership.batch_title }}
</a> </a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown"> <div class="dropdown-menu" aria-labelledby="navbarDropdown">
{% for membership in all_memberships %} {% for data in all_memberships %}
{% if not membership.is_current %} {% if data.batch != membership.batch %}
<a class="dropdown-item switch-batch" href="#" data-batch="{{ membership.batch | urlencode }}" data-course="{{ course.name | urlencode }}">{{ membership.batch_title }}</a> <a class="dropdown-item switch-batch"
href="/courses/{{ course.name }}/home?batch={{ data.batch }}">{{ data.batch_title }}</a>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% if not batch %} {% if not membership %}
{% set display_class = "hide" %} {% set display_class = "hide" %}
{% else %} {% else %}
{% set display_class = "" %} {% set display_class = "" %}
{% endif %} {% endif %}
<ul class="nav nav-tabs mt-4"> <ul class="nav nav-tabs mt-4">
<li class="nav-item {{ display_class }}"> <li class="nav-item">
<a class="nav-link" id="home" href="/courses/{{course.name}}/home">Home</a> <a class="nav-link" id="home" href="/courses/{{course.name}}/home{{ course.query_parameter }}">Home</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" id="learn" href="/courses/{{course.name}}/learn">Lessons</a> {% set lesson_index = course.get_lesson_index(membership.current_lesson) if membership and membership.current_lesson
else '1.1' %}
<a class="nav-link" id="learn"
href="{{ course.get_learn_url(lesson_index) }}{{ course.query_parameter }}">Lessons</a>
</li> </li>
<!-- <li class="nav-item"> <!-- <li class="nav-item">
<a class="nav-link" id="schedule" href="/courses/{{course.name}}/schedule">Schedule</a> <a class="nav-link" id="schedule" href="/courses/{{course.name}}/schedule">Schedule</a>
</li> --> </li> -->
<li class="nav-item {{ display_class }}"> <li class="nav-item {{ display_class }}">
<a class="nav-link" id="members" href="/courses/{{course.name}}/members">Members</a> <a class="nav-link" id="members" href="/courses/{{course.name}}/members{{ course.query_parameter }}">Members</a>
</li> </li>
<li class="nav-item {{ display_class }}"> <!-- <li class="nav-item {{ display_class }}">
<a class="nav-link" id="discussion" href="/courses/{{course.name}}/discuss">Discussion</a> <a class="nav-link" id="discussion" href="/courses/{{course.name}}/discuss">Discussion</a>
</li> </li> -->
<!-- <li class="nav-item"> <!-- <li class="nav-item">
<a class="nav-link" id="about" href="/courses/{{course.name}}/about">About</a> <a class="nav-link" id="about" href="/courses/{{course.name}}/about">About</a>
</li> --> </li> -->
{% if batch and batch.is_member(frappe.session.user, member_type="Mentor") %} {% if membership and membership.batch and course.is_mentor(frappe.session.user) %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" id="progress" href="/courses/{{course.name}}/progress">Progress</a> <a class="nav-link" id="progress" href="/courses/{{course.name}}/progress{{ course.query_parameter }}">Progress</a>
</li> </li>
{% endif %} {% endif %}
</ul> </ul>
{% block script %}
<script> <script>
frappe.ready(() => { frappe.ready(() => {
var selector = document.querySelector(`a[href="${decodeURIComponent(window.location.pathname)}"]`) var selector = document.querySelector(`a[href="${decodeURIComponent(window.location.pathname)}{{ course.query_parameter }}"]`)
if (selector) { if (selector) {
selector.classList.add('active'); selector.classList.add('active');
} }
else { else {
$("#learn").addClass('active') $("#learn").addClass('active')
} }
$(".switch-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.reload();
}
})
})
}) })
</script> </script>
{% endblock %}

View File

@@ -7,48 +7,22 @@
<div class="chapter-lessons"> <div class="chapter-lessons">
{% for lesson in chapter.get_lessons() %} {% for lesson in chapter.get_lessons() %}
<div class="lesson-teaser"> <div class="lesson-teaser">
<a {% if show_link or lesson.include_in_preview %} {% if show_link or lesson.include_in_preview %}
href="{{ course.get_learn_url(course.get_lesson_index(lesson.name)) }}" {% else %} href="" class="no-preview" <a class="" href="{{ course.get_learn_url(course.get_lesson_index(lesson.name)) }}{{course.query_parameter}}"
{% endif %} data-course="{{ course.name }}">{{ lesson.title }}</a> data-course="{{ course.name }}">{{ lesson.title }}</a>
{% else %}
<div class="no-preview" title="This lesson is not available for preview">
<span style="color: #2490ef;">
{{ lesson.title }}
</span>
<i class="fa fa-lock ml-2"></i>
</div>
{% endif %}
{% if show_progress and not course.is_mentor(frappe.session.user) and lesson.get_progress() %} {% if show_progress and not course.is_mentor(frappe.session.user) and lesson.get_progress() %}
<a class="ml-5 badge p-1 {{ lesson.get_slugified_class() }}"> <img class="progress-image" <span class="ml-5 badge p-2 {{ lesson.get_slugified_class() }}"> {{ lesson.get_progress() }}</span>
src="/assets/community/images/Vector.png"> {{ lesson.get_progress() }}</a>
{% endif %} {% endif %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
</div> </div>
<script>
frappe.ready(() => {
var d;
$(".no-preview").click((e) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
var message = __("Please enroll for this course to access the lesson.");
var label = __("Checkout Upcoming Batches");
var action = "checkout_upcoming_batches";
d = frappe.msgprint({
title: __("This lesson is not available for preview!"),
message: message,
primary_action: {
"label": label,
"client_action": action,
}
});
})
window.redirect_to_login = () => {
window.location.href = `/login?redirect-to=/courses/${$(".no-preview").attr("data-course")}`
}
window.checkout_upcoming_batches = () => {
if ($(".upcoming").length > 0) {
$('html,body').animate({ scrollTop: $(".upcoming").offset().top }, 300);
}
frappe.hide_msgprint();
}
})
</script>

View File

@@ -7,7 +7,7 @@
<div>Starting {{frappe.utils.format_date(batch.start_date, "medium")}}</div> <div>Starting {{frappe.utils.format_date(batch.start_date, "medium")}}</div>
<div class="course-type" style="color: #888; padding: 10px 0px;">mentors</div> <div class="course-type" style="color: #888; padding: 10px 0px;">mentors</div>
{% for m in batch.get_mentors() %} {% for m in course.get_mentors(batch.name) %}
<div> <div>
{{ widgets.Avatar(member=m, avatar_class="avatar-medium" ) }} {{ widgets.Avatar(member=m, avatar_class="avatar-medium" ) }}
<span class="instructor-title">{{m.full_name}}</span> <span class="instructor-title">{{m.full_name}}</span>
@@ -18,10 +18,10 @@
<div class="cta"> <div class="cta">
<div class=""> <div class="">
{% if can_manage %} {% if can_manage %}
<a href="" class="btn btn-primary manage-batch" data-batch="{{ batch.name | urlencode }}" <a href="/courses/{{ course.name }}/home?batch={{ batch.name }}" class="btn btn-primary manage-batch" data-batch="{{ batch.name | urlencode }}"
data-course="{{ course.name | urlencode }}">Manage</a> data-course="{{ course.name | urlencode }}">Manage</a>
{% elif can_join %} {% elif can_join %}
<button class="join-batch btn btn-primary" data-batch="{{ batch.name | urlencode }}" <button class="join-batch btn btn-secondary" data-batch="{{ batch.name | urlencode }}"
data-course="{{ course.name | urlencode }}">Join this Batch</button> data-course="{{ course.name | urlencode }}">Join this Batch</button>
{% endif %} {% endif %}
</div> </div>

View File

@@ -0,0 +1,160 @@
{
"category": "Modules",
"charts": [],
"creation": "2021-06-29 13:05:28.741459",
"developer_mode_only": 0,
"disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
"extends_another_page": 0,
"hide_custom": 0,
"icon": "education",
"idx": 0,
"is_default": 0,
"is_standard": 1,
"label": "LMS",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "LMS",
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "LMS Course",
"link_to": "LMS Course",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "LMS Batch",
"link_to": "LMS Batch",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "LMS Batch Membership",
"link_to": "LMS Batch Membership",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "LMS Course Mentor Mapping",
"link_to": "LMS Course Mentor Mapping",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Content",
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Chapter",
"link_to": "Chapter",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Lesson",
"link_to": "Lesson",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Exercise",
"link_to": "Exercise",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Exercise Submission",
"link_to": "Exercise Submission",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "LMS Quiz",
"link_to": "LMS Quiz",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "LMS Quiz Submission",
"link_to": "LMS Quiz Submission",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
}
],
"modified": "2021-06-29 15:11:07.324651",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS",
"owner": "Administrator",
"pin_to_bottom": 0,
"pin_to_top": 1,
"shortcuts": [
{
"color": "#29CD42",
"doc_view": "List",
"format": "{} Published",
"label": "Courses",
"link_to": "LMS Course",
"stats_filter": "{\"is_published\": 1}",
"type": "DocType"
},
{
"color": "#29CD42",
"doc_view": "List",
"format": "{} Active ",
"label": "Batches",
"link_to": "LMS Batch",
"stats_filter": "{\"status\": \"Active\"}",
"type": "DocType"
},
{
"color": "#39E4A5",
"doc_view": "List",
"format": "{} Students",
"label": "Memberships",
"link_to": "LMS Batch Membership",
"stats_filter": "{\"member_type\": \"Student\"}",
"type": "DocType"
}
]
}

View File

@@ -14,6 +14,8 @@ The PageExtension is used to load additinal stylesheets and scripts to
be loaded in a webpage. be loaded in a webpage.
""" """
import frappe
class PageExtension: class PageExtension:
"""PageExtension is a plugin to inject custom styles and scripts """PageExtension is a plugin to inject custom styles and scripts
into a web page. into a web page.
@@ -64,3 +66,43 @@ class ProfileTab:
Every subclass must implement this. Every subclass must implement this.
""" """
raise NotImplementedError() raise NotImplementedError()
class LiveCodeExtension(PageExtension):
def render_header(self):
livecode_url = frappe.get_value("LMS Settings", None, "livecode_url")
context = {
"livecode_url": livecode_url
}
return frappe.render_template(
"templates/livecode/extension_header.html",
context)
def render_footer(self):
livecode_url = frappe.get_value("LMS Settings", None, "livecode_url")
context = {
"livecode_url": livecode_url
}
return frappe.render_template(
"templates/livecode/extension_footer.html",
context)
def quiz_renderer(quiz_name):
quiz = frappe.get_doc("LMS Quiz", quiz_name)
context = dict(quiz=quiz)
return frappe.render_template("templates/quiz.html", context)
def exercise_renderer(argument):
exercise = frappe.get_doc("Exercise", argument)
context = dict(exercise=exercise)
return frappe.render_template("templates/exercise.html", context)
def youtube_video_renderer(video_id):
return f"""
<iframe width="560" height="315"
src="https://www.youtube.com/embed/{video_id}"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen>
</iframe>
"""

View File

@@ -23,6 +23,8 @@
--cta-color: var(--c4); --cta-color: var(--c4);
--send-message: var(--c7); --send-message: var(--c7);
--received-message: var(--c8); --received-message: var(--c8);
--checkbox-size: 14px;
--control-bg: var(--gray-100);
} }
body { body {
@@ -227,6 +229,10 @@ section {
margin-top: 30px; margin-top: 30px;
} }
input[type=checkbox] {
appearance: auto;
}
.partiallycomplete { .partiallycomplete {
background: #FEF4E2; background: #FEF4E2;
color: #976417; color: #976417;

View File

@@ -76,11 +76,6 @@ h2 {
text-decoration: none text-decoration: none
} }
.no-preview:hover {
cursor: pointer;
color: #2490ef;
}
section { section {
padding: 60px 0px; padding: 60px 0px;
} }

View File

@@ -0,0 +1,10 @@
<div class="exercise">
<h3>Exercise {{exercise.index_label}}: {{ exercise.title }}</h3>
<div class="exercise-description">{{frappe.utils.md_to_html(exercise.description)}}</div>
{% set submission = exercise.get_user_submission() %}
<pre class="exercise" id="exercise-{{exercise.name}}"
data-last-submitted="{{ submission and submission.creation or '' }}" data-name="{{ exercise.name }}"
data-image='{{ (submission and submission.image or "") | tojson }}'><code class="language-joy">{{ submission.solution if submission else exercise.code }}</code></pre>
</div>

View File

@@ -0,0 +1,168 @@
<script type="text/javascript" src="/assets/frappe/node_modules/moment/min/moment-with-locales.min.js"></script>
<script type="text/javascript" src="/assets/frappe/node_modules/moment-timezone/builds/moment-timezone-with-data.min.js"></script>
<script type="text/javascript" src="/assets/frappe/js/frappe/utils/datetime.js"></script>
<script type="text/javascript">
// comment_when is failing because of this
if (!frappe.sys_defaults) {
frappe.sys_defaults = {}
}
</script>
<script type="text/javascript" src="{{ livecode_url }}/static/livecode.js"></script>
<script type="text/javascript" src="/assets/mon_school/js/livecode-files.js"></script>
<template id="livecode-template">
<div class="livecode-editor livecode-editor-inline">
<div class="row">
<div class="col-lg-8 col-md-6">
<div class="controls">
<button class="run">Run</button>
<div class="exercise-controls pull-right">
<span style="padding-right: 10px;"><span class="last-submitted human-time" data-timestamp=""></span></span>
<button class="submit btn-primary">Submit</button>
</div>
</div>
</div>
</div>
<div class="code-editor">
<div class="row">
<div class="col-lg-8 col-md-6">
<div class="code-wrapper">
<textarea class="code"></textarea>
</div>
</div>
<div class="col-lg-4 col-md-6 canvas-wrapper">
<div class="svg-image" width="300" height="300"></div>
<pre class="output"></pre>
</div>
</div>
</div>
</div>
</template>
<script type="text/javascript">
function getLiveCodeOptions() {
return {
base_url: "{{ livecode_url }}",
runtime: "python",
files: LIVECODE_FILES, // loaded from livecode-files.js
command: ["python", "start.py"],
codemirror: true,
onMessage: {
image: function(editor, msg) {
const element = editor.parent.querySelector(".svg-image");
element.innerHTML = msg.image;
}
}
}
}
$(function() {
var editorLookup = {};
$("pre.example, pre.exercise").each((i, e) => {
var code = $(e).text();
var template = document.querySelector('#livecode-template');
var clone = template.content.cloneNode(true);
$(e)
.wrap('<div></div>')
.hide()
.parent()
.append(clone)
.find("textarea.code")
.val(code);
if ($(e).hasClass("exercise")) {
var last_submitted = $(e).data("last-submitted");
if (last_submitted) {
$(e).parent().find(".last-submitted")
.data("timestamp", last_submitted)
.html(__("Submitted {0}", [comment_when(last_submitted)]));
}
}
else {
$(e).parent().find(".exercise-controls").remove();
}
var editor = new LiveCodeEditor(e.parentElement, {
...getLiveCodeOptions(),
codemirror: true,
onMessage: {
image: function(editor, msg) {
const canvasElement = editor.parent.querySelector("div.svg-image");
canvasElement.innerHTML = msg.image;
}
}
});
$(e).parent().find(".submit").on('click', function() {
var name = $(e).data("name");
let code = editor.codemirror.doc.getValue();
frappe.call("community.lms.api.submit_solution", {
"exercise": name,
"code": code
}).then(r => {
if (r.message.name) {
frappe.msgprint("Submitted successfully!");
let d = r.message.creation;
$(e).parent().find(".human-time").html(__("Submitted {0}", [comment_when(d)]));
}
});
});
});
$(".exercise-image").each((i, e) => {
var svg = JSON.parse($(e).data("image"));
$(e).html(svg);
});
$("pre.exercise").each((i, e) => {
var svg = JSON.parse($(e).data("image"));
$(e).parent().find(".svg-image").html(svg);
});
});
</script>
<style type="text/css">
.svg-image {
border: 5px solid #ddd;
position: relative;
z-index: 0;
width: 310px;
height: 310px;
}
.livecode-editor {
margin-bottom: 30px;
}
.livecode-editor-small .svg-image {
border: 5px solid #ddd;
position: relative;
z-index: 0;
width: 210px;
height: 210px;
}
/* work-in-progress styles for showing admonition */
.admonition {
border: 1px solid #aaa;
border-left: .5rem solid #888;
border-radius: .3em;
font-size: 0.9em;
margin: 1.5em 0;
padding: 0 0.5em;
}
.admonition-title {
padding: 0.5em 0px;
font-weight: bold;
padding-top:
}
</style>

View File

@@ -0,0 +1,8 @@
<link rel="stylesheet" href="{{ livecode_url }}/static/codemirror/lib/codemirror.css">
<script src="{{ livecode_url }}/static/codemirror/lib/codemirror.js"></script>
<script src="{{ livecode_url }}/static/codemirror/mode/python/python.js"></script>
<script src="{{ livecode_url }}/static/codemirror/keymap/sublime.js"></script>
<script src="{{ livecode_url }}/static/codemirror/addon/edit/matchbrackets.js"></script>
<script src="{{ livecode_url }}/static/codemirror/addon/comment/comment.js"></script>

View File

@@ -0,0 +1,30 @@
{% set last_submission = quiz.get_last_submission_details() %}
{% if last_submission %}
<div class="mb-5 pull-right">
<div class="text-muted">Last Submitted On: {{ frappe.utils.pretty_date(last_submission.creation) }}</div>
<div class="text-muted">Last Submission Score: {{ last_submission.score }}</div>
</div>
{% endif %}
<h3 id="title" class="mb-5">{{ quiz.title }}</h3>
<form id="quiz-form">
{% for question in quiz.questions %}
<div class="question mb-5" data-question="{{ question.question }}"
data-multi="{{ question.multiple_correct_answers}}">
<p> {{ loop.index }}. {{ question.question }}</p>
{% set options = [question.option_1, question.option_2, question.option_3, question.option_4] %}
{% for option in options %}
{% if option %}
<div class="checkbox mb-2">
<input {% if question.multiple %} type="checkbox" {% else %} type="radio"
name="{{ question.question | urlencode }}" {% endif %} class="option" value="{{ option | urlencode }}">
<span class="label-area">{{ option }}</span>
</div>
{% endif %}
{% endfor %}
</div>
{% endfor %}
<button class="btn btn-secondary hide mb-5" id="try-again">Try Again</button>
<button class="btn btn-primary" id="submit-quiz">Submit</button>
<h4 class="success-message"></h4>
<h5 class="score text-muted"></h5>
</form>

View File

@@ -11,7 +11,7 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
{{ widgets.BatchTabs(course=course, batch=batch) }} {{ widgets.BatchTabs(course=course, membership=membership) }}
<div class="messages-container mt-5"> <div class="messages-container mt-5">
{{ widgets.BatchHeader(batch_name=batch.title, member_count=member_count)}} {{ widgets.BatchHeader(batch_name=batch.title, member_count=member_count)}}
<ol class="messages"> <ol class="messages">

View File

@@ -4,5 +4,5 @@ from . import utils
def get_context(context): def get_context(context):
utils.get_common_context(context) utils.get_common_context(context)
context.messages = context.batch.get_messages() context.messages = context.batch.get_messages()
if not context.batch: if not context.membership:
utils.redirect_to_lesson(context.course) utils.redirect_to_lesson(context.course)

View File

@@ -8,15 +8,13 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% set invite_link = frappe.utils.get_url() + "/courses/" + course.name + "/join?batch=" + batch.name %}
<div class="container mt-5"> <div class="container mt-5">
{{ widgets.BatchTabs(course=course, batch=batch) }} {{ widgets.BatchTabs(course=course, membership=membership) }}
<!-- <div>
<h1 class="mt-5">{{ batch.title }}</h1>
</div> -->
<div class="course-details mt-5"> <div class="course-details mt-5">
{{ widgets.CourseOutline(course=course, batch=batch, show_link=True, show_progress=True) }} {{ widgets.CourseOutline(course=course, batch=batch, show_link=membership, show_progress=True) }}
</div> </div>
{% if batch %}
<div class="w-25"> <div class="w-25">
<h3>Batch Schedule</h3> <h3>Batch Schedule</h3>
{{ widgets.RenderBatch(course=course, batch=batch) }} {{ widgets.RenderBatch(course=course, batch=batch) }}
@@ -28,8 +26,9 @@
{{ frappe.utils.md_to_html(batch.description) }} {{ frappe.utils.md_to_html(batch.description) }}
</div> </div>
{% endif %} {% endif %}
{% endif %}
{% if course.is_mentor(frappe.session.user) %} {% if course.is_mentor(frappe.session.user) %}
{% set invite_link = frappe.utils.get_url() + "/courses/" + course.name + "/join?batch=" + batch.name %}
<div class=""> <div class="">
<h3> Invite Members </h3> <h3> Invite Members </h3>
<a href="" class="" id="invite-link" data-link="{{ invite_link }}">Get Batch Invitation <a href="" class="" id="invite-link" data-link="{{ invite_link }}">Get Batch Invitation

View File

@@ -3,5 +3,3 @@ from . import utils
def get_context(context): def get_context(context):
utils.get_common_context(context) utils.get_common_context(context)
if not context.batch:
utils.redirect_to_lesson(context.course)

View File

@@ -51,7 +51,8 @@ frappe.ready(() => {
frappe.call({ frappe.call({
"method": "community.lms.doctype.lms_batch_membership.lms_batch_membership.create_membership", "method": "community.lms.doctype.lms_batch_membership.lms_batch_membership.create_membership",
"args": { "args": {
"batch": "{{ batch.name }}" "batch": {{ batch.name }},
"course": {{ batch.course }}
}, },
"callback": (data) => { "callback": (data) => {
if (data.message == "OK") { if (data.message == "OK") {
@@ -66,20 +67,6 @@ frappe.ready(() => {
} }
}) })
}) })
$("#batch-home").click((e) => {
frappe.call({
method: "community.lms.doctype.lms_batch_membership.lms_batch_membership.update_current_membership",
args: {
"batch": "{{ batch.name }}",
"course": "{{ batch.course}}",
"member": frappe.session.user
},
callback: (data) => {
window.location.href = "/courses/{{ batch.course }}/home"
}
})
})
}) })
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -13,7 +13,7 @@
<link rel="stylesheet" href="/assets/frappe/css/hljs-night-owl.css"> <link rel="stylesheet" href="/assets/frappe/css/hljs-night-owl.css">
{% for ext in page_extensions %} {% for ext in page_extensions %}
{{ ext.render_header() }} {{ ext.render_header() }}
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}
@@ -22,26 +22,27 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
{{ widgets.BatchTabs(course=course, batch=batch) }} {{ widgets.BatchTabs(course=course, membership=membership) }}
<div class="lesson-page"> <div class="lesson-page">
<h2 class="title {% if course.is_mentor(frappe.session.user) %} is_mentor {% endif %}" data-name="{{ lesson.name }}" {% if batch %} data-batch="{{ batch.name }}" {% endif %}>{{ lesson.title }}</h2> <h2 class="title {% if course.is_mentor(frappe.session.user) %} is_mentor {% endif %}" data-lesson="{{ lesson.name }}"
data-course="{{ course.name }}" {% if membership%} data-membership="{{membership.name}}" {% endif %}>{{ lesson.title }}</h2>
{% if batch or lesson.include_in_preview %} {% if membership or lesson.include_in_preview %}
{{ lesson.render_html() }} {{ lesson.render_html() }}
{% else %} {% else %}
<div class="no-preview-message"> <div class="no-preview-message">
<span>This lesson is not available for Preview. Please join a batch to access the complete course.</span> <span>This lesson is not available for Preview. Please join the course to access this lesson.</span>
<a href="/courses/{{ course.name }}">Checkout Upcoming Batches</a> <a href="/courses/{{ course.name }}">Checkout Course Details.</a>
</div> </div>
{% endif %} {% endif %}
{% if membership %}
{{ pagination(prev_chap, prev_url, next_chap, next_url) }} {{ pagination(prev_chap, prev_url, next_chap, next_url) }}
{% endif %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% macro pagination(prev_chap, prev_url, next_chap, next_url) %} {% macro pagination(prev_chap, prev_url, next_chap, next_url) %}
<div class="lesson-pagination"> <div class="lesson-pagination">
{% if prev_url %} {% if prev_url %}
@@ -60,8 +61,12 @@
{%- block script %} {%- block script %}
{{ super() }} {{ super() }}
{% for ext in page_extensions %}
{{ ext.render_footer() }}
{% endfor %}
<script type="text/javascript">
var page_context = {{ page_context | tojson }};
</script>
{% for ext in page_extensions %}
{{ ext.render_footer() }}
{% endfor %}
{%- endblock %} {%- endblock %}

View File

@@ -1,17 +1,69 @@
frappe.ready(() => { frappe.ready(() => {
if ($(".title").attr("data-batch") && !$(".title").hasClass("is_mentor")) {
/* Save Lesson Progress */
if ($(".title").attr("data-membership") && !$(".title").hasClass("is_mentor")) {
frappe.call({ frappe.call({
method: "community.lms.doctype.lesson.lesson.save_progress", method: "community.lms.doctype.lesson.lesson.save_progress",
args: { args: {
lesson: $(".title").attr("data-name"), lesson: $(".title").attr("data-lesson"),
batch: $(".title").attr("data-batch") course: $(".title").attr("data-course")
} }
}) })
} }
if ($(".title").attr("data-batch")) {
/* Save Current Lesson */
if ($(".title").attr("data-membership")) {
frappe.call("community.lms.api.save_current_lesson", { frappe.call("community.lms.api.save_current_lesson", {
"batch_name": $(".title").attr("data-batch"), course_name: $(".title").attr("data-course"),
"lesson_name": $(".title").attr("data-name") lesson_name: $(".title").attr("data-lesson")
}) })
} }
/* Submit Quiz */
$("#submit-quiz").click((e) => {
e.preventDefault();
console.log("click")
var result = [];
$('.question').each((i, element) => {
var options = $(element).find(".option");
var answers = [];
options.filter((i, op) => $(op).prop("checked")).each((i, elem) => answers.push(decodeURIComponent(elem.value)));
result.push({
"question": element.dataset.question,
"answer": answers
});
});
frappe.call({
method: "community.lms.doctype.lms_quiz.lms_quiz.submit",
args: {
quiz: $("#title").text(),
result: result
},
callback: (data) => {
$("#submit-quiz").addClass("hide");
$("#try-again").removeClass("hide");
$(":input[type='checkbox']").prop("disabled", true);
$(":input[type='radio']").prop("disabled", true);
if (data.message == result.length) {
$(".success-message").text("Congratulations, you cleared the quiz!");
}
else {
$(".success-message").text("Some of your answers weren't correct. You can give it another shot.");
}
$(".score").text(`Score: ${data.message}/${result.length}`);
}
})
})
/* Try the quiz again */
$("#try-again").click((e) => {
e.preventDefault();
$(":input[type='checkbox']").prop("disabled", false);
$(":input[type='radio']").prop("disabled", false);
$("#quiz-form").trigger("reset");
$(".success-message").text("");
$(".score").text("");
$("#submit-quiz").removeClass("hide");
$("#try-again").addClass("hide");
})
}) })

View File

@@ -18,7 +18,7 @@ def get_context(context):
index_ = get_lesson_index(context.course, context.batch, frappe.session.user) or "1.1" index_ = get_lesson_index(context.course, context.batch, frappe.session.user) or "1.1"
else: else:
index_ = "1.1" index_ = "1.1"
frappe.local.flags.redirect_location = context.course.get_learn_url(index_) frappe.local.flags.redirect_location = context.course.get_learn_url(index_) + context.course.query_parameter
raise frappe.Redirect raise frappe.Redirect
context.lesson = context.course.get_lesson(chapter_index, lesson_index) context.lesson = context.course.get_lesson(chapter_index, lesson_index)
@@ -30,11 +30,17 @@ def get_context(context):
next_ = outline.get_next(lesson_number) next_ = outline.get_next(lesson_number)
context.prev_chap = get_chapter_title(course_name, prev_) context.prev_chap = get_chapter_title(course_name, prev_)
context.next_chap = get_chapter_title(course_name, next_) context.next_chap = get_chapter_title(course_name, next_)
context.next_url = context.course.get_learn_url(next_) context.next_url = context.course.get_learn_url(next_) and context.course.get_learn_url(next_) + context.course.query_parameter
context.prev_url = context.course.get_learn_url(prev_) context.prev_url = context.course.get_learn_url(prev_) and context.course.get_learn_url(prev_) + context.course.query_parameter
context.page_extensions = get_page_extensions() context.page_extensions = get_page_extensions()
context.page_context = {
"course": context.course.name,
"batch": context.get("batch") and context.batch.name,
"lesson": context.lesson.name
}
def get_chapter_title(course_name, lesson_number): def get_chapter_title(course_name, lesson_number):
if not lesson_number: if not lesson_number:
return return
@@ -49,7 +55,7 @@ def get_lesson_index(course, batch, user):
return lesson and course.get_lesson_index(lesson) return lesson and course.get_lesson_index(lesson)
def get_page_extensions(): def get_page_extensions():
default_value = ["community.community.plugins.PageExtension"] default_value = ["community.plugins.PageExtension"]
classnames = frappe.get_hooks("community_lesson_page_extensions") or default_value classnames = frappe.get_hooks("community_lesson_page_extensions") or default_value
extensions = [frappe.get_attr(name)() for name in classnames] extensions = [frappe.get_attr(name)() for name in classnames]
return extensions return extensions

View File

@@ -10,7 +10,7 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
{{ widgets.BatchTabs(course=course, batch=batch) }} {{ widgets.BatchTabs(course=course, membership=membership) }}
{{ MembersList(members)}} {{ MembersList(members)}}
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -3,5 +3,5 @@ from . import utils
def get_context(context): def get_context(context):
utils.get_common_context(context) utils.get_common_context(context)
if not context.batch: if not context.membership:
utils.redirect_to_lesson(context.course) utils.redirect_to_lesson(context.course)

View File

@@ -24,7 +24,7 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
{{ widgets.BatchTabs(course=course, batch=batch) }} {{ widgets.BatchTabs(course=course, membership=membership) }}
<div class="mentor-dashboard"> <div class="mentor-dashboard">
<h3>Batch Progress</h3> <h3>Batch Progress</h3>
{% for exercise in report.exercises %} {% for exercise in report.exercises %}

View File

@@ -17,7 +17,7 @@ def get_context(context):
class BatchReport: class BatchReport:
def __init__(self, course, batch): def __init__(self, course, batch):
self.submissions = get_submissions(batch) self.submissions = get_submissions(course, batch)
self.exercises = self.get_exercises(course.name) self.exercises = self.get_exercises(course.name)
self.submissions_by_exercise = defaultdict(list) self.submissions_by_exercise = defaultdict(list)
for s in self.submissions: for s in self.submissions:
@@ -29,8 +29,10 @@ class BatchReport:
def get_submissions_of_exercise(self, exercise_name): def get_submissions_of_exercise(self, exercise_name):
return self.submissions_by_exercise[exercise_name] return self.submissions_by_exercise[exercise_name]
def get_submissions(batch): def get_submissions(course, batch):
students = batch.get_students() students = course.get_students(batch.name)
if not len(students):
return []
students_map = {s.email: s for s in students} students_map = {s.email: s for s in students}
names, values = nparams("s", students_map.keys()) names, values = nparams("s", students_map.keys())
sql = """ sql = """

View File

@@ -5,26 +5,34 @@ def get_common_context(context):
context.no_cache = 1 context.no_cache = 1
course_name = frappe.form_dict["course"] course_name = frappe.form_dict["course"]
try:
batch_name = frappe.form_dict["batch"]
except KeyError:
batch_name = None
course = Course.find(course_name) course = Course.find(course_name)
if not course: if not course:
context.template = "www/404.html" context.template = "www/404.html"
return return
context.course = course
batch_name = course.get_current_batch(frappe.session.user) membership = course.get_membership(frappe.session.user, batch_name)
batch = course.get_batch(batch_name) if membership:
context.batch = batch context.membership = membership
if batch_name: batch = course.get_batch(membership.batch)
context.members = batch.get_mentors() + batch.get_students()
if batch:
context.batch = batch
context.members = course.get_mentors(membership.batch) + course.get_students(membership.batch)
context.member_count = len(context.members) context.member_count = len(context.members)
context.course.query_parameter = "?batch=" + membership.batch if membership and membership.batch else " "
context.course = course
context.livecode_url = get_livecode_url() context.livecode_url = get_livecode_url()
def get_livecode_url(): def get_livecode_url():
return frappe.db.get_single_value("LMS Settings", "livecode_url") return frappe.db.get_single_value("LMS Settings", "livecode_url")
def redirect_to_lesson(course, index_="1.1"): def redirect_to_lesson(course, index_="1.1"):
frappe.local.flags.redirect_location = course.get_learn_url(index_) frappe.local.flags.redirect_location = course.get_learn_url(index_) + course.query_parameter
raise frappe.Redirect raise frappe.Redirect

View File

@@ -4,6 +4,7 @@
{% block head_include %} {% block head_include %}
<meta name="description" content="Courses" /> <meta name="description" content="Courses" />
<meta name="keywords" content="Courses {{course.title}}" /> <meta name="keywords" content="Courses {{course.title}}" />
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
{% endblock %} {% endblock %}
{% block content %} {% block content %}
@@ -12,7 +13,14 @@
<div class="mb-5"> <div class="mb-5">
<a class="anchor_style" href="/courses">Courses</a> / <span class="text-muted">{{ course.title }}</span> <a class="anchor_style" href="/courses">Courses</a> / <span class="text-muted">{{ course.title }}</span>
</div> </div>
<h2 id="course-title" data-course="{{course.name}}">{{course.title}}</h2> <div class="d-flex justify-content-between align-items-end">
<h2 id="course-title" data-course="{{course.name}}">{{course.title}}</h2>
{% if not course.disable_self_learning and not course.is_mentor(frappe.session.user) %}
<div>
<button class="btn btn-primary join-batch" data-course="{{ course.name | urlencode }}"> Start Learning </button>
</div>
{% endif %}
</div>
<div class="course-short-intro">{{ course.short_introduction }}</div> <div class="course-short-intro">{{ course.short_introduction }}</div>
</div> </div>
@@ -23,7 +31,7 @@
{{ CourseDescription(course) }} {{ CourseDescription(course) }}
{{ widgets.InstructorSection(instructor=course.get_instructor()) }} {{ widgets.InstructorSection(instructor=course.get_instructor()) }}
{{ BatchSection(course) }} {{ BatchSection(course) }}
{{ widgets.CourseOutline(course=course, show_link=False) }} {{ widgets.CourseOutline(course=course, show_link=membership) }}
</div> </div>
</div> </div>
</div> </div>
@@ -75,12 +83,12 @@
{% endfor %} {% endfor %}
</div> </div>
<a class="add-batch margin-bottom" href="/add-a-new-batch?new=1&course={{course.title}}&slug={{course.name}}">Add a new <a class="add-batch margin-bottom" href="/add-a-new-batch?new=1&course={{course.name}}">Add a new
batch</a> batch</a>
{% else %} {% else %}
<div class="mentor_message"> <div class="mentor_message">
<p> You are a mentor for this course. </p> <p> You are a mentor for this course. </p>
<a class="" href="/add-a-new-batch?new=1&course={{course.title}}&slug={{course.name}}">Create your first batch</a> <a class="" href="/add-a-new-batch?new=1&course={{course.name}}">Create your first batch</a>
</div> </div>
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}
@@ -96,8 +104,8 @@
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}
<div class="mt-5 upcoming">There are no Upcoming Batches for this course currently.</div> <div class="mt-5 upcoming">There are no Upcoming Batches for this course currently.</div>
{% endif %} {% endif %}
</div> </div>
{% endmacro %} {% endmacro %}

View File

@@ -57,11 +57,13 @@ frappe.ready(() => {
window.location.href = `/login?redirect-to=/courses/${course}`; window.location.href = `/login?redirect-to=/courses/${course}`;
return; return;
} }
batch = decodeURIComponent($(e.currentTarget).attr("data-batch")) var batch = $(e.currentTarget).attr("data-batch");
batch = batch ? decodeURIComponent(batch) : "";
frappe.call({ frappe.call({
"method": "community.lms.doctype.lms_batch_membership.lms_batch_membership.create_membership", "method": "community.lms.doctype.lms_batch_membership.lms_batch_membership.create_membership",
"args": { "args": {
"batch": batch "batch": batch ? batch : "",
"course": course
}, },
"callback": (data) => { "callback": (data) => {
if (data.message == "OK") { if (data.message == "OK") {
@@ -73,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`;
}
})
})
}) })

View File

@@ -16,9 +16,9 @@ def get_context(context):
raise frappe.Redirect raise frappe.Redirect
context.course = course context.course = course
membership = course.get_membership(frappe.session.user)
batch = course.get_student_batch(frappe.session.user) context.course.query_parameter = "?batch=" + membership.batch if membership and membership.batch else ""
if batch: context.membership = membership
if not course.is_mentor(frappe.session.user) and membership:
frappe.local.flags.redirect_location = f"/courses/{course.name}/learn" frappe.local.flags.redirect_location = f"/courses/{course.name}/learn"
raise frappe.Redirect raise frappe.Redirect

View File

@@ -1,4 +1,3 @@
{% macro LiveCodeEditorLarge(name, code) %} {% macro LiveCodeEditorLarge(name, code) %}
<div class="livecode-editor livecode-editor-large" id="editor-{{name}}"> <div class="livecode-editor livecode-editor-large" id="editor-{{name}}">
<div class="row"> <div class="row">

View File

@@ -1,28 +0,0 @@
{% extends "templates/base.html" %}
{% from "www/macros/livecode.html" import LiveCodeEditor, LiveCodeEditorJS %}
{% block title %}Sketches{% endblock %}
{% block head_include %}
<meta name="description" content="Sketches" />
<meta name="keywords" content="sketches" />
<link rel="stylesheet" href="/assets/css/lms.css">
{% endblock %}
{% block content %}
<section class="top-section" style="padding: 1rem 0rem;">
<div class='container pb-5'>
<h1>Recent Sketches</h1>
<a href="/sketches/new" class="btn btn-primary">Create a New Sketch</a>
</div>
<div class='container'>
<div class="row">
{% for sketch in sketches %}
<div class="col-md-3">
{{ widgets.SketchTeaser(sketch=sketch) }}
</div>
{% endfor %}
</div>
</div>
</section>
{% endblock %}

View File

@@ -1,7 +0,0 @@
import frappe
from community.lms.models import Sketch
def get_context(context):
context.no_cache = 1
context.sketches = Sketch.get_recent_sketches()

View File

@@ -1,112 +0,0 @@
{% extends "templates/base.html" %}
{% from "www/macros/livecode.html" import LiveCodeEditorLarge, LiveCodeEditorJS with context %}
{% block title %}{{sketch.title}}{% endblock %}
{% block head_include %}
<meta name="description" content="Sketch {{sketch.title}}" />
<meta name="keywords" content="sketch {{sketch.title}}" />
<style>
</style>
<link rel="stylesheet" href="{{ livecode_url }}/static/codemirror/lib/codemirror.css">
<link rel="stylesheet" href="/assets/css/lms.css">
<script src="{{ livecode_url }}/static/codemirror/lib/codemirror.js"></script>
<script src="{{ livecode_url }}/static/codemirror/mode/python/python.js"></script>
<script src="{{ livecode_url }}/static/codemirror/keymap/sublime.js"></script>
<script src="{{ livecode_url }}/static/codemirror/addon/edit/matchbrackets.js"></script>
<script src="{{ livecode_url }}/static/codemirror/addon/comment/comment.js"></script>
{% endblock %}
{% block content %}
<section class="top-section" style="padding: 1rem 0rem;">
<div class='container pb-5'>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item" aria-current="page"><a href="/sketches">Sketches</a></li>
</ol>
</nav>
<div class="sketch-header">
{% if editable %}
<div class="form-row">
<div class="col-lg-8 col-md-6">
<input type="text" id="sketch-title" name="title" class="form-control" value="{{ sketch.title }}">
</div>
<div class="col-lg-4 col-md-6">
<button type="submit" id="sketch-save" class="btn-save btn btn-primary">Save</button>
</div>
</div>
{% else %}
<h1 class="sketch-title">{{sketch.title}}</h1>
<div class="sketch-owner-wrapper">By <span class="sketch-owner">{{sketch.get_owner_name()}}</span></div>
{% endif %}
</div>
{% if sketch.is_new() and not editable %}
<div class="alert alert-warning">
Please login to save this sketch.
</div>
{% endif %}
<div class="sketch-editor">
{{LiveCodeEditorLarge(sketch.name, sketch.code) }}
</div>
{% endblock %}
{%- block script %}
{{ super() }}
{{ LiveCodeEditorJS() }}
<script type="text/javascript">
var sketch_name = {{ sketch.name | tojson }};
function saveSketch() {
var title = $("#sketch-title").val()
var code = livecodeEditors[0].codemirror.doc.getValue()
frappe.call('community.lms.doctype.lms_sketch.lms_sketch.save_sketch', {
name: sketch_name,
title: title,
code: code
})
.then(r => {
var msg = r.message;
if (!msg.ok) {
var error = msg.error || "Save failed."
frappe.msgprint({
"title": "Error",
"indicator": "red",
"message": error
});
}
else if (msg.status == "created") {
var path = "/sketches/sketch?sketch=" + msg.name;
var url = window.location.protocol + "//" + window.location.host + path
window.history.pushState({path: url}, '', url);
sketch_name = name;
frappe.msgprint({
"title": "Notification",
"indicator": "green",
"message": "New sketch has been saved!"
});
}
else if (msg.status == "updated") {
frappe.msgprint({
"title": "Notification",
"indicator": "green",
"message": "The sketch has been saved!"
});
}
})
}
$(function() {
$("#sketch-save").click(function() {
saveSketch();
});
})
</script>
{%- endblock %}

View File

@@ -1,46 +0,0 @@
import frappe
def get_context(context):
context.no_cache = 1
try:
sketch_id = frappe.form_dict["sketch"]
except KeyError:
context.template = "www/404.html"
return
sketch = get_sketch(sketch_id)
if not sketch:
context.template = "www/404.html"
return
context.sketch = sketch
context.livecode_url = get_livecode_url()
context.editable = is_editable(context.sketch, frappe.session.user)
def is_editable(sketch, user):
if sketch.is_new():
# new sketches can be editable by any logged in user
return user != "Guest"
else:
# existing sketches are editable by the owner
return sketch.owner == user
def get_livecode_url():
doc = frappe.get_doc("LMS Settings")
return doc.livecode_url
def get_sketch(sketch_id):
if sketch_id == 'new':
sketch = frappe.new_doc('LMS Sketch')
sketch.name = "new"
sketch.title = "New Sketch"
sketch.code = "circle(100, 100, 50)"
return sketch
try:
name = "SKETCH-" + sketch_id
return frappe.get_doc('LMS Sketch', name)
except frappe.exceptions.DoesNotExistError:
return