Compare commits
8 Commits
lesson-pro
...
profile-ta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef0c3e4a24 | ||
|
|
3619b136f8 | ||
|
|
586b39c0fd | ||
|
|
0dc4743556 | ||
|
|
c96a14c972 | ||
|
|
400e706be1 | ||
|
|
a12a52747e | ||
|
|
b9a93bb160 |
38
community/community/profile_tab.py
Normal file
38
community/community/profile_tab.py
Normal 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()
|
||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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]
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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.")
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
Reference in New Issue
Block a user