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):
|
||||
rows = frappe.db.get_all("Lesson",
|
||||
filters={"chapter": self.name},
|
||||
fields='*',
|
||||
fields='name',
|
||||
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",
|
||||
"tests",
|
||||
"image",
|
||||
"lesson"
|
||||
"lesson",
|
||||
"index_",
|
||||
"index_label"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -27,6 +29,7 @@
|
||||
{
|
||||
"fieldname": "course",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Course",
|
||||
"options": "LMS Course"
|
||||
},
|
||||
@@ -73,13 +76,27 @@
|
||||
{
|
||||
"fieldname": "lesson",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "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,
|
||||
"links": [],
|
||||
"modified": "2021-05-20 13:23:12.340928",
|
||||
"modified": "2021-06-01 05:22:15.656013",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Exercise",
|
||||
@@ -99,8 +116,8 @@
|
||||
}
|
||||
],
|
||||
"search_fields": "title",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"sort_field": "index_label",
|
||||
"sort_order": "ASC",
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -25,7 +25,6 @@ class Exercise(Document):
|
||||
order_by="creation desc",
|
||||
page_length=1)
|
||||
|
||||
print("get_user_submission", result)
|
||||
if result:
|
||||
return result[0]
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"lesson_type",
|
||||
"title",
|
||||
"index_",
|
||||
"index_label",
|
||||
"body",
|
||||
"sections"
|
||||
],
|
||||
@@ -51,11 +52,18 @@
|
||||
"fieldtype": "Table",
|
||||
"label": "Sections",
|
||||
"options": "LMS Section"
|
||||
},
|
||||
{
|
||||
"fieldname": "index_label",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Index Label",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-05-13 20:03:51.510605",
|
||||
"modified": "2021-06-01 05:30:48.127494",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Lesson",
|
||||
|
||||
@@ -11,15 +11,37 @@ class Lesson(Document):
|
||||
def before_save(self):
|
||||
sections = SectionParser().parse(self.body or "")
|
||||
self.sections = [self.make_lms_section(i, s) for i, s in enumerate(sections)]
|
||||
|
||||
index = 1
|
||||
for s in self.sections:
|
||||
if s.type == "exercise":
|
||||
e = s.get_exercise()
|
||||
e.lesson = self.name
|
||||
e.index_ = index
|
||||
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):
|
||||
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):
|
||||
s = frappe.new_doc('LMS Section', parent_doc=self, parentfield='sections')
|
||||
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_rename": 1,
|
||||
"creation": "2021-03-01 16:49:33.622422",
|
||||
@@ -86,7 +99,7 @@
|
||||
"link_fieldname": "course"
|
||||
}
|
||||
],
|
||||
"modified": "2021-05-23 18:14:32.602647",
|
||||
"modified": "2021-06-01 04:36:45.696776",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Course",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
import json
|
||||
from ...utils import slugify
|
||||
from community.query import find, find_all
|
||||
|
||||
@@ -157,6 +158,35 @@ class LMSCourse(Document):
|
||||
chapter = frappe.get_doc("Chapter", lesson.chapter)
|
||||
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):
|
||||
return CourseOutline(self)
|
||||
|
||||
@@ -187,7 +217,8 @@ class CourseOutline:
|
||||
def get_chapters(self):
|
||||
return frappe.db.get_all("Chapter",
|
||||
filters={"course": self.course.name},
|
||||
fields=["name", "title", "index_"])
|
||||
fields=["name", "title", "index_"],
|
||||
order_by="index_")
|
||||
|
||||
def get_lessons(self):
|
||||
chapters = [c['name'] for c in self.chapters]
|
||||
@@ -199,3 +230,17 @@ class CourseOutline:
|
||||
for lesson in lessons:
|
||||
lesson['number'] = "{}.{}".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.")
|
||||
|
||||
@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 %}
|
||||
|
||||
<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>
|
||||
|
||||
{% if exercise.image %}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<h1>Batch Progress</h1>
|
||||
{% for exercise in report.exercises %}
|
||||
<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) %}
|
||||
<div class="submission">
|
||||
<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)
|
||||
|
||||
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):
|
||||
return self.submissions_by_exercise[exercise_name]
|
||||
|
||||
@@ -72,29 +72,26 @@
|
||||
</div>
|
||||
<div>
|
||||
<ul class="nav nav-tabs mt-4" id="myTab" role="tablist">
|
||||
{% for tab in profile_tabs %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" id="home-tab" data-toggle="tab" href="#home" role="tab" aria-controls="home"
|
||||
aria-selected="true">Sketches</a>
|
||||
{% set slug = title.lower().replace(" ", "-") %}
|
||||
{% 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>
|
||||
</div>
|
||||
<div>
|
||||
{% for tab in profile_tabs %}
|
||||
{% set slug = title.lower().replace(" ", "-") %}
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade py-4 show active" role="tabpanel" id="home">
|
||||
<div class="row">
|
||||
{% if sketches %}
|
||||
{% for sketch in sketches %}
|
||||
<div class="col-md-4 col-sm-6">
|
||||
{{ widgets.SketchTeaser(sketch=sketch) }}
|
||||
<div class="tab-pane fade py-4 show active" role="tabpanel" id="slug">
|
||||
{{ tab.render() }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if not sketches %}
|
||||
<p class="text-center">{{member.full_name}} has not created any skecth yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,9 +3,20 @@ from community.lms.models import Sketch
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
|
||||
try:
|
||||
context.member = frappe.get_doc("User", {"username": frappe.form_dict["username"]})
|
||||
except:
|
||||
context.template = "www/404.html"
|
||||
else:
|
||||
context.sketches = Sketch.get_recent_sketches(owner=context.member.email)
|
||||
return
|
||||
|
||||
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