Compare commits

...

10 Commits

Author SHA1 Message Date
Anand Chitipothu
d9185c0b6b feat: integrated lesson markup
- added PageExtension plugin to inject custom styles scripts in a page
- removed the livecode integration and enabled PageExtension plugins for
  learn page
- also merged the profile_tab.py with plugins.py
- added a utility to find the macros from given text
- updated the before_save of lesson to find exercises using the macros
  and update the exercises as before

Issue #115
2021-06-09 23:58:21 +05:30
Anand Chitipothu
5363fb7eb3 feat: extend markdown to support macros
With this feature, the exercises can be added to the lesson as:

    {{ Exercise("two-circles") }}

This also added fenced_code extension that allows adding id and classes
to code blocks.

This uses Python-Markdown library instead of Markdown2 that is used
everywhere in Frappe. The Python-Markdown is more easily extensible than
Markdown2.

Issue #115
2021-06-09 23:22:00 +05:30
Jannat Patel
d90a1247f1 Merge pull request #120 from fossunited/profile-tabs
feat: pluggable profile tabs
2021-06-08 14:53:38 +05:30
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
25 changed files with 483 additions and 83 deletions

View File

@@ -176,3 +176,15 @@ profile_rules = [
website_route_rules = primary_rules + whitelist_rules + profile_rules website_route_rules = primary_rules + whitelist_rules + profile_rules
update_website_context = 'community.widgets.update_website_context' update_website_context = 'community.widgets.update_website_context'
## Specify the additional tabs to be included in the user profile page.
## Each entry must be a subclass of community.community.plugins.ProfileTab
# profile_tabs = []
## Specify the extension to be used to control what scripts and stylesheets
## to be included in lesson pages. The specified value must be be a
## subclass of community.community.plugins.PageExtension
# community_lesson_page_extension = None
## Markdown Macros for Lessons
# community_markdown_macro_renderers = {"Exercise": "myapp.mymodule.plugins.render_exercise"}

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:

View File

@@ -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

View File

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

View File

@@ -6,24 +6,28 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from ...section_parser import SectionParser from ...section_parser import SectionParser
from ...md import markdown_to_html, find_macros
class Lesson(Document): class Lesson(Document):
def before_save(self): def before_save(self):
sections = SectionParser().parse(self.body or "") macros = find_macros(self.body)
self.sections = [self.make_lms_section(i, s) for i, s in enumerate(sections)] exercises = [value for name, value in macros if name == "Exercise"]
for s in self.sections:
if s.type == "exercise":
e = s.get_exercise()
e.lesson = self.name
e.save()
self.update_orphan_exercises()
def update_orphan_exercises(self): index = 1
for name in exercises:
e = frappe.get_doc("Exercise", name)
e.lesson = self.name
e.index_ = index
e.save()
index += 1
self.update_orphan_exercises(exercises)
def update_orphan_exercises(self, active_exercises):
"""Updates the exercises that were previously part of this lesson, """Updates the exercises that were previously part of this lesson,
but not any more. but not any more.
""" """
linked_exercises = {row['name'] for row in frappe.get_all('Exercise', {"lesson": self.name})} 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"} active_exercises = set(active_exercises)
orphan_exercises = linked_exercises - active_exercises orphan_exercises = linked_exercises - active_exercises
for name in orphan_exercises: for name in orphan_exercises:
ex = frappe.get_doc("Exercise", name) ex = frappe.get_doc("Exercise", name)
@@ -32,11 +36,19 @@ class Lesson(Document):
ex.index_label = "" ex.index_label = ""
ex.save() ex.save()
def render_html(self):
return markdown_to_html(self.body)
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): def get_exercises(self):
return [frappe.get_doc("Exercise", s.id) for s in self.get("sections") if s.type=="exercise"] if not self.body:
return []
macros = find_macros(self.body)
exercises = [value for name, value in macros if name == "Exercise"]
return [frappe.get_doc("Exercise", name) for name in exercises]
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')
@@ -61,3 +73,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

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 Course Progress', {
// refresh: function(frm) {
// }
});

View File

@@ -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
}

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 LMSCourseProgress(Document):
pass

View File

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

107
community/lms/md.py Normal file
View File

