Compare commits

...

37 Commits

Author SHA1 Message Date
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
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
9c1506d3c8 fix: minor issues 2021-06-15 18:40:14 +05:30
Jannat Patel
e94c3f27ab Merge pull request #128 from fossunited/ui-fixes
fix: UI fixes
2021-06-15 13:19:03 +05:30
pateljannat
5fa8bdd40c fix: invite request test, removed print statements and unused classes' 2021-06-15 13:09:48 +05:30
pateljannat
17f03aeee7 fix: join batch, removed code revision, redirects for other pages if batch missing 2021-06-15 13:01:57 +05:30
pateljannat
7840512a13 fix: ui, preview, progress, batches 2021-06-14 18:45:46 +05:30
Anand Chitipothu
526ded784b Merge pull request #125 from fossunited/hotfix-exercise-image
fix: fixed error on saving exercises
2021-06-12 21:42:27 +05:30
Anand Chitipothu
6b5ddcd54a fix: fixed error on saving exercises
Removed the image generation when exercise is saved. The library used
for exercises has changed and generating the image doesn't work any
more.
2021-06-12 10:49:27 +05:30
Jannat Patel
c42247db42 Merge pull request #122 from fderyckel/patch-1
frappe wasn't imported
2021-06-10 20:47:05 +05:30
François de Ryckel
8f8d4901ff frappe wasn't imported
error with NameError: name 'frappe' is not defined
2021-06-10 18:04:40 +03:00
pateljannat
f5f3c808d4 Merge branch 'main' of https://github.com/frappe/community into ui-fixes 2021-06-10 13:41:31 +05:30
pateljannat
1e3152e303 fix: ui 2021-06-10 13:41:11 +05:30
Jannat Patel
344661cf83 Merge pull request #121 from fossunited/lesson-markup
Lesson markup
2021-06-10 12:32:16 +05:30
Anand Chitipothu
d9185c0b6b feat: integrated lesson markup
- added PageExtension plugin to inject custom styles scripts in a page
- removed the livecode integration and enabled PageExtension plugins for
  learn page
- also merged the profile_tab.py with plugins.py
- added a utility to find the macros from given text
- updated the before_save of lesson to find exercises using the macros
  and update the exercises as before

Issue #115
2021-06-09 23:58:21 +05:30
Anand Chitipothu
5363fb7eb3 feat: extend markdown to support macros
With this feature, the exercises can be added to the lesson as:

    {{ Exercise("two-circles") }}

This also added fenced_code extension that allows adding id and classes
to code blocks.

This uses Python-Markdown library instead of Markdown2 that is used
everywhere in Frappe. The Python-Markdown is more easily extensible than
Markdown2.

Issue #115
2021-06-09 23:22:00 +05:30
pateljannat
1cb81de5c0 feat: lms quizzes 2021-06-09 13:17:42 +05:30
Jannat Patel
d90a1247f1 Merge pull request #120 from fossunited/profile-tabs
feat: pluggable profile tabs
2021-06-08 14:53:38 +05:30
80 changed files with 1494 additions and 923 deletions

View File

@@ -1,38 +0,0 @@
"""
The profile_tab module provides a pluggable way to add tabs to user
profiles.
This is achieved by specifying the profile_tabs in the hooks.
profile_tabs = [
'myapp.myapp.profile_tabs.SketchesTab'
]
When a profile page is rendered, these classes specified in the
profile_hooks are instanciated with the user as argument and used to
render the tabs.
"""
class ProfileTab:
"""Base class for profile tabs.
Every subclass of ProfileTab must implement two methods:
- get_title()
- render()
"""
def __init__(self, user):
self.user = user
def get_title(self):
"""Returns the title of the tab.
Every subclass must implement this.
"""
raise NotImplementedError()
def render(self):
"""Renders the contents of the tab as HTML.
Every subclass must implement this.
"""
raise NotImplementedError()

View File

@@ -3,13 +3,14 @@
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
import frappe
from frappe.model.document import Document
from frappe import _
class CommunityProjectMember(Document):
def validate(self):
self.validate_if_already_member()
def validate_if_already_member(self):
if frappe.get_all("Community Project Member", {"owner": self.owner}):
frappe.throw(_("You have already applied for the membership of this project."))

View File

@@ -136,15 +136,15 @@ primary_rules = [
{"from_route": "/hackathons/<hackathon>/<project>", "to_route": "hackathons/project"},
{"from_route": "/dashboard", "to_route": ""},
{"from_route": "/add-a-new-batch", "to_route": "add-a-new-batch"},
{"from_route": "/courses/<course>/<batch>/home", "to_route": "batch/home"},
{"from_route": "/courses/<course>/<batch>/learn", "to_route": "batch/learn"},
{"from_route": "/courses/<course>/<batch>/learn/<int:chapter>.<int:lesson>", "to_route": "batch/learn"},
{"from_route": "/courses/<course>/<batch>/schedule", "to_route": "batch/schedule"},
{"from_route": "/courses/<course>/<batch>/members", "to_route": "batch/members"},
{"from_route": "/courses/<course>/<batch>/discuss", "to_route": "batch/discuss"},
{"from_route": "/courses/<course>/<batch>/about", "to_route": "batch/about"},
{"from_route": "/courses/<course>/<batch>/progress", "to_route": "batch/progress"},
{"from_route": "/courses/<course>/<batch>/join", "to_route": "batch/join"}
{"from_route": "/courses/<course>/home", "to_route": "batch/home"},
{"from_route": "/courses/<course>/learn", "to_route": "batch/learn"},
{"from_route": "/courses/<course>/learn/<int:chapter>.<int:lesson>", "to_route": "batch/learn"},
{"from_route": "/courses/<course>/schedule", "to_route": "batch/schedule"},
{"from_route": "/courses/<course>/members", "to_route": "batch/members"},
{"from_route": "/courses/<course>/discuss", "to_route": "batch/discuss"},
{"from_route": "/courses/<course>/about", "to_route": "batch/about"},
{"from_route": "/courses/<course>/progress", "to_route": "batch/progress"},
{"from_route": "/courses/<course>/join", "to_route": "batch/join"}
]
# Any frappe default URL is blocked by profile-rules, add it here to unblock it
@@ -161,10 +161,11 @@ 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]
@@ -176,3 +177,23 @@ profile_rules = [
website_route_rules = primary_rules + whitelist_rules + profile_rules
update_website_context = 'community.widgets.update_website_context'
## Specify the additional tabs to be included in the user profile page.
## Each entry must be a subclass of community.community.plugins.ProfileTab
# profile_tabs = []
## Specify the extension to be used to control what scripts and stylesheets
## to be included in lesson pages. The specified value must be be a
## subclass of community.community.plugins.PageExtension
# community_lesson_page_extension = None
community_lesson_page_extensions = [
"community.plugins.LiveCodeExtension"
]
## Markdown Macros for Lessons
community_markdown_macro_renderers = {
"Exercise": "community.plugins.exercise_renderer",
"Quiz": "community.plugins.quiz_renderer",
"YouTubeVideo": "community.plugins.youtube_video_renderer",
}

View File

@@ -15,13 +15,6 @@ def autosave_section(section, code):
doc.insert()
return {"name": doc.name}
@frappe.whitelist()
def get_section(name):
"""Saves the code edited in one of the sections.
"""
doc = frappe.get_doc("LMS Section", name)
return doc and doc.as_dict()
@frappe.whitelist()
def submit_solution(exercise, code):
"""Submits a solution.
@@ -36,13 +29,13 @@ def submit_solution(exercise, code):
return {"name": doc.name, "creation": doc.creation}
@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.
"""
name = frappe.get_value(
doctype="LMS Batch Membership",
filters={
"batch": batch_name,
"course": course_name,
"member": frappe.session.user
},
fieldname="name")

View File

@@ -3,12 +3,9 @@
import frappe
from frappe.model.document import Document
from ..lms_sketch.livecode import livecode_to_svg
# from ..lms_sketch.livecode import livecode_to_svg
class Exercise(Document):
def before_save(self):
self.image = livecode_to_svg(None, self.answer)
def get_user_submission(self):
"""Returns the latest submission for this user.
"""
@@ -42,8 +39,6 @@ class Exercise(Document):
course = frappe.get_doc("LMS Course", self.course)
batch = course.get_student_batch(user)
image = livecode_to_svg(None, code)
doc = frappe.get_doc(
doctype="Exercise Submission",
exercise=self.name,
@@ -51,7 +46,6 @@ class Exercise(Document):
course=self.course,
lesson=self.lesson,
batch=batch and batch.name,
image=image,
solution=code)
doc.insert(ignore_permissions=True)

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

@@ -58,7 +58,8 @@ def create_invite_request(invite_email):
frappe.get_doc({
"doctype": "Invite Request",
"invite_email": invite_email
"invite_email": invite_email,
"status": "Approved"
}).save(ignore_permissions=True)
return "OK"

View File

@@ -18,7 +18,7 @@ class TestInviteRequest(unittest.TestCase):
filters={"invite_email": "test_invite@example.com"},
fieldname=["invite_email", "status", "signup_email"],
as_dict=True)
self.assertEqual(invite.status, "Pending")
self.assertEqual(invite.status, "Approved")
self.assertEqual(invite.signup_email, None)
def test_create_invite_request_update(self):

