Compare commits

..

1 Commits

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

View File

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

View File

@@ -143,8 +143,7 @@ primary_rules = [
{"from_route": "/courses/<course>/<batch>/members", "to_route": "batch/members"}, {"from_route": "/courses/<course>/<batch>/members", "to_route": "batch/members"},
{"from_route": "/courses/<course>/<batch>/discuss", "to_route": "batch/discuss"}, {"from_route": "/courses/<course>/<batch>/discuss", "to_route": "batch/discuss"},
{"from_route": "/courses/<course>/<batch>/about", "to_route": "batch/about"}, {"from_route": "/courses/<course>/<batch>/about", "to_route": "batch/about"},
{"from_route": "/courses/<course>/<batch>/progress", "to_route": "batch/progress"}, {"from_route": "/courses/<course>/<batch>/progress", "to_route": "batch/progress"}
{"from_route": "/courses/<course>/<batch>/join", "to_route": "batch/join"}
] ]
# Any frappe default URL is blocked by profile-rules, add it here to unblock it # Any frappe default URL is blocked by profile-rules, add it here to unblock it

View File

@@ -43,7 +43,7 @@ def save_current_lesson(batch_name, lesson_name):
doctype="LMS Batch Membership", doctype="LMS Batch Membership",
filters={ filters={
"batch": batch_name, "batch": batch_name,
"member": frappe.session.user "member_email": frappe.session.user
}, },
fieldname="name") fieldname="name")
if not name: if not name:

View File

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

View File

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

View File

@@ -25,6 +25,7 @@ class Exercise(Document):
order_by="creation desc", order_by="creation desc",
page_length=1) page_length=1)
print("get_user_submission", result)
if result: if result:
return result[0] return result[0]
@@ -54,6 +55,5 @@ class Exercise(Document):
image=image, image=image,
solution=code) solution=code)
doc.insert(ignore_permissions=True) doc.insert(ignore_permissions=True)
return doc return doc

View File

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

View File