@@ -0,0 +1,107 @@
"""
The md module extends markdown to add macros.
Macros can be added to the markdown text in the following format.
{{ MacroName("macro-argument") }}
These macros will be rendered using a pluggable mechanism.
Apps can provide a hook community_markdown_macro_renderers, a
dictionary mapping the macro name to the function that to render
that macro. The function will get the argument passed to the macro
as argument.
"""
import frappe
import re
from bs4 import BeautifulSoup
import markdown
from markdown import Extension
from markdown.inlinepatterns import InlineProcessor
import xml.etree.ElementTree as etree
def markdown_to_html(text):
"""Renders markdown text into html.
"""
return markdown.markdown(text, extensions=['fenced_code', MacroExtension()])
def find_macros(text):
"""Returns all macros in the given text.
>>> find_macros(text)
[
('YouTubeVideo': 'abcd1234')
('Exercise', 'two-circles'),
('Exercise', 'four-circles')
]
"""
macros = re.findall(MACRO_RE, text)
# remove the quotes around the argument
return [(name, _remove_quotes(arg)) for name, arg in macros]
def _remove_quotes(value):
"""Removes quotes around a value.
Also strips the whitespace.
>>> _remove_quotes('"hello"')
'hello'
>>> _remove_quotes("'hello'")
'hello'
>>> _remove_quotes("hello")
'hello'
"""
return value.strip(" '\"")
def get_macro_registry():
d = frappe.get_hooks("community_markdown_macro_renderers") or {}
return {name: frappe.get_attr(klass[0]) for name, klass in d.items()}
def render_macro(macro_name, macro_argument):
# stripping the quotes on either side of the argument
macro_argument = _remove_quotes(macro_argument)
registry = get_macro_registry()
if macro_name in registry:
return registry[macro_name](macro_argument)
else:
return f"<p>Unknown macro: {macro_name}</p>"
MACRO_RE = r'{{ *(\w+)\(([^{}]*)\) *}}'
class MacroExtension(Extension):
"""MacroExtension is a markdown extension to support macro syntax.
"""
def extendMarkdown(self, md):
self.md = md
pattern = MacroInlineProcessor(MACRO_RE)
pattern.md = md
md.inlinePatterns.register(pattern, 'macro', 75)
class MacroInlineProcessor(InlineProcessor):
"""MacroInlineProcessor is class that is handles the logic
of how to render each macro occurence in the markdown text.
"""
def handleMatch(self, m, data):
"""Handles each macro match and return rendered contents
for that macro as an etree node.
"""
macro = m.group(1)
arg = m.group(2)
html = render_macro(macro, arg)
html = sanitize_html(str(html))
e = etree.fromstring(html)
return e, m.start(0), m.end(0)
def sanitize_html(html):
"""Sanotize the html using BeautifulSoup.
The markdown processor request the correct markup and crashes on
any broken tags. This makes sures that all those things are fixed
before passing to the etree parser.
"""
soup = BeautifulSoup(html, features="lxml")
nodes = soup.body.children
return "<div>" + "\n".join(str(node) for node in nodes) + "</div>"

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-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
}, },

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>

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 %}

66
community/plugins.py Normal file
View File

@@ -0,0 +1,66 @@
"""
The plugins module provides various plugins to change the default
behaviour some parts of the community app.
A site specify what plugins to use using appropriate entries in the frappe
hooks, written in the `hooks.py`.
This module exposes two plugins: ProfileTab and PageExtension.
The ProfileTab is used to specify any additional tabs to be displayed
on the profile page of the user.
The PageExtension is used to load additinal stylesheets and scripts to
be loaded in a webpage.
"""
class PageExtension:
"""PageExtension is a plugin to inject custom styles and scripts
into a web page.
The subclasses should overwrite the `render_header()` and
`render_footer()` methods to inject whatever styles/scripts into
the webpage.
"""
def render_header(self):
"""Returns the HTML snippet to be included in the head section
of the web page.
Typically used to include the stylesheets and javascripts to be
included in the <head> of the webpage.
"""
return ""
def render_footer(self):
"""Returns the HTML snippet to be included in the body tag at
the end of web page.
Typically used to include javascripts that need to be executed
after the page is loaded.
"""
return ""
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

