Compare commits

..

1 Commits

Author SHA1 Message Date
pateljannat
05f28430b9 feat: quiz doctypes 2021-05-31 10:16:21 +05:30
179 changed files with 2713 additions and 5395 deletions

3
.gitignore vendored
View File

@@ -5,6 +5,3 @@
tags tags
community/docs/current community/docs/current
community/public/dist community/public/dist
__pycache__/
*.py[cod]
*$py.class

View File

@@ -1,5 +1,5 @@
{% set color = member.get_palette() %} {% set color = member.get_palette() %}
<a class="button-links" href="/{{member.username}}"> <a href="/{{member.username}}">
<span class="avatar {{ avatar_class }}" title="{{ member.full_name }}"> <span class="avatar {{ avatar_class }}" title="{{ member.full_name }}">
{% if member.user_image %} {% if member.user_image %}
<img class="avatar-frame standard-image" style="object-fit: cover;" src="{{ member.user_image }}" title="{{ member.full_name }}"> <img class="avatar-frame standard-image" style="object-fit: cover;" src="{{ member.user_image }}" title="{{ member.full_name }}">

View File

@@ -1,373 +0,0 @@
[
{
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "User",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "linkedin",
"fieldtype": "Data",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "mobile_no",
"label": "LinkedIn ID",
"length": 0,
"mandatory_depends_on": null,
"modified": "2021-06-30 14:46:55.834145",
"name": "User-linkedin",
"no_copy": 0,
"non_negative": 0,
"options": null,
"parent": null,
"parentfield": null,
"parenttype": null,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"translatable": 1,
"unique": 0,
"width": null
},
{
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "User",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "github",
"fieldtype": "Data",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "linkedin",
"label": "Github ID",
"length": 0,
"mandatory_depends_on": null,
"modified": "2021-06-30 14:46:55.834145",
"name": "User-github",
"no_copy": 0,
"non_negative": 0,
"options": null,
"parent": null,
"parentfield": null,
"parenttype": null,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"translatable": 1,
"unique": 0,
"width": null
},
{
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "User",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "medium",
"fieldtype": "Data",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "github",
"label": "Medium ID",
"length": 0,
"mandatory_depends_on": null,
"modified": "2021-06-30 14:46:55.834145",
"name": "User-medium",
"no_copy": 0,
"non_negative": 0,
"options": null,
"parent": null,
"parentfield": null,
"parenttype": null,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"translatable": 1,
"unique": 0,
"width": null
},
{
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "User",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "city",
"fieldtype": "Data",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "mute_sounds",
"label": "City",
"length": 0,
"mandatory_depends_on": null,
"modified": "2021-06-30 14:46:55.834145",
"name": "User-city",
"no_copy": 0,
"non_negative": 0,
"options": null,
"parent": null,
"parentfield": null,
"parenttype": null,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"translatable": 1,
"unique": 0,
"width": null
},
{
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "User",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "college",
"fieldtype": "Data",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "city",
"label": "College Name",
"length": 0,
"mandatory_depends_on": null,
"modified": "2021-06-30 14:46:55.834145",
"name": "User-college",
"no_copy": 0,
"non_negative": 0,
"options": null,
"parent": null,
"parentfield": null,
"parenttype": null,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"translatable": 1,
"unique": 0,
"width": null
},
{
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "User",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "branch",
"fieldtype": "Data",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "college",
"label": "Branch",
"length": 0,
"mandatory_depends_on": null,
"modified": "2021-06-30 14:46:55.834145",
"name": "User-branch",
"no_copy": 0,
"non_negative": 0,
"options": null,
"parent": null,
"parentfield": null,
"parenttype": null,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"translatable": 1,
"unique": 0,
"width": null
},
{
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "User",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "profession",
"fieldtype": "Data",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "medium",
"label": "Profession",
"length": 0,
"mandatory_depends_on": null,
"modified": "2021-06-30 14:46:55.834145",
"name": "User-profession",
"no_copy": 0,
"non_negative": 0,
"options": null,
"parent": null,
"parentfield": null,
"parenttype": null,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"translatable": 1,
"unique": 0,
"width": null
}
]

View File

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

View File