@@ -10,7 +10,6 @@
"lesson_type", "lesson_type",
"title", "title",
"index_", "index_",
"index_label",
"body", "body",
"sections" "sections"
], ],
@@ -52,18 +51,11 @@
"fieldtype": "Table", "fieldtype": "Table",
"label": "Sections", "label": "Sections",
"options": "LMS Section" "options": "LMS Section"
},
{
"fieldname": "index_label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Index Label",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-06-01 05:30:48.127494", "modified": "2021-05-13 20:03:51.510605",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Lesson", "name": "Lesson",

View File

@@ -11,37 +11,15 @@ class Lesson(Document):
def before_save(self): def before_save(self):
sections = SectionParser().parse(self.body or "") sections = SectionParser().parse(self.body or "")
self.sections = [self.make_lms_section(i, s) for i, s in enumerate(sections)] self.sections = [self.make_lms_section(i, s) for i, s in enumerate(sections)]
index = 1
for s in self.sections: for s in self.sections:
if s.type == "exercise": if s.type == "exercise":
e = s.get_exercise() e = s.get_exercise()
e.lesson = self.name e.lesson = self.name
e.index_ = index
e.save() e.save()
index += 1
self.update_orphan_exercises()
def update_orphan_exercises(self):
"""Updates the exercises that were previously part of this lesson,
but not any more.
"""
linked_exercises = {row['name'] for row in frappe.get_all('Exercise', {"lesson": self.name})}
active_exercises = {s.id for s in self.get("sections") if s.type=="exercise"}
orphan_exercises = linked_exercises - active_exercises
for name in orphan_exercises:
ex = frappe.get_doc("Exercise", name)
ex.lesson = None
ex.index_ = 0
ex.index_label = ""
ex.save()
def get_sections(self): def get_sections(self):
return sorted(self.get('sections'), key=lambda s: s.index) return sorted(self.get('sections'), key=lambda s: s.index)
def get_exercises(self):
return [frappe.get_doc("Exercise", s.id) for s in self.get("sections") if s.type=="exercise"]
def make_lms_section(self, index, section): def make_lms_section(self, index, section):
s = frappe.new_doc('LMS Section', parent_doc=self, parentfield='sections') s = frappe.new_doc('LMS Section', parent_doc=self, parentfield='sections')
s.type = section.type s.type = section.type
@@ -65,65 +43,3 @@ class Lesson(Document):
The return value would be like 1.2, 2.1 etc. The return value would be like 1.2, 2.1 etc.
It will be None if there is no next lesson. It will be None if there is no next lesson.
""" """
def get_progress(self):
return frappe.db.get_value("LMS Course Progress", {"lesson": self.name, "owner": frappe.session.user}, "status")
def get_slugified_class(self):
if self.get_progress():
return ("").join([ s for s in self.get_progress().lower().split() ])
return
@frappe.whitelist()
def save_progress(lesson, batch):
if not frappe.db.exists("LMS Batch Membership",
{
"member": frappe.session.user,
"batch": batch
}):
return
if frappe.db.exists("LMS Course Progress",
{
"lesson": lesson,
"owner": frappe.session.user
}):
return
lesson_details = frappe.get_doc("Lesson", lesson)
dynamic_content = frappe.db.count("LMS Section",
filters={
"type": ["not in", ["example", "text"]],
"parent": lesson_details.name
})
status = "Complete"
if dynamic_content:
status = "Partially Complete"
frappe.get_doc({
"doctype": "LMS Course Progress",
"lesson": lesson_details.name,
"status": status
}).save(ignore_permissions=True)
def update_progress(lesson):
user = frappe.session.user
if not all_dynamic_content_submitted(lesson, user):
return
if frappe.db.exists("LMS Course Progress", {"lesson": lesson, "owner": user}):
course_progress = frappe.get_doc("LMS Course Progress", {"lesson": lesson, "owner": user})
course_progress.status = "Complete"
course_progress.save()
def all_dynamic_content_submitted(lesson, user):
exercise_names = frappe.get_list("Exercise", {"lesson": lesson}, ["name"], pluck="name")
all_exercises_submitted = False
print(exercise_names)
query = {
"exercise": ["in", exercise_names],
"owner": user
}
if frappe.db.count("Exercise Submission", query) == len(exercise_names):
all_exercises_submitted = True
return all_exercises_submitted

View File

@@ -1,18 +1,5 @@
{ {
"actions": [ "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",
"group": "Reindex",
"label": "Reindex Exercises"
}
],
"allow_guest_to_view": 1, "allow_guest_to_view": 1,
"allow_rename": 1, "allow_rename": 1,
"creation": "2021-03-01 16:49:33.622422", "creation": "2021-03-01 16:49:33.622422",
@@ -99,7 +86,7 @@
"link_fieldname": "course" "link_fieldname": "course"
} }
], ],
"modified": "2021-06-01 04:36:45.696776", "modified": "2021-05-23 18:14:32.602647",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Course", "name": "LMS Course",

View File