@@ -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;
}

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

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>

View File

@@ -9,16 +9,13 @@
</style> </style>
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css"> <link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
<link rel="stylesheet" href="{{ livecode_url }}/static/codemirror/lib/codemirror.css">
<link rel="stylesheet" href="/assets/css/lms.css"> <link rel="stylesheet" href="/assets/css/lms.css">
<link rel="stylesheet" href="/assets/frappe/css/hljs-night-owl.css"> <link rel="stylesheet" href="/assets/frappe/css/hljs-night-owl.css">
<script src="{{ livecode_url }}/static/codemirror/lib/codemirror.js"></script> {% for ext in page_extensions %}
<script src="{{ livecode_url }}/static/codemirror/mode/python/python.js"></script> {{ ext.render_header() }}
<script src="{{ livecode_url }}/static/codemirror/keymap/sublime.js"></script> {% endfor %}
<script src="{{ livecode_url }}/static/codemirror/addon/edit/matchbrackets.js"></script>
<script src="{{ livecode_url }}/static/codemirror/addon/comment/comment.js"></script>
{% endblock %} {% endblock %}
@@ -28,13 +25,9 @@
{{ 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() %} {{ lesson.render_html() }}
<div class="section section-{{ s.type }}">
{{ render_section(s) }}
</div>
{% endfor %}
{{ pagination(prev_chap, prev_url, next_chap, next_url) }} {{ pagination(prev_chap, prev_url, next_chap, next_url) }}
</div> </div>
@@ -42,30 +35,6 @@
{% endblock %} {% endblock %}
{% macro render_section(s) %}
{% if s.type == "text" %}
{{ render_section_text(s) }}
{% elif s.type == "example" or s.type == "code" %}
{{ LiveCodeEditor(s.name,
code=s.get_latest_code_for_user(),
reset_code=s.contents,
is_exercise=False)
}}
{% elif s.type == "exercise" %}
{{ widgets.Exercise(exercise=s.get_exercise())}}
{% else %}
<div>Unknown section type: {{s.type}}</div>
{% endif %}
{% endmacro %}
{% macro render_section_text(s) %}
<div class="row">
<div class="col-md-9">
{{ frappe.utils.md_to_html(s.contents) }}
</div>
</div>
{% endmacro %}
{% macro pagination(prev_chap, prev_url, next_chap, next_url) %} {% macro pagination(prev_chap, prev_url, next_chap, next_url) %}
<div class="lesson-pagination"> <div class="lesson-pagination">
{% if prev_url %} {% if prev_url %}
@@ -84,8 +53,6 @@
{%- block script %} {%- block script %}
{{ super() }} {{ super() }}
{{ LiveCodeEditorJS() }}
<script type="text/javascript"> <script type="text/javascript">
$(function() { $(function() {
var batch_name = "{{ batch.name }}"; var batch_name = "{{ batch.name }}";
@@ -98,4 +65,8 @@
}) })
</script> </script>
{% for ext in page_extensions %}
{{ ext.render_footer() }}
{% endfor %}
{%- endblock %} {%- endblock %}

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")
}
})
}
})

View File

@@ -28,7 +28,7 @@ def get_context(context):
context.next_url = context.batch.get_learn_url(next_) context.next_url = context.batch.get_learn_url(next_)
context.prev_url = context.batch.get_learn_url(prev_) context.prev_url = context.batch.get_learn_url(prev_)
context.page_extensions = get_page_extensions()
def get_chapter_title(course_name, lesson_number): def get_chapter_title(course_name, lesson_number):
if not lesson_number: if not lesson_number:
@@ -42,4 +42,8 @@ def get_lesson_index(course, batch, user):
lesson = batch.get_current_lesson(user) lesson = batch.get_current_lesson(user)
return lesson and course.get_lesson_index(lesson) return lesson and course.get_lesson_index(lesson)
def get_page_extensions():
default_value = ["community.community.plugins.PageExtension"]
classnames = frappe.get_hooks("community_lesson_page_extensions") or default_value
extensions = [frappe.get_attr(name)() for name in classnames]
return extensions

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]

View File

@@ -1,2 +1,5 @@
frappe frappe
websocket_client websocket_client
markdown
beautifulsoup4
lxml