@@ -104,8 +104,6 @@ doc_events = {
# ] # ]
#} #}
fixtures = ["Custom Field"]
# Testing # Testing
# ------- # -------
@@ -136,16 +134,16 @@ primary_rules = [
{"from_route": "/courses/<course>/<topic>", "to_route": "courses/topic"}, {"from_route": "/courses/<course>/<topic>", "to_route": "courses/topic"},
{"from_route": "/hackathons/<hackathon>", "to_route": "hackathons/hackathon"}, {"from_route": "/hackathons/<hackathon>", "to_route": "hackathons/hackathon"},
{"from_route": "/hackathons/<hackathon>/<project>", "to_route": "hackathons/project"}, {"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": "/add-a-new-batch", "to_route": "add-a-new-batch"},
{"from_route": "/courses/<course>/home", "to_route": "batch/home"}, {"from_route": "/courses/<course>/<batch>/home", "to_route": "batch/home"},
{"from_route": "/courses/<course>/learn", "to_route": "batch/learn"}, {"from_route": "/courses/<course>/<batch>/learn", "to_route": "batch/learn"},
{"from_route": "/courses/<course>/learn/<int:chapter>.<int:lesson>", "to_route": "batch/learn"}, {"from_route": "/courses/<course>/<batch>/learn/<int:chapter>.<int:lesson>", "to_route": "batch/learn"},
{"from_route": "/courses/<course>/schedule", "to_route": "batch/schedule"}, {"from_route": "/courses/<course>/<batch>/schedule", "to_route": "batch/schedule"},
{"from_route": "/courses/<course>/members", "to_route": "batch/members"}, {"from_route": "/courses/<course>/<batch>/members", "to_route": "batch/members"},
{"from_route": "/courses/<course>/discuss", "to_route": "batch/discuss"}, {"from_route": "/courses/<course>/<batch>/discuss", "to_route": "batch/discuss"},
{"from_route": "/courses/<course>/about", "to_route": "batch/about"}, {"from_route": "/courses/<course>/<batch>/about", "to_route": "batch/about"},
{"from_route": "/courses/<course>/progress", "to_route": "batch/progress"}, {"from_route": "/courses/<course>/<batch>/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 # Any frappe default URL is blocked by profile-rules, add it here to unblock it
@@ -162,12 +160,10 @@ whitelist = [
"/socket.io", "/socket.io",
"/hackathons", "/hackathons",
"/dashboard", "/dashboard",
"/join-request", "/join-request"
"/add-a-new-batch", "/add-a-new-batch",
"/new-sign-up", "/new-sign-up",
"/message", "/message"
"/about",
"/edit-profile"
] ]
whitelist_rules = [{"from_route": p, "to_route": p[1:]} for p in whitelist] whitelist_rules = [{"from_route": p, "to_route": p[1:]} for p in whitelist]
@@ -178,28 +174,4 @@ profile_rules = [
website_route_rules = primary_rules + whitelist_rules + profile_rules website_route_rules = primary_rules + whitelist_rules + profile_rules
website_redirects = [
{"source": "/update-profile", "target": "/edit-profile"},
]
update_website_context = 'community.widgets.update_website_context' 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,6 +15,13 @@ def autosave_section(section, code):
doc.insert() doc.insert()
return {"name": doc.name} 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() @frappe.whitelist()
def submit_solution(exercise, code): def submit_solution(exercise, code):
"""Submits a solution. """Submits a solution.
@@ -29,14 +36,14 @@ def submit_solution(exercise, code):
return {"name": doc.name, "creation": doc.creation} return {"name": doc.name, "creation": doc.creation}
@frappe.whitelist() @frappe.whitelist()
def save_current_lesson(course_name, lesson_name): def save_current_lesson(batch_name, lesson_name):
"""Saves the current lesson for a student/mentor. """Saves the current lesson for a student/mentor.
""" """
name = frappe.get_value( name = frappe.get_value(
doctype="LMS Batch Membership", doctype="LMS Batch Membership",
filters={ filters={
"course": course_name, "batch": batch_name,
"member": frappe.session.user "member_email": frappe.session.user
}, },
fieldname="name") fieldname="name")
if not name: if not name:

View File

@@ -2,15 +2,7 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('Chapter', { frappe.ui.form.on('Chapter', {
// refresh: function(frm) {
onload: function (frm) { // }
frm.set_query("lesson", "lessons", function () {
return {
filters: {
"chapter": frm.doc.name,
}
};
});
}
}); });

View File

@@ -9,7 +9,8 @@
"course", "course",
"title", "title",
"description", "description",
"lessons" "locked",
"index_"
], ],
"fields": [ "fields": [
{ {
@@ -23,6 +24,12 @@
"fieldtype": "Markdown Editor", "fieldtype": "Markdown Editor",
"label": "Description" "label": "Description"
}, },
{
"default": "0",
"fieldname": "locked",
"fieldtype": "Check",
"label": "Locked"
},
{ {
"fieldname": "course", "fieldname": "course",
"fieldtype": "Link", "fieldtype": "Link",
@@ -31,10 +38,10 @@
"options": "LMS Course" "options": "LMS Course"
}, },
{ {
"fieldname": "lessons", "default": "1",
"fieldtype": "Table", "fieldname": "index_",
"label": "Lessons", "fieldtype": "Int",
"options": "Lessons" "label": "Index"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
@@ -45,7 +52,7 @@
"link_fieldname": "chapter" "link_fieldname": "chapter"
} }
], ],
"modified": "2021-07-27 16:28:08.667964", "modified": "2021-05-13 21:05:20.531890",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Chapter", "name": "Chapter",

View File

@@ -7,4 +7,9 @@ import frappe
from frappe.model.document import Document from frappe.model.document import Document
class Chapter(Document): class Chapter(Document):
pass def get_lessons(self):
rows = frappe.db.get_all("Lesson",
filters={"chapter": self.name},
fields='*',
order_by="index_")
return [frappe.get_doc(dict(row, doctype='Lesson')) for row in rows]

View File

@@ -1,32 +0,0 @@
{
"actions": [],
"creation": "2021-07-27 16:25:02.903245",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"chapter"
],
"fields": [
{
"fieldname": "chapter",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Chapter",
"options": "Chapter",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-07-27 16:25:02.903245",
"modified_by": "Administrator",
"module": "LMS",
"name": "Chapters",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -1,8 +0,0 @@
# Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class Chapters(Document):
pass

View File

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

View File

@@ -1,41 +1,41 @@
{ {
"actions": [], "actions": [],
"creation": "2021-06-28 13:36:36.146718", "creation": "2021-04-07 00:26:28.806520",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"review", "section",
"rating", "code",
"course" "author"
], ],
"fields": [ "fields": [
{ {
"fieldname": "review", "fieldname": "section",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Review"
},
{
"fieldname": "rating",
"fieldtype": "Rating",
"in_list_view": 1,
"label": "Rating"
},
{
"fieldname": "course",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Course", "label": "Section",
"options": "LMS Course" "options": "LMS Section"
},
{
"fieldname": "code",
"fieldtype": "Code",
"label": "Code"
},
{
"fieldname": "author",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Author",
"options": "User"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-07-05 14:57:03.841430", "modified": "2021-04-14 11:26:19.628317",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Course Review", "name": "Code Revision",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@@ -53,5 +53,6 @@
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"title_field": "section",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -1,8 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, FOSS United and contributors # Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt # For license information, please see license.txt
from __future__ import unicode_literals
# import frappe # import frappe
from frappe.model.document import Document from frappe.model.document import Document
class LMSCourseProgress(Document): class CodeRevision(Document):
pass pass

View File

@@ -1,8 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, FOSS United and Contributors # Copyright (c) 2021, FOSS United and Contributors
# See license.txt # See license.txt
from __future__ import unicode_literals
# import frappe # import frappe
import unittest import unittest
class TestLMSCourseInterest(unittest.TestCase): class TestCodeRevision(unittest.TestCase):
pass pass

View File

@@ -15,9 +15,7 @@
"hints", "hints",
"tests", "tests",
"image", "image",
"lesson", "lesson"
"index_",
"index_label"
], ],
"fields": [ "fields": [
{ {
@@ -29,7 +27,6 @@
{ {
"fieldname": "course", "fieldname": "course",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1,
"label": "Course", "label": "Course",
"options": "LMS Course" "options": "LMS Course"
}, },
@@ -76,27 +73,13 @@
{ {
"fieldname": "lesson", "fieldname": "lesson",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1,
"label": "Lesson", "label": "Lesson",
"options": "Lesson" "options": "Lesson"
},
{
"fieldname": "index_",
"fieldtype": "Int",
"label": "Index",
"read_only": 1
},
{
"fieldname": "index_label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Index Label",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-06-01 05:22:15.656013", "modified": "2021-05-20 13:23:12.340928",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Exercise", "name": "Exercise",
@@ -116,8 +99,8 @@
} }
], ],
"search_fields": "title", "search_fields": "title",
"sort_field": "index_label", "sort_field": "modified",
"sort_order": "ASC", "sort_order": "DESC",
"title_field": "title", "title_field": "title",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -3,8 +3,12 @@
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from ..lms_sketch.livecode import livecode_to_svg
class Exercise(Document): class Exercise(Document):
def before_save(self):
self.image = livecode_to_svg(None, self.answer)
def get_user_submission(self): def get_user_submission(self):
"""Returns the latest submission for this user. """Returns the latest submission for this user.
""" """
@@ -21,6 +25,7 @@ class Exercise(Document):
order_by="creation desc", order_by="creation desc",
page_length=1) page_length=1)
print("get_user_submission", result)
if result: if result:
return result[0] return result[0]
@@ -38,6 +43,8 @@ class Exercise(Document):
course = frappe.get_doc("LMS Course", self.course) course = frappe.get_doc("LMS Course", self.course)
batch = course.get_student_batch(user) batch = course.get_student_batch(user)
image = livecode_to_svg(None, code)
doc = frappe.get_doc( doc = frappe.get_doc(
doctype="Exercise Submission", doctype="Exercise Submission",
exercise=self.name, exercise=self.name,
@@ -45,8 +52,8 @@ class Exercise(Document):
course=self.course, course=self.course,
lesson=self.lesson, lesson=self.lesson,
batch=batch and batch.name, batch=batch and batch.name,
image=image,
solution=code) solution=code)
doc.insert(ignore_permissions=True) doc.insert(ignore_permissions=True)
return doc return doc

View File

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

View File

@@ -1,13 +1,8 @@
# Copyright (c) 2021, FOSS United and contributors # Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt # For license information, please see license.txt
import frappe # import frappe
from frappe.model.document import Document from frappe.model.document import Document
from ..lesson.lesson import update_progress
class ExerciseSubmission(Document): class ExerciseSubmission(Document):
pass
def after_insert(self):
course_details = frappe.get_doc("LMS Course", self.course)
if not (course_details.is_mentor(frappe.session.user) or frappe.flags.in_test):
update_progress(self.lesson)

View File

@@ -30,13 +30,10 @@ class InviteRequest(Document):
return user return user
def send_email(self): def send_email(self):
site_name = "Mon.School" subject = _("Your request has been approved.")
subject = _("Welcome to {0}!").format(site_name)
args = { args = {
"full_name": self.full_name, "full_name": self.full_name,
"signup_form_link": "/new-sign-up?invite_code={0}".format(self.name), "signup_form_link": "/new-sign-up?invite_code={0}".format(self.name),
"site_name": site_name,
"site_url": frappe.utils.get_url() "site_url": frappe.utils.get_url()
} }
frappe.sendmail( frappe.sendmail(
@@ -45,8 +42,7 @@ class InviteRequest(Document):
subject=subject, subject=subject,
header=[subject, "green"], header=[subject, "green"],
template = "lms_invite_request_approved", template = "lms_invite_request_approved",
args=args, args=args)
now=True)
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
def create_invite_request(invite_email): def create_invite_request(invite_email):
@@ -62,8 +58,7 @@ def create_invite_request(invite_email):
frappe.get_doc({ frappe.get_doc({
"doctype": "Invite Request", "doctype": "Invite Request",
"invite_email": invite_email, "invite_email": invite_email
"status": "Approved"
}).save(ignore_permissions=True) }).save(ignore_permissions=True)
return "OK" return "OK"

View File

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

View File

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

View File

@@ -7,14 +7,11 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"chapter", "chapter",
"include_in_preview", "lesson_type",
"column_break_4",
"title", "title",
"index_label", "index_",
"section_break_6",
"body", "body",
"help_section", "sections"
"help"
], ],
"fields": [ "fields": [
{ {
@@ -24,50 +21,41 @@
"label": "Chapter", "label": "Chapter",
"options": "Chapter" "options": "Chapter"
}, },
{
"default": "Video",
"fieldname": "lesson_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Lesson Type",
"options": "Video\nText\nQuiz"
},
{ {
"fieldname": "title", "fieldname": "title",
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1, "in_list_view": 1,
"label": "Title" "label": "Title"
}, },
{
"default": "1",
"fieldname": "index_",
"fieldtype": "Int",
"label": "Index"
},
{ {
"fieldname": "body", "fieldname": "body",
"fieldtype": "Markdown Editor", "fieldtype": "Markdown Editor",
"label": "Body" "label": "Body"
}, },
{ {
"fieldname": "index_label", "fieldname": "sections",
"fieldtype": "Data", "fieldtype": "Table",
"label": "Index Label", "label": "Sections",
"read_only": 1 "options": "LMS Section"
},
{
"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"
},
{
"fieldname": "help_section",
"fieldtype": "Section Break",
"label": "Help"
},
{
"fieldname": "help",
"fieldtype": "HTML"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-07-27 16:28:29.203624", "modified": "2021-05-13 20:03:51.510605",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Lesson", "name": "Lesson",

View File

@@ -5,128 +5,41 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from ...md import markdown_to_html, find_macros from ...section_parser import SectionParser
class Lesson(Document): class Lesson(Document):
def before_save(self): def before_save(self):
dynamic_documents = ["Exercise", "Quiz"] sections = SectionParser().parse(self.body or "")
for section in dynamic_documents: self.sections = [self.make_lms_section(i, s) for i, s in enumerate(sections)]
self.update_lesson_name_in_document(section) for s in self.sections:
if s.type == "exercise":
e = s.get_exercise()
e.lesson = self.name
e.save()
def update_lesson_name_in_document(self, section): def get_sections(self):
doctype_map= { return sorted(self.get('sections'), key=lambda s: s.index)
"Exercise": "Exercise",
"Quiz": "LMS Quiz"
}
macros = find_macros(self.body)
documents = [value for name, value in macros if name == section]
index = 1
for name in 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_documents(self, doctype, documents): def make_lms_section(self, index, section):
"""Updates the documents that were previously part of this lesson, s = frappe.new_doc('LMS Section', parent_doc=self, parentfield='sections')
but not any more. 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.
""" """
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 render_html(self):
return markdown_to_html(self.body)
def get_exercises(self): def get_prev(self):
if not self.body: """Returns the number for the prev lesson.
return []
macros = find_macros(self.body) The return value would be like 1.2, 2.1 etc.
exercises = [value for name, value in macros if name == "Exercise"] It will be None if there is no next lesson.
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")
def get_slugified_class(self):
if self.get_progress():
return ("").join([ s for s in self.get_progress().lower().split() ])
return
@frappe.whitelist()
def save_progress(lesson, course, status):
if not frappe.db.exists("LMS Batch Membership",
{
"member": frappe.session.user,
"course": course
}):
return
if frappe.db.exists("LMS Course Progress",
{
"lesson": lesson,
"owner": frappe.session.user,
"course": course
}):
doc = frappe.get_doc("LMS Course Progress",
{
"lesson": lesson,
"owner": frappe.session.user,
"course": course
})
doc.status = status
doc.save(ignore_permissions=True)
else:
frappe.get_doc({
"doctype": "LMS Course Progress",
"lesson": lesson,
"status": status,
}).save(ignore_permissions=True)
return "OK"
def update_progress(lesson):
user = frappe.session.user
if not all_dynamic_content_submitted(lesson, user):
return
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(ignore_permissions=True)
def all_dynamic_content_submitted(lesson, user):
all_exercises_submitted = check_all_exercise_submission(lesson, user)
all_quiz_submitted = check_all_quiz_submitted(lesson, user)
return all_exercises_submitted and all_quiz_submitted
def check_all_exercise_submission(lesson, user):
exercise_names = frappe.get_list("Exercise", {"lesson": lesson}, pluck="name", ignore_permissions=True)
if not len(exercise_names):
return True
query = {
"exercise": ["in", exercise_names],
"owner": user
}
if frappe.db.count("Exercise Submission", query) == len(exercise_names):
return True
return False
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

@@ -1,31 +0,0 @@
{
"actions": [],
"creation": "2021-07-27 16:25:48.269536",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"lesson"
],
"fields": [
{
"fieldname": "lesson",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Lesson",
"options": "Lesson"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-07-27 16:53:52.732191",
"modified_by": "Administrator",
"module": "LMS",
"name": "Lessons",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

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

View File

@@ -19,7 +19,15 @@ class LMSBatch(Document):
frappe.throw(_("You are not a mentor of the course {0}").format(course.title)) frappe.throw(_("You are not a mentor of the course {0}").format(course.title))
def after_insert(self): def after_insert(self):
create_membership(batch=self.name, course=self.course, member_type="Mentor") 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])
def is_member(self, email, member_type=None): def is_member(self, email, member_type=None):
"""Checks if a person is part of a batch. """Checks if a person is part of a batch.
@@ -35,6 +43,16 @@ class LMSBatch(Document):
filters['member_type'] = member_type filters['member_type'] = member_type
return frappe.db.exists("LMS Batch Membership", filters) return frappe.db.exists("LMS Batch Membership", filters)
def get_students(self):
"""Returns (email, full_name, username) of all the students of this batch as a list of dict.
"""
memberships = frappe.get_all(
"LMS Batch Membership",
{"batch": self.name, "member_type": "Student"},
["member"])
member_names = [m['member'] for m in memberships]
return find_all("User", name=["IN", member_names])
def get_messages(self): def get_messages(self):
messages = frappe.get_all("LMS Message", {"batch": self.name}, ["*"], order_by="creation") messages = frappe.get_all("LMS Message", {"batch": self.name}, ["*"], order_by="creation")
for message in messages: for message in messages:
@@ -62,6 +80,11 @@ class LMSBatch(Document):
membership = self.get_membership(user) membership = self.get_membership(user)
return membership and membership.current_lesson 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() @frappe.whitelist()
def save_message(message, batch): def save_message(message, batch):
doc = frappe.get_doc({ doc = frappe.get_doc({
@@ -71,38 +94,3 @@ def save_message(message, batch):
"message": message "message": message
}) })
doc.save(ignore_permissions=True) doc.save(ignore_permissions=True)
def switch_batch(course_name, email, batch_name):
"""Switches the user from the current batch of the course to a new batch.
"""
membership = frappe.get_last_doc(
"LMS Batch Membership",
filters={"course": course_name, "member": email})
batch = frappe.get_doc("LMS Batch", batch_name)
if not batch:
raise ValueError(f"Invalid Batch: {batch_name}")
if batch.course != course_name:
raise ValueError("Can not switch batches across courses")
if batch.is_member(email):
print(f"{email} is already a member of {batch.title}")
return
old_batch = frappe.get_doc("LMS Batch", membership.batch)
print("updating membership", membership.name)
membership.batch = batch_name
membership.save()
# update exercise submissions
filters = {
"owner": email,
"batch": old_batch.name
}
for name in frappe.db.get_all("Exercise Submission", filters=filters, pluck='name'):
doc = frappe.get_doc("Exercise Submission", name)
print("updating exercise submission", name)
doc.batch = batch_name
doc.save()

View File

@@ -10,5 +10,5 @@ frappe.ui.form.on('LMS Batch Membership', {
} }
}; };
}); });
} },
}); });