@@ -5,7 +5,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
import json
from ...utils import slugify from ...utils import slugify
from community.query import find, find_all from community.query import find, find_all
@@ -158,35 +157,6 @@ class LMSCourse(Document):
chapter = frappe.get_doc("Chapter", lesson.chapter) chapter = frappe.get_doc("Chapter", lesson.chapter)
return f"{chapter.index_}.{lesson.index_}" return f"{chapter.index_}.{lesson.index_}"
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)
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()
def reindex_exercises(self):
for i, c in enumerate(self.get_chapters(), start=1):
if c.index_ != i:
c.index_ = i
c.save()
self._reindex_exercises_in_chapter(c)
def _reindex_exercises_in_chapter(self, c):
i = 1
for lesson in c.get_lessons():
for exercise in lesson.get_exercises():
exercise.index_ = i
exercise.index_label = f"{c.index_}.{i}"
exercise.save()
i += 1
def get_outline(self): def get_outline(self):
return CourseOutline(self) return CourseOutline(self)
@@ -217,8 +187,7 @@ class CourseOutline:
def get_chapters(self): def get_chapters(self):
return frappe.db.get_all("Chapter", return frappe.db.get_all("Chapter",
filters={"course": self.course.name}, filters={"course": self.course.name},
fields=["name", "title", "index_"], fields=["name", "title", "index_"])
order_by="index_")
def get_lessons(self): def get_lessons(self):
chapters = [c['name'] for c in self.chapters] chapters = [c['name'] for c in self.chapters]
@@ -230,17 +199,3 @@ class CourseOutline:
for lesson in lessons: for lesson in lessons:
lesson['number'] = "{}.{}".format(chapter_numbers[lesson['chapter']], lesson['index_']) lesson['number'] = "{}.{}".format(chapter_numbers[lesson['chapter']], lesson['index_'])
return lessons 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.")
@frappe.whitelist()
def reindex_exercises(doc):
course_data = json.loads(doc)
course = frappe.get_doc("LMS Course", course_data['name'])
course.reindex_exercises()
frappe.msgprint("All exercises in this course have been re-indexed.")

View File

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

View File

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

View File

@@ -0,0 +1,54 @@
{
"actions": [],
"creation": "2021-05-28 19:09:44.418823",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"maximum_attempts",
"questions"
],
"fields": [
{
"fieldname": "title",
"fieldtype": "Data",
"label": "Title"
},
{
"fieldname": "maximum_attempts",
"fieldtype": "Int",
"label": "Maximum Attempts"
},
{
"fieldname": "questions",
"fieldtype": "Link",
"label": "Questions",
"options": "LMS Quiz Question"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-05-28 19:18:38.688885",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Quiz",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

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

View File

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

View File

@@ -0,0 +1,38 @@
{
"actions": [],
"creation": "2021-05-28 19:04:49.312616",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"option",
"is_correct"
],
"fields": [
{
"fieldname": "option",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Option"
},
{
"default": "0",
"fieldname": "is_correct",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Is Correct"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-05-28 19:13:48.869416",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Quiz Option",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

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

View File

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

View File

@@ -0,0 +1,44 @@
{
"actions": [],
"creation": "2021-05-28 19:07:17.347423",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"question",
"options",
"question_type"
],
"fields": [
{
"fieldname": "question",
"fieldtype": "Markdown Editor",
"in_list_view": 1,
"label": "Question"
},
{
"fieldname": "options",
"fieldtype": "Table",
"label": "Options",
"options": "LMS Quiz Option"
},
{
"fieldname": "question_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Question Type",
"options": "Single Correct Answer\nMultiple Correct Answers"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-05-28 19:18:04.939778",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Quiz Question",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

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

View File

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

View File

@@ -19,7 +19,7 @@
"is_standard": 1, "is_standard": 1,
"login_required": 1, "login_required": 1,
"max_attachment_size": 0, "max_attachment_size": 0,
"modified": "2021-06-02 15:52:06.383260", "modified": "2021-04-30 11:22:18.188712",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "add-a-new-batch", "name": "add-a-new-batch",
@@ -38,13 +38,13 @@
{ {
"allow_read_on_all_link_options": 0, "allow_read_on_all_link_options": 0,
"fieldname": "course", "fieldname": "course",
"fieldtype": "Link", "fieldtype": "Data",
"hidden": 1, "hidden": 0,
"label": "Course", "label": "Course",
"max_length": 0, "max_length": 0,
"max_value": 0, "max_value": 0,
"options": "LMS Course", "options": "",
"read_only": 0, "read_only": 1,
"reqd": 0, "reqd": 0,
"show_in_filter": 0 "show_in_filter": 0
}, },
@@ -111,4 +111,4 @@
"show_in_filter": 0 "show_in_filter": 0
} }
] ]
} }

View File

