Compare commits

...

19 Commits

Author SHA1 Message Date
Anand Chitipothu
2c24412633 refactor: removed the unused dashboard portal page 2021-07-06 14:20:00 +05:30
Anand Chitipothu
1b8a45ba4a refactor: removed sketch doctype and portal page for home
Both of these will be moved to mon_school.
2021-07-06 13:20:41 +05:30
Jannat Patel
3dd4adbc1f Merge pull request #143 from fossunited/switch-batch
feat: added a utililty to switch a student from one batch to another
2021-07-05 19:04:52 +05:30
Jannat Patel
0c52c9c4bc Merge pull request #144 from fossunited/page-context
feat: make it possible to enable tracking for livecode execution
2021-07-05 19:03:47 +05:30
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
pateljannat
5d96bf544d fix: conflicts 2021-06-22 12:28:12 +05:30
pateljannat
1cb81de5c0 feat: lms quizzes 2021-06-09 13:17:42 +05:30
57 changed files with 1109 additions and 982 deletions

View File

@@ -161,10 +161,10 @@ whitelist = [
"/socket.io",
"/hackathons",
"/dashboard",
"/join-request"
"/join-request",
"/add-a-new-batch",
"/new-sign-up",
"/message"
"/message",
"/about"
]
whitelist_rules = [{"from_route": p, "to_route": p[1:]} for p in whitelist]
@@ -187,5 +187,13 @@ update_website_context = 'community.widgets.update_website_context'
## subclass of community.community.plugins.PageExtension
# community_lesson_page_extension = None
community_lesson_page_extensions = [
"community.plugins.LiveCodeExtension"
]
## 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

@@ -3,7 +3,6 @@
import frappe
from frappe.model.document import Document
# from ..lms_sketch.livecode import livecode_to_svg
class Exercise(Document):
def get_user_submission(self):

View File

@@ -6,12 +6,17 @@
"engine": "InnoDB",
"field_order": [
"exercise",
"solution",
"status",
"batch",
"column_break_4",
"exercise_title",
"course",
"batch",
"lesson",
"image"
"section_break_8",
"solution",
"image",
"test_results",
"comments"
],
"fields": [
{
@@ -21,12 +26,6 @@
"label": "Exercise",
"options": "Exercise"
},
{
"fieldname": "solution",
"fieldtype": "Code",
"in_list_view": 1,
"label": "Solution"
},
{
"fetch_from": "exercise.title",
"fieldname": "exercise_title",
@@ -61,11 +60,41 @@
"fieldtype": "Code",
"label": "Image",
"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,
"links": [],
"modified": "2021-05-21 11:28:45.833018",
"modified": "2021-06-24 16:22:50.570845",
"modified_by": "Administrator",
"module": "LMS",
"name": "Exercise Submission",

View File

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

View File

@@ -9,27 +9,35 @@ from ...md import markdown_to_html, find_macros
class Lesson(Document):
def before_save(self):
macros = find_macros(self.body)
exercises = [value for name, value in macros if name == "Exercise"]
dynamic_documents = ["Exercise", "Quiz"]
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
for name in exercises:
e = frappe.get_doc("Exercise", name)
for name in documents:
e = frappe.get_doc(doctype_map[section], name)
e.lesson = self.name
e.index_ = index
e.save()
index += 1
self.update_orphan_exercises(exercises)
self.update_orphan_documents(doctype_map[section], documents)
def update_orphan_exercises(self, active_exercises):
"""Updates the exercises that were previously part of this lesson,
def update_orphan_documents(self, doctype, documents):
"""Updates the documents 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 = set(active_exercises)
orphan_exercises = linked_exercises - active_exercises
for name in orphan_exercises:
ex = frappe.get_doc("Exercise", name)
linked_documents = {row['name'] for row in frappe.get_all(doctype, {"lesson": self.name})}
active_documents = set(documents)
orphan_documents = linked_documents - active_documents
for name in orphan_documents:
ex = frappe.get_doc(doctype, name)
ex.lesson = None
ex.index_ = 0
ex.index_label = ""
@@ -92,13 +100,30 @@ def update_progress(lesson):
course_progress.save(ignore_permissions=True)
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)
all_exercises_submitted = False
if not len(exercise_names):
return True
query = {
"exercise": ["in", exercise_names],
"owner": user
}
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

@@ -199,13 +199,15 @@ class LMSCourse(Document):
}
if batch:
filters["batch"] = batch
return frappe.db.get_value("LMS Batch Membership", filters, ["name","batch", "current_lesson"], as_dict=True)
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):
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=None):

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

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