View File

@@ -37,7 +37,6 @@
"fieldname": "member_type", "fieldname": "member_type",
"fieldtype": "Select", "fieldtype": "Select",
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1,
"label": "Member Type", "label": "Member Type",
"options": "\nStudent\nMentor\nStaff" "options": "\nStudent\nMentor\nStaff"
}, },
@@ -45,6 +44,7 @@
"default": "Member", "default": "Member",
"fieldname": "role", "fieldname": "role",
"fieldtype": "Select", "fieldtype": "Select",
"in_standard_filter": 1,
"label": "Role", "label": "Role",
"options": "\nMember\nAdmin" "options": "\nMember\nAdmin"
}, },
@@ -63,10 +63,10 @@
{ {
"fetch_from": "batch.course", "fetch_from": "batch.course",
"fieldname": "course", "fieldname": "course",
"fieldtype": "Link", "fieldtype": "Data",
"in_list_view": 1, "in_list_view": 1,
"label": "Course", "label": "Course",
"options": "LMS Course" "read_only": 1
}, },
{ {
"fieldname": "current_lesson", "fieldname": "current_lesson",
@@ -84,7 +84,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-08-04 17:10:42.708479", "modified": "2021-05-24 12:40:57.125694",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Batch Membership", "name": "LMS Batch Membership",

View File

@@ -14,65 +14,42 @@ class LMSBatchMembership(Document):
self.validate_membership_in_different_batch_same_course() self.validate_membership_in_different_batch_same_course()
def validate_membership_in_same_batch(self): def validate_membership_in_same_batch(self):
filters={
"member": self.member,
"course": self.course,
"name": ["!=", self.name]
}
if self.batch:
filters["batch"] = self.batch
previous_membership = frappe.db.get_value("LMS Batch Membership", previous_membership = frappe.db.get_value("LMS Batch Membership",
filters, filters={
"member": self.member,
"batch": self.batch,
"name": ["!=", self.name]
},
fieldname=["member_type","member"], fieldname=["member_type","member"],
as_dict=1) as_dict=1)
if previous_membership: if previous_membership:
member_name = frappe.db.get_value("User", self.member, "full_name") member_name = frappe.db.get_value("User", self.member, "full_name")
course_title = frappe.db.get_value("LMS Course", self.course, "title") frappe.throw(_("{0} is already a {1} of {2}").format(member_name, previous_membership.member_type, self.batch))
frappe.throw(_("{0} is already a {1} of the course {2}").format(member_name, previous_membership.member_type, course_title))
def validate_membership_in_different_batch_same_course(self): def validate_membership_in_different_batch_same_course(self):
"""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
course = frappe.db.get_value("LMS Batch", self.batch, "course") course = frappe.db.get_value("LMS Batch", self.batch, "course")
memberships = frappe.get_all( previous_membership = frappe.get_all("LMS Batch Membership",
"LMS Batch Membership", filters={
filters={ "member": self.member,
"member": self.member, "name": ["!=", self.name]
"name": ["!=", self.name], },
"member_type": "Student", fields=["batch", "member_type", "name"]
"course": self.course )
},
fields=["batch", "member_type", "name"]
)
if memberships: for membership in previous_membership:
membership = memberships[0] batch_course = frappe.db.get_value("LMS Batch", membership.batch, "course")
member_name = frappe.db.get_value("User", self.member, "full_name") if batch_course == course and (membership.member_type == "Student" or self.member_type == "Student"):
frappe.throw(_("{0} is already a Student of {1} course through {2} batch").format(member_name, course, membership.batch)) 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))
@frappe.whitelist() @frappe.whitelist()
def create_membership(course, batch=None, member=None, member_type="Student", role="Member"): def create_membership(batch, member=None, member_type="Student", role="Member"):
frappe.get_doc({ frappe.get_doc({
"doctype": "LMS Batch Membership", "doctype": "LMS Batch Membership",
"batch": batch, "batch": batch,
"course": course,
"role": role, "role": role,
"member_type": member_type, "member_type": member_type,
"member": member or frappe.session.user "member": member or frappe.session.user
}).save(ignore_permissions=True) }).save(ignore_permissions=True)
return "OK" 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

@@ -2,15 +2,7 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('LMS Course', { frappe.ui.form.on('LMS Course', {
// refresh: function(frm) {
onload: function (frm) { // }
frm.set_query("chapter", "chapters", function () {
return {
filters: {
"course": frm.doc.name,
}
};
});
}
}); });

View File

@@ -1,12 +1,5 @@
{ {
"actions": [ "actions": [],
{
"action": "community.lms.doctype.lms_course.lms_course.reindex_exercises",
"action_type": "Server Action",
"group": "Reindex",
"label": "Reindex Exercises"
}
],
"allow_guest_to_view": 1, "allow_guest_to_view": 1,
"allow_rename": 1, "allow_rename": 1,
"creation": "2021-03-01 16:49:33.622422", "creation": "2021-03-01 16:49:33.622422",
@@ -15,17 +8,13 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"title", "title",
"video_link",
"image",
"column_break_3",
"tags",
"is_published", "is_published",
"upcoming", "column_break_3",
"disable_self_learning", "short_code",
"video_link",
"section_break_5", "section_break_5",
"short_introduction", "short_introduction",
"description", "description"
"chapters"
], ],
"fields": [ "fields": [
{ {
@@ -48,6 +37,11 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Published" "label": "Published"
}, },
{
"fieldname": "short_code",
"fieldtype": "Data",
"label": "Short Code"
},
{ {
"fieldname": "column_break_3", "fieldname": "column_break_3",
"fieldtype": "Column Break" "fieldtype": "Column Break"
@@ -66,34 +60,6 @@
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Short Introduction", "label": "Short Introduction",
"reqd": 1 "reqd": 1
},
{
"default": "0",
"fieldname": "disable_self_learning",
"fieldtype": "Check",
"label": "Disable Self Learning"
},
{
"fieldname": "image",
"fieldtype": "Attach Image",
"label": "Preview Image"
},
{
"fieldname": "tags",
"fieldtype": "Data",
"label": "Tags"
},
{
"default": "0",
"fieldname": "upcoming",
"fieldtype": "Check",
"label": "Is an Upcoming Course"
},
{
"fieldname": "chapters",
"fieldtype": "Table",
"label": "Chapters",
"options": "Chapters"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
@@ -113,9 +79,14 @@
"group": "Mentors", "group": "Mentors",
"link_doctype": "LMS Course Mentor Mapping", "link_doctype": "LMS Course Mentor Mapping",
"link_fieldname": "course" "link_fieldname": "course"
},
{
"group": "Mentors",
"link_doctype": "LMS Mentor Request",
"link_fieldname": "course"
} }
], ],
"modified": "2021-07-28 19:01:50.677445", "modified": "2021-05-23 18:14:32.602647",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Course", "name": "LMS Course",
@@ -138,5 +109,6 @@
"sort_field": "creation", "sort_field": "creation",
"sort_order": "DESC", "sort_order": "DESC",
"title_field": "title", "title_field": "title",
"track_changes": 1 "track_changes": 1,
"track_views": 1
} }

View File

@@ -5,46 +5,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
import json
from ...utils import slugify from ...utils import slugify
from community.query import find, find_all from community.query import find, find_all
from frappe.utils import flt, cint
from ...utils import slugify
class LMSCourse(Document): class LMSCourse(Document):
def on_update(self):
if not self.upcoming and self.has_value_changed("upcoming"):
self.send_email_to_interested_users()
def send_email_to_interested_users(self):
interested_users = frappe.get_all("LMS Course Interest",
{
"course": self.name
},
["name", "user"])
subject = self.title + " is available!"
args = {
"title": self.title,
"course_link": "/courses/{0}".format(self.name),
"app_name": frappe.db.get_single_value("System Settings", "app_name"),
"site_url": frappe.utils.get_url()
}
for user in interested_users:
args["first_name"] = frappe.db.get_value("User", user.user, "first_name")
email_args = frappe._dict(
recipients = user.user,
sender = frappe.db.get_single_value("LMS Settings", "email_sender"),
subject = subject,
header = [subject, "green"],
template = "lms_course_interest",
args = args,
now = True
)
frappe.enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args)
frappe.db.set_value("LMS Course Interest", user.name, "email_sent", True)
@staticmethod @staticmethod
def find(name): def find(name):
"""Returns the course with specified name. """Returns the course with specified name.
@@ -106,11 +70,8 @@ class LMSCourse(Document):
mentors = frappe.get_all("LMS Course Mentor Mapping", {"course": self.name}, ["mentor"]) mentors = frappe.get_all("LMS Course Mentor Mapping", {"course": self.name}, ["mentor"])
for mentor in mentors: for mentor in mentors:
member = frappe.get_doc("User", mentor.mentor) member = frappe.get_doc("User", mentor.mentor)
member.batch_count = frappe.db.count("LMS Batch Membership", # TODO: change this to count query
{ member.batch_count = len(frappe.get_all("LMS Batch Membership", {"member": member.name, "member_type": "Mentor"}))
"member": member.name,
"member_type": "Mentor"
})
course_mentors.append(member) course_mentors.append(member)
return course_mentors return course_mentors
@@ -119,11 +80,11 @@ class LMSCourse(Document):
""" """
if not email: if not email:
return False return False
return frappe.db.count("LMS Course Mentor Mapping", return frappe.db.exists({
{ "doctype": "LMS Course Mentor Mapping",
"course": self.name, "course": self.name,
"mentor": email "mentor": email
}) })
def get_student_batch(self, email): def get_student_batch(self, email):
"""Returns the batch the given student is part of. """Returns the batch the given student is part of.
@@ -149,55 +110,8 @@ class LMSCourse(Document):
def get_chapters(self): def get_chapters(self):
"""Returns all chapters of this course. """Returns all chapters of this course.
""" """
chapters = [] # TODO: chapters should have a way to specify the order
for row in self.chapters: return find_all("Chapter", course=self.name, order_by="creation")
chapter_details = frappe.db.get_value("Chapter", row.chapter,
["name", "title", "description"],
as_dict=True)
chapter_details.idx = row.idx
chapters.append(chapter_details)
return chapters
def get_lessons(self, chapter=None):
""" If chapter is passed, returns lessons of only that chapter.
Else returns lessons of all chapters of the course """
lessons = []
if chapter:
return self.get_lesson_details(chapter)
for chapter in self.get_chapters():
lesson = self.get_lesson_details(chapter)
lessons += lesson
return lessons
def get_lesson_details(self, chapter):
lessons = []
lesson_list = frappe.get_all("Lessons", {"parent": chapter.name},
["lesson", "idx"], order_by="idx")
for row in lesson_list:
lesson_details = frappe.get_doc("Lesson", row.lesson)
lesson_details.number = flt("{}.{}".format(chapter.idx, row.idx))
lessons.append(lesson_details)
return lessons
def get_slugified_chapter_title(self, chapter):
return slugify(chapter)
def get_course_progress(self):
""" Returns the course progress of the session user """
lesson_count = len(self.get_lessons())
completed_lessons = frappe.db.count("LMS Course Progress",
{
"course": self.name,
"owner": frappe.session.user,
"status": "Complete"
})
precision = cint(frappe.db.get_default("float_precision")) or 3
if not lesson_count:
return 0
return flt(((completed_lessons/lesson_count) * 100), precision)
def get_batch(self, batch_name): def get_batch(self, batch_name):
return find("LMS Batch", name=batch_name, course=self.name) return find("LMS Batch", name=batch_name, course=self.name)
@@ -222,134 +136,66 @@ class LMSCourse(Document):
visibility="Public") visibility="Public")
return batches return batches
def get_chapter(self, index):
return find("Chapter", course=self.name, index_=index)
def get_lesson(self, chapter_index, lesson_index):
chapter_name = frappe.get_value(
"Chapter",
{"course": self.name, "index_": chapter_index},
"name")
lesson_name = chapter_name and frappe.get_value(
"Lesson",
{"chapter": chapter_name, "index_": lesson_index},
"name")
return lesson_name and frappe.get_doc("Lesson", lesson_name)
def get_lesson_index(self, lesson_name): def get_lesson_index(self, lesson_name):
"""Returns the {chapter_index}.{lesson_index} for the lesson. """Returns the {chapter_index}.{lesson_index} for the lesson.
""" """
lesson = frappe.db.get_value("Lessons", {"lesson": lesson_name}, ["idx", "parent"], as_dict=True) lesson = frappe.get_doc("Lesson", lesson_name)
if not lesson: chapter = frappe.get_doc("Chapter", lesson.chapter)
return f"{chapter.index_}.{lesson.index_}"
def get_outline(self):
return CourseOutline(self)
class CourseOutline:
def __init__(self, course):
self.course = course
self.chapters = self.get_chapters()
self.lessons = self.get_lessons()
def get_next(self, current):
numbers = sorted(lesson['number'] for lesson in self.lessons)
try:
index = numbers.index(current)
return numbers[index+1]
except IndexError:
return None return None
chapter = frappe.db.get_value("Chapters", {"chapter": lesson.parent}, ["idx"], as_dict=True) def get_prev(self, current):
if not chapter: numbers = sorted(lesson['number'] for lesson in self.lessons)
try:
index = numbers.index(current)
if index == 0:
return None
return numbers[index-1]
except IndexError:
return None return None
return f"{chapter.idx}.{lesson.idx}" def get_chapters(self):
return frappe.db.get_all("Chapter",
filters={"course": self.course.name},
fields=["name", "title", "index_"])
def reindex_exercises(self): def get_lessons(self):
for i, c in enumerate(self.get_chapters(), start=1): chapters = [c['name'] for c in self.chapters]
if c.index_ != i: lessons = frappe.db.get_all("Lesson",
c.index_ = i filters={"chapter": ["IN", chapters]},
c.save() fields=["name", "title", "chapter", "index_"])
self._reindex_exercises_in_chapter(c)
def _reindex_exercises_in_chapter(self, c): chapter_numbers = {c['name']: c['index_'] for c in self.chapters}
i = 1 for lesson in lessons:
for lesson in self.get_lessons(c): lesson['number'] = "{}.{}".format(chapter_numbers[lesson['chapter']], lesson['index_'])
for exercise in lesson.get_exercises(): return lessons
exercise.index_ = i
exercise.index_label = f"{c.index_}.{i}"
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", "member_type"],
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_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_tags(self):
return self.tags.split(",") if self.tags else []
def get_reviews(self):
reviews = frappe.get_all("LMS Course Review",
{
"course": self.name
},
["review", "rating", "owner"],
order_by= "creation desc")
for review in reviews:
review.owner_details = frappe.get_doc("User", review.owner)
return reviews
def is_eligible_to_review(self, membership):
""" Checks if user is eligible to review the course """
if not membership:
return False
if frappe.db.count("LMS Course Review",
{
"course": self.name,
"owner": frappe.session.user
}):
return False
return True
def get_average_rating(self):
ratings = [review.rating for review in self.get_reviews()]
if not len(ratings):
return None
return sum(ratings)/len(ratings)
def get_progress(self, lesson):
return frappe.db.get_value("LMS Course Progress",
{
"course": self.name,
"owner": frappe.session.user,
"lesson": lesson
},
["status"])
def get_neighbours(self, current, lessons):
current = flt(current)
numbers = sorted(lesson.number for lesson in lessons)
index = numbers.index(current)
return {
"prev": numbers[index-1] if index-1 >= 0 else None,
"next": numbers[index+1] if index+1 < len(numbers) else None
}
@frappe.whitelist()
def reindex_exercises(doc):
course_data = json.loads(doc)
course = frappe.get_doc("LMS Course", course_data['name'])
course.reindex_exercises()
frappe.msgprint("All exercises in this course have been re-indexed.")

View File

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

View File

@@ -1,61 +0,0 @@
{
"actions": [],
"creation": "2021-08-06 17:37:20.184849",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"course",
"user",
"email_sent"
],
"fields": [
{
"fieldname": "course",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Course",
"options": "LMS Course"
},
{
"fieldname": "user",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "User",
"options": "User"
},
{
"default": "0",
"fieldname": "email_sent",
"fieldtype": "Check",
"label": "Email Sent",
"options": "email_sent"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-08-06 18:06:21.370741",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Course Interest",
"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

@@ -1,17 +0,0 @@
# Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
class LMSCourseInterest(Document):
pass
@frappe.whitelist()
def capture_interest(course):
frappe.get_doc({
"doctype": "LMS Course Interest",
"course": course,
"user": frappe.session.user
}).save(ignore_permissions=True)
return "OK"

View File

@@ -2,13 +2,7 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('LMS Course Mentor Mapping', { frappe.ui.form.on('LMS Course Mentor Mapping', {
onload: function(frm) { // refresh: function(frm) {
frm.set_query('mentor', function(doc) {
return { // }
filters: {
"ignore_user_type": 1,
}
};
});
},
}); });

View File

@@ -1,78 +0,0 @@
{
"actions": [],
"creation": "2021-05-31 17:20:13.388453",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"status",
"column_break_3",
"lesson",
"chapter",
"course"
],
"fields": [
{
"fetch_from": "chapter.course",
"fieldname": "course",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Course",
"options": "LMS Course",
"read_only": 1
},
{
"fetch_from": "lesson.chapter",
"fieldname": "chapter",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Chapter",
"options": "Chapter",
"read_only": 1
},
{
"fieldname": "lesson",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Lesson",
"options": "Lesson"
},
{
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
"options": "Complete\nPartially Complete\nIncomplete"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-06-02 13:05:31.114939",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Course Progress",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -1,18 +0,0 @@
# Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
class LMSCourseReview(Document):
pass
@frappe.whitelist()
def submit_review(rating, review, course):
frappe.get_doc({
"doctype": "LMS Course Review",
"rating": rating,
"review": review,
"course": course
}).save(ignore_permissions=True)
return "OK"

View File

@@ -1,8 +0,0 @@
# Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class LMSOption(Document):
pass

View File

@@ -1,39 +1,35 @@
{ {
"actions": [], "actions": [],
"autoname": "field:title", "creation": "2021-05-28 19:09:44.418823",
"creation": "2021-06-07 10:50:17.893625",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"title", "title",
"questions", "maximum_attempts",
"lesson" "questions"
], ],
"fields": [ "fields": [
{ {
"fieldname": "title", "fieldname": "title",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Title", "label": "Title"
"unique": 1 },
{
"fieldname": "maximum_attempts",
"fieldtype": "Int",
"label": "Maximum Attempts"
}, },
{ {
"fieldname": "questions", "fieldname": "questions",
"fieldtype": "Table", "fieldtype": "Link",
"label": "Questions", "label": "Questions",
"options": "LMS Quiz Question" "options": "LMS Quiz Question"
},
{
"fieldname": "lesson",
"fieldtype": "Link",
"label": "Lesson",
"options": "Lesson",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-07-23 19:06:12.551633", "modified": "2021-05-28 19:18:38.688885",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Quiz", "name": "LMS Quiz",

View File

@@ -1,75 +1,8 @@
# Copyright (c) 2021, FOSS United and contributors # Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt # For license information, please see license.txt
from community.lms.doctype.lesson.lesson import update_progress # import frappe
import frappe
from frappe.model.document import Document from frappe.model.document import Document
import json
from frappe import _
from ..lesson.lesson import update_progress
class LMSQuiz(Document): class LMSQuiz(Document):
def validate(self): pass
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 quiz_summary(quiz, results):
score = 0
results = json.loads(results)
for result in results:
correct = result["is_correct"][0]
result["question"] = frappe.db.get_value("LMS Quiz Question",
{"parent": quiz, "idx": result["question_index"]},
["question"])
for point in result["is_correct"]:
correct = correct and point
result["result"] = "Right" if correct else "Wrong"
score += correct
del result["is_correct"]
del result["question_index"]
frappe.get_doc({
"doctype": "LMS Quiz Submission",
"quiz": quiz,
"result": results,
"score": score
}).save(ignore_permissions=True)
return score

View File

@@ -3,39 +3,6 @@
# import frappe # import frappe
import unittest import unittest
import frappe
class TestLMSQuiz(unittest.TestCase): class TestLMSQuiz(unittest.TestCase):
pass
@classmethod
def setUpClass(cls) -> None:
frappe.get_doc({
"doctype": "LMS Quiz",
"title": "Test Quiz"
}).save()
def test_with_multiple_options(self):
quiz = frappe.get_doc("LMS Quiz", "Test Quiz")
quiz.append("questions", {
"question": "Question multiple",
"option_1": "Option 1",
"is_correct_1": 1,
"option_2": "Option 2",
"is_correct_2": 1
})
quiz.save()
self.assertTrue(quiz.questions[0].multiple)
def test_with_no_correct_option(self):
quiz = frappe.get_doc("LMS Quiz", "Test Quiz")
quiz.append("questions", {
"question": "Question no correct option",
"option_1": "Option 1",
"option_2": "Option 2",
})
self.assertRaises(frappe.ValidationError, quiz.save)
@classmethod
def tearDownClass(cls) -> None:
frappe.db.delete("LMS Quiz", "Test Quiz")
frappe.db.delete("LMS Quiz Question", {"parent": "Test Quiz"})

View File

@@ -1,6 +1,6 @@
{ {
"actions": [], "actions": [],
"creation": "2021-06-07 10:46:10.402684", "creation": "2021-05-28 19:04:49.312616",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
@@ -11,23 +11,25 @@
"fields": [ "fields": [
{ {
"fieldname": "option", "fieldname": "option",
"fieldtype": "Data", "fieldtype": "Small Text",
"in_list_view": 1,
"label": "Option" "label": "Option"
}, },
{ {
"default": "0", "default": "0",
"fieldname": "is_correct", "fieldname": "is_correct",
"fieldtype": "Check", "fieldtype": "Check",
"in_list_view": 1,
"label": "Is Correct" "label": "Is Correct"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-06-07 10:48:45.290227", "modified": "2021-05-28 19:13:48.869416",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Option", "name": "LMS Quiz Option",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",

View File

@@ -4,5 +4,5 @@
# import frappe # import frappe
from frappe.model.document import Document from frappe.model.document import Document
class Lessons(Document): class LMSQuizOption(Document):
pass pass

View File

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

View File

@@ -1,160 +1,38 @@
{ {
"actions": [], "actions": [],
"creation": "2021-06-07 10:48:57.994714", "creation": "2021-05-28 19:07:17.347423",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"question", "question",
"options_section", "options",
"option_1", "question_type"
"is_correct_1",
"column_break_5",
"explanation_1",
"section_break_5",
"option_2",
"is_correct_2",
"column_break_10",
"explanation_2",
"column_break_4",
"option_3",
"is_correct_3",
"column_break_15",
"explanation_3",
"section_break_11",
"option_4",
"is_correct_4",
"column_break_20",
"explanation_4",
"multiple"
], ],
"fields": [ "fields": [
{ {
"fieldname": "question", "fieldname": "question",
"fieldtype": "Text", "fieldtype": "Markdown Editor",
"in_list_view": 1, "in_list_view": 1,
"label": "Question", "label": "Question"
"reqd": 1
}, },
{ {
"fieldname": "option_1", "fieldname": "options",
"fieldtype": "Data", "fieldtype": "Table",
"label": "Option 1", "label": "Options",
"reqd": 1 "options": "LMS Quiz Option"
}, },
{ {
"fieldname": "option_2", "fieldname": "question_type",
"fieldtype": "Data", "fieldtype": "Select",
"label": "Option 2", "in_list_view": 1,
"reqd": 1 "label": "Question Type",
}, "options": "Single Correct Answer\nMultiple Correct Answers"
{
"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"
},
{
"depends_on": "option_1",
"fieldname": "explanation_1",
"fieldtype": "Data",
"label": "Explanation"
},
{
"depends_on": "option_2",
"fieldname": "explanation_2",
"fieldtype": "Data",
"label": "Explanation"
},
{
"depends_on": "option_3",
"fieldname": "explanation_3",
"fieldtype": "Data",
"label": "Explanation"
},
{
"depends_on": "option_4",
"fieldname": "explanation_4",
"fieldtype": "Data",
"label": "Explanation"
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_15",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_20",
"fieldtype": "Column Break"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1,
"links": [], "links": [],
"modified": "2021-07-19 19:35:28.446236", "modified": "2021-05-28 19:18:04.939778",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Quiz Question", "name": "LMS Quiz Question",

View File

@@ -4,5 +4,5 @@
# import frappe # import frappe
import unittest import unittest
class TestLMSCourseReview(unittest.TestCase): class TestLMSQuizQuestion(unittest.TestCase):
pass pass

View File

@@ -1,45 +0,0 @@
{
"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

@@ -1,8 +0,0 @@
# 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

@@ -1,8 +0,0 @@
// 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

@@ -1,8 +0,0 @@
# 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

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

View File

@@ -0,0 +1,66 @@
{
"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,10 +14,6 @@ class LMSSection(Document):
if self.type == "exercise": if self.type == "exercise":
return frappe.get_doc("Exercise", self.id) 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): def get_latest_code_for_user(self):
"""Returns the latest code for the logged in user. """Returns the latest code for the logged in user.
""" """

View File

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

View File

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

View File

@@ -1,39 +1,45 @@
{ {
"actions": [], "actions": [],
"creation": "2021-06-07 14:19:54.958989", "autoname": "format:SKETCH-{#}",
"creation": "2021-03-09 16:31:50.523524",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"quiz", "title",
"result", "runtime",
"score" "code",
"svg"
], ],
"fields": [ "fields": [
{ {
"fieldname": "quiz", "fieldname": "title",
"fieldtype": "Link",
"label": "Quiz",
"options": "LMS Quiz"
},
{
"fieldname": "result",
"fieldtype": "Table",
"label": "Result",
"options": "LMS Quiz Result"
},
{
"fieldname": "score",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Score" "label": "Title"
},
{
"fieldname": "runtime",
"fieldtype": "Data",
"label": "Runtime"
},
{
"fieldname": "code",
"fieldtype": "Code",
"label": "Code"
},
{
"fieldname": "svg",
"fieldtype": "Code",
"label": "SVG",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-06-07 14:19:54.958989", "modified": "2021-03-12 08:42:56.671658",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Quiz Submission", "name": "LMS Sketch",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@@ -51,5 +57,6 @@
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1 "track_changes": 1,
"track_views": 1
} }

View File

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

View File

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

View File

@@ -1,8 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, FOSS United and Contributors # Copyright (c) 2021, FOSS United and Contributors
# See license.txt # See license.txt
from __future__ import unicode_literals
# import frappe # import frappe
import unittest import unittest
class TestLMSCourseProgress(unittest.TestCase): class TestLMSSketch(unittest.TestCase):
pass pass

View File

@@ -1,112 +0,0 @@
"""
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), macro)
e = etree.fromstring(html)
return e, m.start(0), m.end(0)
def sanitize_html(html, macro):
"""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
classname = ""
if macro == "YouTubeVideo":
classname = "lesson-video"
return "<div class='" + classname + "'>" + "\n".join(str(node) for node in nodes) + "</div>"

View File

@@ -1,4 +1,5 @@
"""Handy module to make access to all doctypes from a single place. """Handy module to make access to all doctypes from a single place.
""" """
from .doctype.lms_course.lms_course import LMSCourse as Course from .doctype.lms_course.lms_course import LMSCourse as Course
from .doctype.lms_batch_membership.lms_batch_membership import LMSBatchMembership as Membership from .doctype.lms_sketch.lms_sketch import LMSSketch as Sketch

View File

@@ -0,0 +1,84 @@
"""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, "apply_document_permissions": 0,
"button_label": "Save", "button_label": "Save",
"creation": "2021-04-20 11:37:49.135114", "creation": "2021-04-20 11:37:49.135114",
"custom_css": ".datepicker.active {\n background-color: white;\n}\n\n[data-doctype=\"Web Form\"] {\n max-width: 720px;\n margin: 6rem auto;\n}", "custom_css": ".datepicker.active {\n background-color: white;\n}",
"doc_type": "LMS Batch", "doc_type": "LMS Batch",
"docstatus": 0, "docstatus": 0,
"doctype": "Web Form", "doctype": "Web Form",
@@ -19,7 +19,7 @@
"is_standard": 1, "is_standard": 1,
"login_required": 1, "login_required": 1,
"max_attachment_size": 0, "max_attachment_size": 0,
"modified": "2021-06-15 18:49:50.530001", "modified": "2021-04-30 11:22:18.188712",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "add-a-new-batch", "name": "add-a-new-batch",
@@ -39,12 +39,12 @@
"allow_read_on_all_link_options": 0, "allow_read_on_all_link_options": 0,
"fieldname": "course", "fieldname": "course",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 0,
"label": "Course", "label": "Course",
"max_length": 0, "max_length": 0,
"max_value": 0, "max_value": 0,
"options": "LMS Course", "options": "",
"read_only": 0, "read_only": 1,
"reqd": 0, "reqd": 0,
"show_in_filter": 0 "show_in_filter": 0
}, },
@@ -111,4 +111,4 @@
"show_in_filter": 0 "show_in_filter": 0
} }
] ]
} }