@@ -8,9 +8,6 @@
{% for lesson in chapter.get_lessons() %} {% for lesson in chapter.get_lessons() %}
<div class="lesson-teaser"> <div class="lesson-teaser">
<a {% if show_link %} class="anchor_style" href="{{ batch.get_learn_url(course.get_lesson_index(lesson.name)) }}" {% endif %}>{{ lesson.title }}</a> <a {% if show_link %} class="anchor_style" href="{{ batch.get_learn_url(course.get_lesson_index(lesson.name)) }}" {% endif %}>{{ lesson.title }}</a>
{% if show_progress and not course.is_mentor(frappe.session.user) and lesson.get_progress() %}
<a class="pull-right badge p-1 {{ lesson.get_slugified_class() }}"> <img class="progress-image" src="/assets/community/images/Vector.png"> {{ lesson.get_progress() }}</a>
{% endif %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>

View File

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

View File

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

View File

@@ -1,29 +0,0 @@
<div class="batch">
<div class="batch-details">
<div>Session every {{batch.sessions_on}}</div>
<div>{{frappe.utils.format_time(batch.start_time, "short")}} -
{{frappe.utils.format_time(batch.end_time, "short")}}
</div>
<div>Starting {{frappe.utils.format_date(batch.start_date, "medium")}}</div>
<div class="course-type" style="color: #888; padding: 10px 0px;">mentors</div>
{% for m in batch.get_mentors() %}
<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}}/{{batch.name}}/home" class="btn btn-primary">Manage</a>
{% elif can_join %}
<button class="join-batch btn btn-primary" data-batch="{{ batch.name | urlencode }}"
data-course="{{ course.name | urlencode }}">Join this Batch</button>
{% endif %}
</div>
</div>
{% endif %}
</div>

View File

@@ -211,63 +211,3 @@ section {
background: #eee; background: #eee;
border: 2px solid #ddd; border: 2px solid #ddd;
} }
.page-card {
max-width: 360px;
padding: 15px;
margin: 70px auto;
border: 1px solid #d1d8dd;
border-radius: 4px;
background-color: #fff;
box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.1);
}
.page-card .page-card-head {
padding: 10px 15px;
margin: -15px;
margin-bottom: 15px;
border-bottom: 1px solid #d1d8dd;
}
.page-card .page-card-head .indicator {
color: #36414C;
font-size: 14px;
}
.page-card .page-card-head .indicator::before {
margin: 0 6px 0.5px 0px;
}
.page-card .btn {
margin-top: 30px;
}
.partiallycomplete {
background: #FEF4E2;
color: #976417;
}
.partiallycomplete img {
background: #976417;
}
.complete {
background: #EAF5EE;
color: #38A160;
}
.complete img {
background: #38A160;
}
.incomplete {
background: #FEECEC;
color: #E24C4C;
}
.incomplete img {
background: #E24C4C;
}
.progress-image {
margin-right: 3px;
border-radius: 50px;
padding: 5px;
}

View File

@@ -285,7 +285,7 @@ section.lightgray {
} }
.lesson-teaser { .lesson-teaser {
line-height: 40px; line-height: 35px;
} }
#hero h1 { #hero h1 {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 B

View File