View File

@@ -7,12 +7,13 @@
"engine": "InnoDB",
"field_order": [
"chapter",
"lesson_type",
"include_in_preview",
"column_break_4",
"title",
"index_",
"index_label",
"body",
"sections"
"section_break_6",
"body"
],
"fields": [
{
@@ -22,14 +23,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",
@@ -40,6 +33,7 @@
"default": "1",
"fieldname": "index_",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Index"
},
{
@@ -47,23 +41,30 @@
"fieldtype": "Markdown Editor",
"label": "Body"
},
{
"fieldname": "sections",
"fieldtype": "Table",
"label": "Sections",
"options": "LMS Section"
},
{
"fieldname": "index_label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Index Label",
"read_only": 1
},
{
"default": "0",
"fieldname": "include_in_preview",
"fieldtype": "Check",
"label": "Include In Preview"
},
{
"fieldname": "section_break_6",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-06-01 05:30:48.127494",
"modified": "2021-06-23 17:59:52.946515",
"modified_by": "Administrator",
"module": "LMS",
"name": "Lesson",

View File

@@ -5,66 +5,54 @@
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from ...section_parser import SectionParser
from ...md import markdown_to_html, find_macros
class Lesson(Document):
def before_save(self):
sections = SectionParser().parse(self.body or "")
self.sections = [self.make_lms_section(i, s) for i, s in enumerate(sections)]
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 s in self.sections:
if s.type == "exercise":
e = s.get_exercise()
e.lesson = self.name
e.index_ = index
e.save()
index += 1
self.update_orphan_exercises()
for name in documents:
e = frappe.get_doc(doctype_map[section], name)
e.lesson = self.name
e.index_ = index
e.save()
index += 1
self.update_orphan_documents(doctype_map[section], documents)
def update_orphan_exercises(self):
"""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 = {s.id for s in self.get("sections") if s.type=="exercise"}
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 = ""
ex.save()
def get_sections(self):
return sorted(self.get('sections'), key=lambda s: s.index)
def render_html(self):
return markdown_to_html(self.body)
def get_exercises(self):
return [frappe.get_doc("Exercise", s.id) for s in self.get("sections") if s.type=="exercise"]
if not self.body:
return []
def make_lms_section(self, index, section):
s = frappe.new_doc('LMS Section', parent_doc=self, parentfield='sections')
s.type = section.type
s.id = section.id
s.label = section.label
s.contents = section.contents
s.index = index
return s
def get_next(self):
"""Returns the number for the next lesson.
The return value would be like 1.2, 2.1 etc.
It will be None if there is no next lesson.
"""
def get_prev(self):
"""Returns the number for the prev lesson.
The return value would be like 1.2, 2.1 etc.
It will be None if there is no next lesson.
"""
macros = find_macros(self.body)
exercises = [value for name, value in macros if name == "Exercise"]
return [frappe.get_doc("Exercise", name) for name in exercises]
def get_progress(self):
return frappe.db.get_value("LMS Course Progress", {"lesson": self.name, "owner": frappe.session.user}, "status")
@@ -75,11 +63,11 @@ class Lesson(Document):
return
@frappe.whitelist()
def save_progress(lesson, batch):
def save_progress(lesson, course):
if not frappe.db.exists("LMS Batch Membership",
{
"member": frappe.session.user,
"batch": batch
"course": course
}):
return
if frappe.db.exists("LMS Course Progress",
@@ -90,11 +78,7 @@ def save_progress(lesson, batch):
return
lesson_details = frappe.get_doc("Lesson", lesson)
dynamic_content = frappe.db.count("LMS Section",
filters={
"type": ["not in", ["example", "text"]],
"parent": lesson_details.name
})
dynamic_content = find_macros(lesson_details.body)
status = "Complete"
if dynamic_content:
@@ -113,17 +97,33 @@ def update_progress(lesson):
if frappe.db.exists("LMS Course Progress", {"lesson": lesson, "owner": user}):
course_progress = frappe.get_doc("LMS Course Progress", {"lesson": lesson, "owner": user})
course_progress.status = "Complete"
course_progress.save()
course_progress.save(ignore_permissions=True)
def all_dynamic_content_submitted(lesson, user):
exercise_names = frappe.get_list("Exercise", {"lesson": lesson}, ["name"], pluck="name")
all_exercises_submitted = False
print(exercise_names)
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)
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

@@ -33,8 +33,7 @@
{
"fieldname": "title",
"fieldtype": "Data",
"label": "Title",
"unique": 1
"label": "Title"
},
{
"fieldname": "description",
@@ -120,7 +119,7 @@
"link_fieldname": "batch"
}
],
"modified": "2021-05-26 16:43:57.399747",
"modified": "2021-06-16 10:51:05.403726",
"modified_by": "Administrator",
"module": "LMS",
"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))
def after_insert(self):
create_membership(batch=self.name, 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])
create_membership(batch=self.name, course=self.course, member_type="Mentor")
def is_member(self, email, member_type=None):
"""Checks if a person is part of a batch.
@@ -43,16 +35,6 @@ class LMSBatch(Document):
filters['member_type'] = member_type
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):
messages = frappe.get_all("LMS Message", {"batch": self.name}, ["*"], order_by="creation")
for message in messages:
@@ -80,11 +62,6 @@ class LMSBatch(Document):
membership = self.get_membership(user)
return membership and membership.current_lesson
def get_learn_url(self, lesson_number):
if not lesson_number:
return
return f"/courses/{self.course}/{self.name}/learn/{lesson_number}"
@frappe.whitelist()
def save_message(message, batch):
doc = frappe.get_doc({

View File

@@ -84,7 +84,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-05-24 12:40:57.125694",
"modified": "2021-06-21 12:10:28.808803",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Batch Membership",

View File

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

View File

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

View File

@@ -8,6 +8,7 @@ from frappe.model.document import Document
import json
from ...utils import slugify
from community.query import find, find_all
from frappe.utils import flt
class LMSCourse(Document):
@staticmethod
@@ -81,11 +82,11 @@ class LMSCourse(Document):
"""
if not email:
return False
return frappe.db.exists({
"doctype": "LMS Course Mentor Mapping",
"course": self.name,
"mentor": email
})
return frappe.db.count("LMS Course Mentor Mapping",
{
"course": self.name,
"mentor": email
})
def get_student_batch(self, email):
"""Returns the batch the given student is part of.
@@ -112,7 +113,7 @@ class LMSCourse(Document):
"""Returns all chapters of this course.
"""
# TODO: chapters should have a way to specify the order
return find_all("Chapter", course=self.name, order_by="creation")
return find_all("Chapter", course=self.name, order_by="index_")
def get_batch(self, batch_name):
return find("LMS Batch", name=batch_name, course=self.name)
@@ -186,6 +187,59 @@ class LMSCourse(Document):
exercise.save()
i += 1
def get_learn_url(self, lesson_number):
if not lesson_number:
return
return f"/courses/{self.name}/learn/{lesson_number}"
def get_membership(self, member, batch=None):
filters = {
"member": member,
"course": self.name
}
if 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):
all_memberships = frappe.get_all("LMS Batch Membership", {"member": member, "course": self.name}, ["batch"])
for membership in all_memberships:
membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title")
return all_memberships
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):
return CourseOutline(self)
@@ -197,6 +251,7 @@ class CourseOutline:
self.lessons = self.get_lessons()
def get_next(self, current):
current = flt(current)
numbers = sorted(lesson['number'] for lesson in self.lessons)
try:
index = numbers.index(current)
@@ -205,6 +260,7 @@ class CourseOutline:
return None
def get_prev(self, current):
current = flt(current)
numbers = sorted(lesson['number'] for lesson in self.lessons)
try:
index = numbers.index(current)
@@ -228,7 +284,7 @@ class CourseOutline:
chapter_numbers = {c['name']: c['index_'] for c in self.chapters}
for lesson in lessons:
lesson['number'] = "{}.{}".format(chapter_numbers[lesson['chapter']], lesson['index_'])
lesson['number'] = flt("{}.{}".format(chapter_numbers[lesson['chapter']], lesson['index_']))
return lessons
@frappe.whitelist()

View File

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

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

@@ -1,10 +1,8 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class CodeRevision(Document):
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('Code Revision', {
frappe.ui.form.on('LMS Quiz', {
// refresh: function(frm) {
// }

View File

@@ -1,41 +1,41 @@
{
"actions": [],
"creation": "2021-04-07 00:26:28.806520",
"autoname": "field:title",
"creation": "2021-06-07 10:50:17.893625",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"section",
"code",
"author"
"title",
"questions",
"lesson"
],
"fields": [
{
"fieldname": "section",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Section",
"options": "LMS Section"
"fieldname": "title",
"fieldtype": "Data",
"label": "Title",
"unique": 1
},
{
"fieldname": "code",
"fieldtype": "Code",
"label": "Code"
"fieldname": "questions",
"fieldtype": "Table",
"label": "Questions",
"options": "LMS Quiz Question"
},
{
"fieldname": "author",
"fieldname": "lesson",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Author",
"options": "User"
"label": "Lesson",
"options": "Lesson"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-04-14 11:26:19.628317",
"modified": "2021-06-23 17:58:57.642873",
"modified_by": "Administrator",
"module": "LMS",
"name": "Code Revision",
"name": "LMS Quiz",
"owner": "Administrator",
"permissions": [
{
@@ -53,6 +53,5 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "section",
"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 TestCodeRevision(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

@@ -1,66 +0,0 @@
{
"actions": [],
"creation": "2021-03-05 15:10:53.906006",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"label",
"type",
"contents",
"code",
"attrs",
"index",
"id"
],
"fields": [
{
"fieldname": "label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Label"
},
{
"fieldname": "type",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Type"
},
{
"fieldname": "contents",
"fieldtype": "Markdown Editor",
"label": "Contents"
},
{
"fieldname": "code",
"fieldtype": "Code",
"label": "Code"
},
{
"fieldname": "attrs",
"fieldtype": "Long Text",
"label": "attrs"
},
{
"fieldname": "index",
"fieldtype": "Int",
"label": "Index"
},
{
"fieldname": "id",
"fieldtype": "Data",
"label": "id"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-05-19 18:55:26.019625",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Section",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -14,6 +14,10 @@ class LMSSection(Document):
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.
"""

109
community/lms/md.py Normal file
View File

@@ -0,0 +1,109 @@
"""
The md module extends markdown to add macros.
Macros can be added to the markdown text in the following format.
{{ MacroName("macro-argument") }}
These macros will be rendered using a pluggable mechanism.
Apps can provide a hook community_markdown_macro_renderers, a
dictionary mapping the macro name to the function that to render
that macro. The function will get the argument passed to the macro
as argument.
"""
import frappe
import re
from bs4 import BeautifulSoup
import markdown
from markdown import Extension
from markdown.inlinepatterns import InlineProcessor
import xml.etree.ElementTree as etree
def markdown_to_html(text):
"""Renders markdown text into html.
"""
return markdown.markdown(text, extensions=['fenced_code', MacroExtension()])
def find_macros(text):
"""Returns all macros in the given text.
>>> find_macros(text)
[
('YouTubeVideo': 'abcd1234')
('Exercise', 'two-circles'),
('Exercise', 'four-circles')
]
"""
if not text:
return []
macros = re.findall(MACRO_RE, text)
# remove the quotes around the argument
return [(name, _remove_quotes(arg)) for name, arg in macros]
def _remove_quotes(value):
"""Removes quotes around a value.
Also strips the whitespace.
>>> _remove_quotes('"hello"')
'hello'
>>> _remove_quotes("'hello'")
'hello'
>>> _remove_quotes("hello")
'hello'
"""
return value.strip(" '\"")
def get_macro_registry():
d = frappe.get_hooks("community_markdown_macro_renderers") or {}
return {name: frappe.get_attr(klass[0]) for name, klass in d.items()}
def render_macro(macro_name, macro_argument):
# stripping the quotes on either side of the argument
macro_argument = _remove_quotes(macro_argument)
registry = get_macro_registry()
if macro_name in registry:
return registry[macro_name](macro_argument)
else:
return f"<p>Unknown macro: {macro_name}</p>"
MACRO_RE = r'{{ *(\w+)\(([^{}]*)\) *}}'
class MacroExtension(Extension):
"""MacroExtension is a markdown extension to support macro syntax.
"""
def extendMarkdown(self, md):
self.md = md
pattern = MacroInlineProcessor(MACRO_RE)
pattern.md = md
md.inlinePatterns.register(pattern, 'macro', 75)
class MacroInlineProcessor(InlineProcessor):
"""MacroInlineProcessor is class that is handles the logic
of how to render each macro occurence in the markdown text.
"""
def handleMatch(self, m, data):
"""Handles each macro match and return rendered contents
for that macro as an etree node.
"""
macro = m.group(1)
arg = m.group(2)
html = render_macro(macro, arg)
html = sanitize_html(str(html))
e = etree.fromstring(html)
return e, m.start(0), m.end(0)
def sanitize_html(html):
"""Sanotize the html using BeautifulSoup.
The markdown processor request the correct markup and crashes on
any broken tags. This makes sures that all those things are fixed
before passing to the etree parser.
"""
soup = BeautifulSoup(html, features="lxml")
nodes = soup.body.children
return "<div>" + "\n".join(str(node) for node in nodes) + "</div>"

View File

@@ -2,4 +2,4 @@
"""
from .doctype.lms_course.lms_course import LMSCourse as Course
from .doctype.lms_sketch.lms_sketch import LMSSketch as Sketch
from .doctype.lms_batch_membership.lms_batch_membership import LMSBatchMembership as Membership

View File

@@ -1,84 +0,0 @@
"""Utility to split the text in the topic into multiple sections.
{{ section(type="example", id="foo") }}
circle(100, 100, 50)
{{ end }}
"""
from __future__ import annotations
from dataclasses import dataclass
import re
from typing import List, Tuple, Dict, Iterator
RE_SECTION = re.compile(r"^\{\{\s(\w+)\s*(?:\((.*)\))?\s*\}\}\s*")
class SectionParser:
def parse(self, text: str) -> Iterator[Section]:
"""Parses given text into sections and return an iterator over sections.
"""
lines = text.splitlines()
marked_lines = self.parse_lines(lines)
return self.group_sections(marked_lines)
def parse_lines(self, lines: List[str]) -> List[Tuple[str, str, str]]:
for line in lines:
m = RE_SECTION.match(line)
if m:
yield m.group(1), self.parse_attrs(m.group(2)), None
else:
yield None, None, line
def parse_attrs(self, attrs_str: str) -> Dict[str, str]:
# XXX-Anand: Hack
code = "dict({})".format(attrs_str or "")
return eval(code)
def group_sections(self, marked_lines) -> Iterator[Section]:
index = 0
def make_section(type='text', id=None, label=None, **attrs):
nonlocal index
index += 1
id = id or f"section-{index}"
label = label or id
return Section(
type=type,
id=id,
label=label,
attrs=attrs)
section = make_section("text")
for mark, attrs, line in marked_lines:
if not mark:
section.append(line)
continue
yield section
if mark == 'end':
section = make_section(type='text')
else:
section = make_section(**attrs)
yield section
@dataclass
class Section:
"""One section of the Topic.
"""
type: str
id: str
label: str
contents: str = ""
attrs: dict = None
def append(self, line):
if not line.endswith("\n"):
line = line + "\n"
self.contents += line
def __repr__(self):
attrs = dict(type=self.type, id=self.id, label=self.label, **self.attrs)
attrs_str = ", ".join(f'{k}="{v}"' for k, v in attrs.items())
return f'<Section({attrs_str})>'

View File

@@ -11,7 +11,7 @@
"apply_document_permissions": 0,
"button_label": "Save",
"creation": "2021-04-20 11:37:49.135114",
"custom_css": ".datepicker.active {\n background-color: white;\n}",
"custom_css": ".datepicker.active {\n background-color: white;\n}\n\n[data-doctype=\"Web Form\"] {\n max-width: 720px;\n margin: 6rem auto;\n}",
"doc_type": "LMS Batch",
"docstatus": 0,
"doctype": "Web Form",
@@ -19,7 +19,7 @@
"is_standard": 1,
"login_required": 1,
"max_attachment_size": 0,
"modified": "2021-06-02 15:52:06.383260",
"modified": "2021-06-15 18:49:50.530001",
"modified_by": "Administrator",
"module": "LMS",
"name": "add-a-new-batch",
@@ -38,7 +38,7 @@
{
"allow_read_on_all_link_options": 0,
"fieldname": "course",
"fieldtype": "Link",
"fieldtype": "Data",
"hidden": 1,
"label": "Course",
"max_length": 0,

View File

@@ -1,3 +0,0 @@
frappe.ready(function() {
// bind events here
})

View File

@@ -1,48 +0,0 @@
{
"accept_payment": 0,
"allow_comments": 0,
"allow_delete": 0,
"allow_edit": 0,
"allow_incomplete": 0,
"allow_multiple": 0,
"allow_print": 0,
"amount": 0.0,
"amount_based_on_field": 0,
"apply_document_permissions": 0,
"button_label": "Save",
"creation": "2021-04-15 13:32:14.171328",
"doc_type": "LMS Batch Membership",
"docstatus": 0,
"doctype": "Web Form",
"idx": 0,
"is_standard": 1,
"login_required": 1,
"max_attachment_size": 0,
"modified": "2021-04-15 13:32:14.171328",
"modified_by": "Administrator",
"module": "LMS",
"name": "join-a-batch",
"owner": "Administrator",
"payment_button_label": "Buy Now",
"published": 1,
"route": "join-a-batch",
"route_to_success_link": 0,
"show_attachments": 0,
"show_in_grid": 0,
"show_sidebar": 0,
"sidebar_items": [],
"success_url": "/join-a-batch",
"title": "Join a Batch",
"web_form_fields": [
{
"allow_read_on_all_link_options": 0,
"fieldtype": "Attach",
"hidden": 0,
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
}
]
}

View File

@@ -1,7 +0,0 @@
from __future__ import unicode_literals
import frappe
def get_context(context):
# do your magic here
pass

View File

@@ -1,34 +1,61 @@
<div class="mt-5">
<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 %}
<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(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>
{% endif %}
{% endfor %}
</div>
{% endif %}
</div>
{% if not membership %}
{% set display_class = "hide" %}
{% else %}
{% set display_class = "" %}
{% endif %}
<ul class="nav nav-tabs mt-4">
<li class="nav-item">
<a class="nav-link" id="home" href="/courses/{{course.name}}/{{batch.name}}/home">Home</a>
<a class="nav-link" id="home" href="/courses/{{course.name}}/home{{ course.query_parameter }}">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" id="learn" href="/courses/{{course.name}}/{{batch.name}}/learn">Learn</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 class="nav-item">
<a class="nav-link" id="schedule" href="/courses/{{course.name}}/{{batch.name}}/schedule">Schedule</a>
<a class="nav-link" id="schedule" href="/courses/{{course.name}}/schedule">Schedule</a>
</li> -->
<li class="nav-item">
<a class="nav-link" id="members" href="/courses/{{course.name}}/{{batch.name}}/members">Members</a>
</li>
<li class="nav-item">
<a class="nav-link" id="discussion" href="/courses/{{course.name}}/{{batch.name}}/discuss">Discussion</a>
<li class="nav-item {{ display_class }}">
<a class="nav-link" id="members" href="/courses/{{course.name}}/members{{ course.query_parameter }}">Members</a>
</li>
<!-- <li class="nav-item {{ display_class }}">
<a class="nav-link" id="discussion" href="/courses/{{course.name}}/discuss">Discussion</a>
</li> -->
<!-- <li class="nav-item">
<a class="nav-link" id="about" href="/courses/{{course.name}}/{{batch.name}}/about">About</a>
<a class="nav-link" id="about" href="/courses/{{course.name}}/about">About</a>
</li> -->
{% if 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">
<a class="nav-link" id="progress" href="/courses/{{course.name}}/{{batch.name}}/progress">Progress</a>
<a class="nav-link" id="progress" href="/courses/{{course.name}}/progress{{ course.query_parameter }}">Progress</a>
</li>
{% endif %}
</ul>
{% block script %}
<script>
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) {
selector.classList.add('active');
}
@@ -37,3 +64,4 @@
}
})
</script>
{% endblock %}

View File

@@ -1,15 +1,25 @@
<div class="chapter-teaser">
<div class="teaser-body">
<h3 class="chapter-title"><span class="mr-1">{{index}}.</span> {{ chapter.title }}</h3>
<div class="chapter-title mb-5 font-weight-bold"><span class="mr-1">{{index}}.</span> {{ chapter.title }}</div>
<div class="chapter-description">
{{ chapter.description or "" }}
</div>
<div class="chapter-lessons">
{% for lesson in chapter.get_lessons() %}
<div class="lesson-teaser">
<a {% if show_link %} class="anchor_style" href="{{ batch.get_learn_url(course.get_lesson_index(lesson.name)) }}" {% endif %}>{{ 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() %}
<a class="pull-right badge p-1 {{ lesson.get_slugified_class() }}"> <img class="progress-image" src="/assets/community/images/Vector.png"> {{ lesson.get_progress() }}</a>
<span class="ml-5 badge p-2 {{ lesson.get_slugified_class() }}"> {{ lesson.get_progress() }}</span>
{% endif %}
</div>
{% endfor %}

View File

@@ -1,5 +1,7 @@
<h2>Course Outline</h2>
<div class="mt-5">
<h3> Course Outline </h3>
{% for chapter in course.get_chapters() %}
{{ widgets.ChapterTeaser(index=loop.index, chapter=chapter, course=course, batch=batch, show_link=show_link, show_progress=show_progress)}}
{% endfor %}
{% for chapter in course.get_chapters() %}
{{ widgets.ChapterTeaser(index=loop.index, chapter=chapter, course=course, batch=batch, show_link=show_link, show_progress=show_progress)}}
{% endfor %}
</div>

View File

@@ -1,5 +1,6 @@
<h3>Instructor</h3>
<div class="instructor">
<div class="instructor-title">{{instructor.full_name}}</div>
<div class="instructor-subtitle">Created {{instructor.get_course_count()}} courses</div>
{{ widgets.Avatar(member=instructor, avatar_class="avatar-medium") }}
<a class="ml-1 instructor-title" href="/{{instructor.username}}">{{ instructor.full_name }}</a>
<div class="instructor-subtitle">Course Creator</div>
<!-- <div class="instructor-subtitle">Created {{instructor.get_course_count()}} courses</div> -->
</div>

View File

@@ -1,13 +1,13 @@
<div class="batch">
<div class="batch-details">
<div>Session every {{batch.sessions_on}}</div>
<div class="">Session every {{batch.sessions_on}}</div>
<div>{{frappe.utils.format_time(batch.start_time, "short")}} -
{{frappe.utils.format_time(batch.end_time, "short")}}
</div>
<div>Starting {{frappe.utils.format_date(batch.start_date, "medium")}}</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>
{{ widgets.Avatar(member=m, avatar_class="avatar-medium" ) }}
<span class="instructor-title">{{m.full_name}}</span>
@@ -18,9 +18,10 @@
<div class="cta">
<div class="">
{% if can_manage %}
<a href="/courses/{{course.name}}/{{batch.name}}/home" class="btn btn-primary">Manage</a>
<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>
{% 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>
{% endif %}
</div>

108
community/plugins.py Normal file
View File

@@ -0,0 +1,108 @@
"""
The plugins module provides various plugins to change the default
behaviour some parts of the community app.
A site specify what plugins to use using appropriate entries in the frappe
hooks, written in the `hooks.py`.
This module exposes two plugins: ProfileTab and PageExtension.
The ProfileTab is used to specify any additional tabs to be displayed
on the profile page of the user.
The PageExtension is used to load additinal stylesheets and scripts to
be loaded in a webpage.
"""
import frappe
class PageExtension:
"""PageExtension is a plugin to inject custom styles and scripts
into a web page.
The subclasses should overwrite the `render_header()` and
`render_footer()` methods to inject whatever styles/scripts into
the webpage.
"""
def render_header(self):
"""Returns the HTML snippet to be included in the head section
of the web page.
Typically used to include the stylesheets and javascripts to be
included in the <head> of the webpage.
"""
return ""
def render_footer(self):
"""Returns the HTML snippet to be included in the body tag at
the end of web page.
Typically used to include javascripts that need to be executed
after the page is loaded.
"""
return ""
class ProfileTab:
"""Base class for profile tabs.
Every subclass of ProfileTab must implement two methods:
- get_title()
- render()
"""
def __init__(self, user):
self.user = user
def get_title(self):
"""Returns the title of the tab.
Every subclass must implement this.
"""
raise NotImplementedError()
def render(self):
"""Renders the contents of the tab as HTML.
Every subclass must implement this.
"""
raise NotImplementedError()
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);
--send-message: var(--c7);
--received-message: var(--c8);
--checkbox-size: 14px;
--control-bg: var(--gray-100);
}
body {
@@ -82,6 +84,7 @@ body {
border-radius: 10px;
margin: 10px 0px;
background: white;
box-shadow: 0px 5px 10px rgb(0 0 0 / 10%);
border: 1px solid #ddc;
}
@@ -156,19 +159,6 @@ img.profile-photo {
line-height: 51px;
}
.anchor_style {
color: inherit;
}
a:hover {
text-decoration: none;
color: inherit;
}
.anchor_style:hover {
text-decoration: underline
}
section {
padding: 5rem 0 5rem 0;
}
@@ -239,6 +229,10 @@ section {
margin-top: 30px;
}
input[type=checkbox] {
appearance: auto;
}
.partiallycomplete {
background: #FEF4E2;
color: #976417;

View File

@@ -10,6 +10,7 @@ h2 {
.teaser-body {
padding: 20px;
box-shadow: 0px 5px 10px rgb(0 0 0 / 10%)
}
.teaser-footer {
padding: 20px;
@@ -66,6 +67,15 @@ h2 {
}
}
.anchor_style {
color: inherit;
}
.anchor_style:hover {
text-decoration: none
}
section {
padding: 60px 0px;
}
@@ -162,7 +172,6 @@ section.lightgray {
// }
.instructor-title {
font-weight: bold;
color: black;
}
@@ -331,3 +340,9 @@ section.lightgray {
margin: 40px 0px 0px 20px;
}
}
.no-preview-message {
width: fit-content;
margin: 50px 0px 50px;
color: black;
}

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 %}
<div class="container">
{{ widgets.BatchTabs(course=course, batch=batch) }}
{{ widgets.BatchTabs(course=course, membership=membership) }}
<div class="messages-container mt-5">
{{ widgets.BatchHeader(batch_name=batch.title, member_count=member_count)}}
<ol class="messages">

View File

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

View File

@@ -8,31 +8,32 @@
{% endblock %}
{% block content %}
{% set invite_link = frappe.utils.get_url() + "/courses/" + course.name + "/" + batch.name + "/join" %}
<div class="container mt-5">
{{ widgets.BatchTabs(course=course, batch=batch) }}
<div>
<h1 class="mt-5">{{ batch.title }}</h1>
</div>
<div class="course-details">
{{ widgets.CourseOutline(course=course, batch=batch, show_link=True, show_progress=True) }}
{{ widgets.BatchTabs(course=course, membership=membership) }}
<div class="course-details mt-5">
{{ widgets.CourseOutline(course=course, batch=batch, show_link=membership, show_progress=True) }}
</div>
{% if batch %}
<div class="w-25">
<h2>Batch Schedule</h2>
<h3>Batch Schedule</h3>
{{ widgets.RenderBatch(course=course, batch=batch) }}
</div>
{% if batch.description %}
<h2>Batch Details</h2>
{{ frappe.utils.md_to_html(batch.description) }}
<div class="mt-5">
<h3>Batch Details</h3>
{{ frappe.utils.md_to_html(batch.description) }}
</div>
{% endif %}
{% endif %}
{% if course.is_mentor(frappe.session.user) %}
{% set invite_link = frappe.utils.get_url() + "/courses/" + course.name + "/join?batch=" + batch.name %}
<div class="">
<h2> Invite Members </h2>
<a href="" class="anchor_style mr-5" id="invite-link" data-link="{{ invite_link }}">Get Batch Invitation
<h3> Invite Members </h3>
<a href="" class="" id="invite-link" data-link="{{ invite_link }}">Get Batch Invitation
Link</a>
<small id="copy-message" class="text-muted pull-right" style="display: none;">Copied to Clipboard.</small>
<small id="copy-message" class="text-muted" style="display: none;">Copied to Clipboard.</small>
</div>
{% endif %}
@@ -52,7 +53,7 @@
$("#copy-message").slideDown(function () {
setTimeout(function () {
$("#copy-message").slideUp();
}, 5000);
}, 2000);
});
})
})

View File

@@ -13,9 +13,9 @@
<div class='page-card-head'>
<span class='indicator blue password-box'>Login Required</span>
</div>
<div class=''>Please log in to confirm to join the course {{ batch.course_title }}.</div>
<div class=''>Please log in to confirm joining the course {{ batch.course_title }}.</div>
<a type="submit" id="login" class="btn btn-primary w-100"
href="/login?redirect-to=/courses/{{ batch.course }}/{{ batch.name }}/join">{{_("Login")}}</a>
href="/login?redirect-to=/courses/{{ batch.course }}/join?batch={{ batch.name }}">{{_("Login")}}</a>
</div>
{% elif already_a_member %}
@@ -26,7 +26,7 @@
</div>
<div class=''>You are already a member of the batch {{ batch.title }} for the course {{ batch.course_title }}.
</div>
<a type="submit" id="batch-home" class="btn btn-primary w-100" href="/courses/{{batch.course}}/{{batch.name}}/home">{{_("Go to Batch Home")}}</a>
<a type="submit" id="batch-home" class="btn btn-primary w-100" href="">{{_("Go to Batch Home")}}</a>
</div>
{% else %}
@@ -38,23 +38,21 @@
<div>Please provide your confirmation to be a part of the batch {{ batch.title }} for the course
{{ batch.course_title }}.
</div>
<a type="submit" id="confirm" class="btn btn-primary w-100" data-batch="{{ batch.name | urlencode }}"
data-course="{{ batch.course | urlencode }}">{{_("Confirm")}}</a>
<a type="submit" id="confirm" class="btn btn-primary w-100">{{_("Confirm")}}</a>
</div>
{% endif %}
{% endblock %}
{% block script %}
<script>
frappe.ready(() => {
var confirm_element = $("#confirm");
var batch = decodeURIComponent(confirm_element.attr("data-batch"));
var course = decodeURIComponent(confirm_element.attr("data-course"));
confirm_element.click((e) => {
frappe.ready(() => {
$("#confirm").click((e) => {
frappe.call({
"method": "community.lms.doctype.lms_batch_membership.lms_batch_membership.create_membership",
"args": {
"batch": batch
"batch": {{ batch.name }},
"course": {{ batch.course }}
},
"callback": (data) => {
if (data.message == "OK") {
@@ -63,7 +61,7 @@
clear: true
});
setTimeout(function () {
window.location.href = "/courses/" + course + "/" + batch + "/home";
window.location.href = "/courses/{{ batch.course }}/home";
}, 2000);
}
}

View File

@@ -9,63 +9,40 @@
</style>
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
<link rel="stylesheet" href="{{ livecode_url }}/static/codemirror/lib/codemirror.css">
<link rel="stylesheet" href="/assets/css/lms.css">
<link rel="stylesheet" href="/assets/frappe/css/hljs-night-owl.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>
{% for ext in page_extensions %}
{{ ext.render_header() }}
{% endfor %}
<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 %}
<div class="container">
{{ widgets.BatchTabs(course=course, batch=batch) }}
{{ widgets.BatchTabs(course=course, membership=membership) }}
<div class="lesson-page">
<h2 class="title {% if course.is_mentor(frappe.session.user) %} is_mentor {% endif %}" data-name="{{ lesson.name }}" data-batch="{{ batch.name }}">{{ 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>
{% for s in lesson.get_sections() %}
<div class="section section-{{ s.type }}">
{{ render_section(s) }}
{% if membership or lesson.include_in_preview %}
{{ lesson.render_html() }}
{% else %}
<div class="no-preview-message">
<span>This lesson is not available for Preview. Please join the course to access this lesson.</span>
<a href="/courses/{{ course.name }}">Checkout Course Details.</a>
</div>
{% endfor %}
{% endif %}
{% if membership %}
{{ pagination(prev_chap, prev_url, next_chap, next_url) }}
{% endif %}
</div>
</div>
{% endblock %}
{% macro render_section(s) %}
{% if s.type == "text" %}
{{ render_section_text(s) }}
{% elif s.type == "example" or s.type == "code" %}
{{ LiveCodeEditor(s.name,
code=s.get_latest_code_for_user(),
reset_code=s.contents,
is_exercise=False)
}}
{% elif s.type == "exercise" %}
{{ widgets.Exercise(exercise=s.get_exercise())}}
{% else %}
<div>Unknown section type: {{s.type}}</div>
{% endif %}
{% endmacro %}
{% macro render_section_text(s) %}
<div class="row">
<div class="col-md-9">
{{ frappe.utils.md_to_html(s.contents) }}
</div>
</div>
{% endmacro %}
{% macro pagination(prev_chap, prev_url, next_chap, next_url) %}
<div class="lesson-pagination">
{% if prev_url %}
@@ -84,18 +61,7 @@
{%- block script %}
{{ super() }}
{{ LiveCodeEditorJS() }}
<script type="text/javascript">
$(function() {
var batch_name = "{{ batch.name }}";
var lesson_name = "{{ lesson.name }}";
frappe.call("community.lms.api.save_current_lesson", {
"batch_name": batch_name,
"lesson_name": lesson_name
})
})
</script>
{% for ext in page_extensions %}
{{ ext.render_footer() }}
{% endfor %}
{%- endblock %}

View File

@@ -1,11 +1,69 @@
frappe.ready(() => {
if (!$(".title").hasClass("is_mentor")) {
/* Save Lesson Progress */
if ($(".title").attr("data-membership") && !$(".title").hasClass("is_mentor")) {
frappe.call({
method: "community.lms.doctype.lesson.lesson.save_progress",
args: {
lesson: $(".title").attr("data-name"),
batch: $(".title").attr("data-batch")
lesson: $(".title").attr("data-lesson"),
course: $(".title").attr("data-course")
}
})
}
/* 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

@@ -1,6 +1,9 @@
from re import I
import frappe
from . import utils
from frappe.utils import cstr
from community.www import batch
def get_context(context):
utils.get_common_context(context)
@@ -10,10 +13,12 @@ def get_context(context):
lesson_number = f"{chapter_index}.{lesson_index}"
course_name = context.course.name
if not chapter_index or not lesson_index:
index_ = get_lesson_index(context.course, context.batch, frappe.session.user) or "1.1"
frappe.local.flags.redirect_location = context.batch.get_learn_url(index_)
if context.batch:
index_ = get_lesson_index(context.course, context.batch, frappe.session.user) or "1.1"
else:
index_ = "1.1"
frappe.local.flags.redirect_location = context.course.get_learn_url(index_) + context.course.query_parameter
raise frappe.Redirect
context.lesson = context.course.get_lesson(chapter_index, lesson_index)
@@ -25,16 +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.batch.get_learn_url(next_)
context.prev_url = context.batch.get_learn_url(prev_)
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.page_extensions = get_page_extensions()
def get_chapter_title(course_name, lesson_number):
if not lesson_number:
return
chapter_index = lesson_number.split(".")[0]
lesson_index = lesson_number.split(".")[1]
lesson_split = cstr(lesson_number).split(".")
chapter_index = lesson_split[0]
lesson_index = lesson_split[1]
chapter_name = frappe.db.get_value("Chapter", {"course": course_name, "index_": chapter_index}, "name")
return frappe.db.get_value("Lesson", {"chapter": chapter_name, "index_": lesson_index}, "title")
@@ -42,4 +48,8 @@ def get_lesson_index(course, batch, user):
lesson = batch.get_current_lesson(user)
return lesson and course.get_lesson_index(lesson)
def get_page_extensions():
default_value = ["community.community.plugins.PageExtension"]
classnames = frappe.get_hooks("community_lesson_page_extensions") or default_value
extensions = [frappe.get_attr(name)() for name in classnames]
return extensions

View File

@@ -10,7 +10,7 @@
{% block content %}
<div class="container">
{{ widgets.BatchTabs(course=course, batch=batch) }}
{{ widgets.BatchTabs(course=course, membership=membership) }}
{{ MembersList(members)}}
</div>
{% endblock %}
@@ -19,23 +19,19 @@
{% macro MembersList(members) %}
<div class="mt-5">
{% for member in members %}
<div class="row mb-5">
<div>
{{ widgets.Avatar(member=member, avatar_class="avatar-large") }}
</div>
<div class="col">
<div class="row ml-1">
<a class="anchor_style" href="/{{member.username}}">
<div class="d-flex align-items-center">
{{ widgets.Avatar(member=member, avatar_class="avatar-large") }}
<div class="d-flex flex-column ml-2">
<div class="d-flex">
<a class="anchor_style ml-2" href="/{{member.username}}">
<h3>{{ member.full_name }}</h3>
</a>
{% if course.is_mentor(member.name) %}
<div class="ml-2">
<div class="badge badge-success">Mentor</div>
</div>
<div class="badge badge-success ml-2 align-self-start">Mentor</div>
{% endif %}
</div>
{% if member.bio %}
<i>{{member.bio}}</i>
<i class="ml-2">{{member.bio}}</i>
{% endif %}
</div>
</div>

View File

@@ -3,4 +3,5 @@ from . import utils
def get_context(context):
utils.get_common_context(context)
print(context.members[0].bio)
if not context.membership:
utils.redirect_to_lesson(context.course)

View File

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

View File

@@ -17,9 +17,8 @@ def get_context(context):
class BatchReport:
def __init__(self, course, batch):
self.submissions = get_submissions(batch)
self.submissions = get_submissions(course, batch)
self.exercises = self.get_exercises(course.name)
self.submissions_by_exercise = defaultdict(list)
for s in self.submissions:
self.submissions_by_exercise[s.exercise].append(s)
@@ -30,11 +29,12 @@ class BatchReport:
def get_submissions_of_exercise(self, exercise_name):
return self.submissions_by_exercise[exercise_name]
def get_submissions(batch):
students = batch.get_students()
def get_submissions(course, batch):
students = course.get_students(batch.name)
if not len(students):
return []
students_map = {s.email: s for s in students}
names, values = nparams("s", students_map.keys())
sql = """
select owner, exercise, name, solution, creation, image
from (
@@ -45,7 +45,6 @@ def get_submissions(batch):
""".format(names)
data = frappe.db.sql(sql, values=values, as_dict=True)
for row in data:
row['owner'] = students_map[row['owner']]
return data

View File

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

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 %}
@@ -12,26 +13,25 @@
<div class="mb-5">
<a class="anchor_style" href="/courses">Courses</a> / <span class="text-muted">{{ course.title }}</span>
</div>
<h1 id="course-title" data-course="{{course.name}}">{{course.title}}</h1>
<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>
<div class="row">
<div class="col-lg-8 col-md-12">
<div class="">
<div class="">
<div class="course-details">
{{ CourseVideo(course) }}
{{ CourseDescription(course) }}
{{ BatchSection(course) }}
{{ widgets.CourseOutline(course=course, show_link=False) }}
</div>
</div>
<div class="col-lg-4 col-md-12">
<div class="sidebar">
{{ widgets.InstructorSection(instructor=course.get_instructor()) }}
</div>
<div class="sidebar">
{{ MentorsSection(course.get_mentors(), course.is_mentor(frappe.session.user), course.name) }}
{{ BatchSection(course) }}
{{ widgets.CourseOutline(course=course, show_link=membership) }}
</div>
</div>
</div>
@@ -49,28 +49,31 @@
{% endmacro %}
{% macro CourseDescription(course) %}
<h2>Course Description</h2>
<div class="mt-5">
<h3>Course Description</h3>
<div class="course-description">
{{ frappe.utils.md_to_html(course.description) }}
<div class="course-description text-justify">
{{ frappe.utils.md_to_html(course.description) }}
</div>
</div>
{% endmacro %}
{% macro BatchSection(course) %}
{% if course.is_mentor(frappe.session.user) %}
{{ BatchSectionForMentors(course, course.get_batches(mentor=frappe.session.user)) }}
{% else %}
{{ BatchSectionForStudents(course, course.get_upcoming_batches()) }}
{% endif %}
<div class="row">
<div class="col-lg-8 col-md-12">
{% if course.is_mentor(frappe.session.user) %}
{{ BatchSectionForMentors(course, course.get_batches(mentor=frappe.session.user)) }}
{% else %}
{{ BatchSectionForStudents(course, course.get_upcoming_batches()) }}
{% endif %}
</div>
</div>
{% endmacro %}
{% macro BatchSectionForMentors(course, mentor_batches) %}
<h2>Your Batches</h2>
{% if mentor_batches %}
<!-- <div class="alert alert-secondary">
You are a mentor for this course. Manage your batches or create a new batch from here.
</div> -->
<div class="row">
{% for batch in mentor_batches %}
@@ -80,26 +83,29 @@
{% endfor %}
</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>
{% else %}
<div class="mentor_message">
<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>
{% endif %}
{% endmacro %}
{% macro BatchSectionForStudents(course, upcoming_batches) %}
{% if upcoming_batches %}
<h2>Upcoming Batches</h2>
<div class="row">
{% for batch in upcoming_batches %}
<div class="col-lg-4 col-md-6">
{{ widgets.RenderBatch(course=course, batch=batch, can_join=True) }}
<div class="mt-5">
<h3 class="upcoming">Upcoming Batches</h3>
<div class="row">
{% for batch in upcoming_batches %}
<div class="col-lg-4 col-md-6">
{{ widgets.RenderBatch(course=course, batch=batch, can_join=True) }}
</div>
{% endfor %}
</div>
{% endfor %}
{% else %}
<div class="mt-5 upcoming">There are no Upcoming Batches for this course currently.</div>
{% endif %}
</div>
{% endif %}
{% endmacro %}

View File

@@ -1,72 +1,78 @@
frappe.ready(() => {
if (frappe.session.user != "Guest") {
frappe.call({
'method': 'community.lms.doctype.lms_mentor_request.lms_mentor_request.has_requested',
'args': {
course: decodeURIComponent($("#course-title").attr("data-course")),
},
'callback': (data) => {
if (data.message > 0) {
$("#mentor-request").addClass("hide");
$("#already-applied").removeClass("hide")
}
}
})
}
if (frappe.session.user != "Guest") {
frappe.call({
'method': 'community.lms.doctype.lms_mentor_request.lms_mentor_request.has_requested',
'args': {
course: decodeURIComponent($("#course-title").attr("data-course")),
},
'callback': (data) => {
if (data.message > 0) {
$("#mentor-request").addClass("hide");
$("#already-applied").removeClass("hide")
}
}
})
}
$("#apply-now").click((e) => {
$("#apply-now").click((e) => {
e.preventDefault();
if (frappe.session.user == "Guest") {
window.location.href = `/login?redirect-to=/courses/${$(e.currentTarget).attr("data-course")}`;
return;
}
frappe.call({
"method": "community.lms.doctype.lms_mentor_request.lms_mentor_request.create_request",
"args": {
"course": decodeURIComponent($(e.currentTarget).attr("data-course"))
},
"callback": (data) => {
if (data.message == "OK") {
$("#mentor-request").addClass("hide");
$("#already-applied").removeClass("hide")
}
}
})
})
if (frappe.session.user == "Guest") {
window.location.href = `/login?redirect-to=/courses/${$(e.currentTarget).attr("data-course")}`;
return;
}
frappe.call({
"method": "community.lms.doctype.lms_mentor_request.lms_mentor_request.create_request",
"args": {
"course": decodeURIComponent($(e.currentTarget).attr("data-course"))
},
"callback": (data) => {
if (data.message == "OK") {
$("#mentor-request").addClass("hide");
$("#already-applied").removeClass("hide")
}
}
})
})
$("#cancel-request").click((e) => {
$("#cancel-request").click((e) => {
e.preventDefault()
frappe.call({
"method": "community.lms.doctype.lms_mentor_request.lms_mentor_request.cancel_request",
"args": {
"course": decodeURIComponent($(e.currentTarget).attr("data-course"))
},
"callback": (data) => {
if (data.message == "OK") {
$("#mentor-request").removeClass("hide");
$("#already-applied").addClass("hide")
}
}
})
})
frappe.call({
"method": "community.lms.doctype.lms_mentor_request.lms_mentor_request.cancel_request",
"args": {
"course": decodeURIComponent($(e.currentTarget).attr("data-course"))
},
"callback": (data) => {
if (data.message == "OK") {
$("#mentor-request").removeClass("hide");
$("#already-applied").addClass("hide")
}
}
})
})
$(".join-batch").click((e) => {
e.preventDefault()
if (frappe.session.user == "Guest") {
window.location.href = `/login?redirect-to=/courses/${$(e.currentTarget).attr("data-course")}`;
return;
}
batch = decodeURIComponent($(e.currentTarget).attr("data-batch"))
frappe.call({
"method": "community.lms.doctype.lms_batch_membership.lms_batch_membership.create_membership",
"args": {
"batch": batch
},
"callback": (data) => {
if (data.message == "OK") {
frappe.msgprint(__("You are now a student of this course."))
}
}
})
})
$(".join-batch").click((e) => {
e.preventDefault();
var course = $(e.currentTarget).attr("data-course")
if (frappe.session.user == "Guest") {
window.location.href = `/login?redirect-to=/courses/${course}`;
return;
}
var batch = $(e.currentTarget).attr("data-batch");
batch = batch ? decodeURIComponent(batch) : "";
frappe.call({
"method": "community.lms.doctype.lms_batch_membership.lms_batch_membership.create_membership",
"args": {
"batch": batch ? batch : "",
"course": course
},
"callback": (data) => {
if (data.message == "OK") {
frappe.msgprint(__("You are now a student of this course."));
setTimeout(function () {
window.location.href = `/courses/${course}/home`;
}, 2000);
}
}
})
})
})

View File

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

@@ -10,19 +10,17 @@
{% block content %}
<section class="top-section" style="padding: 1rem 0rem;">
<div class='container pb-5'>
<h1>{{ 'Courses' }}</h1>
</div>
<div class='container'>
<h4 class="mt-5">{{ 'All Courses' }}</h4>
<div class="row mt-5">
{% for course in courses %}
{{ course_card(course) }}
{% endfor %}
{% if courses %}
<!-- {% if courses %}
{% for n in range( (3 - (courses|length)) %3) %}
{{ null_card() }}
{% endfor %}
{% endif %}
{% endif %} -->
</div>
</div>
</section>
@@ -31,8 +29,8 @@
{% macro course_card(course) %}
<div class="col-sm-4 mb-4 text-left">
<a class="card-links" style="color: inherit;" href="/courses/{{course.name}}">
<div class="card h-100">
<a class="anchor_style" style="color: inherit;" href="/courses/{{course.name}}">
<div class="card h-100" style="box-shadow: 0px 5px 10px rgb(0 0 0 / 10%);">
<div class='card-body'>
<h5 class='card-title'>{{ course.title }}</h5>
{% if course.description %}

View File

@@ -1,7 +1,7 @@
{% macro hackathon_card(hackathon) %}
<div class="col-sm-4 mb-4 text-left">
<a href="/hackathons/{{ hackathon.name }}" class="no-decoration no-underline">
<div class="card h-100">
<div class="card h-100" style="box-shadow: 0px 5px 10px rgb(0 0 0 / 10%);">
<div class='card-body'>
<h5 class='card-title'>{{ hackathon.name }}</h5>
</div>
@@ -12,7 +12,7 @@
{% macro null_card() %}
<div class="col-sm-4 mb-4 text-left">
<div class="h-100 d-none d-sm-block" style="border: 1px solid rgba(209,216,221,0.5);border-radius: 0.25rem;background-color: rgb(250, 251, 252);">
<div class="h-100 d-none d-sm-block" style="box-shadow: 0px 5px 10px rgb(0 0 0 / 10%);border-radius: 0.25rem;background-color: rgb(250, 251, 252);">
</div>
</div>
{% endmacro %}
{% endmacro %}

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

View File

@@ -1,2 +1,5 @@
frappe
websocket_client
markdown
beautifulsoup4
lxml