View File

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

View File

@@ -0,0 +1,48 @@
{
"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,3 +1,5 @@
from __future__ import unicode_literals
import frappe import frappe
def get_context(context): def get_context(context):

View File

@@ -1,14 +0,0 @@
frappe.ready(function () {
frappe.web_form.after_load = () => {
if (!frappe.utils.get_url_arg("name")) {
window.location.href = `/edit-profile?name=${frappe.session.user}`;
}
}
frappe.web_form.after_save = () => {
setTimeout(() => {
window.location.href = `/${frappe.web_form.get_value(["username"])}`;
})
}
})

View File

@@ -1,212 +0,0 @@
{
"accept_payment": 0,
"allow_comments": 0,
"allow_delete": 0,
"allow_edit": 1,
"allow_incomplete": 0,
"allow_multiple": 0,
"allow_print": 0,
"amount": 0.0,
"amount_based_on_field": 0,
"apply_document_permissions": 0,
"breadcrumbs": "",
"button_label": "Save",
"client_script": "",
"creation": "2021-06-30 13:48:13.682851",
"custom_css": "[data-doctype=\"Web Form\"] {\n max-width: 720px;\n margin: 6rem auto;\n}",
"doc_type": "User",
"docstatus": 0,
"doctype": "Web Form",
"idx": 0,
"is_standard": 1,
"login_required": 1,
"max_attachment_size": 0,
"modified": "2021-08-06 14:40:39.013776",
"modified_by": "Administrator",
"module": "LMS",
"name": "profile",
"owner": "Administrator",
"payment_button_label": "Buy Now",
"published": 1,
"route": "edit-profile",
"route_to_success_link": 0,
"show_attachments": 0,
"show_in_grid": 0,
"show_sidebar": 0,
"sidebar_items": [],
"success_url": "/profile",
"title": "Profile",
"web_form_fields": [
{
"allow_read_on_all_link_options": 0,
"fieldname": "first_name",
"fieldtype": "Data",
"hidden": 0,
"label": "First Name",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "middle_name",
"fieldtype": "Data",
"hidden": 0,
"label": "Middle Name (Optional)",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "last_name",
"fieldtype": "Data",
"hidden": 0,
"label": "Last Name",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "username",
"fieldtype": "Data",
"hidden": 0,
"label": "Username",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"description": "Get your globally recognized avatar from Gravatar.com",
"fieldname": "user_image",
"fieldtype": "Attach Image",
"hidden": 0,
"label": "User Image",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "mobile_no",
"fieldtype": "Data",
"hidden": 0,
"label": "Mobile No",
"max_length": 0,
"max_value": 0,
"options": "Phone",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "bio",
"fieldtype": "Small Text",
"hidden": 0,
"label": "Bio",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "linkedin",
"fieldtype": "Data",
"hidden": 0,
"label": "LinkedIn ID",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "github",
"fieldtype": "Data",
"hidden": 0,
"label": "Github ID",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "medium",
"fieldtype": "Data",
"hidden": 0,
"label": "Medium ID",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "city",
"fieldtype": "Data",
"hidden": 0,
"label": "City",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "college",
"fieldtype": "Data",
"hidden": 0,
"label": "College Name",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"depends_on": "college",
"fieldname": "branch",
"fieldtype": "Data",
"hidden": 0,
"label": "Branch",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "profession",
"fieldtype": "Data",
"hidden": 0,
"label": "Profession",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
}
]
}

View File

@@ -0,0 +1,39 @@
<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 %}
</div>
<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>
</li>
<li class="nav-item">
<a class="nav-link" id="learn" href="/courses/{{course.name}}/{{batch.name}}/learn">Learn</a>
</li>
<!-- <li class="nav-item">
<a class="nav-link" id="schedule" href="/courses/{{course.name}}/{{batch.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>
<!-- <li class="nav-item">
<a class="nav-link" id="about" href="/courses/{{course.name}}/{{batch.name}}/about">About</a>
</li> -->
{% if batch.is_member(frappe.session.user, member_type="Mentor") %}
<li class="nav-item">
<a class="nav-link" id="progress" href="/courses/{{course.name}}/{{batch.name}}/progress">Progress</a>
</li>
{% endif %}
</ul>
<script>
frappe.ready(() => {
var selector = document.querySelector(`a[href="${decodeURIComponent(window.location.pathname)}"]`)
if (selector) {
selector.classList.add('active');
}
else {
$("#learn").addClass('active')
}
})
</script>