View File

@@ -1,45 +1,41 @@
{
"actions": [],
"autoname": "format:SKETCH-{#}",
"creation": "2021-03-09 16:31:50.523524",
"autoname": "field:title",
"creation": "2021-06-07 10:50:17.893625",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"runtime",
"code",
"svg"
"questions",
"lesson"
],
"fields": [
{
"fieldname": "title",
"fieldtype": "Data",
"label": "Title"
"label": "Title",
"unique": 1
},
{
"fieldname": "runtime",
"fieldtype": "Data",
"label": "Runtime"
"fieldname": "questions",
"fieldtype": "Table",
"label": "Questions",
"options": "LMS Quiz Question"
},
{
"fieldname": "code",
"fieldtype": "Code",
"label": "Code"
},
{
"fieldname": "svg",
"fieldtype": "Code",
"label": "SVG",
"read_only": 1
"fieldname": "lesson",
"fieldtype": "Link",
"label": "Lesson",
"options": "Lesson"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-03-12 08:42:56.671658",
"modified": "2021-06-23 17:58:57.642873",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Sketch",
"name": "LMS Quiz",
"owner": "Administrator",
"permissions": [
{
@@ -57,6 +53,5 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_views": 1
"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

@@ -1,10 +1,8 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, FOSS United and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestLMSSketch(unittest.TestCase):
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

@@ -1,126 +0,0 @@
"""Utilities to work with livecode service.
"""
import websocket
import json
from .svg import SVG
import frappe
from urllib.parse import urlparse
# Files to pass to livecode server
# The same code is part of livecode-canvas.js
# TODO: generate livecode-canvas.js from this file
START = '''
import sketch
code = open("main.py").read()
env = dict(sketch.__dict__)
exec(code, env)
'''
SKETCH = '''
import json
def sendmsg(msgtype, function, args):
"""Sends a message to the frontend.
The frontend will receive the specified message whenever
this function is called. The frontend can decide to some
action on each of these messages.
"""
msg = dict(msgtype=msgtype, function=function, args=args)
print("--MSG--", json.dumps(msg))
def _draw(func, **kwargs):
sendmsg(msgtype="draw", function=func, args=kwargs)
def circle(x, y, d):
"""Draws a circle of diameter d with center (x, y).
"""
_draw("circle", x=x, y=y, d=d)
def line(x1, y1, x2, y2):
"""Draws a line from point (x1, y1) to point (x2, y2).
"""
_draw("line", x1=x1, y1=y1, x2=x2, y2=y2)
def rect(x, y, w, h):
"""Draws a rectangle on the canvas.
Parameters
----------
x: x coordinate of the top-left corner of the rectangle
y: y coordinate of the top-left corner of the rectangle
w: width of the rectangle
h: height of the rectangle
"""
_draw("rect", x=x, y=y, w=w, h=h)
def clear():
_draw("clear")
# clear the canvas on start
clear()
'''
def get_livecode_url():
doc = frappe.get_cached_doc("LMS Settings")
return doc.livecode_url
def get_livecode_ws_url():
url = urlparse(get_livecode_url())
protocol = "wss" if url.scheme == "https" else "ws"
return protocol + "://" + url.netloc + "/livecode"
def livecode_to_svg(livecode_ws_url, code, *, timeout=3):
"""Renders the code as svg.
"""
if livecode_ws_url is None:
livecode_ws_url = get_livecode_ws_url()
try:
ws = websocket.WebSocket()
ws.settimeout(timeout)
ws.connect(livecode_ws_url)
msg = {
"msgtype": "exec",
"runtime": "python",
"code": code,
"files": [
{"filename": "start.py", "contents": START},
{"filename": "sketch.py", "contents": SKETCH},
],
"command": ["python", "start.py"]
}
ws.send(json.dumps(msg))
messages = _read_messages(ws)
commands = [m for m in messages if m['msgtype'] == 'draw']
img = draw_image(commands)
return img.tostring()
except websocket.WebSocketException as e:
frappe.log_error(frappe.get_traceback(), 'livecode_to_svg failed')
def _read_messages(ws):
messages = []
try:
while True:
msg = ws.recv()
if not msg:
break
messages.append(json.loads(msg))
except websocket.WebSocketTimeoutException as e:
print("Error:", e)
pass
return messages
def draw_image(commands):
img = SVG(width=300, height=300, viewBox="0 0 300 300", fill='none', stroke='black')
for c in commands:
args = c['args']
if c['function'] == 'circle':
img.circle(cx=args['x'], cy=args['y'], r=args['d']/2)
elif c['function'] == 'line':
img.line(x1=args['x1'], y1=args['y1'], x2=args['x2'], y2=args['y2'])
elif c['function'] == 'rect':
img.rect(x=args['x'], y=args['y'], width=args['w'], height=args['h'])
return img

View File

@@ -1,105 +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 hashlib
from urllib.parse import urlparse
import frappe
from frappe.model.document import Document
from . import livecode
DEFAULT_IMAGE = """
<svg viewBox="0 0 300 300" width="300" xmlns="http://www.w3.org/2000/svg">
</svg>
"""
class LMSSketch(Document):
@property
def sketch_id(self):
"""Returns the numeric part of the name.
For example, the skech_id will be "123" for sketch with name "SKETCH-123".
"""
return self.name.replace("SKETCH-", "")
def get_owner(self):
"""Returns the owner of this sketch as a document.
"""
return frappe.get_doc("User", self.owner)
def get_owner_name(self):
return self.get_owner().full_name
def get_livecode_url(self):
doc = frappe.get_cached_doc("LMS Settings")
return doc.livecode_url
def get_livecode_ws_url(self):
url = urlparse(self.get_livecode_url())
protocol = "wss" if url.scheme == "https" else "ws"
return protocol + "://" + url.netloc + "/livecode"
def to_svg(self):
return self.svg or self.render_svg()
def render_svg(self):
h = hashlib.md5(self.code.encode('utf-8')).hexdigest()
cache = frappe.cache()
key = "sketch-" + h
value = cache.get(key)
if value:
value = value.decode('utf-8')
else:
ws_url = self.get_livecode_ws_url()
value = livecode.livecode_to_svg(ws_url, self.code)
if value:
cache.set(key, value)
return value or DEFAULT_IMAGE
@staticmethod
def get_recent_sketches(limit=100, owner=None):
"""Returns the recent sketches.
"""
filters = {}
if owner:
filters = {"owner": owner}
sketches = frappe.get_all(
"LMS Sketch",
filters=filters,
fields='*',
order_by='modified desc',
page_length=limit
)
return [frappe.get_doc(doctype='LMS Sketch', **doc) for doc in sketches]
def __repr__(self):
return f"<LMSSketch {self.name}>"
@frappe.whitelist()
def save_sketch(name, title, code):
if not name or name == "new":
doc = frappe.new_doc('LMS Sketch')
doc.title = title
doc.code = code
doc.runtime = 'python-canvas'
doc.insert(ignore_permissions=True)
status = "created"
else:
doc = frappe.get_doc("LMS Sketch", name)
if doc.owner != frappe.session.user:
return {
"ok": False,
"error": "Permission Denied"
}
doc.title = title
doc.code = code
doc.svg = ''
doc.save()
status = "updated"
return {
"ok": True,
"status": status,
"name": doc.name,
}

View File

@@ -1,143 +0,0 @@
"""SVG rendering library.
USAGE:
from svg import SVG
svg = SVG(width=200, height=200)
svg.circle(cx=100, cy=200, r=50)
print(svg.tostring())
"""
from xml.etree import ElementTree
TAGNAMES = set([
"circle", "ellipse",
"line", "path", "rect", "polygon", "polyline",
"text", "textPath", "title",
"marker", "defs",
"g"
])
class Node:
"""SVG Node"""
def __init__(self, tag, **attrs):
self.tag = tag
self.attrs = dict((k.replace('_', '-'), str(v)) for k, v in attrs.items())
self.children = []
def node(self, tag, **attrs):
n = Node(tag, **attrs)
self.children.append(n)
return n
def apply(self, func):
"""Applies a function to this node and
all the children recursively.
"""
func(self)
for n in self.children:
n.apply(func)
def clone(self):
node = Node(self.tag, **self.attrs)
node.children = [n.clone() for n in self.children]
return node
def add_node(self, node):
if not isinstance(node, Node):
node = Text(node)
self.children.append(node)
def __getattr__(self, tag):
if tag not in TAGNAMES:
raise AttributeError(tag)
return lambda **attrs: self.node(tag, **attrs)
def translate(self, x, y):
return self.g(transform="translate(%s, %s)" % (x, y))
def scale(self, *args):
return self.g(transform="scale(%s)" % ", ".join(str(a) for a in args))
def __repr__(self):
return "<%s .../>" % self.tag
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
pass
def build_tree(self, builder):
builder.start(self.tag, self.attrs)
for node in self.children:
node.build_tree(builder)
return builder.end(self.tag)
def _indent(self, elem, level=0):
"""Indent etree node for prettyprinting."""
i = "\n" + level*" "
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = i + " "
if not elem.tail or not elem.tail.strip():
elem.tail = i
for elem in elem:
self._indent(elem, level+1)
if not elem.tail or not elem.tail.strip():
elem.tail = i
else:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i
def save(self, filename, encoding='utf-8'):
f = open(filename, 'w')
f.write(self.tostring())
f.close()
def tostring(self, encoding='utf-8'):
builder = ElementTree.TreeBuilder()
self.build_tree(builder)
e = builder.close()
self._indent(e)
return ElementTree.tostring(e, encoding).decode(encoding)
class Text(Node):
"""Text Node
>>> p = Node("p")
>>> p.add_node("hello, world!")
>>> p.tostring()
'<p>hello, world!</p>'
"""
def __init__(self, text):
Node.__init__(self, "__text__")
self._text = text
def build_tree(self, builder):
builder.data(str(self._text))
class SVG(Node):
"""
>>> svg = SVG(width=200, height=200)
>>> svg.rect(x=0, y=0, width=200, height=200, fill="blue")
<rect .../>
>>> with svg.translate(-50, -50) as g:
... g.rect(x=0, y=0, width=50, height=100, fill="red")
... g.rect(x=50, y=0, width=50, height=100, fill="green")
<rect .../>
<rect .../>
>>> print(svg.tostring())
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="200" height="200" fill="blue" />
<g transform="translate(-50, -50)">
<rect x="0" y="0" width="50" height="100" fill="red" />
<rect x="50" y="0" width="50" height="100" fill="green" />
</g>
</svg>
"""
def __init__(self, **attrs):
attrs['xmlns'] = "http://www.w3.org/2000/svg"
Node.__init__(self, 'svg', **attrs)

View File

@@ -1,5 +1,4 @@
"""Handy module to make access to all doctypes from a single place.
"""
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

View File

@@ -2,16 +2,17 @@
<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">
{{ course.title }}</span> {% endif %}
{% set all_memberships = course.get_all_memberships() %}
{% if all_memberships | length > 1 %}
<a class="nav-link pull-right" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">
Switch Batch
{% set all_memberships = course.get_all_memberships(frappe.session.user) %}
{% if membership and membership.batch and all_memberships | length > 1 %}
<a class="pull-right dropdown-item border rounded" style="width: 10rem;" href="#" id="navbarDropdown" role="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ membership.batch_title }}
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
{% for data in all_memberships %}
{% if data.batch != membership.batch %}
<a class="dropdown-item switch-batch" href="/courses/{{ course.name }}/home?batch={{ data.batch }}">{{ data.batch_title }}</a>
<a class="dropdown-item switch-batch"
href="/courses/{{ course.name }}/home?batch={{ data.batch }}">{{ data.batch_title }}</a>
{% endif %}
{% endfor %}
</div>
@@ -28,7 +29,8 @@
<a class="nav-link" id="home" href="/courses/{{course.name}}/home{{ course.query_parameter }}">Home</a>
</li>
<li class="nav-item">
{% set lesson_index = course.get_lesson_index(membership.current_lesson) if membership and membership.current_lesson else '1.1' %}
{% 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>

View File

@@ -7,9 +7,17 @@
<div class="chapter-lessons">
{% for lesson in chapter.get_lessons() %}
<div class="lesson-teaser">
<a {% if show_link or lesson.include_in_preview %}
href="{{ course.get_learn_url(course.get_lesson_index(lesson.name)) }}{{course.query_parameter}}" {% else %} href="" class="no-preview"
{% endif %} data-course="{{ course.name }}">{{ lesson.title }}</a>
{% if show_link or lesson.include_in_preview %}
<a class="" href="{{ course.get_learn_url(course.get_lesson_index(lesson.name)) }}{{course.query_parameter}}"
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() %}
<span class="ml-5 badge p-2 {{ lesson.get_slugified_class() }}"> {{ lesson.get_progress() }}</span>
{% endif %}
@@ -18,36 +26,3 @@
</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

@@ -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.
"""
import frappe
class PageExtension:
"""PageExtension is a plugin to inject custom styles and scripts
into a web page.
@@ -64,3 +66,43 @@ class ProfileTab:
Every subclass must implement this.
"""
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,7 @@
--cta-color: var(--c4);
--send-message: var(--c7);
--received-message: var(--c8);
--checkbox-size: 14px;
--control-bg: var(--gray-100);
}
@@ -228,6 +229,10 @@ section {
margin-top: 30px;
}
input[type=checkbox] {
appearance: auto;
}
.partiallycomplete {
background: #FEF4E2;
color: #976417;

View File

@@ -76,11 +76,6 @@ h2 {
text-decoration: none
}
.no-preview:hover {
cursor: pointer;
color: #2490ef;
}
section {
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 @@
<div class="container mt-5">
{{ widgets.BatchTabs(course=course, membership=membership) }}
<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>
{% if batch %}

View File

@@ -36,13 +36,13 @@
<a href="/courses/{{ course.name }}">Checkout Course Details.</a>
</div>
{% endif %}
{% if membership %}
{{ pagination(prev_chap, prev_url, next_chap, next_url) }}
{% endif %}
</div>
</div>
{% endblock %}
{% macro pagination(prev_chap, prev_url, next_chap, next_url) %}
<div class="lesson-pagination">
{% if prev_url %}
@@ -61,6 +61,11 @@
{%- block script %}
{{ super() }}
<script type="text/javascript">
var page_context = {{ page_context | tojson }};
</script>
{% for ext in page_extensions %}
{{ ext.render_footer() }}
{% endfor %}

View File

@@ -1,4 +1,6 @@
frappe.ready(() => {
/* Save Lesson Progress */
if ($(".title").attr("data-membership") && !$(".title").hasClass("is_mentor")) {
frappe.call({
method: "community.lms.doctype.lesson.lesson.save_progress",
@@ -8,10 +10,60 @@ frappe.ready(() => {
}
})
}
/* Save Current Lesson */
if ($(".title").attr("data-membership")) {
frappe.call("community.lms.api.save_current_lesson", {
course_name: $(".title").attr("data-course"),
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

@@ -30,11 +30,17 @@ 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.course.get_learn_url(next_) + context.course.query_parameter
context.prev_url = context.course.get_learn_url(prev_) + context.course.query_parameter
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_) and context.course.get_learn_url(prev_) + context.course.query_parameter
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):
if not lesson_number:
return
@@ -49,7 +55,7 @@ def get_lesson_index(course, batch, user):
return lesson and course.get_lesson_index(lesson)
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
extensions = [frappe.get_attr(name)() for name in classnames]
return extensions

View File

@@ -27,7 +27,7 @@ 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=" + membership.batch if membership and membership.batch else ""
context.course.query_parameter = "?batch=" + membership.batch if membership and membership.batch else " "
context.livecode_url = get_livecode_url()
def get_livecode_url():

View File

@@ -4,6 +4,7 @@
{% block head_include %}
<meta name="description" content="Courses" />
<meta name="keywords" content="Courses {{course.title}}" />
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
{% endblock %}
{% block content %}
@@ -30,7 +31,7 @@
{{ CourseDescription(course) }}
{{ widgets.InstructorSection(instructor=course.get_instructor()) }}
{{ BatchSection(course) }}
{{ widgets.CourseOutline(course=course, show_link=False) }}
{{ widgets.CourseOutline(course=course, show_link=membership) }}
</div>
</div>
</div>

View File

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

View File

@@ -1,181 +0,0 @@
{% extends "templates/base.html" %}
{% block title %}{{ _("Community") }}{% endblock %}
{% block head_include %}
<meta name="description" content="{{ 'Community' }}" />
<meta name="keywords" content="An app that supports Communities." />
<style>
section {
padding: 2rem;
color: #000000;
}
svg {
width: 200px;
height: 200px;
}
.dashboard__parent {
display: flex;
}
.dashboard__name {
font-weight: normal;
font-style: normal;
font-size: 36px;
line-height: 42px;
}
.dashboard__details {
padding-top: 2rem;
width: 80%;
}
.dashboard__course {
border: 1px solid black;
padding: 1rem;
margin: 0.5rem;
width: 48%;
}
.dashboard__courseHeader {
display: flex;
justify-content: space-between;
height: 50px;
margin-bottom: 3px;
}
.dashboard__badge {
background: #D6D6FF;
border-radius: 20px;
color: #1712FE;
padding: 0.5rem;
height: fit-content;
}
.dashboard__description {
height: 100px;
}
</style>
{% endblock %}
{% block content %}
<section>
<div class="dashboard__parent">
<div class="mr-5">
{{ widgets.Avatar(member=member, avatar_class="avatar-xl") }}
</div>
<div class="dashboard__details">
<div class="dashboard__name">
<a class="anchor_style" href="/{{member.username}}">{{ member.full_name }}</a>
</div>
<div>
<ul class="nav nav-tabs mt-4" id="myTab" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="home-tab" data-toggle="tab" href="#home" role="tab"
aria-controls="home" aria-selected="true">Activity</a>
</li>
<li class="nav-item">
<a class="nav-link" id="sketches-tab" data-toggle="tab" href="#sketches" role="tab"
aria-controls="sketches" aria-selected="false">Sketches</a>
</li>
<li class="nav-item">
<a class="nav-link" id="courses-tab" data-toggle="tab" href="#courses" role="tab"
aria-controls="courses" aria-selected="false">Courses</a>
</li>
</ul>
</div>
<div>
<div class="tab-content">
<div class="tab-pane fade py-4 show active" role="tabpanel" id="home">
<div class='container'>
{% if activity %}
{% for message in activity %}
<div class="dashboard__message border m-5 p-3">
<a class="anchor_style bold" href="/{{message.member.username}}">{{ message.member.full_name }}</a>
<div class="text-muted float-right">
{{ message.course }} ({{message.batch}})
</div>
<div class="d-flex align-items-center w-100">
<div>
{{ widgets.Avatar(member=message.member, avatar_class="avatar-medium") }}
</div>
<div class="ml-5 mt-5">{{ frappe.utils.md_to_html(message.message) }}</div>
</div>
<div class="d-flex">
<div class="text-muted float-right">
{{ frappe.utils.pretty_date(message.creation) }}
</div>
</div>
</div>
{% endfor %}
{% else %}
<p class="text-center">You have not received any messages yet.</p>
{% endif %}
</div>
</div>
<div class="tab-pane fade py-4" role="tabpanel" id="sketches">
<div class="container">
<a href="/sketches/new">Create a New Sketch</a>
<div class="row row-cols-1 row-cols-xl-5 row-cols-lg-4 row-cols-md-3 row-cols-sm-2">
{% if sketches %}
{% for sketch in sketches %}
<div class="col mb-4">
<div class="card sketch-card" style="width: 200px;">
<div class="card-img-top">
<a href="/sketches/{{sketch.sketch_id}}">
{{ sketch.to_svg() }}
</a>
</div>
<div class="card-footer">
<div class="sketch-title">
<a href="sketches/{{sketch.sketch_id}}">{{sketch.title}}</a>
</div>
<div class="sketch-author">
by {{sketch.get_owner_name()}}
</div>
</div>
</div>
</div>
{% endfor %}
{% endif %}
</div>
{% if not sketches %}
<p class="text-center">You have not created any sketches.</p>
{% endif %}
</div>
</div>
<div class="tab-pane fade py-4" role="tabpanel" id="courses">
{% if courses %}
<p class="ml-3">Your Courses</p>
<div class="d-flex flex-wrap">
{% for course in courses %}
<div class="dashboard__course">
<div class="dashboard__courseHeader">
<a class="text-decoration-none" target="_blank" href="/courses/{{course.name}}">
<h5 class="w-75">{{ course.title }}</h5>
</a>
{% if course.member_type %}
<div class="dashboard__badge">
{{ course.member_type }}
</div>
{% endif %}
</div>
<div class="dashboard__description">
{{ frappe.utils.md_to_html(course.description) }}
</div>
<div class="text-muted"> Joined {{ frappe.utils.pretty_date(course.joining) }} </div>
</div>
{% endfor %}
{% else %}
<p class="text-center">You are not a member of any course yet. Checkout our <a
href="/courses" target="_blank">Courses</a>.</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{% endblock %}

View File

@@ -1,42 +0,0 @@
import frappe
from community.lms.models import Sketch
def get_context(context):
context.no_cache = 1
if frappe.session.user == "Guest":
frappe.local.flags.redirect_location = "/login"
raise frappe.Redirect
context.member = frappe.get_doc("User", frappe.session.user)
context.memberships = get_memberships()
context.courses = get_courses(context.memberships)
context.activity = get_activity(context.memberships)
context.sketches = list(filter(lambda x: x.owner == frappe.session.user, Sketch.get_recent_sketches(owner=context.member.email)))
def get_memberships():
return frappe.get_all("LMS Batch Membership", {"member": frappe.session.user}, ["batch", "member_type", "creation"])
def get_courses(memberships):
courses = []
for membership in memberships:
course = frappe.db.get_value("LMS Batch", membership.batch, "course")
course_details = frappe.get_doc("LMS Course", course)
course_in_list = list(filter(lambda x: x.name == course_details.name, courses))
if not len(course_in_list):
course_details.description = course_details.description[0:100] + "..."
course_details.joining = membership.creation
if membership.member_type != "Student":
course_details.member_type = membership.member_type
courses.append(course_details)
return courses
def get_activity(memberships):
messages, courses = [], {}
batches = [x.batch for x in memberships]
for batch in batches:
courses[batch] = frappe.db.get_value("LMS Batch", batch, "course")
messages = frappe.get_all("LMS Message", {"batch": ["in", ",".join(batches)]}, ["message", "author", "creation", "batch"], order_by='creation desc')
for message in messages:
message.course = courses[message.batch]
message.member = frappe.get_doc("User", message.author)
return messages

View File

@@ -1,49 +0,0 @@
{% extends "templates/base.html" %}
{% block content %}
{{ HeroSection() }}
{{ ExploreCourses(courses) }}
{{ RecentSketches(recent_sketches) }}
{% endblock %}
{% macro HeroSection() %}
<section id="hero">
<div class="container">
<div class="jumbotron">
<h1 class="display-4">Guided online programming courses, with a <br />mentor at your back.</h1>
<p class="lead">Hands-on programming courses designed by experts, delivered by passionate mentors.</p>
{{ widgets.RequestInvite() }}
</div>
</div>
</section>
{% endmacro %}
{% macro ExploreCourses(courses) %}
<section id="explore-courses" class="lightgray">
<div class="container lightgray">
<h2>Explore Courses</h2>
<div class="row">
{% for course in courses %}
<div class="col-md-6">
{{ widgets.CourseTeaser(course=course) }}
</div>
{% endfor %}
</div>
</div>
</section>
{% endmacro %}
{% macro RecentSketches(sketches) %}
<section id="recet-sketches">
<div class="container">
<h2>Recent Sketches</h2>
<div class="row">
{% for sketch in sketches %}
<div class="col-md-3">
{{ widgets.SketchTeaser(sketch=sketch) }}
</div>
{% endfor %}
</div>
</div>
</section>
{% endmacro %}

View File

@@ -1,7 +0,0 @@
import frappe
from community.lms.models import Course, Sketch
def get_context(context):
context.no_cache = 1
context.courses = Course.find_all()
context.recent_sketches = Sketch.get_recent_sketches(limit=8)

View File

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