@@ -1,60 +1,46 @@
{% extends "templates/base.html" %} {% extends "templates/base.html" %}
{% block title %} Batch {% endblock %} {% block title %}Batch{% endblock %}
{% block head_include %} {% block head_include %}
<meta name="description" content="Courses" /> <meta name="description" content="Courses" />
<meta name="keywords" content="" /> <meta name="keywords" content="" />
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css"> <link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% set invite_link = frappe.utils.get_url() + "/courses/" + course.name + "/" + batch.name + "/join" %}
<div class="container mt-5"> <div class="container mt-5">
{{ widgets.BatchTabs(course=course, batch=batch) }} {{ widgets.BatchTabs(course=course, batch=batch) }}
<div> <h1 class="mt-5">{{ batch.title }}</h1>
<h1 class="mt-5">{{ batch.title }}</h1>
</div>
<div class="course-details"> <div class="course-details">
{{ widgets.CourseOutline(course=course, batch=batch, show_link=True, show_progress=True) }} {{ widgets.CourseOutline(course=course, batch=batch, show_link=True) }}
</div> </div>
<div class="w-25"> <div class="col-lg-4 col-md-12">
<h2>Batch Schedule</h2> <h2>Batch Schedule</h2>
{{ widgets.RenderBatch(course=course, batch=batch) }} {{ BatchDetails(batch) }}
</div> </div>
{% if batch.description %}
<h2>Batch Details</h2> <h2>Batch Details</h2>
{{ frappe.utils.md_to_html(batch.description) }} {{ frappe.utils.md_to_html(batch.description) }}
{% endif %}
{% if course.is_mentor(frappe.session.user) %}
<div class="">
<h2> Invite Members </h2>
<a href="" class="anchor_style mr-5" id="invite-link" data-link="{{ invite_link }}">Get Batch Invitation
Link</a>
<small id="copy-message" class="text-muted pull-right" style="display: none;">Copied to Clipboard.</small>
</div>
{% endif %}
</div> </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();
}, 5000);
});
})
})
</script>
{% endblock %} {% endblock %}
{% macro BatchDetails(batch) %}
<div class="batch">
<div class="batch-details">
<div>Session every {{batch.sessions_on}}</div>
<div>{{frappe.utils.format_time(batch.start_time, "short")}} -
{{frappe.utils.format_time(batch.end_time, "short")}}
</div>
<div>Starting {{frappe.utils.format_date(batch.start_date, "medium")}}</div>
<div class="course-type" style="color: #888; padding: 10px 0px;">mentors</div>
{% for m in batch.get_mentors() %}
<div>
{{ widgets.Avatar(member=m, avatar_class="avatar-medium" ) }}
<span class="instructor-title">{{m.full_name}}</span>
</div>
{% endfor %}
</div>
</div>
{% endmacro %}

View File

@@ -1,74 +0,0 @@
% extends "templates/base.html" %}
{% block title %}Join a Course{% endblock %}
{% block head_include %}
<meta name="description" content="Join a Course"/>
<meta name="keywords" content="" />
{% endblock %}
{% block content %}
{% if frappe.session.user == "Guest" %}
<div class="page-card">
<div class='page-card-head'>
<span class='indicator blue password-box'>Login Required</span>
</div>
<div class=''>Please log in to confirm to join the course {{ batch.course_title }}.</div>
<a type="submit" id="login" class="btn btn-primary w-100"
href="/login?redirect-to=/courses/{{ batch.course }}/{{ batch.name }}/join">{{_("Login")}}</a>
</div>
{% elif already_a_member %}
<div class="page-card">
<div class='page-card-head'>
<span class='indicator blue password-box'>Already a member</span>
</div>
<div class=''>You are already a member of the batch {{ batch.title }} for the course {{ batch.course_title }}.
</div>
<a type="submit" id="batch-home" class="btn btn-primary w-100" href="/courses/{{batch.course}}/{{batch.name}}/home">{{_("Go to Batch Home")}}</a>
</div>
{% else %}
<div class="page-card">
<div class='page-card-head'>
<span class='indicator blue password-box'>Confirm your membership</span>
</div>
<div>Please provide your confirmation to be a part of the batch {{ batch.title }} for the course
{{ batch.course_title }}.
</div>
<a type="submit" id="confirm" class="btn btn-primary w-100" data-batch="{{ batch.name | urlencode }}"
data-course="{{ batch.course | urlencode }}">{{_("Confirm")}}</a>
</div>
{% endif %}
<script>
frappe.ready(() => {
var confirm_element = $("#confirm");
var batch = decodeURIComponent(confirm_element.attr("data-batch"));
var course = decodeURIComponent(confirm_element.attr("data-course"));
confirm_element.click((e) => {
frappe.call({
"method": "community.lms.doctype.lms_batch_membership.lms_batch_membership.create_membership",
"args": {
"batch": batch
},
"callback": (data) => {
if (data.message == "OK") {
frappe.msgprint({
message: __("You are now a member of this batch!"),
clear: true
});
setTimeout(function () {
window.location.href = "/courses/" + course + "/" + batch + "/home";
}, 2000);
}
}
})
})
})
</script>
{% endblock %}