View File

@@ -1,19 +0,0 @@
<div class="breadcrumb">
<a class="dark-links" href="/courses">All Courses</a>
<img class="ml-1 mr-1" src="/assets/community/icons/chevron-right.svg">
{% if course %}
{% if lesson %}
<a class="dark-links" href="/courses/{{ course.name }}">{{ course.title }}</a>
<img class="ml-1 mr-1" src="/assets/community/icons/chevron-right.svg">
<span class="muted-text"> {{ lesson.title }}</span>
{% else %}
<span class="muted-text">{{ course.title }}</span>
{% endif %}
{% endif %}
{% if member_name %}
<span class="muted-text">{{ member_name }}</span>
{% endif %}
</div>

View File

@@ -1,100 +1,15 @@
<div> <div class="chapter-teaser">
<div class="teaser-body">
<div class="small-title chapter-title" data-target="#{{ course.get_slugified_chapter_title(chapter.title) }}" <h3 class="chapter-title"><span class="mr-1">{{index}}.</span> {{ chapter.title }}</h3>
data-toggle="collapse" aria-expanded="false"> <div class="chapter-description">
<img class="chapter-icon" src="/assets/community/icons/chevron-right.svg"> {{ chapter.description or "" }}
{{ index }}. {{ chapter.title }}
</div>
<div class="chapter-content collapse navbar-collapse" id="{{ course.get_slugified_chapter_title(chapter.title) }}">
{% if chapter.description %}
<div class="chapter-description muted-text">
{{ chapter.description }}
</div> </div>
{% endif %} <div class="chapter-lessons">
{% for lesson in chapter.get_lessons() %}
<div class="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>
{% for lesson in course.get_lessons(chapter) %}
<div class="lesson-info{% if membership.current_lesson == lesson.name %} active-lesson {% endif %}">
{% if membership or lesson.include_in_preview %}
<a class="lesson-links"
href="{{ course.get_learn_url(lesson.number) }}{{course.query_parameter}}"
data-course="{{ course.name }}">
{{ lesson.title }}
{% if membership %}
<img class="ml-1 lesson-progress-tick {{ course.get_progress(lesson.name) != 'Complete' and 'hide' }}"
src="/assets/community/icons/check.svg">
{% endif %}
</a>
{% else %}
<div class="no-preview" title="This lesson is not available for preview">
<div class="lesson-links">
{{ lesson.title }}
<img class="ml-2" src="/assets/community/icons/lock.svg">
</div>
</div>
{% endif %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
</div> </div>
{% if index != course.get_chapters() | length %}
<div class="card-divider"></div>
{% endif %}
<script>
frappe.ready(() => {
expand_the_active_chapter();
})
var expand_the_first_chapter = () => {
var elements = $(".collapse");
elements.each((i, element) => {
if (i <= 1) {
show_section(element);
}
});
}
var expand_the_active_chapter = () => {
/* Find anchor matching the URL for course details page */
var selector = $(`a[href="${decodeURIComponent(window.location.pathname)}"]`).parent();
if (!selector.length) {
selector = $(`a[href^="${decodeURIComponent(window.location.pathname)}"]`).parent();
}
if (selector.length && $(".course-details-page").length) {
$(".lesson-info").removeClass("active-lesson")
selector.addClass("active-lesson");
show_section(selector.parent().parent());
}
/* For course home page */
else if ($(".active-lesson").length) {
selector = $(".active-lesson")
show_section(selector.parent().parent());
}
/* If no active chapter then exapand the first chapter */
else {
expand_the_first_chapter();
}
}
var show_section = (element) => {
$(element).addClass("show");
$(element).siblings(".chapter-title").children(".chapter-icon").css("transform", "rotate(90deg)");
}
</script>

View File

@@ -1,90 +0,0 @@
<div class="common-card-style course-card">
<div class="course-image {% if not course.image %}default-image{% endif %}"
{% if course.image %} style="background-image: url( {{ course.image }} );" {% endif %}>
<div class="course-tags">
{% for tag in course.get_tags() %}
<div class="course-card-pills">{{ tag }}</div>
{% endfor %}
</div>
{% if not course.image %}
<div class="default-image-text">{{ course.title[0] }}</div>
{% endif %}
</div>
<div class="course-card-content">
<div class="course-card-meta muted-text">
{% if course.get_chapters() | length %}
<span>
{{ course.get_chapters() | length }} Chapters
</span>
{% endif %}
{% if course.get_chapters() | length and course.get_upcoming_batches() | length %}
<span class="font-weight-bold ml-3 mr-3"> . </span>
{% endif %}
{% if course.get_upcoming_batches() | length %}
<span class="">
{{ course.get_upcoming_batches() | length }} Open Batches
</span>
{% endif %}
</div>
<div class="course-card-title">{{ course.title }}</div>
<div class="card-divider"></div>
<div class="course-card-meta-2">
{{ widgets.Avatar(member=course.get_instructor(), avatar_class="avatar-small") }}
<span class="course-instructor">
{{ course.get_instructor().full_name }}
</span>
<span class="course-student-count">
{% if course.get_students() | length %}
<span class="mr-4">
<img class="icon-background" src="/assets/community/icons/user.svg" />
{{ course.get_students() | length }}
</span>
{% endif %}
{% set avg_rating = course.get_average_rating() %}
{% if avg_rating %}
<span>
<img class="icon-background" src="/assets/community/icons/rating.svg" />
{{ avg_rating }}
</span>
{% endif %}
</span>
</div>
{% set membership = course.get_membership(frappe.session.user) %}
{% set lesson_index = course.get_lesson_index(membership.current_lesson) if membership and
membership.current_lesson
else '1.1' %}
{% set query_parameter = "?batch=" + membership.batch if membership and membership.batch else "" %}
{% if course.upcoming %}
<div class="view-course-link is-secondary border">
Upcoming Course <img class="ml-3" src="/assets/community/icons/black-arrow.svg" />
</div>
<a class="stretched-link" href="/courses/{{ course.name }}"></a>
{% elif membership %}
<div class="view-course-link is-primary">
Continue Course <img class="ml-3" src="/assets/community/icons/white-arrow.svg" />
</div>
<a class="stretched-link" href="{{ course.get_learn_url(lesson_index) }}{{ query_parameter }}"></a>
{% else %}
<div class="view-course-link">
View Course <img class="ml-3" src="/assets/community/icons/black-arrow.svg" />
</div>
<a class="stretched-link" href="/courses/{{ course.name }}"></a>
{% endif %}
</div>
</div>
<script>
frappe.ready(() => {
$(".course-card-title").each((i, element) => {
var title = $(element).text();
var length = $(window).width() <= 375 ? 60 : 65;
var suffix = title.length > length ? "..." : "";
$(element).text(title.substring(0, length) + suffix);
})
})
</script>

View File

@@ -1,12 +1,5 @@
{% if course.get_chapters() | length %} <h2>Course Outline</h2>
<div class="">
<div class="course-home-headings"> {% for chapter in course.get_chapters() %}
Course Outline {{ widgets.ChapterTeaser(index=loop.index, chapter=chapter, course=course, batch=batch, show_link=show_link)}}
</div> {% endfor %}
<div class="coure-outline">
{% for chapter in course.get_chapters() %}
{{ widgets.ChapterTeaser(index=loop.index, chapter=chapter, course=course, batch=batch, membership=membership) }}
{% endfor %}
</div>
</div>
{% endif %}

View File

@@ -1,7 +1,7 @@
{% from "www/macros/livecode.html" import LiveCodeEditorJS, LiveCodeEditor with context %} {% from "www/macros/livecode.html" import LiveCodeEditorJS, LiveCodeEditor with context %}
<div class="exercise"> <div class="exercise">
<h2>Exercise {{exercise.index_label}}: {{ exercise.title }}</h2> <h2>{{ exercise.title }}</h2>
<div class="exercise-description">{{frappe.utils.md_to_html(exercise.description)}}</div> <div class="exercise-description">{{frappe.utils.md_to_html(exercise.description)}}</div>
{% if exercise.image %} {% if exercise.image %}

View File

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

View File

@@ -1,15 +0,0 @@
<div class="common-card-style member-card {{dimension_class}} ">
{% set avatar_class = "avatar-large" if not dimension_class else "avatar-large"%}
{{ widgets.Avatar(member=member, avatar_class=avatar_class) }}
<div class="small-title member-card-title">
{{ member.full_name }}
</div>
{% set course_count = member.get_authored_courses() | length %}
{% if show_course_count and course_count > 0 %}
{% set suffix = "Courses" if course_count > 1 else "Course" %}
<div class="small-title">
Created {{ course_count }} {{ suffix }}
</div>
{% endif %}
<a class="stretched-link" href="/{{ member.username }}"></a>
</div>

View File

@@ -1,91 +0,0 @@
<div class="reviews-parent">
{% set reviews = course.get_reviews() %}
{% if reviews | length or course.is_eligible_to_review(membership) %}
<div class="mb-5">
<span class="course-home-headings">Reviews</span>
{% if course.is_eligible_to_review(membership) %}
<span class="review-link button is-secondary pull-right">
Write a review
</span>
{% endif %}
</div>
{% endif %}
{% if reviews | length %}
<div class="reviews-section">
{% for review in reviews %}
<div class="review-card">
<div class="common-card-style review-content small-title"> {{ review.review }} </div>
<div class="review-card-footer">
<div>
{{ widgets.Avatar(member=review.owner_details, avatar_class="avatar-medium") }}
<a class="button-links" href="/{{review.owner_details.username}}">
<span class="course-instructor">
{{ review.owner_details.full_name }}
</span>
</a>
</div>
<div class="rating">
{% for i in [1, 2, 3, 4, 5] %}
<svg class="icon icon-md {% if i <= review.rating %} star-click {% endif %}" data-rating="{{ i }}">
<use href="#icon-star"></use>
</svg>
{% endfor %}
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
</div>
<div class="modal fade review-modal" id="review-modal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<div class="course-home-headings modal-headings">Review</div>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form class="review-form" id="review-form">
<div class="form-group">
<div class="clearfix">
<label class="control-label reqd" style="padding-right: 0px;">Rating</label>
</div>
<div class="control-input-wrapper">
<div class="control-input">
<div class="rating rating-field" id="rating">
{% for i in [1, 2, 3, 4, 5] %}
<svg class="icon icon-md icon-rating" data-rating="{{ i }}">
<use href="#icon-star"></use>
</svg>
{% endfor %}
</div>
</div>
</div>
</div>
<div class="form-group">
<div class="clearfix">
<label class="control-label reqd" style="padding-right: 0px;">Review</label>
</div>
<div class="control-input-wrapper">
<div class="control-input">
<textarea type="text" autocomplete="off" class="input-with-feedback form-control review-field"
data-fieldtype="Text" data-fieldname="feedback_comments" placeholder="" style="height: 300px;"
spellcheck="false"></textarea>
</div>
</div>
</div>
<p class="error-field muted-text"></p>
</form>
</div>
<div class="modal-footer">
<div class="button submit-review is-primary" data-course="{{ course.name | urlencode}}" id="submit-review">
Submit</div>
</div>
</div>
</div>
</div>

View File

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

View File

@@ -1,54 +0,0 @@
# Copyright (c) 2021, FOSS United and Contributors
# See license.txt
# import frappe
import unittest
import frappe
class TestCustomUser(unittest.TestCase):
def test_with_basic_username(self):
new_user = frappe.get_doc({
"doctype": "User",
"email": "test_with_basic_username@example.com",
"first_name": "Username"
}).insert()
self.assertEqual(new_user.username, "username")
def test_without_username(self):
""" The user in this test has the same first name as the user of the test test_with_basic_username.
In such cases frappe makes the username of the second user empty.
The condition in community app should override this and save a username. """
new_user = frappe.get_doc({
"doctype": "User",
"email": "test-without-username@example.com",
"first_name": "Username"
}).insert()
self.assertTrue(new_user.username)
def test_with_illegal_characters(self):
new_user = frappe.get_doc({
"doctype": "User",
"email": "test_with_illegal_characters@example.com",
"first_name": "Username$$"
}).insert()
self.assertEqual(new_user.username[:8], "username")
def test_with_hyphen_at_end(self):
new_user = frappe.get_doc({
"doctype": "User",
"email": "test_with_hyphen_at_end@example.com",
"first_name": "Username---"
}).insert()
length = len(new_user.username)
self.assertNotEqual(new_user.username[length-1], "-")
@classmethod
def tearDownClass(cls) -> None:
users = [
"test_with_basic_username@example.com",
"test-without-username@example.com",
"test_with_illegal_characters@example.com",
"test_with_hyphen_at_end@example.com"
]
frappe.db.delete("User", {"name": ["in", users]})

View File

@@ -2,63 +2,18 @@ import frappe
from frappe.core.doctype.user.user import User from frappe.core.doctype.user.user import User
from frappe.utils import cint from frappe.utils import cint
import hashlib import hashlib
import random
import re
from frappe import _
class CustomUser(User): class CustomUser(User):
def validate(self): def get_course_count(self) -> int:
super(CustomUser, self).validate()
self.validate_username_characters()
def validate_username_characters(self):
if self.is_new():
if self.username.find(" "):
self.username.replace(" ", "")
if not re.match("^[A-Za-z0-9_]*$", self.username):
self.username = self.remove_illegal_characters()
if not self.username:
self.username = self.get_username_from_first_name()
if self.username_exists():
self.username = self.remove_illegal_characters() + str(random.randint(0, 99))
else:
if not re.match("^[A-Za-z0-9_-]*$", self.username):
frappe.throw(_("Username can only contain alphabets, numbers, hyphen and underscore."))
if self.username[0] == "-" or self.username[len(self.username) - 1] == "-":
frappe.throw(_("First and Last character of username cannot be Hyphen(-)."))
def get_username_from_first_name(self):
return frappe.scrub(self.first_name) + str(random.randint(0, 99))
def remove_illegal_characters(self):
username = ''.join([c for c in self.username if c.isalnum() or c in ['-', '_']])
while username[0] == "-" or username[len(username) - 1] == "-":
if username[0] == "-":
username = username[1:]
if username[len(username) - 1]:
username = username[:1]
return username
def get_authored_courses(self) -> int:
"""Returns the number of courses authored by this user. """Returns the number of courses authored by this user.
""" """
return frappe.get_all( return frappe.db.count(
'LMS Course', { 'LMS Course', {
'owner': self.name, 'owner': self.email
'is_published': True
}) })
def get_palette(self): def get_palette(self):
"""
Returns a color unique to each member for Avatar """
palette = [ palette = [
['--orange-avatar-bg', '--orange-avatar-color'], ['--orange-avatar-bg', '--orange-avatar-color'],
['--pink-avatar-bg', '--pink-avatar-color'], ['--pink-avatar-bg', '--pink-avatar-color'],
@@ -85,35 +40,3 @@ class CustomUser(User):
'member_type': 'Mentor' 'member_type': 'Mentor'
}) })
def get_user_reviews(self):
""" Returns the reviews created by user """
return frappe.get_all("LMS Course Review",
{
"owner": self.name
})
def get_course_membership(self, member_type=None):
""" Returns all memberships of the user """
filters = {
"member": self.name
}
if member_type:
filters["member_type"] = member_type
return frappe.get_all("LMS Batch Membership", filters, ["name", "course"])
def get_mentored_courses(self):
""" Returns all courses mentored by this user """
mentored_courses = []
mapping = frappe.get_all("LMS Course Mentor Mapping",
{
"mentor": self.name,
},
["name", "course"]
)
for map in mapping:
if frappe.db.get_value("LMS Course", map.course, "is_published"):
mentored_courses.append(map)
return mentored_courses

