Compare commits

...

7 Commits

Author SHA1 Message Date
Anand Chitipothu ef0c3e4a24 feat: pluggable profile tabs
Added ProfileTab class to represent a profile tab and made the profile
page render the tabs specified in the hook `profile_tabs`. This allows
plugging in new tabs in the profile page without makeing any changes to
the community module.
2021-06-08 10:36:12 +05:30
Anand Chitipothu 3619b136f8 Merge pull request #117 from fossunited/lesson-progress
feat: lesson progress
2021-06-07 11:24:52 +05:30
pateljannat 671b4a0650 fix: api and orm 2021-06-02 20:19:36 +05:30
Anand Chitipothu 586b39c0fd fix: issue with numbering the exercises
The exercises being listed in unpredicted order instead of the order
they were listed in the lesson. The was because the `index_` of the
exercise was never updated. Fixed this by updating the `index_` whenever
a lesson edited. However, the user still need to run reindex exercises
on the course correct the ordering, which wasn't possible earlier.
2021-06-02 17:48:02 +05:30
pateljannat 4fd7af053b fix: tests 2021-06-02 16:47:17 +05:30
pateljannat 5fd1143f76 feat: lesson progress 2021-06-02 13:52:50 +05:30
Jannat Patel 0dc4743556 Merge pull request #116 from fossunited/reindex-exercises
feat: actions to reindex lessons and exercises
2021-06-01 11:40:52 +05:30
21 changed files with 300 additions and 33 deletions
+38
View File
@@ -0,0 +1,38 @@
"""
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()
+1 -1
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_email": frappe.session.user "member": frappe.session.user
}, },
fieldname="name") fieldname="name")
if not name: if not name:
@@ -54,5 +54,6 @@ 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
@@ -1,8 +1,13 @@
# 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)
+66
View File
@@ -11,11 +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() self.update_orphan_exercises()
def update_orphan_exercises(self): def update_orphan_exercises(self):
@@ -61,3 +65,65 @@ 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
@@ -0,0 +1,8 @@
// Copyright (c) 2021, FOSS United and contributors
// For license information, please see license.txt
frappe.ui.form.on('LMS Course Progress', {
// refresh: function(frm) {
// }
});
@@ -0,0 +1,78 @@
{
"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
}
@@ -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 LMSCourseProgress(Document):
pass
@@ -0,0 +1,8 @@
# Copyright (c) 2021, FOSS United and Contributors
# See license.txt
# import frappe
import unittest
class TestLMSCourseProgress(unittest.TestCase):
pass
@@ -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-04-30 11:22:18.188712", "modified": "2021-06-02 15:52:06.383260",
"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": "Data", "fieldtype": "Link",
"hidden": 0, "hidden": 1,
"label": "Course", "label": "Course",
"max_length": 0, "max_length": 0,
"max_value": 0, "max_value": 0,
"options": "", "options": "LMS Course",
"read_only": 1, "read_only": 0,
"reqd": 0, "reqd": 0,
"show_in_filter": 0 "show_in_filter": 0
}, },
@@ -111,4 +111,4 @@
"show_in_filter": 0 "show_in_filter": 0
} }
] ]
} }
+3
View File
@@ -8,6 +8,9 @@
{% 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>
+1 -1
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)}} {{ widgets.ChapterTeaser(index=loop.index, chapter=chapter, course=course, batch=batch, show_link=show_link, show_progress=show_progress)}}
{% endfor %} {% endfor %}
+33
View File
@@ -238,3 +238,36 @@ section {
.page-card .btn { .page-card .btn {
margin-top: 30px; 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;
}
+1 -1
View File
@@ -285,7 +285,7 @@ section.lightgray {
} }
.lesson-teaser { .lesson-teaser {
line-height: 35px; line-height: 40px;
} }
#hero h1 { #hero h1 {
Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

+1 -1
View File
@@ -15,7 +15,7 @@
<h1 class="mt-5">{{ batch.title }}</h1> <h1 class="mt-5">{{ batch.title }}</h1>
</div> </div>
<div class="course-details"> <div class="course-details">
{{ widgets.CourseOutline(course=course, batch=batch, show_link=True) }} {{ widgets.CourseOutline(course=course, batch=batch, show_link=True, show_progress=True) }}
</div> </div>
<div class="w-25"> <div class="w-25">
<h2>Batch Schedule</h2> <h2>Batch Schedule</h2>
+1 -1
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>{{ lesson.title }}</h2> <h2 class="title {% if course.is_mentor(frappe.session.user) %} is_mentor {% endif %}" data-name="{{ lesson.name }}" data-batch="{{ batch.name }}">{{ 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 }}">
+11
View File
@@ -0,0 +1,11 @@
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")
}
})
}
})
+15 -18
View File
@@ -72,29 +72,26 @@
</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">
<li class="nav-item"> {% for tab in profile_tabs %}
<a class="nav-link active" id="home-tab" data-toggle="tab" href="#home" role="tab" aria-controls="home" <li class="nav-item">
aria-selected="true">Sketches</a> {% set slug = title.lower().replace(" ", "-") %}
</li> {% set selected = loop.index == 1 %}
{% 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>
<div class="tab-content"> {% for tab in profile_tabs %}
<div class="tab-pane fade py-4 show active" role="tabpanel" id="home"> {% set slug = title.lower().replace(" ", "-") %}
<div class="row"> <div class="tab-content">
{% if sketches %} <div class="tab-pane fade py-4 show active" role="tabpanel" id="slug">
{% for sketch in sketches %} {{ tab.render() }}
<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>
</div> {% endfor %}
</div> </div>
</div> </div>
</div> </div>
+13 -2
View File
@@ -3,9 +3,20 @@ 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"
else: return
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]