View File

@@ -1,8 +0,0 @@
import frappe
def get_context(context):
context.no_cache = 1
batch_name = frappe.form_dict["batch"]
context.batch = frappe.get_doc("LMS Batch", batch_name)
context.already_a_member = context.batch.is_member(frappe.session.user)
context.batch.course_title = frappe.db.get_value("LMS Course", context.batch.course, "title")

View File

@@ -28,7 +28,7 @@
{{ widgets.BatchTabs(course=course, batch=batch) }} {{ widgets.BatchTabs(course=course, batch=batch) }}
<div class="lesson-page"> <div class="lesson-page">
<h2 class="title {% if course.is_mentor(frappe.session.user) %} is_mentor {% endif %}" data-name="{{ lesson.name }}" data-batch="{{ batch.name }}">{{ lesson.title }}</h2> <h2>{{ lesson.title }}</h2>
{% for s in lesson.get_sections() %} {% for s in lesson.get_sections() %}
<div class="section section-{{ s.type }}"> <div class="section section-{{ s.type }}">

View File

@@ -1,11 +0,0 @@
frappe.ready(() => {
if (!$(".title").hasClass("is_mentor")) {
frappe.call({
method: "community.lms.doctype.lesson.lesson.save_progress",
args: {
lesson: $(".title").attr("data-name"),
batch: $(".title").attr("data-batch")
}
})
}
})

View File

@@ -29,7 +29,7 @@
<h1>Batch Progress</h1> <h1>Batch Progress</h1>
{% for exercise in report.exercises %} {% for exercise in report.exercises %}
<div class="exercise-submissions"> <div class="exercise-submissions">
<h2>Exercise {{exercise.index_label}}: {{exercise.title}}</h2> <h2>{{exercise.title}}</h2>
{% for s in report.get_submissions_of_exercise(exercise.name) %} {% for s in report.get_submissions_of_exercise(exercise.name) %}
<div class="submission"> <div class="submission">
<h4><a href="/{{s.owner.username}}">{{s.owner.full_name}}</a></h4> <h4><a href="/{{s.owner.username}}">{{s.owner.full_name}}</a></h4>

View File

@@ -25,7 +25,7 @@ class BatchReport:
self.submissions_by_exercise[s.exercise].append(s) self.submissions_by_exercise[s.exercise].append(s)
def get_exercises(self, course_name): def get_exercises(self, course_name):
return frappe.get_all("Exercise", {"course": course_name, "lesson": ["!=", ""]}, ["name", "title", "index_label"], order_by="index_label") return frappe.get_all("Exercise", {"course": course_name}, ["name", "title"])
def get_submissions_of_exercise(self, exercise_name): def get_submissions_of_exercise(self, exercise_name):
return self.submissions_by_exercise[exercise_name] return self.submissions_by_exercise[exercise_name]

View File