View File

@@ -6,4 +6,3 @@ community.patches.replace_member_with_user_in_batch_membership
community.patches.replace_member_with_user_in_course_mentor_mapping community.patches.replace_member_with_user_in_course_mentor_mapping
community.patches.replace_member_with_user_in_lms_message community.patches.replace_member_with_user_in_lms_message
community.patches.replace_member_with_user_in_mentor_request community.patches.replace_member_with_user_in_mentor_request
community.patches.v0_0.chapter_lesson_index_table

View File

@@ -1,50 +0,0 @@
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc("lms", "doctype", "lms_course")
frappe.reload_doc("lms", "doctype", "chapter")
frappe.reload_doc("lms", "doctype", "lesson")
frappe.reload_doc("lms", "doctype", "lessons")
frappe.reload_doc("lms", "doctype", "chapters")
update_chapters()
update_lessons()
def update_chapters():
courses = frappe.get_all("LMS Course", pluck="name")
for course in courses:
course_details = frappe.get_doc("LMS Course", course)
chapters = frappe.get_all("Chapter",
{
"course": course
},
["name"],
order_by= "index_"
)
for chapter in chapters:
course_details.append("chapters",
{
"chapter": chapter.name
})
course_details.save()
def update_lessons():
chapters = frappe.get_all("Chapter", pluck="name")
for chapter in chapters:
chapter_details = frappe.get_doc("Chapter", chapter)
lessons = frappe.get_all("Lesson",
{
"chapter": chapter
},
["name"],
order_by= "index_"
)
for lesson in lessons:
chapter_details.append("lessons",
{
"lesson": lesson.name
})
chapter_details.save()

