Compare commits

...

8 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
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
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
Anand Chitipothu
c96a14c972 feat: ignore orphan exercises in the progress
Don't show exercises that are not added to any lesson in the progress.
2021-06-01 08:15:52 +05:30
Anand Chitipothu
400e706be1 feat: update the index of orphan exercises
When an exercise is removed from a lesson, the link to the lesson is
removed from that exercise and the index is reset. This will make sure
the removed exercises won't show up in places like progress.
2021-06-01 05:59:01 +05:30
Anand Chitipothu
a12a52747e feat: show exercise index in the title
Show exercise as "Exercise 2.1: Draw a Circle".
2021-06-01 05:49:45 +05:30
Anand Chitipothu
b9a93bb160 feat: added actions to reindex lessons and exercises
Some lessons gets deleted and some new ones get added in the progress of
course creation and it may happen then some of the lesson index may
become inconsistent.  Also, we would like to maintain an index for the
exercises. To support both of these, added actions to reindex lessons
and exercises to the course doctype.
2021-06-01 05:46:32 +05:30
13 changed files with 184 additions and 34 deletions

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()

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='*', fields='name',
order_by="index_") order_by="index_")
return [frappe.get_doc(dict(row, doctype='Lesson')) for row in rows] return [frappe.get_doc('Lesson', row['name']) for row in rows]

View File

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

View File

@@ -25,7 +25,6 @@ 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]

View File

@@ -10,6 +10,7 @@
"lesson_type", "lesson_type",
"title", "title",
"index_", "index_",
"index_label",
"body", "body",
"sections" "sections"
], ],
@@ -51,11 +52,18 @@
"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-05-13 20:03:51.510605", "modified": "2021-06-01 05:30:48.127494",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Lesson", "name": "Lesson",

View File

@@ -11,15 +11,37 @@ 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

View File

@@ -1,5 +1,18 @@
{ {
"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",
@@ -86,7 +99,7 @@
"link_fieldname": "course" "link_fieldname": "course"
} }
], ],
"modified": "2021-05-23 18:14:32.602647", "modified": "2021-06-01 04:36:45.696776",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Course", "name": "LMS Course",

View File

@@ -5,6 +5,7 @@
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
@@ -157,6 +158,35 @@ 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)
@@ -187,7 +217,8 @@ 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]
@@ -199,3 +230,17 @@ 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,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.title }}</h2> <h2>Exercise {{exercise.index_label}}: {{ 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

@@ -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.title}}</h2> <h2>Exercise {{exercise.index_label}}: {{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}, ["name", "title"]) 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): def get_submissions_of_exercise(self, exercise_name):
return self.submissions_by_exercise[exercise_name] return self.submissions_by_exercise[exercise_name]

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>

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]