Compare commits
48 Commits
redesign-f
...
username-f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66aace247c | ||
|
|
bc3db06960 | ||
|
|
d5067a4bcd | ||
|
|
04d44510de | ||
|
|
844fcc9bca | ||
|
|
145b5efab0 | ||
|
|
4079ed97b9 | ||
|
|
63d70fc037 | ||
|
|
ce86b5deda | ||
|
|
037e946bbe | ||
|
|
a51c8de1eb | ||
|
|
53dc517180 | ||
|
|
44ca940c6b | ||
|
|
c0b688c720 | ||
|
|
861d5f231d | ||
|
|
d14b4f55a6 | ||
|
|
db9a6c3eda | ||
|
|
a667643681 | ||
|
|
f278e4b6a5 | ||
|
|
33a12c2dec | ||
|
|
508f90f459 | ||
|
|
709f0c2274 | ||
|
|
be47700e7c | ||
|
|
40842830a4 | ||
|
|
11d070fa0d | ||
|
|
dd2f830a33 | ||
|
|
5431fcb450 | ||
|
|
324033e9ee | ||
|
|
86596d0cfe | ||
|
|
9323cfd748 | ||
|
|
d125b02cec | ||
|
|
282c4c5351 | ||
|
|
276d64a66a | ||
|
|
79eb381a41 | ||
|
|
44f9c0dfd3 | ||
|
|
0ca4cd724e | ||
|
|
8a3e31f021 | ||
|
|
9be8a1af0b | ||
|
|
b9cac20613 | ||
|
|
e6d5e6d37b | ||
|
|
0abfcac7da | ||
|
|
b70e8b9acc | ||
|
|
3b1e1aa3c3 | ||
|
|
8f74c74d50 | ||
|
|
d2f435016c | ||
|
|
389b35802b | ||
|
|
a9192a74f9 | ||
|
|
5ecae0df61 |
@@ -1,5 +1,5 @@
|
||||
{% set color = member.get_palette() %}
|
||||
<a href="/{{member.username}}">
|
||||
<a class="button-links" href="/{{member.username}}">
|
||||
<span class="avatar {{ avatar_class }}" title="{{ member.full_name }}">
|
||||
{% if member.user_image %}
|
||||
<img class="avatar-frame standard-image" style="object-fit: cover;" src="{{ member.user_image }}" title="{{ member.full_name }}">
|
||||
|
||||
@@ -136,7 +136,6 @@ primary_rules = [
|
||||
{"from_route": "/courses/<course>/<topic>", "to_route": "courses/topic"},
|
||||
{"from_route": "/hackathons/<hackathon>", "to_route": "hackathons/hackathon"},
|
||||
{"from_route": "/hackathons/<hackathon>/<project>", "to_route": "hackathons/project"},
|
||||
{"from_route": "/dashboard", "to_route": ""},
|
||||
{"from_route": "/add-a-new-batch", "to_route": "add-a-new-batch"},
|
||||
{"from_route": "/courses/<course>/home", "to_route": "batch/home"},
|
||||
{"from_route": "/courses/<course>/learn", "to_route": "batch/learn"},
|
||||
|
||||
@@ -2,7 +2,15 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Chapter', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
onload: function (frm) {
|
||||
frm.set_query("lesson", "lessons", function () {
|
||||
return {
|
||||
filters: {
|
||||
"chapter": frm.doc.name,
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
"course",
|
||||
"title",
|
||||
"description",
|
||||
"locked",
|
||||
"index_"
|
||||
"lessons"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -24,12 +23,6 @@
|
||||
"fieldtype": "Markdown Editor",
|
||||
"label": "Description"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "locked",
|
||||
"fieldtype": "Check",
|
||||
"label": "Locked"
|
||||
},
|
||||
{
|
||||
"fieldname": "course",
|
||||
"fieldtype": "Link",
|
||||
@@ -38,10 +31,10 @@
|
||||
"options": "LMS Course"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "index_",
|
||||
"fieldtype": "Int",
|
||||
"label": "Index"
|
||||
"fieldname": "lessons",
|
||||
"fieldtype": "Table",
|
||||
"label": "Lessons",
|
||||
"options": "Lessons"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
@@ -52,7 +45,7 @@
|
||||
"link_fieldname": "chapter"
|
||||
}
|
||||
],
|
||||
"modified": "2021-05-13 21:05:20.531890",
|
||||
"modified": "2021-07-27 16:28:08.667964",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Chapter",
|
||||
|
||||
@@ -5,15 +5,6 @@
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from ...utils import slugify
|
||||
|
||||
class Chapter(Document):
|
||||
def get_lessons(self):
|
||||
rows = frappe.db.get_all("Lesson",
|
||||
filters={"chapter": self.name},
|
||||
fields='name',
|
||||
order_by="index_")
|
||||
return [frappe.get_doc('Lesson', row['name']) for row in rows]
|
||||
|
||||
def get_slugified_chapter_title(self):
|
||||
return slugify(self.title)
|
||||
pass
|
||||
|
||||
32
community/lms/doctype/chapters/chapters.json
Normal file
32
community/lms/doctype/chapters/chapters.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
8
community/lms/doctype/chapters/chapters.py
Normal file
8
community/lms/doctype/chapters/chapters.py
Normal file
@@ -0,0 +1,8 @@
|
||||
# Copyright (c) 2021, FOSS United and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class Chapters(Document):
|
||||
pass
|
||||
@@ -30,10 +30,13 @@ class InviteRequest(Document):
|
||||
return user
|
||||
|
||||
def send_email(self):
|
||||
subject = _("Your request has been approved.")
|
||||
site_name = "Mon.School"
|
||||
subject = _("Welcome to {0}!").format(site_name)
|
||||
|
||||
args = {
|
||||
"full_name": self.full_name,
|
||||
"signup_form_link": "/new-sign-up?invite_code={0}".format(self.name),
|
||||
"site_name": site_name,
|
||||
"site_url": frappe.utils.get_url()
|
||||
}
|
||||
frappe.sendmail(
|
||||
@@ -42,7 +45,8 @@ class InviteRequest(Document):
|
||||
subject=subject,
|
||||
header=[subject, "green"],
|
||||
template = "lms_invite_request_approved",
|
||||
args=args)
|
||||
args=args,
|
||||
now=True)
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def create_invite_request(invite_email):
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"include_in_preview",
|
||||
"column_break_4",
|
||||
"title",
|
||||
"index_",
|
||||
"index_label",
|
||||
"section_break_6",
|
||||
"body",
|
||||
@@ -31,13 +30,6 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Title"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "index_",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Index"
|
||||
},
|
||||
{
|
||||
"fieldname": "body",
|
||||
"fieldtype": "Markdown Editor",
|
||||
@@ -75,7 +67,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-29 13:34:49.077363",
|
||||
"modified": "2021-07-27 16:28:29.203624",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Lesson",
|
||||
|
||||
31
community/lms/doctype/lessons/lessons.json
Normal file
31
community/lms/doctype/lessons/lessons.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
8
community/lms/doctype/lessons/lessons.py
Normal file
8
community/lms/doctype/lessons/lessons.py
Normal file
@@ -0,0 +1,8 @@
|
||||
# Copyright (c) 2021, FOSS United and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class Lessons(Document):
|
||||
pass
|
||||
@@ -37,6 +37,7 @@
|
||||
"fieldname": "member_type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Member Type",
|
||||
"options": "\nStudent\nMentor\nStaff"
|
||||
},
|
||||
@@ -44,7 +45,6 @@
|
||||
"default": "Member",
|
||||
"fieldname": "role",
|
||||
"fieldtype": "Select",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Role",
|
||||
"options": "\nMember\nAdmin"
|
||||
},
|
||||
@@ -63,9 +63,10 @@
|
||||
{
|
||||
"fetch_from": "batch.course",
|
||||
"fieldname": "course",
|
||||
"fieldtype": "Data",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Course"
|
||||
"label": "Course",
|
||||
"options": "LMS Course"
|
||||
},
|
||||
{
|
||||
"fieldname": "current_lesson",
|
||||
@@ -83,7 +84,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-07-06 20:50:46.885325",
|
||||
"modified": "2021-08-04 17:10:42.708479",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Batch Membership",
|
||||
|
||||
@@ -2,7 +2,15 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('LMS Course', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
onload: function (frm) {
|
||||
frm.set_query("chapter", "chapters", function () {
|
||||
return {
|
||||
filters: {
|
||||
"course": frm.doc.name,
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"action": "community.lms.doctype.lms_course.lms_course.reindex_lessons",
|
||||
"action_type": "Server Action",
|
||||
"group": "Reindex",
|
||||
"label": "Reindex Lessons"
|
||||
},
|
||||
{
|
||||
"action": "community.lms.doctype.lms_course.lms_course.reindex_exercises",
|
||||
"action_type": "Server Action",
|
||||
@@ -21,16 +15,17 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"short_code",
|
||||
"video_link",
|
||||
"column_break_3",
|
||||
"is_published",
|
||||
"disable_self_learning",
|
||||
"image",
|
||||
"section_break_5",
|
||||
"column_break_3",
|
||||
"tags",
|
||||
"is_published",
|
||||
"upcoming",
|
||||
"disable_self_learning",
|
||||
"section_break_5",
|
||||
"short_introduction",
|
||||
"description"
|
||||
"description",
|
||||
"chapters"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -53,11 +48,6 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Published"
|
||||
},
|
||||
{
|
||||
"fieldname": "short_code",
|
||||
"fieldtype": "Data",
|
||||
"label": "Short Code"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
@@ -92,6 +82,18 @@
|
||||
"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,
|
||||
@@ -111,14 +113,9 @@
|
||||
"group": "Mentors",
|
||||
"link_doctype": "LMS Course Mentor Mapping",
|
||||
"link_fieldname": "course"
|
||||
},
|
||||
{
|
||||
"group": "Mentors",
|
||||
"link_doctype": "LMS Mentor Request",
|
||||
"link_fieldname": "course"
|
||||
}
|
||||
],
|
||||
"modified": "2021-07-09 15:05:05.372430",
|
||||
"modified": "2021-07-28 19:01:50.677445",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Course",
|
||||
@@ -141,6 +138,5 @@
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "title",
|
||||
"track_changes": 1,
|
||||
"track_views": 1
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -9,8 +9,10 @@ import json
|
||||
from ...utils import slugify
|
||||
from community.query import find, find_all
|
||||
from frappe.utils import flt, cint
|
||||
from ...utils import slugify
|
||||
|
||||
class LMSCourse(Document):
|
||||
|
||||
@staticmethod
|
||||
def find(name):
|
||||
"""Returns the course with specified name.
|
||||
@@ -72,8 +74,11 @@ class LMSCourse(Document):
|
||||
mentors = frappe.get_all("LMS Course Mentor Mapping", {"course": self.name}, ["mentor"])
|
||||
for mentor in mentors:
|
||||
member = frappe.get_doc("User", mentor.mentor)
|
||||
# TODO: change this to count query
|
||||
member.batch_count = len(frappe.get_all("LMS Batch Membership", {"member": member.name, "member_type": "Mentor"}))
|
||||
member.batch_count = frappe.db.count("LMS Batch Membership",
|
||||
{
|
||||
"member": member.name,
|
||||
"member_type": "Mentor"
|
||||
})
|
||||
course_mentors.append(member)
|
||||
return course_mentors
|
||||
|
||||
@@ -112,17 +117,42 @@ class LMSCourse(Document):
|
||||
def get_chapters(self):
|
||||
"""Returns all chapters of this course.
|
||||
"""
|
||||
# TODO: chapters should have a way to specify the order
|
||||
return find_all("Chapter", course=self.name, order_by="index_")
|
||||
chapters = []
|
||||
for row in self.chapters:
|
||||
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):
|
||||
""" Returns all lessons of this course """
|
||||
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 = []
|
||||
chapters = self.get_chapters()
|
||||
for chapter in chapters:
|
||||
lessons.append(frappe.get_all("Lesson", {"chapter": chapter.name}))
|
||||
|
||||
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())
|
||||
@@ -160,38 +190,18 @@ class LMSCourse(Document):
|
||||
visibility="Public")
|
||||
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):
|
||||
"""Returns the {chapter_index}.{lesson_index} for the lesson.
|
||||
"""
|
||||
lesson = frappe.get_doc("Lesson", lesson_name)
|
||||
chapter = frappe.get_doc("Chapter", lesson.chapter)
|
||||
return f"{chapter.index_}.{lesson.index_}"
|
||||
lesson = frappe.db.get_value("Lessons", {"lesson": lesson_name}, ["idx", "parent"], as_dict=True)
|
||||
if not lesson:
|
||||
return None
|
||||
|
||||
def reindex_lessons(self):
|
||||
for i, c in enumerate(self.get_chapters(), start=1):
|
||||
c.index_ = i
|
||||
c.save()
|
||||
self._reindex_lessons_in_chapter(c)
|
||||
chapter = frappe.db.get_value("Chapters", {"chapter": lesson.parent}, ["idx"], as_dict=True)
|
||||
if not chapter:
|
||||
return None
|
||||
|
||||
def _reindex_lessons_in_chapter(self, c):
|
||||
for i, lesson in enumerate(c.get_lessons(), start=1):
|
||||
lesson.index = i
|
||||
lesson.index_label = f"{c.index_}.{i}"
|
||||
lesson.save()
|
||||
return f"{chapter.idx}.{lesson.idx}"
|
||||
|
||||
def reindex_exercises(self):
|
||||
for i, c in enumerate(self.get_chapters(), start=1):
|
||||
@@ -202,7 +212,7 @@ class LMSCourse(Document):
|
||||
|
||||
def _reindex_exercises_in_chapter(self, c):
|
||||
i = 1
|
||||
for lesson in c.get_lessons():
|
||||
for lesson in self.get_lessons(c):
|
||||
for exercise in lesson.get_exercises():
|
||||
exercise.index_ = i
|
||||
exercise.index_label = f"{c.index_}.{i}"
|
||||
@@ -237,21 +247,6 @@ class LMSCourse(Document):
|
||||
membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title")
|
||||
return all_memberships
|
||||
|
||||
def get_mentors(self, batch=None):
|
||||
filters = {
|
||||
"course": self.name,
|
||||
"member_type": "Mentor"
|
||||
}
|
||||
if batch:
|
||||
filters["batch"] = batch
|
||||
|
||||
memberships = frappe.get_all(
|
||||
"LMS Batch Membership",
|
||||
filters,
|
||||
["member"])
|
||||
member_names = [m['member'] for m in memberships]
|
||||
return find_all("User", name=["IN", member_names])
|
||||
|
||||
def get_students(self, batch=None):
|
||||
"""Returns (email, full_name, username) of all the students of this batch as a list of dict.
|
||||
"""
|
||||
@@ -302,9 +297,6 @@ class LMSCourse(Document):
|
||||
return None
|
||||
return sum(ratings)/len(ratings)
|
||||
|
||||
def get_outline(self):
|
||||
return CourseOutline(self)
|
||||
|
||||
def get_progress(self, lesson):
|
||||
return frappe.db.get_value("LMS Course Progress",
|
||||
{
|
||||
@@ -314,55 +306,14 @@ class LMSCourse(Document):
|
||||
},
|
||||
["status"])
|
||||
|
||||
class CourseOutline:
|
||||
def __init__(self, course):
|
||||
self.course = course
|
||||
self.chapters = self.get_chapters()
|
||||
self.lessons = self.get_lessons()
|
||||
|
||||
def get_next(self, current):
|
||||
def get_neighbours(self, current, lessons):
|
||||
current = flt(current)
|
||||
numbers = sorted(lesson['number'] for lesson in self.lessons)
|
||||
try:
|
||||
index = numbers.index(current)
|
||||
return numbers[index+1]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
def get_prev(self, current):
|
||||
current = flt(current)
|
||||
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
|
||||
|
||||
def get_chapters(self):
|
||||
return frappe.db.get_all("Chapter",
|
||||
filters={"course": self.course.name},
|
||||
fields=["name", "title", "index_"],
|
||||
order_by="index_")
|
||||
|
||||
def get_lessons(self):
|
||||
chapters = [c['name'] for c in self.chapters]
|
||||
lessons = frappe.db.get_all("Lesson",
|
||||
filters={"chapter": ["IN", chapters]},
|
||||
fields=["name", "title", "chapter", "index_"])
|
||||
|
||||
chapter_numbers = {c['name']: c['index_'] for c in self.chapters}
|
||||
for lesson in lessons:
|
||||
lesson['number'] = flt("{}.{}".format(chapter_numbers[lesson['chapter']], lesson['index_']))
|
||||
return lessons
|
||||
|
||||
@frappe.whitelist()
|
||||
def reindex_lessons(doc):
|
||||
course_data = json.loads(doc)
|
||||
course = frappe.get_doc("LMS Course", course_data['name'])
|
||||
course.reindex_lessons()
|
||||
frappe.msgprint("All lessons in this course have been re-indexed.")
|
||||
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):
|
||||
|
||||
@@ -27,12 +27,13 @@
|
||||
"fieldname": "lesson",
|
||||
"fieldtype": "Link",
|
||||
"label": "Lesson",
|
||||
"options": "Lesson"
|
||||
"options": "Lesson",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-23 17:58:57.642873",
|
||||
"modified": "2021-07-23 19:06:12.551633",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Quiz",
|
||||
|
||||
@@ -46,34 +46,30 @@ class LMSQuiz(Document):
|
||||
return result[0]
|
||||
|
||||
@frappe.whitelist()
|
||||
def submit(quiz, result):
|
||||
def quiz_summary(quiz, results):
|
||||
score = 0
|
||||
answer_map = {
|
||||
"is_correct_1": "option_1",
|
||||
"is_correct_2": "option_2",
|
||||
"is_correct_3": "option_3",
|
||||
"is_correct_4": "option_4"
|
||||
}
|
||||
result = json.loads(result)
|
||||
quiz_details = frappe.get_doc("LMS Quiz", quiz)
|
||||
results = json.loads(results)
|
||||
|
||||
for response in result:
|
||||
match = list(filter(lambda x: x.question == response.get("question"), quiz_details.questions))[0]
|
||||
correct_options = quiz_details.get_correct_options(match)
|
||||
correct_answers = [ match.get(answer_map[option]) for option in correct_options ]
|
||||
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"])
|
||||
|
||||
if response.get("answer") == correct_answers:
|
||||
response["result"] = "Right"
|
||||
score += 1
|
||||
else:
|
||||
response["result"] = "Wrong"
|
||||
response["answer"] = ("").join([ ans if idx == len(response.get("answer")) -1 else ans + ", " for idx, ans in enumerate(response.get("answer")) ])
|
||||
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": result,
|
||||
"result": results,
|
||||
"score": score
|
||||
}).save(ignore_permissions=True)
|
||||
update_progress(quiz_details.lesson)
|
||||
|
||||
return score
|
||||
|
||||
@@ -3,6 +3,39 @@
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
import frappe
|
||||
|
||||
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"})
|
||||
|
||||
@@ -9,15 +9,23 @@
|
||||
"options_section",
|
||||
"option_1",
|
||||
"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": [
|
||||
@@ -101,12 +109,52 @@
|
||||
{
|
||||
"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,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-22 16:54:13.133859",
|
||||
"modified": "2021-07-19 19:35:28.446236",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Quiz Question",
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
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"])}`;
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"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",
|
||||
@@ -20,7 +21,7 @@
|
||||
"is_standard": 1,
|
||||
"login_required": 1,
|
||||
"max_attachment_size": 0,
|
||||
"modified": "2021-07-14 17:15:15.424855",
|
||||
"modified": "2021-08-06 14:40:39.013776",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "profile",
|
||||
@@ -72,6 +73,18 @@
|
||||
"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",
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
<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 %}
|
||||
{% set all_memberships = course.get_all_memberships(frappe.session.user) %}
|
||||
{% if membership and membership.batch and all_memberships | length > 1 %}
|
||||
<a class="pull-right dropdown-item border rounded" style="width: 10rem;" href="#" id="navbarDropdown" role="button"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{{ membership.batch_title }}
|
||||
</a>
|
||||
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||
{% for data in all_memberships %}
|
||||
{% if data.batch != membership.batch %}
|
||||
<a class="dropdown-item switch-batch"
|
||||
href="/courses/{{ course.name }}/home?batch={{ data.batch }}">{{ data.batch_title }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if not membership %}
|
||||
{% set display_class = "hide" %}
|
||||
{% else %}
|
||||
{% set display_class = "" %}
|
||||
{% endif %}
|
||||
|
||||
<ul class="nav nav-tabs mt-4">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="home" href="/courses/{{course.name}}/home{{ course.query_parameter }}">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
{% set lesson_index = course.get_lesson_index(membership.current_lesson) if membership and membership.current_lesson
|
||||
else '1.1' %}
|
||||
<a class="nav-link" id="learn"
|
||||
href="{{ course.get_learn_url(lesson_index) }}{{ course.query_parameter }}">Lessons</a>
|
||||
</li>
|
||||
<!-- <li class="nav-item">
|
||||
<a class="nav-link" id="schedule" href="/courses/{{course.name}}/schedule">Schedule</a>
|
||||
</li> -->
|
||||
<li class="nav-item {{ display_class }}">
|
||||
<a class="nav-link" id="members" href="/courses/{{course.name}}/members{{ course.query_parameter }}">Members</a>
|
||||
</li>
|
||||
<!-- <li class="nav-item {{ display_class }}">
|
||||
<a class="nav-link" id="discussion" href="/courses/{{course.name}}/discuss">Discussion</a>
|
||||
</li> -->
|
||||
<!-- <li class="nav-item">
|
||||
<a class="nav-link" id="about" href="/courses/{{course.name}}/about">About</a>
|
||||
</li> -->
|
||||
{% if membership and membership.batch and course.is_mentor(frappe.session.user) %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="progress" href="/courses/{{course.name}}/progress{{ course.query_parameter }}">Progress</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% block script %}
|
||||
<script>
|
||||
frappe.ready(() => {
|
||||
var selector = document.querySelector(`a[href="${decodeURIComponent(window.location.pathname)}{{ course.query_parameter }}"]`)
|
||||
if (selector) {
|
||||
selector.classList.add('active');
|
||||
}
|
||||
else {
|
||||
$("#learn").addClass('active')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="course-instructor breadcrumb">
|
||||
<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">
|
||||
|
||||
|
||||
@@ -1,31 +1,33 @@
|
||||
<div>
|
||||
|
||||
<div class="small-title chapter-title" data-target="#{{ chapter.get_slugified_chapter_title() }}"
|
||||
<div class="small-title chapter-title" data-target="#{{ course.get_slugified_chapter_title(chapter.title) }}"
|
||||
data-toggle="collapse" aria-expanded="false">
|
||||
<img class="chapter-icon" src="/assets/community/icons/chevron-right.svg">
|
||||
{{ index }}. {{ chapter.title }}
|
||||
</div>
|
||||
|
||||
<div class="chapter-content collapse navbar-collapse" id="{{ chapter.get_slugified_chapter_title() }}">
|
||||
<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>
|
||||
{% endif %}
|
||||
|
||||
<div class="lessons">
|
||||
|
||||
{% for lesson in chapter.get_lessons() %}
|
||||
{% for lesson in course.get_lessons(chapter) %}
|
||||
|
||||
<div class="lesson-info {% if membership.current_lesson == lesson.name %} active-lesson {% endif %}">
|
||||
<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(course.get_lesson_index(lesson.name)) }}{{course.query_parameter}}"
|
||||
href="{{ course.get_learn_url(lesson.number) }}{{course.query_parameter}}"
|
||||
data-course="{{ course.name }}">
|
||||
{{ lesson.title }}
|
||||
|
||||
{% if membership %}
|
||||
<img class="lesson-progress-tick {{ course.get_progress(lesson.name) != 'Complete' and 'hide' }}"
|
||||
<img class="ml-1 lesson-progress-tick {{ course.get_progress(lesson.name) != 'Complete' and 'hide' }}"
|
||||
src="/assets/community/icons/check.svg">
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<div class="common-card-style course-card">
|
||||
<div class="course-image" style="background-image: url({{ course.image }});">
|
||||
<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">
|
||||
@@ -53,15 +57,19 @@
|
||||
|
||||
{% set query_parameter = "?batch=" + membership.batch if membership and membership.batch else "" %}
|
||||
|
||||
{% if membership %}
|
||||
{% if course.upcoming %}
|
||||
<div class="view-course-link is-default">
|
||||
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>
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
<div class="batch">
|
||||
<div class="batch-details">
|
||||
<div class="">Session every {{batch.sessions_on}}</div>
|
||||
<div>{{frappe.utils.format_time(batch.start_time, "short")}} -
|
||||
{{frappe.utils.format_time(batch.end_time, "short")}}
|
||||
</div>
|
||||
<div>Starting {{frappe.utils.format_date(batch.start_date, "medium")}}</div>
|
||||
<div class="course-type" style="color: #888; padding: 10px 0px;">mentors</div>
|
||||
|
||||
{% for m in course.get_mentors(batch.name) %}
|
||||
<div>
|
||||
{{ widgets.Avatar(member=m, avatar_class="avatar-medium" ) }}
|
||||
<span class="instructor-title">{{m.full_name}}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if can_manage or can_join %}
|
||||
<div class="cta">
|
||||
<div class="">
|
||||
{% if can_manage %}
|
||||
<a href="/courses/{{ course.name }}/home?batch={{ batch.name }}" class="btn btn-primary manage-batch" data-batch="{{ batch.name | urlencode }}"
|
||||
data-course="{{ course.name | urlencode }}">Manage</a>
|
||||
{% elif can_join %}
|
||||
<button class="join-batch btn btn-secondary" data-batch="{{ batch.name | urlencode }}"
|
||||
data-course="{{ course.name | urlencode }}">Join this Batch</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -1,23 +1,28 @@
|
||||
{% if course.get_reviews() | length %}
|
||||
<div class="reviews-parent col">
|
||||
<div class="reviews-heading">
|
||||
<div class="course-home-headings">Student Review</div>
|
||||
<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) %}
|
||||
<a class="review-link" href="">
|
||||
Provide your Feedback
|
||||
</a>
|
||||
<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 course.get_reviews() %}
|
||||
{% 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") }}
|
||||
<span class="course-instructor">
|
||||
{{ review.owner_details.full_name }}
|
||||
</span>
|
||||
<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] %}
|
||||
@@ -30,6 +35,7 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="modal fade review-modal" id="review-modal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel"
|
||||
@@ -67,8 +73,8 @@
|
||||
</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;"
|
||||
<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>
|
||||
@@ -77,9 +83,9 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="button submit-review is-primary" data-course="{{ course.name | urlencode}}" id="submit-review">Submit</div>
|
||||
<div class="button submit-review is-primary" data-course="{{ course.name | urlencode}}" id="submit-review">
|
||||
Submit</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
63
community/overrides/test_user.py
Normal file
63
community/overrides/test_user.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# 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_underscore_at_end(self):
|
||||
new_user = frappe.get_doc({
|
||||
"doctype": "User",
|
||||
"email": "test_with_underscore_at_end@example.com",
|
||||
"first_name": "Username___"
|
||||
}).insert()
|
||||
self.assertNotEqual(new_user.username[-1], "_")
|
||||
|
||||
|
||||
def test_with_short_first_name(self):
|
||||
new_user = frappe.get_doc({
|
||||
"doctype": "User",
|
||||
"email": "test_with_short_first_name@example.com",
|
||||
"first_name": "USN"
|
||||
}).insert()
|
||||
self.assertGreaterEqual(len(new_user.username), 4)
|
||||
|
||||
@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_underscore_at_end@example.com",
|
||||
"test_with_short_first_name@example.com"
|
||||
]
|
||||
frappe.db.delete("User", {"name": ["in", users]})
|
||||
@@ -2,15 +2,64 @@ import frappe
|
||||
from frappe.core.doctype.user.user import User
|
||||
from frappe.utils import cint
|
||||
import hashlib
|
||||
import random
|
||||
import re
|
||||
from frappe import _
|
||||
|
||||
class CustomUser(User):
|
||||
|
||||
def validate(self):
|
||||
super(CustomUser, self).validate()
|
||||
self.validate_username_characters()
|
||||
|
||||
def validate_username_characters(self):
|
||||
if len(self.username):
|
||||
underscore_condition = self.username[0] == "_" or self.username[-1] == "_"
|
||||
else:
|
||||
underscore_condition = ''
|
||||
|
||||
if self.is_new():
|
||||
if not self.username:
|
||||
self.username = self.get_username_from_first_name()
|
||||
|
||||
if self.username.find(" "):
|
||||
self.username.replace(" ", "")
|
||||
|
||||
if not re.match("^[A-Za-z0-9_]*$", self.username) or underscore_condition:
|
||||
self.username = self.remove_illegal_characters()
|
||||
|
||||
if len(self.username) < 4:
|
||||
self.username = self.email.replace("@", "").replace(".", "")
|
||||
|
||||
while self.username_exists():
|
||||
self.username = self.remove_illegal_characters() + str(random.randint(0, 99))
|
||||
|
||||
else:
|
||||
if not self.username:
|
||||
frappe.throw(_("Username already exists."))
|
||||
|
||||
if not re.match("^[A-Za-z0-9_]*$", self.username):
|
||||
frappe.throw(_("Username can only contain alphabets, numbers and unedrscore."))
|
||||
|
||||
if underscore_condition:
|
||||
frappe.throw(_("First and Last character of username cannot be Underscore(_)."))
|
||||
|
||||
if len(self.username) < 4:
|
||||
frappe.throw(_("Username cannot be less than 4 characters"))
|
||||
|
||||
def get_username_from_first_name(self):
|
||||
return frappe.scrub(self.first_name) + str(random.randint(0, 99))
|
||||
|
||||
def remove_illegal_characters(self):
|
||||
return re.sub("[^\w]+", "", self.username).strip("_")
|
||||
|
||||
def get_authored_courses(self) -> int:
|
||||
"""Returns the number of courses authored by this user.
|
||||
"""
|
||||
return frappe.get_all(
|
||||
'LMS Course', {
|
||||
'owner': self.name
|
||||
'owner': self.name,
|
||||
'is_published': True
|
||||
})
|
||||
|
||||
def get_palette(self):
|
||||
@@ -62,9 +111,16 @@ class CustomUser(User):
|
||||
|
||||
def get_mentored_courses(self):
|
||||
""" Returns all courses mentored by this user """
|
||||
return frappe.get_all("LMS Course Mentor Mapping",
|
||||
mentored_courses = []
|
||||
mapping = frappe.get_all("LMS Course Mentor Mapping",
|
||||
{
|
||||
"mentor": self.name
|
||||
"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
|
||||
|
||||
@@ -6,3 +6,4 @@ 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_lms_message
|
||||
community.patches.replace_member_with_user_in_mentor_request
|
||||
community.patches.v0_0.chapter_lesson_index_table
|
||||
|
||||
50
community/patches/v0_0/chapter_lesson_index_table.py
Normal file
50
community/patches/v0_0/chapter_lesson_index_table.py
Normal file
@@ -0,0 +1,50 @@
|
||||
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()
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
@import "./style.css";
|
||||
@import "./vars.css";
|
||||
@import "./style.less";
|
||||
|
||||
@@ -234,6 +234,22 @@ input[type=checkbox] {
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.default-image {
|
||||
background-color: var(--avatar-frame-bg);
|
||||
color: var(--avatar-frame-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.default-image-text {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-self: center;
|
||||
justify-content: normal;
|
||||
font-size: 7rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.course-tags {
|
||||
display: flex;
|
||||
position: relative;
|
||||
@@ -259,11 +275,11 @@ input[type=checkbox] {
|
||||
|
||||
.common-page-style {
|
||||
background: #F4F5F6;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
.common-card-style {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
background: #FFFFFF;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
@@ -292,6 +308,12 @@ input[type=checkbox] {
|
||||
padding: 0px 24px 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 350px) {
|
||||
.course-card-content {
|
||||
padding: 0px 10px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.course-card-title {
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
@@ -308,9 +330,10 @@ input[type=checkbox] {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-divider {
|
||||
border: 1px solid #F4F5F6;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.card-divider-dark {
|
||||
@@ -358,7 +381,7 @@ input[type=checkbox] {
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.cards-parent {
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 336px));
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
-moz-column-gap: 16px;
|
||||
column-gap: 16px;
|
||||
row-gap: 16px;
|
||||
@@ -367,7 +390,7 @@ input[type=checkbox] {
|
||||
|
||||
@media (max-width: 375px) {
|
||||
.cards-parent {
|
||||
grid-template-columns: repeat(auto-fill, minmax(100%, 312px));
|
||||
grid-template-columns: repeat(auto-fill, minmax(100%, 1fr));
|
||||
-moz-column-gap: 24px;
|
||||
column-gap: 24px;
|
||||
row-gap: 24px;
|
||||
@@ -375,7 +398,7 @@ input[type=checkbox] {
|
||||
}
|
||||
|
||||
.courses-header {
|
||||
padding: 50px 30px 20px;
|
||||
padding: 50px 20px 20px;
|
||||
color: var(--text-color);
|
||||
font-weight: 600;
|
||||
font-size: 22px;
|
||||
@@ -383,22 +406,10 @@ input[type=checkbox] {
|
||||
letter-spacing: -0.0175em
|
||||
}
|
||||
|
||||
.course-top-section {
|
||||
float: none;
|
||||
margin: 0 auto;
|
||||
max-width: 1150px;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.course-top-section {
|
||||
max-width: 720px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.course-top-section {
|
||||
max-width: 342px;
|
||||
@media (min-width: 576px) and (max-width: 992px) {
|
||||
.container {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,27 +487,33 @@ input[type=checkbox] {
|
||||
--star-fill: #74808B;
|
||||
}
|
||||
|
||||
div.custom-checkbox>label>input {
|
||||
.custom-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.custom-checkbox>label>input {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
div.custom-checkbox>label>img {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
.custom-checkbox>label>.empty-checkbox {
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
border: 1px solid black;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
div.custom-checkbox>label>input:checked+img {
|
||||
background: url(/assets/community/images/Vector.png);
|
||||
.custom-checkbox>label>input:checked+.empty-checkbox {
|
||||
background: url(/assets/community/icons/tick.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: 15px 15px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.quiz-label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.course-card-wide {
|
||||
height: fit-content;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 24px;
|
||||
@@ -528,6 +545,7 @@ div.custom-checkbox>label>input:checked+img {
|
||||
margin-right: 32px;
|
||||
border-radius: 5px;
|
||||
flex: 1;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@@ -546,23 +564,10 @@ div.custom-checkbox>label>input:checked+img {
|
||||
}
|
||||
}
|
||||
|
||||
.course-home-page {
|
||||
max-width: 1150px;
|
||||
margin: 0 auto;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (max-width: 500px) {
|
||||
.course-home-page {
|
||||
max-width: 688px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.course-home-page {
|
||||
width: 100%;
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -570,6 +575,7 @@ div.custom-checkbox>label>input:checked+img {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 2;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@@ -618,7 +624,7 @@ div.custom-checkbox>label>input:checked+img {
|
||||
}
|
||||
|
||||
.button {
|
||||
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.13), 0px 0px 0.5px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: var(--btn-shadow);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
@@ -670,6 +676,12 @@ div.custom-checkbox>label>input:checked+img {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.is-default {
|
||||
background-color: white;
|
||||
border: 1px solid #C8CFD5;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.course-home-outline {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
@@ -689,6 +701,7 @@ div.custom-checkbox>label>input:checked+img {
|
||||
.chapter-description {
|
||||
height: fit-content;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.chapter-icon {
|
||||
@@ -696,14 +709,39 @@ div.custom-checkbox>label>input:checked+img {
|
||||
}
|
||||
|
||||
.course-outline-instructor-parent {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
display: grid;
|
||||
grid-gap: 2rem;
|
||||
grid-template-columns: 4fr 1fr;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.course-outline-instructor-parent {
|
||||
grid-gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.course-outline-instructor-parent {
|
||||
flex-direction: column;
|
||||
padding: 0px 9px 0px;
|
||||
padding: 0px 24px 0px;
|
||||
grid-template-columns: none;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-parent-section {
|
||||
display: grid;
|
||||
grid-gap: 2rem;
|
||||
grid-template-columns: 4fr 1fr;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.profile-parent-section {
|
||||
grid-gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.profile-parent-section {
|
||||
grid-template-columns: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -756,6 +794,13 @@ div.custom-checkbox>label>input:checked+img {
|
||||
border-radius: .25rem;
|
||||
}
|
||||
|
||||
.course-content-parent .lesson-links {
|
||||
padding: 0 0 0 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
line-height: 200%;
|
||||
}
|
||||
|
||||
.chapter-content {
|
||||
margin: 0;
|
||||
margin-left: .875rem;
|
||||
@@ -892,14 +937,18 @@ div.custom-checkbox>label>input:checked+img {
|
||||
}
|
||||
|
||||
.description-card {
|
||||
padding: 24px;
|
||||
padding: 1.5rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.overview-card {
|
||||
padding: 8px 24px 8px;
|
||||
padding: 1.5rem;
|
||||
width: 256px;
|
||||
flex-direction: column;
|
||||
display: grid;
|
||||
-moz-column-gap: 1rem;
|
||||
column-gap: 1rem;
|
||||
row-gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@@ -910,7 +959,7 @@ div.custom-checkbox>label>input:checked+img {
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.overview-card {
|
||||
padding: 8px 9px 8px;
|
||||
padding: 24px 26px 24px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -932,10 +981,6 @@ div.custom-checkbox>label>input:checked+img {
|
||||
}
|
||||
}
|
||||
|
||||
.overtime-item {
|
||||
margin: 16px 0px 16px;
|
||||
}
|
||||
|
||||
.view-all-mentors {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
@@ -982,8 +1027,24 @@ div.custom-checkbox>label>input:checked+img {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.quiz-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.question {
|
||||
flex-direction: column;
|
||||
width: 688px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.question p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.active-question .card-divider {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.dark-links {
|
||||
@@ -995,20 +1056,16 @@ div.custom-checkbox>label>input:checked+img {
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
padding: 1rem 0.5rem 0;
|
||||
padding: 1rem 0 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
line-height: 135%;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.course-details-outline {
|
||||
width: 352px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 1025px) {
|
||||
.course-details-outline {
|
||||
width: 100%;
|
||||
}
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.lesson-content-card {
|
||||
@@ -1017,19 +1074,20 @@ div.custom-checkbox>label>input:checked+img {
|
||||
}
|
||||
|
||||
.course-content-parent {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap-reverse;
|
||||
display: grid;
|
||||
grid-gap: 2rem;
|
||||
grid-template-columns: 2fr minmax(600px, 5fr);
|
||||
}
|
||||
|
||||
@media (max-width: 375px) {
|
||||
@media (max-width: 1024px) {
|
||||
.course-content-parent {
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
}
|
||||
|
||||
.course-content-parent .course-home-headings {
|
||||
margin: 0px 0px 16px;
|
||||
margin: 0px 0px 1rem;
|
||||
}
|
||||
|
||||
.lesson-pagination {
|
||||
@@ -1039,13 +1097,12 @@ div.custom-checkbox>label>input:checked+img {
|
||||
}
|
||||
|
||||
.lesson-pagination-parent {
|
||||
width: 736px;
|
||||
margin-top: 16px;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1025px) {
|
||||
@media (max-width: 768px) {
|
||||
.lesson-pagination-parent {
|
||||
width: 100%;
|
||||
margin-left: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1053,28 +1110,6 @@ div.custom-checkbox>label>input:checked+img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.course-details-page {
|
||||
padding: 0px 0px 80px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 1120px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media (max-width: 1025px) {
|
||||
.course-details-page {
|
||||
padding: 24px 0px 24px;
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.course-details-page {
|
||||
padding: 24px 0px 24px;
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
.active-lesson {
|
||||
background-color: #EBF5FF;
|
||||
border-radius: 4px;
|
||||
@@ -1091,16 +1126,14 @@ div.custom-checkbox>label>input:checked+img {
|
||||
}
|
||||
|
||||
.profile-page {
|
||||
max-width: 1150px;
|
||||
margin: 0 auto;
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.profile-banner {
|
||||
width: 100%;
|
||||
height: 248px;
|
||||
border-radius: 12px 12px 0px 0px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
@@ -1109,6 +1142,11 @@ div.custom-checkbox>label>input:checked+img {
|
||||
}
|
||||
}
|
||||
|
||||
.profile-about-section {
|
||||
flex: 1;
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.profile-info {
|
||||
height: 68px;
|
||||
background: #ffffff;
|
||||
@@ -1143,7 +1181,7 @@ div.custom-checkbox>label>input:checked+img {
|
||||
line-height: 156%;
|
||||
letter-spacing: -0.0175em;
|
||||
text-shadow: 0px 1px 1px rgba(0, 0, 0, 0.64);
|
||||
padding: 20px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 375px) {
|
||||
@@ -1167,7 +1205,7 @@ div.custom-checkbox>label>input:checked+img {
|
||||
height: fit-content;
|
||||
box-shadow: 0px 1px 1px rgb(0 0 0 / 16%);
|
||||
border-radius: 4px;
|
||||
margin-top: 25px;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
@media (max-width: 375px) {
|
||||
@@ -1183,6 +1221,19 @@ div.custom-checkbox>label>input:checked+img {
|
||||
left: 174px;
|
||||
font-size: 12px;
|
||||
line-height: 165%;
|
||||
width: 85%;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.profile-profession {
|
||||
width: 75%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.profile-profession {
|
||||
width: 60%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
@@ -1196,11 +1247,11 @@ div.custom-checkbox>label>input:checked+img {
|
||||
.profile-profession {
|
||||
top: 5px;
|
||||
left: 70px;
|
||||
width: 75%;
|
||||
}
|
||||
}
|
||||
|
||||
.social-icons {
|
||||
float: right;
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
@@ -1220,18 +1271,6 @@ div.custom-checkbox>label>input:checked+img {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.profile-courses {
|
||||
padding: 0px 24px 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
.profile-courses {
|
||||
padding: 0px 10px 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 12px;
|
||||
line-height: 165%;
|
||||
@@ -1291,3 +1330,19 @@ pre {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.markdown-source h1 {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.markdown-source h2 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.markdown-source h3 {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.markdown-source h4 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@@ -1,307 +0,0 @@
|
||||
h2 {
|
||||
margin: 20px 0px;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.teaser {
|
||||
background: white;
|
||||
border-radius: 9px;
|
||||
border: 1px solid #C4C4C4;
|
||||
|
||||
.teaser-body {
|
||||
padding: 20px;
|
||||
box-shadow: 0px 5px 10px rgb(0 0 0 / 10%)
|
||||
}
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.anchor_style {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.anchor_style:hover {
|
||||
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);
|
||||
}
|
||||
|
||||
.instructor-title {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.lesson-teaser {
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
#hero h1 {
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
.lesson-page {
|
||||
margin: 20px 0px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.no-preview-message {
|
||||
width: fit-content;
|
||||
margin: 50px 0px 50px;
|
||||
color: black;
|
||||
}
|
||||
4
community/public/icons/minus-circle-green.svg
Normal file
4
community/public/icons/minus-circle-green.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" fill="#68D391" stroke="#68D391" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8 12H16" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 425 B |
1
community/public/icons/minus-circle.svg
Normal file
1
community/public/icons/minus-circle.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-minus-circle"><circle cx="12" cy="12" r="10"></circle><line x1="8" y1="12" x2="16" y2="12"></line></svg>
|
||||
|
After Width: | Height: | Size: 309 B |
@@ -1,3 +1 @@
|
||||
<svg class="icon">
|
||||
<use href="#icon-tick"></use>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check"><polyline points="20 6 9 17 4 12"></polyline></svg>
|
||||
|
Before Width: | Height: | Size: 58 B After Width: | Height: | Size: 262 B |
3
community/public/icons/wrong.svg
Normal file
3
community/public/icons/wrong.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 20C15.5228 20 20 15.5228 20 10C20 4.47715 15.5228 0 10 0C4.47715 0 0 4.47715 0 10C0 15.5228 4.47715 20 10 20ZM6.0429 6.04289C6.43342 5.65237 7.06659 5.65237 7.45711 6.04289L10.0011 8.58692L12.5451 6.04294C12.9356 5.65242 13.5688 5.65242 13.9593 6.04294C14.3499 6.43347 14.3499 7.06663 13.9593 7.45716L11.4154 10.0011L13.9593 12.5451C14.3499 12.9356 14.3499 13.5688 13.9593 13.9593C13.5688 14.3499 12.9357 14.3499 12.5451 13.9593L10.0011 11.4154L7.45711 13.9594C7.06659 14.3499 6.43342 14.3499 6.0429 13.9594C5.65237 13.5689 5.65237 12.9357 6.0429 12.5452L8.58693 10.0011L6.0429 7.45711C5.65237 7.06658 5.65237 6.43342 6.0429 6.04289Z" fill="#F56B6B"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 808 B |
@@ -1,9 +1,8 @@
|
||||
<div>
|
||||
{% set site_link = "<a href='" + site_url + "'>" + site_url + "</a>" %}
|
||||
<p>{{_("Dear Community Member,")}}</p>
|
||||
<p>{{_("Your Invite Request to be a part of {0} has
|
||||
been approved.").format(site_link)}}</p>
|
||||
<p>Click on the link below to complete your Sign up and set a new password</p>
|
||||
{% set site_link = "<a href='" + site_url + "'>" + site_name + "</a>" %}
|
||||
<p>{{_("Hi,")}}</p>
|
||||
<p>{{_("Welcome to {0}!").format(site_name)}}</p>
|
||||
<p>Click on the link below to complete your sign up and set a new password</p>
|
||||
<p style="margin: 15px 0px;">
|
||||
<a href="{{ signup_form_link }}" rel="nofollow" class="btn btn-primary">{{ _("Complete Sign Up") }}</a>
|
||||
</p>
|
||||
@@ -14,5 +13,5 @@
|
||||
</p>
|
||||
<br>
|
||||
<p>Thanks and Regards,</p>
|
||||
<p>Your Community.</p>
|
||||
</div>
|
||||
<p>{{site_name}}</p>
|
||||
</div>
|
||||
|
||||
@@ -1,33 +1,59 @@
|
||||
{% set last_submission = quiz.get_last_submission_details() %}
|
||||
{% if last_submission %}
|
||||
<div class="mb-5 pull-right">
|
||||
<div class="text-muted">Last Submitted On: {{ frappe.utils.pretty_date(last_submission.creation) }}</div>
|
||||
<div class="text-muted">Last Submission Score: {{ last_submission.score }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<h3 id="title" class="mb-5">{{ quiz.title }}</h3>
|
||||
<form id="quiz-form">
|
||||
{% for question in quiz.questions %}
|
||||
<div class="question mb-5" data-question="{{ question.question }}"
|
||||
data-multi="{{ question.multiple_correct_answers}}">
|
||||
<p> {{ loop.index }}. {{ question.question }}</p>
|
||||
{% set options = [question.option_1, question.option_2, question.option_3, question.option_4] %}
|
||||
{% for option in options %}
|
||||
{% if option %}
|
||||
<div class="custom-checkbox mb-2">
|
||||
<label>
|
||||
<input {% if question.multiple %} type="checkbox" {% else %} type="radio"
|
||||
name="{{ question.question | urlencode }}" {% endif %} class="option" value="{{ option | urlencode }}">
|
||||
<img />
|
||||
</label>
|
||||
<span class="label-area">{{ option }}</span>
|
||||
<div id="quiz-title" class="course-home-headings">{{ quiz.title }}</div>
|
||||
|
||||
<div class="card-divider"></div>
|
||||
|
||||
<div class="mt-5">
|
||||
<form id="quiz-form">
|
||||
<div class="questions">
|
||||
{% for question in quiz.questions %}
|
||||
<div class="question {% if loop.index == 1 %} active-question {% else %} hide {% endif %}"
|
||||
data-question="{{ question.question }}" data-multi="{{ question.multiple}}" data-qt-index="{{ loop.index }}">
|
||||
<p>{{ frappe.utils.md_to_html(question.question) }}</p>
|
||||
|
||||
{% if question.multiple %}
|
||||
<small class="font-weight-bold">Choose all answers that apply:</small>
|
||||
{% else %}
|
||||
<small class="font-weight-bold">Choose 1 answer:</small>
|
||||
{% endif %}
|
||||
|
||||
<div class="card-divider"></div>
|
||||
|
||||
{% set options = [question.option_1, question.option_2, question.option_3, question.option_4] %}
|
||||
|
||||
{% for option in options %}
|
||||
{% if option %}
|
||||
|
||||
<div class="custom-checkbox">
|
||||
<label class="quiz-label">
|
||||
<input class="option" value="{{ option | urlencode }}"
|
||||
data-correct="{{ question['is_correct_' + loop.index | string] }}"
|
||||
{% if question.multiple %} type="checkbox"
|
||||
{% else %} type="radio" name="{{ question.question | urlencode }}" {% endif %}>
|
||||
<img class="empty-checkbox mr-3"/>
|
||||
</label>
|
||||
<span class="label-area">{{ frappe.utils.md_to_html(option) }}</span>
|
||||
</div>
|
||||
|
||||
{% set explanation = question['explanation_' + loop.index | string] %}
|
||||
{% if explanation %}
|
||||
<small class="explanation muted-text hide">{{ explanation }}</small>
|
||||
{% endif %}
|
||||
|
||||
<div class="card-divider"></div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<button class="btn btn-secondary hide mb-5" id="try-again">Try Again</button>
|
||||
<button class="btn btn-primary" id="submit-quiz">Submit</button>
|
||||
<h4 class="success-message"></h4>
|
||||
<h5 class="score text-muted"></h5>
|
||||
</form>
|
||||
<div class="quiz-footer">
|
||||
<span class="font-weight-bold"> <span class="current-question">1</span> of {{ quiz.questions | length }}</span>
|
||||
<button class="btn btn-primary pull-right" id="check" disabled>Check</button>
|
||||
<button class="btn btn-primary hide" id="next">Next</button>
|
||||
<button class="btn btn-primary hide" id="summary">Summary</button>
|
||||
</div>
|
||||
<div class="button is-secondary pull-right hide" id="try-again">Try Again</div>
|
||||
<h4 class="success-message"></h4>
|
||||
<h5 class="score text-muted"></h5>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
{% extends "templates/base.html" %}
|
||||
{% block title %}About{% endblock %}
|
||||
{% block head_include %}
|
||||
<meta name="description" content="Courses" />
|
||||
<meta name="keywords" content="" />
|
||||
|
||||
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
{{ widgets.BatchTabs(course=course, batch=batch) }}
|
||||
<div class="tab-content" id="about">
|
||||
{{ CourseBasicDetail(course)}}
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="col-lg-4 col-md-12">
|
||||
<div class="sidebar">
|
||||
{{ widgets.InstructorSection(instructor=course.get_instructor()) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% macro CourseBasicDetail(course) %}
|
||||
<h2>{{course.title}}</h2>
|
||||
<div class="course-description">
|
||||
{{course.short_introduction}}
|
||||
</div>
|
||||
{% if course.video_link %}
|
||||
<div class="preview-video">
|
||||
<iframe width="560" height="315" src="{{course.video_link}}" title="YouTube video player" frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen></iframe>
|
||||
</div>
|
||||
{% endif %}
|
||||
<h2>About the Course</h2>
|
||||
<div>{{frappe.utils.md_to_html(course.description)}}</div>
|
||||
{% endmacro %}
|
||||
@@ -1,5 +0,0 @@
|
||||
import frappe
|
||||
from . import utils
|
||||
|
||||
def get_context(context):
|
||||
utils.get_common_context(context)
|
||||
@@ -11,7 +11,6 @@
|
||||
{% block content %}
|
||||
|
||||
<div class="container">
|
||||
{{ widgets.BatchTabs(course=course, membership=membership) }}
|
||||
<div class="messages-container mt-5">
|
||||
{{ widgets.BatchHeader(batch_name=batch.title, member_count=member_count)}}
|
||||
<ol class="messages">
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
{% extends "templates/base.html" %}
|
||||
{% block title %} Batch {% endblock %}
|
||||
|
||||
{% block head_include %}
|
||||
<meta name="description" content="Courses" />
|
||||
<meta name="keywords" content="" />
|
||||
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
{{ widgets.BatchTabs(course=course, membership=membership) }}
|
||||
<div class="course-details mt-5">
|
||||
{{ widgets.CourseOutline(course=course, batch=batch, show_link=membership, show_progress=True) }}
|
||||
</div>
|
||||
|
||||
{% if batch %}
|
||||
<div class="w-25">
|
||||
<h3>Batch Schedule</h3>
|
||||
{{ widgets.RenderBatch(course=course, batch=batch) }}
|
||||
</div>
|
||||
|
||||
{% if batch.description %}
|
||||
<div class="mt-5">
|
||||
<h3>Batch Details</h3>
|
||||
{{ frappe.utils.md_to_html(batch.description) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if course.is_mentor(frappe.session.user) %}
|
||||
{% set invite_link = frappe.utils.get_url() + "/courses/" + course.name + "/join?batch=" + batch.name %}
|
||||
<div class="">
|
||||
<h3> Invite Members </h3>
|
||||
<a href="" class="" id="invite-link" data-link="{{ invite_link }}">Get Batch Invitation
|
||||
Link</a>
|
||||
<small id="copy-message" class="text-muted" style="display: none;">Copied to Clipboard.</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
frappe.ready(() => {
|
||||
$("#invite-link").click((e) => {
|
||||
e.preventDefault();
|
||||
var link_element = $("#invite-link");
|
||||
var input_element = document.createElement("input");
|
||||
input_element.value = link_element.attr("data-link")
|
||||
document.body.appendChild(input_element);
|
||||
input_element.select();
|
||||
document.execCommand("copy");
|
||||
input_element.remove();
|
||||
$("#copy-message").slideDown(function () {
|
||||
setTimeout(function () {
|
||||
$("#copy-message").slideUp();
|
||||
}, 2000);
|
||||
});
|
||||
})
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,5 +0,0 @@
|
||||
import frappe
|
||||
from . import utils
|
||||
|
||||
def get_context(context):
|
||||
utils.get_common_context(context)
|
||||
@@ -1,13 +1,8 @@
|
||||
{% extends "templates/base.html" %}
|
||||
{% from "www/macros/livecode.html" import LiveCodeEditorJS, LiveCodeEditor with context %}
|
||||
{% block title %}{{ lesson.title }}{% endblock %}
|
||||
{% block title %} {{ lesson.title }} - {{ course.title }} {% endblock %}
|
||||
|
||||
{% block head_include %}
|
||||
<meta name="description" content="{{lesson.title}} - {{course.title}}" />
|
||||
<meta name="keywords" content="{{lesson.title}} - {{course.title}}" />
|
||||
<style>
|
||||
</style>
|
||||
|
||||
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
|
||||
<link rel="stylesheet" href="/assets/css/lms.css">
|
||||
<link rel="stylesheet" href="/assets/frappe/css/hljs-night-owl.css">
|
||||
@@ -21,7 +16,7 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="common-page-style">
|
||||
<div class="course-details-page">
|
||||
<div class="container course-details-page">
|
||||
{{ widgets.BreadCrumb(course=course, lesson=lesson) }}
|
||||
<div class="course-content-parent">
|
||||
<div class="course-details-outline">
|
||||
@@ -30,7 +25,7 @@
|
||||
<div class="lesson-pagination-parent">
|
||||
{{ LessonContent(lesson) }}
|
||||
{% if membership %}
|
||||
{{ pagination(prev_chap, prev_url, next_chap, next_url) }}
|
||||
{{ pagination(prev_url, next_url) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -47,18 +42,17 @@
|
||||
</div>
|
||||
|
||||
{% if membership or lesson.include_in_preview %}
|
||||
<div class="common-card-style lesson-content-card">{{ lesson.render_html() }}</div>
|
||||
<div class="common-card-style lesson-content-card markdown-source">{{ lesson.render_html() }}</div>
|
||||
{% else %}
|
||||
<div class="no-preview-message">
|
||||
<span>This lesson is not available for Preview. Please join the course to access this lesson.</span>
|
||||
<a href="/courses/{{ course.name }}">Checkout Course Details.</a>
|
||||
<div class="common-card-style lesson-content-card">
|
||||
<span>This lesson is not available for Preview. Please join the course to access this lesson. <a href="/courses/{{ course.name }}">Checkout Course Details.</a></span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro pagination(prev_chap, prev_url, next_chap, next_url) %}
|
||||
{% macro pagination(prev_url, next_url) %}
|
||||
<div class="lesson-pagination">
|
||||
|
||||
<div>
|
||||
|
||||
@@ -1,13 +1,27 @@
|
||||
frappe.ready(() => {
|
||||
|
||||
localStorage.removeItem($("#quiz-title").text());
|
||||
|
||||
save_current_lesson();
|
||||
|
||||
$(".option").click((e) => {
|
||||
enable_check(e);
|
||||
})
|
||||
|
||||
$("#progress").click((e) => {
|
||||
mark_progress(e);
|
||||
});
|
||||
|
||||
$("#submit-quiz").click((e) => {
|
||||
submit_quiz(e);
|
||||
$("#summary").click((e) => {
|
||||
quiz_summary(e);
|
||||
});
|
||||
|
||||
$("#check").click((e) => {
|
||||
check_answer(e);
|
||||
});
|
||||
|
||||
$("#next").click((e) => {
|
||||
mark_active_question(e);
|
||||
});
|
||||
|
||||
$("#try-again").click((e) => {
|
||||
@@ -25,6 +39,28 @@ var save_current_lesson = () => {
|
||||
}
|
||||
}
|
||||
|
||||
var enable_check = (e) => {
|
||||
if ($(".option:checked").length && $("#check").attr("disabled")) {
|
||||
$("#check").removeAttr("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
var mark_active_question = (e = undefined) => {
|
||||
var current_index;
|
||||
var next_index = 1;
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
current_index = $(".active-question").attr("data-qt-index");
|
||||
next_index = parseInt(current_index) + 1;
|
||||
}
|
||||
$(".question").addClass("hide").removeClass("active-question");
|
||||
$(`.question[data-qt-index='${next_index}']`).removeClass("hide").addClass("active-question");
|
||||
$(".current-question").text(`${next_index}`);
|
||||
$("#check").removeClass("hide").attr("disabled", true);
|
||||
$("#next").addClass("hide");
|
||||
$(".explanation").addClass("hide");
|
||||
}
|
||||
|
||||
var mark_progress = (e) => {
|
||||
var status = $(e.currentTarget).attr("data-progress");
|
||||
frappe.call({
|
||||
@@ -56,47 +92,77 @@ var change_progress_indicators = (status, e) => {
|
||||
$(e.currentTarget).text(label).attr("data-progress", data_progress);
|
||||
}
|
||||
|
||||
var submit_quiz = (e) => {
|
||||
var quiz_summary = (e) => {
|
||||
e.preventDefault();
|
||||
var result = [];
|
||||
$('.question').each((i, element) => {
|
||||
var options = $(element).find(".option");
|
||||
var answers = [];
|
||||
options.filter((i, op) => $(op).prop("checked")).each((i, elem) => answers.push(decodeURIComponent(elem.value)));
|
||||
result.push({
|
||||
"question": element.dataset.question,
|
||||
"answer": answers
|
||||
});
|
||||
});
|
||||
var quiz_name = $("#quiz-title").text();
|
||||
var total_questions = $(".question").length;
|
||||
|
||||
frappe.call({
|
||||
method: "community.lms.doctype.lms_quiz.lms_quiz.submit",
|
||||
method: "community.lms.doctype.lms_quiz.lms_quiz.quiz_summary",
|
||||
args: {
|
||||
quiz: $("#title").text(),
|
||||
result: result
|
||||
"quiz": quiz_name,
|
||||
"results": localStorage.getItem(quiz_name)
|
||||
},
|
||||
callback: (data) => {
|
||||
$("#submit-quiz").addClass("hide");
|
||||
var message = data.message == total_questions ? "Excellent Work" : "You were almost there."
|
||||
$(".question").addClass("hide");
|
||||
$(".quiz-footer").addClass("hide");
|
||||
$("#quiz-form").parent().prepend(
|
||||
`<div class="text-center summary"><h2>${message} 👏 </h2>
|
||||
<div class="font-weight-bold">${data.message}/${total_questions} correct.</div></div>`);
|
||||
$("#try-again").removeClass("hide");
|
||||
$(":input[type='checkbox']").prop("disabled", true);
|
||||
$(":input[type='radio']").prop("disabled", true);
|
||||
if (data.message == result.length) {
|
||||
$(".success-message").text("Congratulations, you cleared the quiz!");
|
||||
}
|
||||
else {
|
||||
$(".success-message").text("Some of your answers weren't correct. You can give it another shot.");
|
||||
}
|
||||
$(".score").text(`Score: ${data.message}/${result.length}`);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var try_quiz_again = (e) => {
|
||||
e.preventDefault();
|
||||
$(":input[type='checkbox']").prop("disabled", false);
|
||||
$(":input[type='radio']").prop("disabled", false);
|
||||
$("#quiz-form").trigger("reset");
|
||||
$(".success-message").text("");
|
||||
$(".score").text("");
|
||||
$("#submit-quiz").removeClass("hide");
|
||||
$("#try-again").addClass("hide");
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
var check_answer = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
var quiz_name = $("#quiz-title").text();
|
||||
var total_questions = $(".question").length;
|
||||
var current_index = $(".active-question").attr("data-qt-index");
|
||||
|
||||
$(".explanation").removeClass("hide");
|
||||
$("#check").addClass("hide");
|
||||
current_index == total_questions ? $("#summary").removeClass("hide") : $("#next").removeClass("hide");
|
||||
|
||||
var [answer, is_correct] = parse_options();
|
||||
add_to_local_storage(quiz_name, current_index, answer, is_correct)
|
||||
}
|
||||
|
||||
var parse_options = () => {
|
||||
var answer = [];
|
||||
var is_correct = [];
|
||||
$(".active-question input").each((i, element) => {
|
||||
var correct = parseInt($(element).attr("data-correct"));
|
||||
if ($(element).prop("checked")) {
|
||||
answer.push(decodeURIComponent($(element).val()));
|
||||
correct && is_correct.push(1);
|
||||
correct ? add_icon(element, "check") : add_icon(element, "wrong");
|
||||
}
|
||||
else {
|
||||
correct && is_correct.push(0);
|
||||
correct ? add_icon(element, "minus-circle-green") : add_icon(element, "minus-circle");
|
||||
}
|
||||
})
|
||||
return [answer, is_correct];
|
||||
}
|
||||
|
||||
var add_icon = (element, icon) => {
|
||||
$(element).parent().empty().html(`<img class="mr-3" src="/assets/community/icons/${icon}.svg">`);
|
||||
}
|
||||
|
||||
var add_to_local_storage = (quiz_name, current_index, answer, is_correct) => {
|
||||
var quiz_stored = JSON.parse(localStorage.getItem(quiz_name));
|
||||
var quiz_obj = {
|
||||
"question_index": current_index,
|
||||
"answer": answer.join(),
|
||||
"is_correct": is_correct
|
||||
}
|
||||
quiz_stored ? quiz_stored.push(quiz_obj) : quiz_stored = [quiz_obj]
|
||||
localStorage.setItem(quiz_name, JSON.stringify(quiz_stored))
|
||||
}
|
||||
|
||||
@@ -12,29 +12,26 @@ def get_context(context):
|
||||
lesson_index = frappe.form_dict.get("lesson")
|
||||
lesson_number = f"{chapter_index}.{lesson_index}"
|
||||
|
||||
course_name = context.course.name
|
||||
if not chapter_index or not lesson_index:
|
||||
if context.batch:
|
||||
index_ = get_lesson_index(context.course, context.batch, frappe.session.user) or "1.1"
|
||||
else:
|
||||
index_ = "1.1"
|
||||
frappe.local.flags.redirect_location = context.course.get_learn_url(index_) + context.course.query_parameter
|
||||
raise frappe.Redirect
|
||||
utils.redirect_to_lesson(context.course, index_)
|
||||
|
||||
context.lesson = context.course.get_lesson(chapter_index, lesson_index)
|
||||
context.lesson_index = lesson_index
|
||||
context.chapter_index = chapter_index
|
||||
context.lesson = get_current_lesson_details(lesson_number, context)
|
||||
neighbours = context.course.get_neighbours(lesson_number, context.lessons)
|
||||
context.next_url = get_learn_url(neighbours["next"], context.course)
|
||||
context.prev_url = get_learn_url(neighbours["prev"], context.course)
|
||||
|
||||
outline = context.course.get_outline()
|
||||
prev_ = outline.get_prev(lesson_number)
|
||||
next_ = outline.get_next(lesson_number)
|
||||
context.prev_chap = get_chapter_title(course_name, prev_)
|
||||
context.next_chap = get_chapter_title(course_name, next_)
|
||||
context.next_url = context.course.get_learn_url(next_) and context.course.get_learn_url(next_) + context.course.query_parameter
|
||||
context.prev_url = context.course.get_learn_url(prev_) and context.course.get_learn_url(prev_) + context.course.query_parameter
|
||||
meta_info = context.lesson.title + " - " + context.course.title
|
||||
context.metatags = {
|
||||
"title": meta_info,
|
||||
"keywords": meta_info,
|
||||
"description": meta_info
|
||||
}
|
||||
|
||||
context.page_extensions = get_page_extensions()
|
||||
|
||||
context.page_context = {
|
||||
"course": context.course.name,
|
||||
"batch": context.get("batch") and context.batch.name,
|
||||
@@ -42,6 +39,15 @@ def get_context(context):
|
||||
"is_member": context.membership is not None
|
||||
}
|
||||
|
||||
def get_current_lesson_details(lesson_number, context):
|
||||
details_list = list(filter(lambda x: cstr(x.number) == lesson_number, context.lessons))
|
||||
if not len(details_list):
|
||||
utils.redirect_to_lesson(context.course)
|
||||
return details_list[0]
|
||||
|
||||
def get_learn_url(lesson_number, course):
|
||||
return course.get_learn_url(lesson_number) and course.get_learn_url(lesson_number) + course.query_parameter
|
||||
|
||||
def get_chapter_title(course_name, lesson_number):
|
||||
if not lesson_number:
|
||||
return
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
{% extends "templates/base.html" %}
|
||||
|
||||
{% block title %}Members{% endblock %}
|
||||
{% block head_include %}
|
||||
<meta name="description" content="Courses" />
|
||||
<meta name="keywords" content="" />
|
||||
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="container">
|
||||
{{ widgets.BatchTabs(course=course, membership=membership) }}
|
||||
{{ MembersList(members)}}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% macro MembersList(members) %}
|
||||
<div class="mt-5">
|
||||
{% for member in members %}
|
||||
<div class="d-flex align-items-center">
|
||||
{{ widgets.Avatar(member=member, avatar_class="avatar-medium") }}
|
||||
<div class="d-flex flex-column ml-2">
|
||||
<div class="d-flex">
|
||||
<a class="anchor_style ml-2" href="/{{member.username}}">
|
||||
<div class="review-content">{{ member.full_name }}</div>
|
||||
</a>
|
||||
{% if course.is_mentor(member.name) %}
|
||||
<div class="badge badge-success ml-2 align-self-start">Mentor</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if loop.index != member_count %}
|
||||
<hr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
@@ -1,7 +0,0 @@
|
||||
import frappe
|
||||
from . import utils
|
||||
|
||||
def get_context(context):
|
||||
utils.get_common_context(context)
|
||||
if not context.membership:
|
||||
utils.redirect_to_lesson(context.course)
|
||||
@@ -1,51 +0,0 @@
|
||||
{% extends "templates/base.html" %}
|
||||
{% from "www/macros/livecode.html" import LiveCodeEditorJS, LiveCodeEditor with context %}
|
||||
{% block title %}{{ course.title }} - Batch Dashboard{% endblock %}
|
||||
|
||||
{% block head_include %}
|
||||
<meta name="description" content="{{course.title}} - Batch Dashboard" />
|
||||
<meta name="keywords" content="{{course.title}} - Batch Dashboard" />
|
||||
<style>
|
||||
</style>
|
||||
|
||||
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
|
||||
<link rel="stylesheet" href="{{ livecode_url }}/static/codemirror/lib/codemirror.css">
|
||||
<link rel="stylesheet" href="/assets/css/lms.css">
|
||||
|
||||
<script src="{{ livecode_url }}/static/codemirror/lib/codemirror.js"></script>
|
||||
<script src="{{ livecode_url }}/static/codemirror/mode/python/python.js"></script>
|
||||
<script src="{{ livecode_url }}/static/codemirror/keymap/sublime.js"></script>
|
||||
|
||||
<script src="{{ livecode_url }}/static/codemirror/addon/edit/matchbrackets.js"></script>
|
||||
<script src="{{ livecode_url }}/static/codemirror/addon/comment/comment.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="container">
|
||||
{{ widgets.BatchTabs(course=course, membership=membership) }}
|
||||
<div class="mentor-dashboard">
|
||||
<h3>Batch Progress</h3>
|
||||
{% for exercise in report.exercises %}
|
||||
<div class="exercise-submissions">
|
||||
<h2>Exercise {{exercise.index_label}}: {{exercise.title}}</h2>
|
||||
{% for s in report.get_submissions_of_exercise(exercise.name) %}
|
||||
<div class="submission">
|
||||
<h4><a href="/{{s.owner.username}}">{{s.owner.full_name}}</a></h4>
|
||||
<div class="livecode-editor-small">
|
||||
{{ LiveCodeEditor(name=s.name, code=s.solution, reset_code=s.solution) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{%- block script %}
|
||||
{{ super() }}
|
||||
{{ LiveCodeEditorJS() }}
|
||||
{% endblock %}
|
||||
@@ -1,62 +0,0 @@
|
||||
import frappe
|
||||
from community.lms.models import Course
|
||||
from collections import defaultdict
|
||||
from . import utils
|
||||
|
||||
def get_context(context):
|
||||
utils.get_common_context(context)
|
||||
|
||||
exercise_name = frappe.form_dict.get("exercise")
|
||||
if exercise_name:
|
||||
exercise = frappe.get_doc("Exercise", exercise_name)
|
||||
else:
|
||||
exercise = None
|
||||
|
||||
context.exercise = exercise
|
||||
context.report = BatchReport(context.course, context.batch)
|
||||
|
||||
class BatchReport:
|
||||
def __init__(self, course, batch):
|
||||
self.submissions = get_submissions(course, batch)
|
||||
self.exercises = self.get_exercises(course.name)
|
||||
self.submissions_by_exercise = defaultdict(list)
|
||||
for s in self.submissions:
|
||||
self.submissions_by_exercise[s.exercise].append(s)
|
||||
|
||||
def get_exercises(self, course_name):
|
||||
return frappe.get_all("Exercise", {"course": course_name, "lesson": ["!=", ""]}, ["name", "title", "index_label"], order_by="index_label")
|
||||
|
||||
def get_submissions_of_exercise(self, exercise_name):
|
||||
return self.submissions_by_exercise[exercise_name]
|
||||
|
||||
def get_submissions(course, batch):
|
||||
students = course.get_students(batch.name)
|
||||
if not len(students):
|
||||
return []
|
||||
students_map = {s.email: s for s in students}
|
||||
names, values = nparams("s", students_map.keys())
|
||||
sql = """
|
||||
select owner, exercise, name, solution, creation, image
|
||||
from (
|
||||
select owner, exercise, name, solution, creation, image,
|
||||
row_number() over (partition by owner, exercise order by creation desc) as ix
|
||||
from `tabExercise Submission`) as t
|
||||
where t.ix=1 and owner IN {}
|
||||
""".format(names)
|
||||
|
||||
data = frappe.db.sql(sql, values=values, as_dict=True)
|
||||
for row in data:
|
||||
row['owner'] = students_map[row['owner']]
|
||||
return data
|
||||
|
||||
def nparams(name, values):
|
||||
"""Creates n paramters from a list of values for a db query.
|
||||
|
||||
>>> nparams("name", ["a", "b])
|
||||
("(%(name_1)s, %(name_2)s)", {"name_1": "a", "name_2": "b"})
|
||||
"""
|
||||
keys = [f"{name}_{i}" for i, _ in enumerate(values, start=1)]
|
||||
param_names = [f"%({k})s" for k in keys]
|
||||
param_values = dict(zip(keys, values))
|
||||
joined_names = "(" + ", ".join(param_names) + ")"
|
||||
return joined_names, param_values
|
||||
@@ -1,17 +0,0 @@
|
||||
{% extends "templates/base.html" %}
|
||||
{% block title %}Schedule{% endblock %}
|
||||
|
||||
{% block head_include %}
|
||||
<meta name="description" content="Courses" />
|
||||
<meta name="keywords" content="" />
|
||||
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
{{ widgets.BatchTabs(course=course, batch=batch) }}
|
||||
<h3>
|
||||
Schedule
|
||||
</h3>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,20 +0,0 @@
|
||||
import frappe
|
||||
from community.lms.models import Course
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
course_name = frappe.form_dict["course"]
|
||||
batch_name = frappe.form_dict["batch"]
|
||||
|
||||
course = Course.find(course_name)
|
||||
if not course:
|
||||
context.template = "www/404.html"
|
||||
return
|
||||
|
||||
batch = course.get_batch(batch_name)
|
||||
if not batch:
|
||||
frappe.local.flags.redirect_location = "/courses/" + course_name
|
||||
raise frappe.Redirect
|
||||
|
||||
context.course = course
|
||||
context.batch = batch
|
||||
@@ -10,12 +10,12 @@ def get_common_context(context):
|
||||
except KeyError:
|
||||
batch_name = None
|
||||
|
||||
course = Course.find(course_name)
|
||||
course = frappe.get_doc("LMS Course", course_name)
|
||||
if not course:
|
||||
context.template = "www/404.html"
|
||||
return
|
||||
context.course = course
|
||||
|
||||
context.lessons = course.get_lessons()
|
||||
membership = course.get_membership(frappe.session.user, batch_name)
|
||||
context.membership = membership
|
||||
if membership:
|
||||
@@ -24,9 +24,6 @@ def get_common_context(context):
|
||||
if batch:
|
||||
context.batch = batch
|
||||
|
||||
context.members = course.get_mentors(membership.batch) + course.get_students(membership.batch)
|
||||
context.member_count = len(context.members)
|
||||
|
||||
context.course.query_parameter = "?batch=" + membership.batch if membership and membership.batch else ""
|
||||
context.livecode_url = get_livecode_url()
|
||||
|
||||
|
||||
@@ -3,13 +3,11 @@
|
||||
{% block title %}{{ course.title }}
|
||||
{% endblock %}
|
||||
{% block head_include %}
|
||||
<meta name="description" content="Courses" />
|
||||
<meta name="keywords" content="Courses {{course.title}}" />
|
||||
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="common-page-style">
|
||||
<div class="col course-home-page">
|
||||
<div class="container course-home-page">
|
||||
{{ widgets.BreadCrumb(course=course) }}
|
||||
{{ CourseCardWide(course) }}
|
||||
{{ CourseOutlineAndCreator(course) }}
|
||||
@@ -24,19 +22,25 @@
|
||||
|
||||
{% macro CourseCardWide(course) %}
|
||||
<div class="common-card-style course-card-wide">
|
||||
<div class="course-image-wide" style="background-image: url({{ course.image }});">
|
||||
<div class="course-image-wide {% 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-wide-content">
|
||||
<div class="course-card-wide-title">
|
||||
{{ course.title }}
|
||||
</div>
|
||||
<div class="course-card-wide-intro">
|
||||
{{ course.short_introduction }}
|
||||
<div class="course-info">
|
||||
<div class="course-card-wide-title">
|
||||
{{ course.title }}
|
||||
</div>
|
||||
<div class="course-card-wide-intro">
|
||||
{{ course.short_introduction }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="course-buttons">
|
||||
{% if not course.disable_self_learning and not membership %}
|
||||
@@ -88,34 +92,43 @@
|
||||
|
||||
{% macro CourseOutlineAndCreator(course) %}
|
||||
<div class="course-outline-instructor-parent">
|
||||
<div class="course-home-outline col">
|
||||
<div class="course-home-outline">
|
||||
{{ widgets.CourseOutline(course=course, membership=membership) }}
|
||||
</div>
|
||||
<div class="course-creator-progress-parent col-sm-auto">
|
||||
<div class="course-creator-progress-parent">
|
||||
<div class="course-creator-section">
|
||||
<div class="course-home-headings">
|
||||
Creator
|
||||
</div>
|
||||
{{ widgets.MemberCard(member=course.get_instructor(), show_course_count=True, dimension_class="member-card-large") }}
|
||||
</div>
|
||||
{% if course.get_course_progress() %}
|
||||
{% set progress = course.get_course_progress() %}
|
||||
{% if progress %}
|
||||
<div class="course-progress-section">
|
||||
<div class="course-home-headings">
|
||||
Your Progress
|
||||
</div>
|
||||
<div class="common-card-style progress-card">
|
||||
<p class="small-title">
|
||||
{% if progress != 100 %}
|
||||
Great work so far!
|
||||
{% else %}
|
||||
Excellent Work on completing this course 👏
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="progress-text">
|
||||
{% if progress != 100 %}
|
||||
Challenge yourself to complete the lessons and grow professionally.
|
||||
{% else %}
|
||||
You have reached a new level in your journey to success!
|
||||
{% endif %}
|
||||
</p>
|
||||
<div class="progress-percentage">
|
||||
{{ frappe.utils.rounded(course.get_course_progress()) }}%
|
||||
{{ frappe.utils.rounded(progress) }}%
|
||||
</div>
|
||||
<div class="progress">
|
||||
<div class="progress-bar" role="progressbar" style="width: {{ course.get_course_progress() }}%"
|
||||
aria-valuenow="{{ course.get_course_progress() }}" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
<div class="progress-bar" role="progressbar" style="width: {{ progress }}%"
|
||||
aria-valuenow="{{ progress }}" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -151,7 +164,7 @@
|
||||
|
||||
{% macro CourseDescriptionAndOverview(course) %}
|
||||
<div class="course-outline-instructor-parent">
|
||||
<div class="course-description-section col">
|
||||
<div class="course-description-section">
|
||||
<div class="course-home-headings">
|
||||
Course Description
|
||||
</div>
|
||||
@@ -161,7 +174,7 @@
|
||||
</div>
|
||||
{% set avg_rating = course.get_average_rating() %}
|
||||
{% if course.get_students() | length or avg_rating %}
|
||||
<div class="course-overview-section col-sm-auto">
|
||||
<div class="course-overview-section">
|
||||
<div class="course-home-headings">
|
||||
Overview
|
||||
</div>
|
||||
@@ -183,55 +196,3 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro BatchSection(course) %}
|
||||
<div class="row">
|
||||
<div class="col-lg-8 col-md-12">
|
||||
{% if course.is_mentor(frappe.session.user) %}
|
||||
{{ BatchSectionForMentors(course, course.get_batches(mentor=frappe.session.user)) }}
|
||||
{% else %}
|
||||
{{ BatchSectionForStudents(course, course.get_upcoming_batches()) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
{% macro BatchSectionForMentors(course, mentor_batches) %}
|
||||
<h2>Your Batches</h2>
|
||||
|
||||
{% if mentor_batches %}
|
||||
|
||||
<div class="row">
|
||||
{% for batch in mentor_batches %}
|
||||
<div class="col-lg-4 col-md-6">
|
||||
{{ widgets.RenderBatch(course=course, batch=batch, can_manage=True) }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<a class="add-batch margin-bottom" href="/add-a-new-batch?new=1&course={{course.name}}">Add a new batch</a>
|
||||
{% else %}
|
||||
<div class="mentor_message">
|
||||
<p>
|
||||
You are a mentor for this course.
|
||||
</p>
|
||||
<a class="" href="/add-a-new-batch?new=1&course={{course.name}}">Create your first batch</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro BatchSectionForStudents(course, upcoming_batches) %}
|
||||
{% if upcoming_batches %}
|
||||
<div class="mt-5">
|
||||
<h3 class="upcoming">Upcoming Batches</h3>
|
||||
<div class="row">
|
||||
{% for batch in upcoming_batches %}
|
||||
<div class="col-lg-4 col-md-6">
|
||||
{{ widgets.RenderBatch(course=course, batch=batch, can_join=True) }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="mt-5 upcoming">There are no Upcoming Batches for this course currently.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import frappe
|
||||
from community.lms.models import Course
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
@@ -10,7 +9,7 @@ def get_context(context):
|
||||
frappe.local.flags.redirect_location = "/courses"
|
||||
raise frappe.Redirect
|
||||
|
||||
course = Course.find(course_name)
|
||||
course = frappe.get_doc("LMS Course", course_name)
|
||||
if course is None:
|
||||
frappe.local.flags.redirect_location = "/courses"
|
||||
raise frappe.Redirect
|
||||
@@ -19,3 +18,9 @@ def get_context(context):
|
||||
membership = course.get_membership(frappe.session.user)
|
||||
context.course.query_parameter = "?batch=" + membership.batch if membership and membership.batch else ""
|
||||
context.membership = membership
|
||||
context.metatags = {
|
||||
"title": course.title,
|
||||
"image": course.image,
|
||||
"description": course.short_introduction,
|
||||
"keywords": course.title
|
||||
}
|
||||
|
||||
@@ -2,15 +2,13 @@
|
||||
{% from "www/hackathons/macros/card.html" import null_card %}
|
||||
{% block title %}{{ 'Courses' }}{% endblock %}
|
||||
{% block head_include %}
|
||||
<meta name="description" content="{{ 'Courses' }}" />
|
||||
<meta name="keywords" content="Courses" />
|
||||
<style>
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="common-page-style">
|
||||
<div class="col course-top-section">
|
||||
<div class="container">
|
||||
<div class="courses-header">
|
||||
{{ 'All Courses' }}
|
||||
</div>
|
||||
|
||||
@@ -3,9 +3,15 @@ import frappe
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
context.courses = get_courses()
|
||||
context.metatags = {
|
||||
"title": "All Courses",
|
||||
"image": frappe.db.get_single_value("Website Settings", "banner_image"),
|
||||
"description": "This page lists all the courses published on our website",
|
||||
"keywords": "All Courses, Courses, Learn"
|
||||
}
|
||||
|
||||
def get_courses():
|
||||
course_names = frappe.get_all("LMS Course", pluck="name")
|
||||
course_names = frappe.get_all("LMS Course", filters={"is_published": True}, pluck="name")
|
||||
courses = []
|
||||
for course in course_names:
|
||||
courses.append(frappe.get_doc("LMS Course", course))
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
{% extends "templates/base.html" %}
|
||||
{% from "www/hackathons/macros/card.html" import null_card %}
|
||||
|
||||
{% block title %}{{ 'My Courses' }}{% endblock %}
|
||||
{% block head_include %}
|
||||
<meta name="description" content="My Courses" />
|
||||
<meta name="keywords" content="" />
|
||||
<style>
|
||||
div.card-hero-img {
|
||||
height: 220px;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-color: rgb(250, 251, 252);
|
||||
}
|
||||
|
||||
.card-image-wrapper {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
height: 220px;
|
||||
background-color: rgb(250, 251, 252);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.image-body {
|
||||
align-self: center;
|
||||
color: #d1d8dd;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.no-courses {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
section {
|
||||
padding: 5rem 0 5rem 0;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% macro card(course) %}
|
||||
<div class="col-sm-4 mb-4 text-left">
|
||||
<a href="//courses/course?course={{course.name}}" class="no-decoration no-underline">
|
||||
<div class="card h-100">
|
||||
<div class='card-body'>
|
||||
<h5 class='card-title'>{{ course.title }}</h5>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% block content %}
|
||||
<section class="section">
|
||||
<div class='container'>
|
||||
{% if frappe.session.user != "Guest" %}
|
||||
{% for course in my_courses %}
|
||||
{{ card(course) }}
|
||||
{% endfor %}
|
||||
{% if my_courses %}
|
||||
{% for n in range( (3 - (my_courses|length)) %3) %}
|
||||
{{ null_card() }}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="no-courses">You haven't enrolled in any Course yet. <a href="/courses">Check out the availabe
|
||||
courses.</a></div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="no-courses">
|
||||
<p>Please sign up to access this page.</p>
|
||||
<a id="signup" class="btn btn-primary btn-lg" href="/login#signup">{{_('Sign Up')}}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -1,15 +0,0 @@
|
||||
import frappe
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
context.my_courses = get_my_courses()
|
||||
|
||||
def get_my_courses():
|
||||
my_courses = []
|
||||
courses = frappe.get_all("LMS Course Enrollment", {"owner": frappe.session.user}, ["course"])
|
||||
for course in courses:
|
||||
my_courses.append({
|
||||
"name": course.course,
|
||||
"title": frappe.db.get_value("LMS Course", course.course, ["title"])
|
||||
})
|
||||
return my_courses
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="common-page-style">
|
||||
<div class="col profile-page">
|
||||
<div class="container profile-page">
|
||||
{{ ProfileBanner(member) }}
|
||||
{{ AboutOverviewSection(member) }}
|
||||
{{ CoursesCreated(member) }}
|
||||
@@ -18,35 +18,40 @@
|
||||
{% endblock %}
|
||||
|
||||
{% macro ProfileBanner(member) %}
|
||||
<div class="col">
|
||||
<div class="profile-banner" style="background: url(/assets/community/images/profile-banner.png)">
|
||||
<div class="profile-avatar">
|
||||
{{ widgets.Avatar(member=member, avatar_class="avatar-xl") }}
|
||||
<div class="profile-name"> {{ member.full_name }} </div>
|
||||
{% if member.get_authored_courses() | length %}
|
||||
<div class="creator-badge"> Creator </div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="">
|
||||
<div class="profile-banner" style="background-image: url(/assets/community/images/profile-banner.png)">
|
||||
<div class="profile-avatar">
|
||||
{{ widgets.Avatar(member=member, avatar_class="avatar-xl") }}
|
||||
<div class="profile-name"> {{ member.full_name }} </div>
|
||||
{% if member.get_authored_courses() | length %}
|
||||
<div class="creator-badge"> Creator </div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="profile-info">
|
||||
{% if member.profession %}
|
||||
<span class="profile-profession"> {{ member.profession }} </span>
|
||||
{% endif %}
|
||||
<div class="social-icons">
|
||||
{% if member.linkedin %}
|
||||
<a class="linkedin" href="{{ member.linkedin }}">
|
||||
<img src="/assets/community/images/linkedin.png">
|
||||
</a>
|
||||
<div class="profile-profession">
|
||||
{% if member.profession %}
|
||||
<span class=""> {{ member.profession }} </span>
|
||||
{% endif %}
|
||||
{% if member.medium %}
|
||||
<a class="medium" href="{{ member.medium}}">
|
||||
<img src="/assets/community/icons/medium.svg">
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if member.github %}
|
||||
<a class="github" href="{{ member.github }}">
|
||||
<img src="/assets/community/icons/github.svg">
|
||||
</a>
|
||||
<span class="social-icons">
|
||||
{% if member.linkedin %}
|
||||
<a class="linkedin button-links" href="{{ member.linkedin }}">
|
||||
<img src="/assets/community/images/linkedin.png">
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if member.medium %}
|
||||
<a class="medium button-links" href="{{ member.medium}}">
|
||||
<img src="/assets/community/icons/medium.svg">
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if member.github %}
|
||||
<a class="github button-links" href="{{ member.github }}">
|
||||
<img src="/assets/community/icons/github.svg">
|
||||
</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if frappe.session.user == member.email %}
|
||||
<a class="dark-links pull-right" href="edit-profile?name={{ member.email }}">Edit Profile</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,9 +59,9 @@
|
||||
{% endmacro %}
|
||||
|
||||
{% macro AboutOverviewSection(member) %}
|
||||
<div class="course-outline-instructor-parent">
|
||||
<div class="profile-parent-section">
|
||||
{% if member.bio %}
|
||||
<div class="course-overview-section col">
|
||||
<div class="profile-about-section">
|
||||
<div class="course-home-headings">
|
||||
About
|
||||
</div>
|
||||
@@ -65,7 +70,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="course-overview-section col-sm-auto">
|
||||
<div class="course-overview-section">
|
||||
<div class="course-home-headings">
|
||||
Overview
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user