View File

@@ -1,108 +0,0 @@
"""
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="100%" 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

@@ -1,3 +1,4 @@
@import "./style.css"; @import "./style.css";
@import "./vars.css"; @import "./vars.css";
@import "./style.less";

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,333 @@
h2 {
margin: 20px 0px;
color: black;
}
.teaser {
background: white;
border-radius: 9px;
border: 1px solid #C4C4C4;
.teaser-body {
padding: 20px;
}
.teaser-footer {
padding: 20px;
}
}
.sketch-teaser {
.teaser();
width: 220px;
margin-bottom: 30px;
margin-top: 30px;
svg {
width: 200px;
height: 200px;
}
.sketch-image {
padding: 10px;
}
.sketch-footer {
border-top: 1px solid#C4C4C4;
padding: 10px;
background: #F6F6F6;
border-radius: 0px 0px 10px 10px;
}
}
.course-teaser {
.teaser();
color: #444;
margin-bottom: 20px;
margin-top: 20px;
h3, h4 {
color: black;
font-weight: bold;
}
.course-body, .course-footer {
padding: 20px;
}
.course-body {
min-height: 8em;
}
.course-footer {
border-top: 1px solid #ddd;
}
a, a:hover {
color: inherit;
text-decoration: none;
}
}
section {
padding: 60px 0px;
}
section h2 {
margin-bottom: 40px;
font-size: 48px;
line-height: 58px;
font-weight: bold;
}
section.lightgray {
background: #F6F6F6;
}
#hero .jumbotron {
background: inherit;
}
.chapter-teaser {
.teaser();
color: #444;
margin: 20px 0px;
h3, h4 {
color: black;
font-weight: bold;
}
}
.field-width {
width: 40%;
display: inline-block;
}
.footer-grouped-links {
display: none;
}
.footer-info {
border-top: 0px;
margin-top: 0px;
.footer-col-right {
padding-top: 1.8rem;
}
}
.web-footer {
border-top: 1px solid #E2E6E9;
padding: 0px;
padding: 2rem 0px;
margin-top: 2rem;
}
.course-type {
text-transform: uppercase;
font-size: 1.0em;
font-weight: bold;
color: var(--tag-color);
}
.course-header {
margin-top: 20px;
}
/*
.course-header {
margin-top: 20px;
padding: 20px;
background: var(--header-bg);
color: var(--header-color);
border-radius: 9px;
}
.course-author-avatar {
width: 20px;
height: 20px;
border-radius: 50%;
margin-right: 20px;
}
.course-header h1 {
color: inherit;
}
*/
// .gray-section {
// background:#F6F6F6;
// border: 1px solid #C4C4C4;
// padding: 20px;
// margin: 20px 0px;
// }
.instructor-title {
font-weight: bold;
color: black;
}
.instructor-subtitle {
font-size: 0.8em;
color: var(--text-color);
}
// .mentors-wrapper {
// .gray-section();
// }
.chapter-number {
background: var(--text-color);
color: white;
height: 24px;
min-width: 24px;
margin-right: 5px;
}
.sidebar {
background: var(--sidebar-bg);
border: 1px solid var(--sidebar-border);
margin: 20px 0px;
border-radius: 10px;
padding: 1px 20px 20px 20px;
}
.sidebar h3 {
margin-top: 20px;
color: black;
}
.sidebar-batch {
background: var(--sidebar-bg);
color: var(--text-color);
position: fixed;
left: 0;
height: 100%;
}
.sidebar-batch a {
padding: 16px 8px 8px 16px;
display: block;
}
.sidebar .notice {
margin-top: 10px;
padding: 10px;
border-radius: 10px;
border: 1px dashed var(--text-color);
}
.sidebar .notice a {
color: inherit;
text-decoration: underline;
}
// LiveCode editor
.livecode-editor {
.CodeMirror {
border: 1px solid #ddd;
background: #ffe;
height: auto;
}
.CodeMirror-scroll {
max-height: 310px;
min-height: 310px;
}
.controls {
padding: 10px 0px;
}
canvas {
border: 5px solid #ddd;
position: relative;
z-index: 0;
}
.output {
position: absolute;
z-index: 1;
width: 300px;
left: 0px;
top: 0px;
background-color: rgba(255, 255, 255, 0);
max-height: 300px;
white-space: pre-wrap;
word-wrap: break-word;
margin: 0px;
margin-left: 20px;
padding: 4px;
color: #888;
}
@media (max-width: 768px) {
.canvas-wrapper {
padding-top: 10px;
}
.code-wrapper {
min-height: 50px;
}
.CodeMirror {
min-height: 50px;
}
}
}
.sketch-header {
input#sketch-title {
font-weight: bold;
}
}
.chapter-description {
margin-bottom: 10px;
}
.lesson-teaser {
line-height: 35px;
}
#hero h1 {
color: black !important;
}
.lesson-page {
margin: 20px 0px;
}
.lesson-pagination {
clear: both;
}
.exercise-image svg {
width: 200px;
height: 200px;
border: 1px solid #ddd;
margin-bottom: 20px;
}
.svg-200 svg {
width: 200px;
height: 200px;
}
.livecode-editor-small .livecode-editor {
.CodeMirror-scroll {
max-height: 160px;
min-height: 160px;
}
canvas {
width: 150px;
height: 150px;
}
}
.mentor-dashboard {
margin-top: 20px;
.submission {
margin: 40px 0px 0px 20px;
}
}

View File

@@ -1,3 +0,0 @@
<svg width="13" height="8" viewBox="0 0 13 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.9649 0.313118C8.76964 0.117856 8.45306 0.117856 8.2578 0.313118C8.06253 0.50838 8.06253 0.824963 8.2578 1.02023L10.7374 3.49985L1.16683 3.49719C0.890684 3.49712 0.666764 3.72091 0.666687 3.99705C0.66661 4.2732 0.890406 4.49712 1.16655 4.49719L10.7377 4.49985L8.2578 6.97978C8.06253 7.17505 8.06253 7.49163 8.2578 7.68689C8.45306 7.88215 8.76964 7.88215 8.9649 7.68689L12.2982 4.35356C12.4935 4.1583 12.4935 3.84171 12.2982 3.64645L8.9649 0.313118Z" fill="#4C5A67"/>
</svg>

Before

Width:  |  Height:  |  Size: 619 B

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.8947 2.47619C15.8947 2.2132 15.6885 2 15.4342 2C15.1798 2 14.9737 2.2132 14.9737 2.47619V3.90476H8.52631V2.47619C8.52631 2.2132 8.32013 2 8.06579 2C7.81145 2 7.60526 2.2132 7.60526 2.47619V3.90476H5.30263C4.03092 3.90476 3 4.97074 3 6.28571V10.0952V19.619C3 20.934 4.03092 22 5.30263 22H18.1974C19.4691 22 20.5 20.934 20.5 19.619V10.0952V6.28571C20.5 4.97074 19.4691 3.90476 18.1974 3.90476H15.8947V2.47619ZM19.579 9.61905V6.28571C19.579 5.49673 18.9604 4.85714 18.1974 4.85714H15.8947V6.28572C15.8947 6.54871 15.6885 6.76191 15.4342 6.76191C15.1798 6.76191 14.9737 6.54871 14.9737 6.28572V4.85714H8.52631V6.28572C8.52631 6.54871 8.32013 6.76191 8.06579 6.76191C7.81145 6.76191 7.60526 6.54871 7.60526 6.28572V4.85714H5.30263C4.53961 4.85714 3.92105 5.49673 3.92105 6.28571V9.61905H19.579ZM3.92105 10.5714H19.579V19.619C19.579 20.408 18.9604 21.0476 18.1974 21.0476H5.30263C4.53961 21.0476 3.92105 20.408 3.92105 19.619V10.5714Z" fill="#4C5A67"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12ZM16.8734 10.1402C17.264 9.74969 17.264 9.11652 16.8734 8.726C16.4829 8.33547 15.8498 8.33547 15.4592 8.726L14.6259 9.55933L12.9592 11.226L10.333 13.8522L9.37345 12.8927L8.54011 12.0593C8.14959 11.6688 7.51643 11.6688 7.1259 12.0593C6.73538 12.4499 6.73538 13.083 7.1259 13.4735L7.95923 14.3069L9.6259 15.9735C9.81344 16.1611 10.0678 16.2664 10.333 16.2664C10.5982 16.2664 10.8526 16.1611 11.0401 15.9735L14.3734 12.6402L16.0401 10.9735L16.8734 10.1402Z" fill="#68D391"/>
</svg>

Before

Width:  |  Height:  |  Size: 736 B

Some files were not shown because too many files have changed in this diff Show More