@@ -64,6 +64,36 @@
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}
{% macro RenderBatch(batch, can_manage=False) %}
<div class="batch">
<div class="batch-details">
<div>Session every {{batch.sessions_on}}</div>
<div>{{frappe.utils.format_time(batch.start_time, "short")}} -
{{frappe.utils.format_time(batch.end_time, "short")}}
</div>
<div>Starting {{frappe.utils.format_date(batch.start_date, "medium")}}</div>
<div class="course-type" style="color: #888; padding: 10px 0px;">mentors</div>
{% for m in batch.get_mentors() %}
<div>
{{ widgets.Avatar(member=m, avatar_class="avatar-medium" ) }}
<span class="instructor-title">{{m.full_name}}</span>
</div>
{% endfor %}
</div>
<div class="cta">
<div class="">
{% if can_manage %}
<a href="/courses/{{course.name}}/{{batch.name}}/home" class="btn btn-primary">Manage</a>
{% else %}
<button class="join-batch btn btn-primary" data-batch="{{ batch.name | urlencode }}"
data-course="{{ course.name | urlencode }}">Join this Batch</button>
{% endif %}
</div>
</div>
</div>
{% endmacro %}
{% macro BatchSectionForMentors(course, mentor_batches) %} {% macro BatchSectionForMentors(course, mentor_batches) %}
<h2>Your Batches</h2> <h2>Your Batches</h2>
@@ -75,7 +105,7 @@
<div class="row"> <div class="row">
{% for batch in mentor_batches %} {% for batch in mentor_batches %}
<div class="col-lg-4 col-md-6"> <div class="col-lg-4 col-md-6">
{{ widgets.RenderBatch(course=course, batch=batch, can_manage=True) }} {{ RenderBatch(batch, can_manage=True) }}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
@@ -97,7 +127,7 @@
<div class="row"> <div class="row">
{% for batch in upcoming_batches %} {% for batch in upcoming_batches %}
<div class="col-lg-4 col-md-6"> <div class="col-lg-4 col-md-6">
{{ widgets.RenderBatch(course=course, batch=batch, can_join=True) }} {{ RenderBatch(batch, can_manage=False) }}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>

View File

@@ -0,0 +1,3 @@
frappe.ready(() => {
})

View File

@@ -72,26 +72,29 @@
</div> </div>
<div> <div>
<ul class="nav nav-tabs mt-4" id="myTab" role="tablist"> <ul class="nav nav-tabs mt-4" id="myTab" role="tablist">
{% for tab in profile_tabs %} <li class="nav-item">
<li class="nav-item"> <a class="nav-link active" id="home-tab" data-toggle="tab" href="#home" role="tab" aria-controls="home"
{% set slug = title.lower().replace(" ", "-") %} aria-selected="true">Sketches</a>
{% set selected = loop.index == 1 %} </li>
{% set active = 'active' if loop.index == 1 else '' %}
<a class="nav-link {{ active }}" id="{{ slug }}-tab" data-toggle="tab" href="#{{ slug }}" role="tab" aria-controls="{{ slug }}"
aria-selected="{{ selected }}">Sketches</a>
</li>
{% endfor %}
</ul> </ul>
</div> </div>
<div> <div>
{% for tab in profile_tabs %} <div class="tab-content">
{% set slug = title.lower().replace(" ", "-") %} <div class="tab-pane fade py-4 show active" role="tabpanel" id="home">
<div class="tab-content"> <div class="row">
<div class="tab-pane fade py-4 show active" role="tabpanel" id="slug"> {% if sketches %}
{{ tab.render() }} {% for sketch in sketches %}
<div class="col-md-4 col-sm-6">
{{ widgets.SketchTeaser(sketch=sketch) }}
</div>
{% endfor %}
{% endif %}
</div> </div>
{% if not sketches %}
<p class="text-center">{{member.full_name}} has not created any skecth yet.</p>
{% endif %}
</div> </div>
{% endfor %} </div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -3,20 +3,9 @@ from community.lms.models import Sketch
def get_context(context): def get_context(context):
context.no_cache = 1 context.no_cache = 1
try: try:
context.member = frappe.get_doc("User", {"username": frappe.form_dict["username"]}) context.member = frappe.get_doc("User", {"username": frappe.form_dict["username"]})
except: except:
context.template = "www/404.html" context.template = "www/404.html"
return else:
context.sketches = Sketch.get_recent_sketches(owner=context.member.email)
context.profile_tabs = get_profile_tabs(context.member)
def get_profile_tabs(user):
"""Returns the enabled ProfileTab objects.
Each ProfileTab is rendered as a tab on the profile page and the
they are specified as profile_tabs hook.
"""
tabs = frappe.get_hooks("profile_tabs") or []
return [frappe.get_attr(tab)(user) for tab in tabs]